Commit ac41a093 authored by Brian Egan's avatar Brian Egan

Diagrams and bears oh my

parent 0c4d5783
......@@ -5,88 +5,75 @@ The vanilla example uses only the core Widgets and Classes that Flutter provides
## Key Concepts
* 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.
* Updating State - To update state, pass a function from the Parent `StatefulWidget` to the child widget.
* Updating State with callbacks - To update state, pass a callback function from the Parent `StatefulWidget` to the child widgets.
* Local persistence - The list of todos is serialized to JSON and stored as a file on disk whenever the State is updated.
## Lifting State
## Share State by Lifting State Up
Let's start with a Simple Example. Say our app had only 1 Tab: The `List of Todos Tab`.
```
+------------+
| |
| |
| |
| List |
| of |
| Todos |
| Tab |
| |
| |
+------------+
```
Now, we add a sibling Widget: The `Stats Tab`! But wait, it needs access to the List of Todos so it can calculate how many of them are active and how many are complete. So how do we share that data?
It can be difficult for siblings to pass their state to each other. For example, what if you call `setState` in the `List of Todos Tab` to add a Todo: How would Flutter know it also needs to re-build the `Stats Tab` to reflect the latest count?
It can be difficult for siblings to pass their state to each other. For example, say both Widges were displayed side-by-side at the same time: How would 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. | |
| Tab | No. Mine. | |
| + --------------> | |
| | | |
| | | |
+-------------+ +-------------+
```
So how do we share state between these two Widgets? Let's Lift the state up to a Parent Widget and pass it down to each child that needs it!
So how do we share state between these two sibling Widgets? Let's 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 |
| Tab | | |
| | | |
| | | |
| | | |
| | | |
+-------------+ +-------------+
```
Now, 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 you need to share State between Widgets or Routes, pull it up to a parent Widget. Here's a diagram of what our app actually looks like!
Now, 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 you 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) |
| |
| Contains List<Todo> and other State |
| Manages List<Todo> and "isLoading" |
| |
+------------------------------------------+
+---------+---------------------------+----+
| |
+------------|---------------+ +--------|--------+
| v | | v |
| Main Tabs Screen | | Add Todo Screen |
| (Stateful) | | (Stateless) |
| | | |
| Manages current tab and | | |
| Visibility filter | | |
| | | |
| Main Tabs Screen | | Add Todo Screen |
| +----------+ +----------+ | | |
| | | | | | | |
| | List | | | | | |
......@@ -105,7 +92,6 @@ Now, when we change the List of Todos in the `Keeper of the Todos` widget, both
| |
+------+---------+
|
|
+------|---------+
| v |
| |
......@@ -113,5 +99,80 @@ Now, when we change the List of Todos in the `Keeper of the Todos` widget, both
| Screen |
| |
+----------------+
```
Careful Observers might note: 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 it's own, for example, because this state isn't relevant to other Widgets!
## Updating State with callbacks
Ok, so now we have an idea of how to pass data down from parent to child, but how do we update the list of Todos from these different screens / Routes / Widgets?
We'll also **pass callback functions** from the parent to the children as well. These callback functions are responsible for updating the State at the Parent Widget and calling `setState` so Flutter knows it needs to rebuild!
In this app, we have a few callbacks we will pass down:
1. AddTodo callback
2. RemoveTodo Callback
3. UpdateTodo Callback
4. MarkAllComplete callback
5. ClearComplete callback
In this app, we pass the `AddTodo` callback from our `VanillaApp` 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 `VanillaApp` so it can handle how it adds the new todo to the list!
This demonstrates a core concept of State management in Flutter: Pass data and callback functions down from a parent to a child, and use invoke those callbacks in the Child to send data back up to the parent.
To make this concrete, Let's see how our callbacks flow in the this app.
```
+----------------------------------------------------+
| |
| 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 |
| |
+----------------+
```
\ No newline at end of file
......@@ -49,9 +49,7 @@ class VanillaAppState extends State<VanillaApp> {
FlutterMvcRoutes.home: (context) {
return new TabsScreen(
appState: appState,
updateFiler: updateFilter,
updateTodo: updateTodo,
updateTab: updateTab,
addTodo: addTodo,
removeTodo: removeTodo,
toggleAll: toggleAll,
......@@ -80,18 +78,6 @@ class VanillaAppState extends State<VanillaApp> {
});
}
void updateTab(AppTab tab) {
setState(() {
appState.activeTab = tab;
});
}
void updateFilter(VisibilityFilter filter) {
setState(() {
appState.activeFilter = filter;
});
}
void addTodo(Todo todo) {
setState(() {
appState.todos.add(todo);
......
import 'package:flutter_mvc/flutter_mvc.dart';
class AppState {
VisibilityFilter activeFilter;
AppTab activeTab;
bool isLoading;
List<Todo> todos;
AppState({
this.activeFilter = VisibilityFilter.all,
this.activeTab = AppTab.todos,
this.isLoading = false,
this.todos = const [],
});
......@@ -17,7 +13,8 @@ class AppState {
bool get allComplete => todos.every((todo) => todo.complete);
List<Todo> get filteredTodos => todos.where((todo) {
List<Todo> filteredTodos(VisibilityFilter activeFilter) =>
todos.where((todo) {
if (activeFilter == VisibilityFilter.all) {
return true;
} else if (activeFilter == VisibilityFilter.active) {
......@@ -30,11 +27,7 @@ class AppState {
bool get hasCompletedTodos => todos.any((todo) => todo.complete);
@override
int get hashCode =>
todos.hashCode ^
activeFilter.hashCode ^
isLoading.hashCode ^
activeTab.hashCode;
int get hashCode => todos.hashCode ^ isLoading.hashCode;
int get numActive =>
todos.fold(0, (sum, todo) => !todo.complete ? ++sum : sum);
......@@ -48,9 +41,7 @@ class AppState {
other is AppState &&
runtimeType == other.runtimeType &&
todos == other.todos &&
activeFilter == other.activeFilter &&
isLoading == other.isLoading &&
activeTab == other.activeTab;
isLoading == other.isLoading;
void clearCompleted() {
todos.removeWhere((todo) => todo.complete);
......@@ -65,27 +56,21 @@ class AppState {
Map<String, Object> toJson() {
return {
"todos": todos.map((todo) => todo.toJson()).toList(),
"activeFilter": activeFilter.toString(),
"activeTab": activeTab.toString(),
};
}
@override
String toString() {
return 'AppState{todos: $todos, activeFilter: $activeFilter, isLoading: $isLoading, activeTab: $activeTab}';
return 'AppState{todos: $todos, isLoading: $isLoading}';
}
static AppState fromJson(Map<String, Object> json) {
final todos = (json["todos"] as List<Map<String, Object>>)
.map((todoJson) => Todo.fromJson(todoJson))
.toList();
final activeFilter = getFilterFromString(json["activeFilter"] as String);
final activeTab = getTabFromString(json["activeTab"] as String);
return new AppState(
todos: todos,
activeFilter: activeFilter,
activeTab: activeTab,
);
}
......
......@@ -9,10 +9,8 @@ import 'package:vanilla/widgets/stats_counter.dart';
import 'package:vanilla/widgets/todo_list.dart';
import 'package:vanilla/widgets/typedefs.dart';
class TabsScreen extends StatelessWidget {
class TabsScreen extends StatefulWidget {
final AppState appState;
final TabUpdater updateTab;
final VisibilityFilterUpdater updateFiler;
final TodoAdder addTodo;
final TodoRemover removeTodo;
final TodoUpdater updateTodo;
......@@ -21,8 +19,6 @@ class TabsScreen extends StatelessWidget {
TabsScreen({
@required this.appState,
@required this.updateTab,
@required this.updateFiler,
@required this.addTodo,
@required this.removeTodo,
@required this.updateTodo,
......@@ -32,6 +28,28 @@ class TabsScreen extends StatelessWidget {
})
: super(key: key);
@override
State<StatefulWidget> createState() {
return new TabsScreenState();
}
}
class TabsScreenState extends State<TabsScreen> {
VisibilityFilter activeFilter = VisibilityFilter.all;
AppTab activeTab = AppTab.todos;
_updateVisibility(VisibilityFilter filter) {
setState(() {
activeFilter = filter;
});
}
_updateTab(AppTab tab) {
setState(() {
activeTab = tab;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
......@@ -39,34 +57,34 @@ class TabsScreen extends StatelessWidget {
title: new Text(VanillaLocalizations.of(context).appTitle),
actions: [
new FilterButton(
isActive: appState.activeTab == AppTab.todos,
activeFilter: appState.activeFilter,
onSelected: updateFiler,
isActive: activeTab == AppTab.todos,
activeFilter: activeFilter,
onSelected: _updateVisibility,
),
new ExtraActionsButton(
allComplete: appState.allComplete,
hasCompletedTodos: appState.hasCompletedTodos,
allComplete: widget.appState.allComplete,
hasCompletedTodos: widget.appState.hasCompletedTodos,
onSelected: (action) {
if (action == ExtraAction.toggleAllComplete) {
toggleAll();
widget.toggleAll();
} else if (action == ExtraAction.clearCompleted) {
clearCompleted();
widget.clearCompleted();
}
},
)
],
),
body: appState.activeTab == AppTab.todos
body: activeTab == AppTab.todos
? new TodoList(
filteredTodos: appState.filteredTodos,
loading: appState.isLoading,
removeTodo: removeTodo,
addTodo: addTodo,
updateTodo: updateTodo,
filteredTodos: widget.appState.filteredTodos(activeFilter),
loading: widget.appState.isLoading,
removeTodo: widget.removeTodo,
addTodo: widget.addTodo,
updateTodo: widget.updateTodo,
)
: new StatsCounter(
numActive: appState.numActive,
numCompleted: appState.numCompleted,
numActive: widget.appState.numActive,
numCompleted: widget.appState.numCompleted,
),
floatingActionButton: new FloatingActionButton(
key: FlutterMvcKeys.addTodoFab,
......@@ -78,9 +96,9 @@ class TabsScreen extends StatelessWidget {
),
bottomNavigationBar: new BottomNavigationBar(
key: FlutterMvcKeys.tabs,
currentIndex: AppTab.values.indexOf(appState.activeTab),
currentIndex: AppTab.values.indexOf(activeTab),
onTap: (index) {
updateTab(AppTab.values[index]);
_updateTab(AppTab.values[index]);
},
items: AppTab.values.map((tab) {
return new BottomNavigationBarItem(
......
......@@ -11,7 +11,3 @@ typedef TodoUpdater(
String note,
String task,
});
typedef TabUpdater(AppTab tab);
typedef VisibilityFilterUpdater(VisibilityFilter filter);
\ No newline at end of file
......@@ -41,10 +41,9 @@ main() {
];
final state = new AppState(
todos: todos,
activeFilter: VisibilityFilter.all,
);
expect(state.filteredTodos, todos);
expect(state.filteredTodos(VisibilityFilter.all), todos);
});
test('should return active todos if the VisibilityFilter is active', () {
......@@ -58,10 +57,9 @@ main() {
];
final state = new AppState(
todos: todos,
activeFilter: VisibilityFilter.active,
);
expect(state.filteredTodos, [
expect(state.filteredTodos(VisibilityFilter.active), [
todo1,
todo2,
]);
......@@ -79,10 +77,9 @@ main() {
];
final state = new AppState(
todos: todos,
activeFilter: VisibilityFilter.completed,
);
expect(state.filteredTodos, [todo3]);
expect(state.filteredTodos(VisibilityFilter.completed), [todo3]);
});
test('should clear the completed todos', () {
......@@ -96,7 +93,6 @@ main() {
];
final state = new AppState(
todos: todos,
activeFilter: VisibilityFilter.completed,
);
state.clearCompleted();
......@@ -118,7 +114,6 @@ main() {
];
final state = new AppState(
todos: todos,
activeFilter: VisibilityFilter.completed,
);
// Toggle all complete
......@@ -141,7 +136,6 @@ main() {
];
final state = new AppState(
todos: todos,
activeFilter: VisibilityFilter.completed,
);
state.removeTodo(todo3);
......@@ -158,7 +152,6 @@ main() {
];
final state = new AppState(
todos: todos,
activeFilter: VisibilityFilter.completed,
);
state.addTodo(todo3);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment