User Defined Types
So far we've used cells holding strings and numbers, and an
enum. What about types defined by your own classes? A cell can
generally hold a value of any type. This section goes over the tools
to make working with user defined types more convenient.
Live Cell Extension
The
live_cell_extension
package provides a source code generator that allows you to extend the
core cell interfaces, ValueCell and MutableCell, with accessors
for properties of your own classes.
To understand what this means, consider the following class:
class Person {
final String firstName;
final String lastName;
final int age;
const Person({
required this.firstName,
required this.lastName,
required this.age
});
}
Let's say you have a cell holding a Person:
final person = MutableCell(
Person(
firstName: 'John',
lastName: 'Smith',
age: 25
)
);
To access a property of the Person held in the cell, you will need
to defined a computed cell:
final firstName = ValueCell.computed(() => person().firstName);
If you want the firstName cell to be settable, so that setting the
value of firstName updates the person cell, you'll need to define
a copyWith method and a mutable computed cell:
final firstName = MutableCell.computed(() => person().firstName, (name) {
person.value = person.value.copyWith(
firstName: name
);
});
This is the definition of boilerplate and will quickly become tiring.
The live_cell_extension package automatically generates this code
for you, so that instead of the above, you can write the following:
final firstName = person.firstName;
And to update the value of the firstName property:
person.firstName.value = 'Jane';
That's it, no need to write a copyWith method either. This ties in
with Live Cell's design principle that cells should be
indistinguishable, as much as is possible, from the values they hold.
Generating the Code
To make this work you'll need to add the live_cell_extension package
to the dev_dependencies of your pubspec.yaml:
dart pub add --dev live_cell_extension
Then annotate the classes, for which you want accessors to be
generated, with CellExtension. If you want mutable cell accessors to
also be generated, add mutable: true to the annotation arguments.
part 'person.g.dart';
(mutable: true)
class Person {
final String firstName;
final String lastName;
final int age;
const Person({
required this.firstName,
required this.lastName,
required this.age
});
}
Don't forget to include the <filename>.g.dart file. This is where
the code will be generated.
Next you'll need to run the following command in the root directory of your project:
dart run build_runner build
This will generate the .g.dart files, which contain the generated
class property accessors.
The ValueCell accessors are defined in an extension with the name of
the class followed by CellExtension. The MutableCell accessors are
defined in an extension with the name of the class followed by
MutableCellExtension.
Binding to Properties
Using the generated property accessors, we can define a form for populating the class properties simply by binding the property cells, retrieved using the generated accessors, to the appropriate widgets.
class PersonForm extends CellWidget {
final MutableCell<Person> person;
const PersonForm(this.person);
Widget build(BuildContext context) => Column(
children: [
Text('First Name:'),
LiveTextField(
content: person.firstName
),
Text('Last Name:'),
LiveTextField(
content: person.lastName
),
Text('Age:'),
Numberfield(person.age)
]
);
We used the Numberfield widget, defined earlier,
for entering the age property.
We defined the form as a class because we intend to reuse it in other widgets.
We can then use this form as follows:
CellWidget.builder((_) {
final person = MutableCell(
Person(
firstName: 'John',
lastName: 'Smith',
age: 25
)
);
return Column(
children: [
PersonForm(person),
Text('${person.firstName()} ${person.lastName()}: ${person.age()} years'),
ElevatedButton(
child: Text('Save'),
// A hypothetical savePerson function
onPressed: () => savePerson(person.value)
),
FilledButton(
child: Text('Reset'),
onPressed: () => person.value = Person(
firstName: 'John',
lastName: 'Smith',
age: 25
)
)
]
);
});
In this example we used the PersonForm widget defined earlier.
- The details of the person are displayed in a
Textwidget, which is automatically updated when the person's details are changed. - The "Save" button saves the entered details, which are held in the
personcell. - The "Reset" button resets the form fields to their defaults by
directly assigning a default
Personto thepersoncell.
The benefits of this, as opposed to using the tools already available in Flutter, are:
- No need to write event handlers and state synchronization code for acquiring input from the user. This is all handled automatically.
- You can focus directly on the representation of your data and think
in terms of your data, rather than thinking in terms of widget
State. - Your widgets are bound directly to your data and kept in sync. There is no chance of you accidentally forgetting to synchronize them with your data and vice versa, which eliminates a whole class of bugs.
Equality
It is a good practice to define the == and hashCode methods on
classes which will be used as cell value types. In-fact there are two
situations, in which defining == and hashCode is essential:
-
When a constant cell holding an instance of the class is created:
final person = Person(...).cell;If
Persondoes not override==andhashCode, each call toPerson(...).cellwill create a new cell even if the same values are given for thefirstName,lastNameandageproperties. -
When
changesOnly: trueis given to a cell holding an instance of the class:final person = ValueCell.computed(() => Person(
firstName: firstName(),
lastName: lastName(),
age: age()
), changesOnly: true)If
Persondoes not override==andhashCode, thechangesOnlykeyword has no effect, since every time the cell is recomputed, a newPersonis created that is never equal to the previousPerson.
The live_cell_extension package also generates a comparison and hash
function for classes annotated with CellExtension. The name of the
comparison function is of the form _$<class>Equals and the name of
the hash function is of the form _$<class>HashCode.
Thus to override == and hashCode for the Person class, all that
has to be done is the following:
(mutable: true)
class Person {
final String firstName;
final String lastName;
final int age;
const Person({
required this.firstName,
required this.lastName,
required this.age
});
bool operator ==(Object other) =>
_$PersonEquals(this, other);
int get hashCode => _$PersonHashCode(this);
}
_$PersonEquals and _$PersonHashCode are the generated comparison
and hash functions respectively.
If you don't want comparison and hash functions to be generated, pass
generateEquals: false to the CellExtension annotation.
By default the generated comparison function compares each property
with == and the generated hash function computes the hash code
using the hashCode property of each property. To specify a different
comparison and hash function for a property, annotate it with
DataField.
()
class Point {
(
equals: listEquals,
hash: Object.hashAll
)
final List<int> coordinates;
...
}
The equals argument specifies the comparison function to use instead
of == and the hash argument specifies the hash function to use
instead of the hashCode property.
In this example the generated comparison function for the Point
class, will use listEquals, from the flutter:foundation.dart
library to compare the values of the coordinates
properties. Similarly, the generated hash function will use
Object.hashAll to compute the hash code of the coordinates
property.
If you only want to generate a comparison and hash function but do not
want to generate a cell extension, annotate the class with
@DataClass().