Flutter provides a lot of flexibility in deciding how to organize and architect your apps, but we may develop apps with large classes, inconsistent naming schemes, as well as mismatching or missing architectures.
We need to demonstrate strategies to help solve or avoid common problems.
StatefulWidget
We can use only the core Widgets and Classes that Flutter provides out of the box to manage state.
When a widget's appearance or data needs to change during its lifetime, we need a StatefulWidget and a companion State object, calling setState tells Flutter to rebuild the UI of a widget.
• StatefulWidgetis used to hold state.• The UI update is triggered by setState.
Note that the StatefulWidget itself is still immutable (its properties can't change after creation), the State object is long-lived, can hold mutable data, and can be rebuilt when that data changes, causing the UI to update.
We can create a simple app that uses a StatefulWidget with a counter that increases when the button is pressed.
classExampleWidgetextendsStatefulWidget{
ExampleWidget({super.key});
@override
State<ExampleWidget> createState() => _ExampleWidgetState();
}
class_ExampleWidgetStateextendsState<ExampleWidget> {
@override
Widget build(BuildContext context) {
return Container();
}
}When a widget's appearance or data needs to change during its lifetime, we need a StatefulWidget. The widget itself stays immutable, but its companion State object holds mutable data and triggers rebuilds.
Lift State Up
We can share State by Lifting State Up - If two Widgets need access to the same state (aka data), "lift" the state up to a parent StatefulWidget that passes the state down to each child Widget that needs it.
Let's start with a simple example. For example, the app had only 1 Tab: The List of Todos Tab.
+------------+
| |
| List |
| of |
| Todos |
| Tab |
| |
+------------+Next we add a sibling Widget: The Stats Tab! But it needs access to the List of Todos so it can calculate how many of them are active and how many are complete.
It can be difficult for siblings to pass their state to each other. For example, both Widgets were displayed side-by-side at the same time.
We should let Flutter know when to re-build the Stats Tab to reflect the latest count when the List of Todos changes.
+-------------+ +-------------+
| | | |
| | Gimme dem Todos | |
| List of | <-------------- + Stats |
| Todos | | Tab |
| Tab | No. Mine. | |
| + --------------> | |
| | | |
+-------------+ +-------------+Here the best solution to share state between these two sibling widgets is Lift the state up to a Parent Widget and pass it down to each child that needs it!
+-------------------------+
| Keeper of the Todos |
| (StatefulWidget) |
+-----+--------------+----+
| |
+----------|--+ +---|---------+
| v | | v |
| List of | | Stats |
| Todos | | Tab |
| | | |
+-------------+ +-------------+When we change the List of Todos in the Keeper of the Todos widget, both children will reflect the updated State! This concept scales to an entire app.
Any time we need to share State between Widgets or Routes, lift it up to a common parent Widget.
Here's a diagram of what our app state actually looks like!
+------------------------------------------+
| |
| VanillaApp (StatefulWidget) |
| |
| Manages List<Todo> and "isLoading" |
| |
+---------+---------------------------+----+
| |
+------------|---------------+ +--------|--------+
| v | | v |
| Main Tabs Screen | | Add Todo Screen |
| (Stateful) | | (Stateless) |
| | | |
| Manages current tab and | | |
| Visibility filter | | |
| | | |
| +----------+ +----------+ | | |
| | | | | | | |
| | List | | | | | |
| | of | | Stats | | | |
| | Todos | | Tab | | | |
| | Tab | | | | | |
| +----+-----+ +----------+ | | |
| | | | |
+------|---------------------+ +-----------------+
|
+------|---------+
| v |
| |
| Todo Details |
| Screen |
| |
+------+---------+
|
+------|---------+
| v |
| |
| Edit Todo |
| Screen |
| |
+----------------+We don't lift all state up to the parent in this pattern, only the State that's shared!
The Main Tabs Screen can handle which tab is currently active on its own, for example, because this state isn't relevant to other Widgets!
Update State with callback
When we need to update the list of Todos from these different screens/routes/widgets, we can pass callback functions from the parent to the children.
These callback functions are responsible for updating the State at the Parent Widget and calling setState so Flutter knows it needs to rebuild!
Here we have a few callbacks we will pass down:
• AddTodo callback • RemoveTodo Callback • UpdateTodo Callback • MarkAllComplete callback • ClearComplete callback
We pass the AddTodo callback from the Widget to the Add Todo Screen. Now all our Add Todo Screen needs to do is call the AddTodo callback with a new Todo when a user finishes filling in the form. This will send the Todo up to the app so it can handle how it adds the new todo to the list!
There is a core concept of State management in Flutter:
• Pass data and callback functions down from a parent to a child • Use invoke those callbacks in the Child to send data back up to the parent.
+----------------------------------------------------+
| |
| VanillaApp (StatefulWidget) |
| |
| Manages List<Todo> and "isLoading" |
| ^ |
+---------+----------------------+------------|------+
| | |
| UpdateTodo | AddTodo | Invoke AddTodo
| RemoveTodo | | Callback, sending
| MarkAllComplete | | Data up to the
| ClearComplete | | parent.
| | |
+------------|---------------+ +---|------------+-+
| v | | v |
| Main Tabs Screen | | Add Todo Screen |
| (Stateful) | | (Stateless) |
| | | |
| Manages current tab and | | |
| Visibility filter | | |
| | | |
| +----------+ +----------+ | | |
| | | | | | | |
| | List | | | | | |
| | of | | Stats | | | |
| | Todos | | Tab | | | |
| | Tab | | | | | |
| +----+-----+ +----------+ | | |
| | | | |
+------|---------------------+ +------------------+
|
| UpdateTodo
| RemoveTodo
|
+------|---------+
| v |
| |
| Todo Details |
| Screen |
| |
+------+---------+
|
| UpdateTodo
|
+------|---------+
| v |
| |
| Edit Todo |
| Screen |
| |
+----------------+There are a few Strategies for testing:
1. Store state and the state mutations (methods) within a StatefulWidget2. Extract this State and logic out into a "Plain Old Dart Objects" (PODOs) and test those. The StatefulWidget can then delegate to this object.
While Option #1 works because Flutter provides a nice set of Widget testing utilities out of the box, Option #2 will generally prove easier to test because it has no Flutter dependencies and does not require we to test against a Widget tree, but simply against an Object.
夜雨聆风