Commit 0c4d5783 authored by Brian Egan's avatar Brian Egan

A bit more work on the Vanilla app / start of explanation doc

parent 9820b154
# vanilla
# vanilla example
A new Flutter project.
The vanilla example uses only the core Widgets and Classes that Flutter provides out of the box to manage state. The most important of these: the `StatefulWidget`.
## Getting Started
## Key Concepts
For help getting started with Flutter, view our online
[documentation](http://flutter.io/).
* 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.
* Local persistence - The list of todos is serialized to JSON and stored as a file on disk whenever the State is updated.
## Lifting State
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?
```
+-------------+ +-------------+
| | | |
| | | |
| | | |
| | Gimme dem Todos | |
| List of | <-------------- + Stats |
| Todos | | Tab |
| 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!
```
+-------------------------+
| |
| 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!
```
+------------------------------------------+
| |
| VanillaApp (StatefulWidget) |
| |
| Contains List<Todo> and other State |
| |
+------------------------------------------+
| |
+------------|---------------+ +--------|--------+
| v | | v |
| | | |
| Main Tabs Screen | | Add Todo Screen |
| +----------+ +----------+ | | |
| | | | | | | |
| | List | | | | | |
| | of | | Stats | | | |
| | Todos | | Tab | | | |
| | Tab | | | | | |
| +----+-----+ +----------+ | | |
| | | | |
+------|---------------------+ +-----------------+
|
+------|---------+
| v |
| |
| Todo Details |
| Screen |
| |
+------+---------+
|
|
+------|---------+
| v |
| |
| Edit Todo |
| Screen |
| |
+----------------+
```
\ No newline at end of file
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mvc/flutter_mvc.dart';
import 'package:vanilla/data/todos_service.dart';
......@@ -9,7 +10,7 @@ import 'package:vanilla/localization.dart';
class VanillaApp extends StatefulWidget {
final TodosService service;
VanillaApp({this.service});
VanillaApp({@required this.service});
@override
State<StatefulWidget> createState() {
......
......@@ -4,19 +4,11 @@ import 'dart:convert';
import 'package:path_provider/path_provider.dart';
import 'package:vanilla/models.dart';
/// Create an abstract class that we can use for both the concrete
/// implementation below and a Mock implementation for testing.
abstract class FileStorage {
Future<List<Todo>> loadTodos();
Future<File> saveTodos(List<Todo> todos);
Future<FileSystemEntity> clean();
}
/// Loads and saves a List of Todos using a text file stored on the device.
class VanillaFileStorage implements FileStorage {
class FileStorage {
final String tag;
VanillaFileStorage(this.tag);
FileStorage(this.tag);
Future<List<Todo>> loadTodos() async {
final file = await _getLocalFile();
......
import 'dart:async';
import 'dart:core';
import 'package:flutter/foundation.dart';
import 'package:vanilla/data/file_storage.dart';
import 'package:vanilla/data/web_service.dart';
import 'package:vanilla/models.dart';
/// A class that glues together our
/// A class that glues together our local file storage and a remote web service.
class TodosService {
final FileStorage fileStorage;
final WebService webService;
TodosService({this.fileStorage, this.webService});
TodosService({@required this.fileStorage, @required this.webService});
/// Loads todos first from File storage. If they don't exist or encounter an
/// error, it attempts to load the Todos from a Web Service.
Future<List<Todo>> loadTodos() async {
try {
return (await fileStorage.loadTodos());
return await fileStorage.loadTodos();
} catch (e) {
return webService.fetchTodos();
}
}
/// Attempts to persist the given todos to File Storage and the Web service.
/// Returns true if everything worked, false if not.
Future<bool> saveTodos(List<Todo> todos) async {
try {
// Run the operations in parallel.
await Future.wait([
fileStorage.saveTodos(todos),
webService.postTodos(todos),
]);
return true;
} catch (e) {
return false;
}
// Persists todos to local disk and the web
Future saveTodos(List<Todo> todos) {
return Future.wait([
fileStorage.saveTodos(todos),
webService.postTodos(todos),
]);
}
}
......@@ -4,21 +4,24 @@ import 'package:vanilla/models.dart';
/// A class that is meant to represent a Web Service you would call to fetch
/// and persist Todos to and from the cloud.
///
/// Since we're trying to keep this example simple, it is a Mock implementation.
/// Since we're trying to keep this example simple, it doesn't communicate with
/// a real server but simply emulates the functionality.
class WebService {
WebService();
final Duration delay;
WebService([this.delay = const Duration(milliseconds: 1200)]);
/// Mock that "fetches" some Todos from a "web service" after a short delay
Future<List<Todo>> fetchTodos() async {
return new Future.delayed(
new Duration(milliseconds: 1200),
delay,
() => [
new Todo('Hey', note: 'Ho, let\'s go!'),
new Todo('Wonderwall', complete: true),
new Todo('Everything', note: 'in its right place'),
new Todo('Cheeseburger in Paradise'),
new Todo('If you like', note: 'Pina Coladas', complete: true),
]);
new Todo('Feed da kitty', note: 'With the chickeny bits!'),
new Todo('Find a Red Sea dive trip', note: 'Echo vs MY Dream'),
new Todo('Book flights to Egypt', complete: true),
new Todo('Decide on accommodation'),
new Todo('If you like', note: 'Piña coladas', complete: true),
]);
}
/// Mock that returns true or false for success or failure. In this case,
......
......@@ -8,7 +8,7 @@ void main() {
runApp(
new VanillaApp(
service: new TodosService(
fileStorage: new VanillaFileStorage("vanilla"),
fileStorage: new FileStorage("vanilla"),
webService: new WebService(),
),
),
......
......@@ -23,7 +23,7 @@ class AddEditScreen extends StatelessWidget {
@required this.updateTodo,
this.todo,
})
: super(key: key);
: super(key: key ?? FlutterMvcKeys.addTodoScreen);
@override
Widget build(BuildContext context) {
......
......@@ -69,6 +69,7 @@ class TabsScreen extends StatelessWidget {
numCompleted: appState.numCompleted,
),
floatingActionButton: new FloatingActionButton(
key: FlutterMvcKeys.addTodoFab,
onPressed: () {
Navigator.pushNamed(context, FlutterMvcRoutes.addTodo);
},
......@@ -76,6 +77,7 @@ class TabsScreen extends StatelessWidget {
tooltip: ArchitectureLocalizations.of(context).addTodo,
),
bottomNavigationBar: new BottomNavigationBar(
key: FlutterMvcKeys.tabs,
currentIndex: AppTab.values.indexOf(appState.activeTab),
onTap: (index) {
updateTab(AppTab.values[index]);
......
......@@ -7,12 +7,11 @@ class FilterButton extends StatelessWidget {
final VisibilityFilter activeFilter;
final bool isActive;
FilterButton({this.onSelected, this.activeFilter, this.isActive, Key key}) : super(key: key);
FilterButton({this.onSelected, this.activeFilter, this.isActive, Key key})
: super(key: key);
@override
Widget build(BuildContext context) {
if (!isActive) return new Container();
final defaultStyle = Theme.of(context).textTheme.body1;
final activeStyle = Theme
.of(context)
......@@ -20,39 +19,44 @@ class FilterButton extends StatelessWidget {
.body1
.copyWith(color: Theme.of(context).accentColor);
return new PopupMenuButton<VisibilityFilter>(
tooltip: ArchitectureLocalizations.of(context).filterTodos,
onSelected: onSelected,
itemBuilder: (BuildContext context) => <PopupMenuItem<VisibilityFilter>>[
new PopupMenuItem<VisibilityFilter>(
value: VisibilityFilter.all,
child: new Text(
ArchitectureLocalizations.of(context).showAll,
style: activeFilter == VisibilityFilter.all
? activeStyle
: defaultStyle,
return new AnimatedOpacity(
opacity: isActive ? 1.0 : 0.0,
duration: new Duration(milliseconds: 150),
child: new PopupMenuButton<VisibilityFilter>(
tooltip: ArchitectureLocalizations.of(context).filterTodos,
onSelected: onSelected,
itemBuilder: (BuildContext context) =>
<PopupMenuItem<VisibilityFilter>>[
new PopupMenuItem<VisibilityFilter>(
value: VisibilityFilter.all,
child: new Text(
ArchitectureLocalizations.of(context).showAll,
style: activeFilter == VisibilityFilter.all
? activeStyle
: defaultStyle,
),
),
),
new PopupMenuItem<VisibilityFilter>(
value: VisibilityFilter.active,
child: new Text(
ArchitectureLocalizations.of(context).showActive,
style: activeFilter == VisibilityFilter.active
? activeStyle
: defaultStyle,
new PopupMenuItem<VisibilityFilter>(
value: VisibilityFilter.active,
child: new Text(
ArchitectureLocalizations.of(context).showActive,
style: activeFilter == VisibilityFilter.active
? activeStyle
: defaultStyle,
),
),
),
new PopupMenuItem<VisibilityFilter>(
value: VisibilityFilter.completed,
child: new Text(
ArchitectureLocalizations.of(context).showCompleted,
style: activeFilter == VisibilityFilter.completed
? activeStyle
: defaultStyle,
new PopupMenuItem<VisibilityFilter>(
value: VisibilityFilter.completed,
child: new Text(
ArchitectureLocalizations.of(context).showCompleted,
style: activeFilter == VisibilityFilter.completed
? activeStyle
: defaultStyle,
),
),
),
],
icon: new Icon(Icons.filter_list),
],
icon: new Icon(Icons.filter_list),
),
);
}
}
......@@ -2,7 +2,6 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mvc/flutter_mvc.dart';
import 'package:vanilla/models.dart';
class StatsCounter extends StatelessWidget {
final int numActive;
......
......@@ -25,8 +25,12 @@ class TodoList extends StatelessWidget {
Widget build(BuildContext context) {
return new Container(
child: loading
? new Center(child: new CircularProgressIndicator())
? new Center(
child: new CircularProgressIndicator(
key: FlutterMvcKeys.loading,
))
: new ListView.builder(
key: FlutterMvcKeys.todoList,
itemCount: filteredTodos.length,
itemBuilder: (BuildContext context, int index) {
final todo = filteredTodos[index];
......
......@@ -8,9 +8,12 @@ dependencies:
path: ../../
dev_dependencies:
mockito: 2.2.0
test: ^0.12.0
flutter_test:
sdk: flutter
flutter_driver:
sdk: flutter
# For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec
......
import 'package:vanilla/data/file_storage.dart';
import 'package:vanilla/data/todos_service.dart';
import 'package:vanilla/data/web_service.dart';
import 'package:vanilla/models.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
/// We create two Mocks for our Web Service and File Storage. We will use these
/// mock classes to verify the behavior of the TodosService.
class MockFileStorage extends Mock implements FileStorage {}
class MockWebService extends Mock implements WebService {}
main() {
group('TodosService', () {
test(
'should load todos from File Storage if they exist without calling the web service',
() {
final fileStorage = new MockFileStorage();
final webService = new MockWebService();
final todosService = new TodosService(
fileStorage: fileStorage,
webService: webService,
);
final todos = [new Todo("Task")];
// We'll use our mock throughout the tests to set certain conditions. In
// this first test, we want to mock out our file storage to return a
// list of Todos that we define here in our test!
when(fileStorage.loadTodos()).thenReturn(new Future.value(todos));
expect(todosService.loadTodos(), completion(todos));
verifyNever(webService.fetchTodos());
});
test(
'should fetch todos from the Web Service if the file storage throws a synchronous error',
() async {
final fileStorage = new MockFileStorage();
final webService = new MockWebService();
final todosService = new TodosService(
fileStorage: fileStorage,
webService: webService,
);
final todos = [new Todo("Task")];
// In this instance, we'll ask our Mock to throw an error. When it does,
// we expect the web service to be called instead.
when(fileStorage.loadTodos()).thenReturn("");
when(webService.fetchTodos()).thenReturn(new Future.value(todos));
// We check that the correct todos were returned, and that the
// webService.fetchTodos method was in fact called!
expect(await todosService.loadTodos(), todos);
verify(webService.fetchTodos());
});
test(
'should fetch todos from the Web Service if the File storage returns an async error',
() async {
final fileStorage = new MockFileStorage();
final webService = new MockWebService();
final todosService = new TodosService(
fileStorage: fileStorage,
webService: webService,
);
final todos = [new Todo("Task")];
when(fileStorage.loadTodos())
.thenAnswer((_) => new Future.error("Oh no"));
when(webService.fetchTodos()).thenReturn(new Future.value(todos));
expect(await todosService.loadTodos(), todos);
verify(webService.fetchTodos());
});
test('should persist the todos to local disk and the web service', () {
final fileStorage = new MockFileStorage();
final webService = new MockWebService();
final todosService = new TodosService(
fileStorage: fileStorage,
webService: webService,
);
final todos = [new Todo("Task")];
when(fileStorage.saveTodos(todos)).thenReturn(new Future.value("Cool"));
when(webService.postTodos(todos)).thenReturn(new Future.value("Beans."));
// In this case, we just want to verify we're correctly persisting to all
// the storage mechanisms we care about.
expect(todosService.saveTodos(todos), completes);
verify(fileStorage.saveTodos(todos));
verify(webService.postTodos(todos));
});
});
}
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
<orderEntry type="library" name="Flutter Plugins" level="project" />
</component>
</module>
\ No newline at end of file
......@@ -13,8 +13,7 @@
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$/android">
<sourceFolder url="file://$MODULE_DIR$/android/app/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/android/gen" isTestSource="false" generated="true" />
......
import 'package:flutter/widgets.dart';
class FlutterMvcKeys {
static final inputKey = new UniqueKey();
static final markAllDoneKey = new UniqueKey();
static final itemsLeftKey = new UniqueKey();
static final allKey = new UniqueKey();
static final activeKey = new UniqueKey();
static final completedKey = new UniqueKey();
static final loading = new UniqueKey();
static final input = new UniqueKey();
static final clearCompleted = new UniqueKey();
static final markAllDone = new UniqueKey();
static final statsActiveItems = new UniqueKey();
static final statsCompletedItems = new UniqueKey();
static final allFilter = new UniqueKey();
static final activeFilter = new UniqueKey();
static final completedFilter = new UniqueKey();
static final addTodoScreen = new UniqueKey();
static final addTodoFab = new UniqueKey();
static final editTodoFab = new UniqueKey();
static final saveTodoFab = new UniqueKey();
static final todoList = new UniqueKey();
static final tabs = new UniqueKey();
}
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