Saturday, June 23, 2018

Flutter - Redux architecture example with Flutter.

Redux is a unidirectional application architecture, originally made for JavaScript but now used in mobile applications development (such as React Native or Flutter). Redux makes easy to develop, maintain and test applications.

Redux was created by Dan Abramov around June 2015. It was inspired by Facebook’s Flux and functional programming language Elm. Redux got popular very quickly because of its simplicity, small size (only 2 KB) and great documentation. 




Basically, you should know three things about Redux architecture. 

1. Whole application state is kept only one place that is called store.
2. To change the application state you need to dispatch an action.
3. Changes are made with pure functions. It takes the previous state and an action and returns a new state.




As you can see above, a store which holds a State object that represents the state of the whole application. Every application event is represented as an action that gets dispatched to a Reducer function. This Reducer updates the store with a new State depending on what action it receives and whenever a new state is pushed through the store the View is recreated to reflect the changes. 

In Redux most components are decoupled, making UI changes very easy to make. In addition, the only business logic sits in the Reducer functions. A Reducer is a function that takes an action and the current application state and it returns a new state object, therefore it is straightforward to test because we can write a unit test that sets up an initial State and checks that the Reducer returns the new and modified State.

What are the advantages of Redux?
  • Easy to understand how the data flow works in the application.
  • The use of "pure" reducer functions makes logic easier to test, and enables useful features like "time-travel debugging".
  • Centralizing the state makes it easier to implement things like logging changes to the data, or persisting data between page refreshes.

In this post, I'll explain how you can start writing mobile apps with Flutter using the Redux architecture.


Redux in Flutter
If you going to implement redux in flutter then you have to explore two very useful packages.

redux: The redux package adds all the necessary components to use Redux in Dart like: 

Store:-  holds a State object that represents the state of the whole application.
Reducer:-  update the store with a new state depending on what action it receives.
Middleware:- is a component that processes an action before it reaches the Reducer. It receives the current application state and the action that got dispatched. As you can see below.



flutter_redux: This is a Flutter-specific package which provides additional components on top of the redux library which are useful for implementing Redux in Flutter, such as: 

StoreProvider:- The base Widget for the app that will be used to provide the Store to all the Widgets that need it.
StoreBuilder:-  A Widget that receives the Store from the StoreProvider and StoreConnector.
StoreConnector:- It is very useful Widget that can be used instead of the StoreBuilder as you can convert the Store into a ViewModel to build the Widget tree and whenever the State in the Store is modified, the StoreConnector will get rebuilt.


Let's create a small Flutter application and implement Redux.

We going to create a small redux based flutter application that has create and delete task feature. It will show you a basic idea of redux. Let’s start it some important parts of the project.

1. Add redux dependency in project pubspec.yaml.



2. Open main.dart file (which is our app’s entry point) defines the application Store object from an initial State, a Reducer function and the Middleware
main.dart
void main() => runApp(TaskApp()); class TaskApp extends StatelessWidget { final Store<AppState> store = Store<AppState>( appReducer, initialState: AppState.initial(), middleware: createStoreMiddleware(), ); }

3. After that wraps the MaterialApp object with a StoreProvider that takes the Store and can pass it to its descendant Widgets that need one:
main.dart
@override Widget build(BuildContext context) => StoreProvider( store: this.store, child: MaterialApp( title: 'Flutter Demo', theme: ThemeData( primaryColor: Colors.green, primarySwatch: Colors.green, ), home: TaskListPage(), ), );

4. The AppState class contains the list of task items and a field to decide whether or not to display the TextField to add a new item:
state.dart
class AppState { final List<TaskItem> task; final ListState stateList; AppState(this.task, this.stateList); factory AppState.initial() => AppState(List.unmodifiable([]), ListState.listOnly); } enum ListState { listOnly, listWithNewItem }

5. In order to display the task list, we define a ViewModel class that contains a view-specific representation of the data we need to display, as well as the actions the user can do. This ViewModel gets created from the Store:
task_list_page.dart
class _ViewModel { final String pageTitle; final List<_ItemViewModel> items; final Function() onAddItem; final String newItemToolTip; final IconData newItemIcon; _ViewModel(this.pageTitle, this.items, this.onAddItem, this.newItemToolTip, this.newItemIcon); factory _ViewModel.create(Store<AppState> store) { List<_ItemViewModel> items = store.state.task .map((TaskItem item) => _ToDoItemViewModel(item.title, () { store.dispatch(RemoveItemAction(item)); store.dispatch(SaveListAction()); }, 'Delete', Icons.delete) as _ItemViewModel) .toList(); if (store.state.stateList == ListState.listWithNewItem) { items.add(_EmptyItemViewModel('Type the next task here', (String title) { store.dispatch(DisplayListOnlyAction()); store.dispatch(AddItemAction(TaskItem(title))); store.dispatch(SaveListAction()); }, 'Add')); } return _ViewModel('Task', items, () => store.dispatch(DisplayListWithNewItemAction()), 'Add new detail ', Icons.add); } }

6. Now we can use the ViewModel class to display the task list. Notice that we wrap our Widgets inside a StoreConnector which allows us to create the ViewModel from the Store and build our UI using the ViewModel.
task_list_page.dart
@override Widget build(BuildContext context) => StoreConnector<AppState, _ViewModel>( converter: (Store<AppState> store) => _ViewModel.create(store), builder: (BuildContext context, _ViewModel viewModel) => Scaffold( appBar: AppBar( title: Text(viewModel.pageTitle), ), body: ListView(children: viewModel.items.map((_ItemViewModel item) => _createWidget(item)).toList()), floatingActionButton: FloatingActionButton( onPressed: viewModel.onAddItem, backgroundColor: Colors.green, tooltip: viewModel.newItemToolTip, child: Icon(viewModel.newItemIcon), ), ), );
in the above code, we have to define event when the user presses the ‘Add’ button, we’ll dispatch an action of type.

7. Create task add and delete the widget.
task_list_page.dart
Widget _createWidget(_ItemViewModel item) { if (item is _EmptyItemViewModel) { return _createEmptyTaskItemWidget(item); } else { return _createTaskItemWidget(item); } } Widget _createEmptyTaskItemWidget(_EmptyItemViewModel item) => Container( margin: EdgeInsets.fromLTRB(10.0, 5.0, 10.0, 0.0), child: new Column( children: [ TextField( onSubmitted: item.onCreateItem, autofocus: true, decoration: InputDecoration( hintText: item.createItemToolTip, ), ) ], ), ); Widget _createTaskItemWidget(_ToDoItemViewModel item) => Container( margin: EdgeInsets.fromLTRB(10.0, 5.0, 10.0, 0.0), child: new Row( children: [ Text(item.title), FlatButton( onPressed: item.onDeleteItem, child: Icon( item.deleteItemIcon, color: Colors.green, semanticLabel: item.deleteItemToolTip, ), ) ], ), );
which indicates we need to modify the application state so that we display the TextField that will let the user add a new task item.

8. The action class is Reducer that contains business login:
reducers.dart
AppState appReducer(AppState state, action) => AppState(toDoListReducer(state.task, action), listStateReducer(state.stateList, action)); final Reducer<List<TaskItem>> toDoListReducer = combineReducers([ TypedReducer<List<TaskItem>, AddItemAction>(_addItem), TypedReducer<List<TaskItem>, RemoveItemAction>(_removeItem), ]); List<TaskItem> _removeItem(List<TaskItem> toDos, RemoveItemAction action) => List.unmodifiable(List.from(toDos)..remove(action.item)); List<TaskItem> _addItem(List<TaskItem> toDos, AddItemAction action) => List.unmodifiable(List.from(toDos)..add(action.item)); final Reducer<ListState> listStateReducer = combineReducers<ListState>([ TypedReducer<ListState, DisplayListOnlyAction>(_displayListOnly), TypedReducer<ListState, DisplayListWithNewItemAction>(_displayListWithNewItem), ]); ListState _displayListOnly(ListState listState, DisplayListOnlyAction action) => ListState.listOnly; ListState _displayListWithNewItem(ListState listState, DisplayListWithNewItemAction action) => ListState.listWithNewItem;

9. Create action.dart, here we can declare actions that app will perform.
action.dart
class RemoveItemAction { final TaskItem item; RemoveItemAction(this.item); } class AddItemAction { final TaskItem item; AddItemAction(this.item); } class DisplayListOnlyAction {} class DisplayListWithNewItemAction {} class SaveListAction {}

Now run the project and you will see TextField after clicking on add button. Add some text.



As you can see, Redux is all about the state. You have a single app state, the state is read-only for the view, and to create new state you need to send action. After that reducer creates and emits a new application state.

This is a simple example but demonstrates the concepts. The full source code for this app can be found in Github.

To conclude, using this architecture in Flutter apps keeps all the concerns well-defined and separate from each other. I'd like to see if it'd scale well in a large project, as having one State for the whole app makes me think you'd end up composing this State from smaller State objects. 

If you have any query feel free to ask it in the comment section below.




Share:

Get it on Google Play

React Native - Start Development with Typescript

React Native is a popular framework for building mobile apps for both Android and iOS. It allows developers to write JavaScript code that ca...