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
:
dev_dependencies:
live_cell_extension: 0.6.1
...
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;
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 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)
),
ElevatedButton(
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
Text
, which is automatically updated when the person's details are changed. - The "Save" button saves the entered details, which are held in the
person
cell. - The "Reset" button resets the form fields to their defaults by
directly assigning a default
Person
to theperson
cell.
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
Person
does not override==
andhashCode
, each call toPerson(...).cell
will create a new cell even if the same values are given for thefirstName
,lastName
andage
properties. -
When
changesOnly: true
is given to a cell holding an instance of the class:final person = ValueCell.computed(() => Person(
firstName: firstName(),
lastName: lastName(),
age: age()
), changesOnly: true)If
Person
does not override==
andhashCode
, thechangesOnly
keyword has no effect, since every time the cell is recomputed, a newPerson
is 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()
.