live_cells 0.23.1 copy "live_cells: ^0.23.1" to clipboard
live_cells: ^0.23.1 copied to clipboard

A replacement for ChangeNotifier and ValueNotifier that is easier to use and more flexible

Live Cells is a reactive programming library for Dart, intended to be used with Flutter. Specifically Live Cells provides a replacement (the cell) for ChangeNotifier and ValueNotifier that is simpler to use and more flexible.

Features #

Cells offers the following benefits over ChangeNotifier / ValueNotifier:

  • Implementing a cell which is an expression of other cells, e.g. a + b, can be done in a functional manner without manually adding and removing listeners.
  • Simpler resource management, no need to call dispose.
  • Integrated with a library of widgets which allow their properties to be controlled and observed by cells. This allows for a style of programming which fits in with the reactive paradigm of Flutter.
  • Effortless state restoration with minimal boilerplate.

This library also has the following advantages over other similar libraries:

  • Supports two-way data flow, whereas most other libraries, if not all, only support one-way data flow.
  • Cells are designed to be unobtrusive and indistinguishable, as much as is possible, from the values they hold.
  • Live Cells is unopinionated. You're not forced to change the way you write your apps. Use as much of its functionality as you need.
  • Integrated with a library of widgets which allow for effortless data binding between UI elements and cells.

Getting Started #

If you haven't used Live Cells before, please head to the full documentation.

The remainder of this README shows brief examples that demonstrate the main features of this library.

Usage #

Defining Cells #

Constant cells can be defined either using .cell or ValueCell.value:

final a = 1.cell;
final b = 'hello'.cell;
final c = ValueCell.value(someValue);

Mutable cells are defined using MutableCell:

final a = MutableCell(0);
final b = MutableCell(1);

Computed Cells #

Basic computed cells can be defined directly as an expression of cells:

final sum = a + b;

More complex computed cells are defined using ValueCell.computed:

final computed = ValueCell.computed(() => sqrt(a() * a() + b() * b()));

The values of computed cells are recomputed whenever the values of their argument cells change:

print(sum.value); // 1
a.value = 6;
print(sum.value); // 7

Observing Cells #

Cells can be observed using ValueCell.watch. The watch function is called whenever the values of the cells it references change:

ValueCell.watch(() {
  print('${a()} + ${b()} = ${sum()}');
});

a.value = 8; // Prints: 8 + 1 = 9

Batch Updates #

The values of multiple mutable cells can be set simultaneously using MutableCell.batch:

MutableCell.batch(() {
  a.value = 1;
  b.value = 3;
});

Cells in Widgets #

CellWidget.builder creates a widget that is rebuilt whenever the values of the cells referenced by it change:

// Rebuilt when a, b, or sum change
CellWidget.builder(() => Text('${a()} + ${b()} = ${sum()}'));

Exception Handling #

Exceptions thrown during the computation of a cell's value are propagated to all points where the value is referenced.

This allows exceptions to be handled using try catch:

final str = MutableCell('0');
final n = ValueCell.computed(() => int.parse(str()));

final isValid = ValueCell.computed(() {
  try {
    return n() > 0;
  }
  catch (e) {
    return false;
  }
});

print(isValid.value); // Prints false

str.value = '5';
print(isValid.value); // Prints true

str.value = 'not a number';
print(isValid.value); // Prints false

Or more succinctly using onError:

final str = MutableCell('0');
final n = ValueCell.computed(() => int.parse(str()));
final isValid = (n > 0.cell).onError(false.cell);

Previous Values #

The previous value of a cell can be accessed using .previous, which is itself a cell:

final a = MutableCell(0);
final prev = a.previous;
final sum = a + prev;

a.value = 1;
print(a.value);    // Prints 1
print(prev.value); // Prints 0
print(sum.value);  // Prints 1

a.value = 5;
print(a.value);    // Prints 5
print(prev.value); // Prints 1
print(sum.value);  // Prints 6

Binding Cells to Widget Properties #

This package also provides a collection of widgets, which allow their properties to be controlled and observed by cells:

For example CellSwitch is this library's equivalent of Switch:

final state = MutableCell(false);

return Column(
  children: [
    // This binds the value of the switch to `state`
    CellSwitch(
      value: state
    );
    // This widget is rebuilt whenever the switch is toggled
    CellWidget.builder(() => Text(state() ? 'On' : 'Off'));
  ]
);

Resetting the switch is as simple as setting the value of state:

state.value = false;

Two-Way Data Flow #

Two-way data flow allows for complex logic to be implemented entirely using concise declarative code:

final n = MutableCell<num>(0);

// CellTextField is a Live Cells TextField
//
// This binds the content of the field to `n`
return CellTextField(
  // Example of two-way data flow:
  //
  // When the value of `n` is set, `mutableString()` converts it to
  // a string and forwards it to the field's content.
  //
  // When the content of the field changes, `mutableString()` converts
  // the string content to a number and forwards it to `n`
  content: n.mutableString();
)

Property Accessors for your own types: #

With live_cell_extension you can generate cell accessors for your own classes:

@CellExtension(mutable: true)
class Person {
  final String firstName;
  final String lastName;
  
  Person({
    required this.firstName,
    required this.lastName
  });
}

You can now access firstName and lastName directly on cell's holding a Person:

final person = MutableCell(Person(...));

ValueCell.watch(() {
  print('${person.firstName()} ${person.LastName()}');
});

// This triggers the watch function defined above:
person.firstName.value = 'John';

Asynchronous Cells #

Cells can hold and manipulate a Future like any other value:

ValueCell<Future<int>> cell1;
ValueCell<Future<int>> cell2;
...
final sum = ValueCell.computed(() async {
  final (a, b) = await (cell1(), cell2()).wait;
  return a + b;
});

Cells can await a Future held in another cell:

ValueCell<Future<int>> cell1;
ValueCell<Future<int>> cell2;
...
// The value of `sum` is only computed once the futures
// in both `cell1` and `cell2` have completed
final sum = ValueCell.computed(() {
  final (a, b) = (cell1, cell2).wait();
  return a + b;
});

Cells can also check if, and be notified when, a Future in another cell has completed:

ValueCell<Future<int>> cell1;
ValueCell<Future<int>> cell2;
...
// isLoading is true until both the Futures in cell1
// and cell2 have completed
final isLoading = (cell1, cell2).isCompleted.not();

ValueListenable (Integration with other tools) #

The .listenable property returns a ValueListenable that notifies its observers whenever the value of the cell changes:

final count = MutableCell<int>(0);
final countListenable = count.listenable;

This allows cells to be used as a drop-in replacement for ValueNotifier, whenever a ValueListenable is expected:

ValueListenableBuilder(
  valueListenable: count.listenable,
  ...
)

Additional information #

If you discover any issues or have any feature requests, please open an issue on the package's Github repository.

Please visit the full documentation for a full introduction to the library's features. Also take a look at the example directory for more complete examples of how to use this library.