Complex list editors without state management in Flutter
Imagine an editor like this:
Feels like a lot of state management? In this tutorial we will create such an editor without any manual state management. We will use model_editors package. The editor will allow to:
- Initialize with a list of your models.
- Get the list of your models after editing.
- Add books with (+) button.
- Limit the maximum number of books.
- Delete books with trash button.
- Limit the minimum number of books (disallow deleting the last one).
- Reorder books by dragging handles.
Remember TextEditingController
? It encapsulates the handling of text for input fields so you don’t do setState(){}
when something is typed. You just set the text in the controller, create a field widget, then get the edited text from the controller. The idea is the same here.
We will follow these steps:
- Define models.
- Create a controller for one book with its fields.
- Create a widget to show and edit one book.
- Create a controller for the list of books that will operate on a list of single book controllers.
- Get it together to show an editable list.
The complete code for this example is in main.dart
in model_editors package on GitHub, at v0.1.2
.
1. Define models
2. Create a controller for one book
The book controller holds all the data of the book but not the Book
object itself. The data is stored in TextEditingController
s and a controller for checkboxes.
This controller gets initialized with a Book
object. And when the data is read, it constructs another Book
object from its data.
It subclasses ValueNotifier
which is a Flutter’s class that holds some value, defines its getter an setter and notifies some listeners on its change. Our favorite TextEditingController
is itself another subclass of ValueNotifier
.
CheckboxGroupEditingController
is a class from model_editors
package that handles checkboxes.
Note that we require a book to have both title and author. If any of these is not set, the controller’s value is considered empty.
3. Create a widget to show and edit one book
This widget works with BookEditingController
the same way that a TextField
widget works with TextEditingController
.
MaterialCheckboxColumn
is a widget from model_editors
package that handles checkboxes. It reads and sets values of CheckboxGroupEditingController
that we created earlier. Its layout is opinionated, so you may want to create your own. If you do, check its source on how to work with the controller.
Note that in BookEditor
we do not have to listen to the controller to rebuild. This is because every piece of book data is in its own controller that is listened to in its nested widget: two text fields and one checkbox group.
At this point, if you use the default app template and create BookEditingController
and BookEditor
, you will see one empty editor on your screen. But we will move on to make a list.
4. Create a controller for the list of books
This is the easy one because AbstractListEditingController
of model_editors
takes care for everything.
It only needs an upper limit and an item controller factory. Lower limit is optional.
5. Get it together
Now lets make a minimal app to create this list.
MyHomePage
is the only stateful widget in this example as it holds the list controller.
The list is shown with ReorderableListViewEditor
of model_editors
. It does deletion and reordering with zero effort of yours. It only needs the list controller and a factory method for item widgets.
If you do not need reordering, then use a simpler ColumnListEditor
.
Next goes ListAddButtonBuilder
. It shows the button to add items if the controller allows it.
The controller gets initialized by setting its value
property, in this case in initState()
. Then, in “Save” button handler value
is read. The value is a list that may contain null
values. Remember, we require both title and author for a book to be non-null.
There is a convenience property nonNullValues
which reads value and filters non-nulls.
Again, the complete code for this tutorial is in main.dart
in model_editors package example on GitHub.
Comparison to other state management patterns
Controller vs StatefulWidget
Advantages of a controller:
- Less rebuilds on changes. The editor widget does not rebuilt itself on changes of most pieces of data. This is because data fields are handled with their own controllers.
- Never use
onChanged
again. With traditional approach, widgets report all their changes to their parent, and you get overwhelmed with callbacks. With controller, you only care for children temporary state if you need live validation. - Aggregate anything. If you first design a one-entity editor and then realize that you need to reuse it in an editable list, it’s a 10-lines of code addition. If you aggregate entities, you can aggregate their controllers and editor widgets in a few lines of code. You get the value of the entire list or aggregate with one read of
value
. - Consistency. Flutter uses the same approach for
TextEditingController
. One approach to state management is more intuitive than a mix.
Controller vs BLoC or Cubit
Another approach to state management of editors is to back each form with a BLoC or cubit, then listen to its stream of states to update fields.
You may think of a controller based on ValueNotifier
as just a lightweight version of a cubit. It does not create the output stream.
You generally choose cubit when the main use case is to emit states, and you choose ValueNotifier
otherwise. You can still read the current state of a cubit synchronously, but its exclusive use defeats the point of cubit.
Conclusion
The controllers in this package are pretty stable and are intended for public use. The widgets on the other hand are mostly ad-hoc and are in development. They lack many properties and customization.
If you wish to help, file an issue with your idea before contributing, and I will see how to incorporate it.