Commit 1705ad57 authored by Brian Egan's avatar Brian Egan

Add README to Redux and built_redux

parent 50573fda
......@@ -6,7 +6,7 @@ Contributions are very welcome in many forms :)
If you find a problem with any of the examples, please [file an issue on Gitlab](https://gitlab.com/brianegan/flutter_architecture_samples/issues).
## Merge Requests
## Merge Requests / New Apps
If you would like to fix a bug or make an improvement, please [file a merge request](https://gitlab.com/brianegan/flutter_architecture_samples/merge_requests) explaining your changes!
......
......@@ -2,6 +2,8 @@
<img align="right" src="assets/todo-list.png" alt="List of Todos Screen">
[TodoMVC](http://todomvc.com) for Flutter!
Flutter provides a lot of flexibility in deciding how to organize and architect an your apps. While this freedom is very valuable, it can also lead to apps with large classes, inconsistent naming schemes, as well as mismatching or missing architectures. These types of issues can make testing, maintaining and extending your apps difficult.
The Flutter Architecture Samples project demonstrates strategies to help solve or avoid these common problems. This project implements the same app using different architectural concepts and tools.
......@@ -11,6 +13,8 @@ You can use the samples in this project as a learning reference, or as a startin
### Current Samples
* [Vanilla Example](https://gitlab.com/brianegan/flutter_architecture_samples/tree/master/example/vanilla) - Uses the tools Flutter provides out of the box to manage app state.
* [Redux Example](https://gitlab.com/brianegan/flutter_architecture_samples/tree/master/example/redux) - Uses the [Redux](https://pub.dartlang.org/packages/redux) library to manage app state and update Widgets
* [built_redux Example](https://gitlab.com/brianegan/flutter_architecture_samples/tree/master/example/built_redux) - Uses the [built_redux](https://pub.dartlang.org/packages/redux) library to manage app state and update Widgets
### Why a to-do app?
......
# built_redux
An example Todo app created with built_redux
An example Todo app created with [built_value](https://pub.dartlang.org/packages/built_value), [built_redux](https://pub.dartlang.org/packages/built_redux), and [flutter_built_redux](https://pub.dartlang.org/packages/flutter_built_redux).
## Getting Started
## Key Concepts
For help getting started with Flutter, view our online
[documentation](http://flutter.io/).
* Most of the Key Concepts from the [Redux Example](https://gitlab.com/brianegan/flutter_architecture_samples/tree/master/example/redux) apply to this example as well, but the implementations are slightly different.
* To enforce immutability, `built_redux` apps require you to use a `built_value` Value Object.
* To increase discoverability, all actions are created using `built_redux` and attached to the `Store`.
* To use `built_value` and `built_redux`, you must add a `build` file / step to your project.
* To help with Type Safety, Reducers and Middleware can be created with `ReducerBuilder` and `MiddlewareBuilder` classes.
## Enforcing Immutability
The `State` objects in your app need to be created with `built_value`. `built_value` is a library that generate "Value Classes" from a Class template that you write.
The Value classes can not be directly modified, but instead must be updated by creating a new version of the object.
## Actions Discoverability
One benefit of `built_redux` is that it attaches all possible actions to your store. This makes it very easy to see which actions are available for dispatch within your IDE using autocompletion.
## Build Script
In order to use `built_redux` and `built_value`, you need to include a `tool/build.dart` file in your project. Whenever you update your Value Classes or Redux Actions you'll need to run the build script. You can also create a `tool/watch.dart` script that will rebuild every time you make updates, and this tends to be much faster overall.
Examples of the `build` and `watch` scripts can be found within the `tool` folder of this example.
## Type Safety in Reducers and Middleware
As your app grows, you'll want to break reducers and middleware down into smaller functions.
## Differences to Redux
These two libraries are incredibly similar. These are the minor differences:
* Actions
- `built_redux` - Actions are generated for you by `built_redux` based on a definition. They are then attached to the Store upon creation.
- `Redux`, Actions are plain ol' Dart values, Classes or Enums.
* Reducers
- `built_redux` - Reducers are functions that mutate a `StateBuilder` and return it. The `StateBuilder` is then built after all reducers have run. Enforces immutability.
- `redux` - Reducers are functions in app state and latest action and return a new app state. Since immutability is not enforced, a user could simply mutate the state object instead of returning an updated copy.
- Both - Testing is easy, and both libraries have utilities for binding Reducers to Actions of a specific type.
* Middleware
- Very little difference here. Both libraries have utilities for binding actions of a specific type to a given Middleware.
* Nesting Large State Trees
- `built_redux` - Provides helpers for composing large Action trees that you can attach to your Store upon creation. Reducers can be combined via functional composition and by using utilities from the library.
- `redux` - No need for nesting actions, nesting reducers can be done via functional composition and by using utilities from the library.
- Both - allow you to break down your app into smaller units.
* Flutter integration
- `built_redux` - Maps from a `State` to `Prop`, which is passed to your `build` method along with your `Actions`. You combine the `Prop` wih the actions in the `build` method.
- `redux` - Maps from a `Store` to a `ViewModel`. The `ViewModel` should include both "Props" and callback functions that dispatch actions.
- Both - Store a Widget at the top of your tree containing your State.
import 'package:built_redux_sample/models/models.dart';
import 'package:built_redux_sample/actions/actions.dart';
import 'package:built_redux_sample/widgets/extra_actions_button.dart';
import 'package:built_redux_sample/presentation/extra_actions_button.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_built_redux/flutter_built_redux.dart';
import 'package:built_redux_sample/selectors/selectors.dart';
......
import 'package:built_redux_sample/models/models.dart';
import 'package:built_redux_sample/actions/actions.dart';
import 'package:built_redux_sample/widgets/add_edit_screen.dart';
import 'package:built_redux_sample/presentation/add_edit_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_built_redux/flutter_built_redux.dart';
......
import 'package:built_redux_sample/models/models.dart';
import 'package:built_redux_sample/actions/actions.dart';
import 'package:built_redux_sample/widgets/add_edit_screen.dart';
import 'package:built_redux_sample/presentation/add_edit_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_built_redux/flutter_built_redux.dart';
......
import 'package:built_redux_sample/models/models.dart';
import 'package:built_redux_sample/actions/actions.dart';
import 'package:built_redux_sample/selectors/selectors.dart';
import 'package:built_redux_sample/widgets/todo_list.dart';
import 'package:built_redux_sample/presentation/todo_list.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_built_redux/flutter_built_redux.dart';
......
......@@ -3,7 +3,7 @@ library stats;
import 'package:built_redux_sample/models/models.dart';
import 'package:built_redux_sample/actions/actions.dart';
import 'package:built_redux_sample/selectors/selectors.dart';
import 'package:built_redux_sample/widgets/stats_counter.dart';
import 'package:built_redux_sample/presentation/stats_counter.dart';
import 'package:built_value/built_value.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_built_redux/flutter_built_redux.dart';
......
import 'package:built_redux_sample/models/models.dart';
import 'package:built_redux_sample/actions/actions.dart';
import 'package:built_redux_sample/widgets/details_screen.dart';
import 'package:built_redux_sample/presentation/details_screen.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_built_redux/flutter_built_redux.dart';
......
......@@ -7,7 +7,7 @@ import 'package:built_redux_sample/localization.dart';
import 'package:built_redux_sample/actions/actions.dart';
import 'package:built_redux_sample/reducers/reducers.dart';
import 'package:built_redux_sample/middleware/store_todos_middleware.dart';
import 'package:built_redux_sample/widgets/home_screen.dart';
import 'package:built_redux_sample/presentation/home_screen.dart';
import 'package:flutter_built_redux/flutter_built_redux.dart';
import 'package:flutter/material.dart';
import 'package:flutter_architecture_samples/flutter_architecture_samples.dart';
......
......@@ -6,7 +6,7 @@ import 'package:built_redux_sample/containers/stats.dart';
import 'package:built_redux_sample/containers/tab_selector.dart';
import 'package:built_redux_sample/models/models.dart';
import 'package:built_redux_sample/localization.dart';
import 'package:built_redux_sample/widgets/filter_button.dart';
import 'package:built_redux_sample/presentation/filter_button.dart';
import 'package:flutter/material.dart';
import 'package:flutter_architecture_samples/flutter_architecture_samples.dart';
......
import 'package:built_redux_sample/containers/app_loading.dart';
import 'package:built_redux_sample/containers/todo_details.dart';
import 'package:built_redux_sample/models/models.dart';
import 'package:built_redux_sample/widgets/todo_item.dart';
import 'package:built_redux_sample/presentation/todo_item.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_architecture_samples/flutter_architecture_samples.dart';
......
# redux
# redux sample
A new Flutter project.
This sample makes use of the [Redux](https://pub.dartlang.org/packages/redux) and [flutter_redux](https://pub.dartlang.org/packages/flutter_redux) libraries to manage state.
## Getting Started
## Key Concepts
For help getting started with Flutter, view our online
[documentation](http://flutter.io/).
* App State is an immutable Object that lives at the top of your Widget hierarchy within a `Store`.
* The `Store` is passed down to all ancestors via an `InheritedWidget` called a `StoreProvider`
* The `State` object is immutable. To update the `State`, you must dispatch an `Action`.
* The `Action` will be picked up by a `Reducer`, which is a function that builds and returns a new `State` based on the previous `State` and the `Action` that was dispatched.
* Reducers are pure functions.
* When the app State is updated, all Widgets connected to the `Store` using `StoreConnector` will be rebuilt.
* The Widgets that use `StoreConnectors` are called `container` Widgets. They are only responsible from converting the latest App State to a `ViewModel`.
* The Widgets that display data are called `presentation` widgets. Think of a `Text` widget or `FloatingActionButton`.
* To read data from the `State`, use `selector` functions. These act like queries against your "State Database".
* To handle fetching data from our Database or Web Service, we use a `Middleware`.
## App State Singleton
In Redux, the idea is to store your Application State in a root level singleton.
If you read through the Vanilla example, you'll see this takes advantage of a core principle: Lifting State up. In this case, we're Lifting our App State all the way to the top of our App so that all descendants have access to it.
To accomplish this, you create a Redux `Store` and hand it to a `StoreProvider`. All descendants of the `StoreProvider` can access the store using a `StoreProvider.of(context).store` or by using ` StoreConnector` Widget.
## Updating App State
In order to update the App State in a Redux app, you must dispatch an `Action`.
This `Action` will then be intercepted by your `Reducer` function, which is responsible for updating the App State using the data contained within the `Action`.
`Reducer` functions are pure functions. They are only responsible for taking in the last state and the dispatched action, and returning a new App State.
This means `Reducer` functions should not make any API calls or have side effects such as logging. For this purpose, use `Middleware`.
While this may feel like "Boilerplate" when you first start using Redux, the motivation is thus (from the [original Redux Docs](http://redux.js.org/)):
> If a model can update another model, then a view can update a model, which updates another model, and this, in turn, might cause another view to update. At some point, you no longer understand what happens in your app as you have lost control over the when, why, and how of its state. When a system is opaque and non-deterministic, it's hard to reproduce bugs or add new features.
If you've felt this pain in your app, it might be a good time to consider Redux or another State management pattern.
Dispatching `Actions` and updating the App State in this rigorous way allows you to easily determine:
1. What Action caused a State change
2. What Reducer is responsible for handling that change
3. Why the State was broken in response to an Action.
4. When a View needs to update in response to a State change
## Updating UI
Whenever your the App State changes, in response to an `Action`, you most likely want to update your UI in some way.
To do so, connect to the `StoreProvider` using a `StoreConnector` Widget. The job of the `StoreConnector` widget is simple: Take the latest state of the store and convert it into a `ViewModel`. Then, build a Widget tree using this `ViewModel`.
Whenever the App State changes, the `StoreConnector` will rebuild the `ViewModel` and `Widget` tree.
In order to make it easier to test your Widgets and share functionality, it is recommended you have two types of Widgets:
* `container` Widgets -- These use `StoreConnector` Widgets to build up a `ViewModel` for your `presentation` Widgets.
* `presentation` Widgets -- `StatelessWidget`s that are given all the data they need are are responsible for building the UI.
This allows you to more easily test your `presentation` Widgets, because you only need to pass in the data they require in each test for rendering, and then write assertions against the rendered output. Think of them as the "pure functions" of our UI.
It also allows you to reuse `container` Widgets. For an example, please look at the `AppLoading` Widget.
## Selector Functions
`Selector` functions are simple functions that provide a single point of access to your App State. For a full explanation of why they are useful, please refer to the [reselect](https://pub.dartlang.org/packages/reselect) package.
## Fetching and Storing Data using Middleware
In order to fetch our Todos from the Web or a Database, we need to make an async call. Since `Reducer` functions are pure, we must instead use a `Middleware`.
`Middleware` are run in response to `Actions` that are dispatched, and execute before the `Reducer`. This allows you to intercept an `Action` and fetch data in response!
In this app, we have a "Store Todos Middleware". It responds to `LoadTodos` and `SaveTodos` type of actions by either fetching the todos or persisting them to a Database or Web Service.
## Testing
Generally, this app conforms the "Testing Pyramid": Lots of Unit and Widget tests, fewer integration tests, even fewer end to end tests.
* Unit tests
- `Reducer` functions are very easy to unit test since they are pure functions
- `Middleware` functions that call out to APIs can be tested using Mock implementations. This is done using the Mockito library.
- `selector` functions are also easy to test since they are pure.
- `presentation` Widgets can be unit tested by passing in test data and making assertions against the Widget rendered with that data.
* Integration Tests
- Test the `FileStorage` class to ensure it can save and load data from a local file on device.
- Database tests could also go here
* End to End Test
- Run the app and drive it using the `flutter_driver`.
- Use the "Page Object Model" pattern to make the tests easier to read and maintain.
......@@ -4,7 +4,7 @@ import 'package:redux_sample/actions/actions.dart';
import 'package:redux_sample/models/models.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:redux_sample/widgets/add_edit_screen.dart';
import 'package:redux_sample/presentation/add_edit_screen.dart';
class AddTodo extends StatelessWidget {
AddTodo({Key key}) : super(key: key);
......
......@@ -4,7 +4,7 @@ import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:redux_sample/actions/actions.dart';
import 'package:redux_sample/models/models.dart';
import 'package:redux_sample/widgets/add_edit_screen.dart';
import 'package:redux_sample/presentation/add_edit_screen.dart';
class EditTodo extends StatelessWidget {
final Todo todo;
......
......@@ -5,7 +5,7 @@ import 'package:redux/redux.dart';
import 'package:redux_sample/models/models.dart';
import 'package:redux_sample/selectors/selectors.dart';
import 'package:redux_sample/actions/actions.dart';
import 'package:redux_sample/widgets/extra_actions_button.dart';
import 'package:redux_sample/presentation/extra_actions_button.dart';
class ExtraActionsContainer extends StatelessWidget {
ExtraActionsContainer({Key key}) : super(key: key);
......
......@@ -4,7 +4,7 @@ import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:redux_sample/models/models.dart';
import 'package:redux_sample/actions/actions.dart';
import 'package:redux_sample/widgets/filter_button.dart';
import 'package:redux_sample/presentation/filter_button.dart';
class FilterSelectorViewModel {
final Function(VisibilityFilter) onFilterSelected;
......
......@@ -5,7 +5,7 @@ import 'package:redux/redux.dart';
import 'package:redux_sample/models/models.dart';
import 'package:redux_sample/actions/actions.dart';
import 'package:redux_sample/selectors/selectors.dart';
import 'package:redux_sample/widgets/todo_list.dart';
import 'package:redux_sample/presentation/todo_list.dart';
class FilteredTodosViewModel {
final List<Todo> todos;
......
......@@ -4,7 +4,7 @@ import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'package:redux_sample/models/models.dart';
import 'package:redux_sample/selectors/selectors.dart';
import 'package:redux_sample/widgets/stats_counter.dart';
import 'package:redux_sample/presentation/stats_counter.dart';
class StatsViewModel {
final int numCompleted;
......
......@@ -5,7 +5,7 @@ import 'package:redux/redux.dart';
import 'package:redux_sample/actions/actions.dart';
import 'package:redux_sample/models/models.dart';
import 'package:redux_sample/selectors/selectors.dart';
import 'package:redux_sample/widgets/details_screen.dart';
import 'package:redux_sample/presentation/details_screen.dart';
class TodoDetailsViewModel {
final Todo todo;
......
......@@ -8,7 +8,7 @@ import 'package:redux_sample/localization.dart';
import 'package:redux_sample/models/models.dart';
import 'package:redux_sample/reducers/app_state_reducer.dart';
import 'package:redux_sample/middleware/store_todos_middleware.dart';
import 'package:redux_sample/widgets/home_screen.dart';
import 'package:redux_sample/presentation/home_screen.dart';
void main() {
runApp(new ReduxApp());
......
......@@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_architecture_samples/flutter_architecture_samples.dart';
import 'package:redux_sample/containers/app_loading.dart';
import 'package:redux_sample/widgets/loading_indicator.dart';
import 'package:redux_sample/presentation/loading_indicator.dart';
class StatsCounter extends StatelessWidget {
final int numActive;
......
......@@ -4,8 +4,8 @@ import 'package:flutter_architecture_samples/flutter_architecture_samples.dart';
import 'package:redux_sample/containers/app_loading.dart';
import 'package:redux_sample/containers/todo_details.dart';
import 'package:redux_sample/models/models.dart';
import 'package:redux_sample/widgets/loading_indicator.dart';
import 'package:redux_sample/widgets/todo_item.dart';
import 'package:redux_sample/presentation/loading_indicator.dart';
import 'package:redux_sample/presentation/todo_item.dart';
class TodoList extends StatelessWidget {
final List<Todo> todos;
......
......@@ -6,7 +6,8 @@ The vanilla example uses only the core Widgets and Classes that Flutter provides
* 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 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.
* Local persistence - The list of todos is serialized to JSON and stored as a file on disk whenever the State is updated.
* Testing - Pull Business logic out of Widgets into Plain Old Dart Object (PODOs).
## Share State by Lifting State Up
......@@ -177,6 +178,17 @@ To make this concrete, Let's see how our callbacks flow in the this app.
+----------------+
```
## Testing
There are a few Strategies for testing:
1. Store state and the state mutations (methods) within a `StatefulWidget`
2. Extract this State and logic out into a "Plain Old Dart Objects" (PODOs) and test those. The `StatefulWidget` can then delegate to this object.
So which should you choose? For View-related State: Option #1. For App State / Business Logic: Option #2.
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 you to test against a Widget tree, but simply against an Object.
## Addendum
Since Flutter is quite similar to React with regards to State management, many of the resources on the React site are pertinent when thinking about State in flutter. In fact, the ideas in this example were lifted directly from the React site:
......
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