Implement Redux with Flutter App

The most common answer might be the state management, which is why in this article I want to show Redux state implementation for Flutter application.
Flutter state management
According to the Flutter documentation there are couple state management approaches:
- BLoC / Rx,
- InheritedWidget & Scoped model,
- setState,
- MobX.
- Redux,
Last three may be familiar for React or React Native Developers and there you can find their origin. But let’s dive into the last of those five solutions.
What is Redux?
According to wikipedia “Redux is a small library with a simple, limited API designed to be a predictable container for application state”. Let me present you a diagram of Redux data flow.
It isn't strictly defined by Redux, it's just pattern/architecture that utilises it, but in theory Redux could be used in many different ways.
First and the most important element is Store, which is an object that holds the application's state tree. Two of the three fundamental Redux principles are related to it:
- Store is the single source of true,
- Store is read-only.
Second element is View, which is based on current app state. If we want to make some changes in app state we need to call an Action. Every Action has its unique “type”. This is very important when we go further… But before that we have Middleware which according to documentation provides a third-party extension point between dispatching an action, and the moment it reaches the Reducer. This is our last stepover to complete the cycle and it is related to the last fundamental Redux principle. „Changes are made with pure functions”, so Reducer basically is a pure function which takes current state and an Action, and based on Action type provides some computations to return updated state. Any impure login needs to be handled by Middleware e.g. logging, crash reporting, API communication or routing.
If this is too much for you, don’t be afraid. I also needed some time to wrap my head around this topic. Sometimes you need to bang your head against the wall for a while and wait for that 'aha' moment.⠀
Setup Redux with Flutter
Code samples are taken from our internal project which is an app for ordering food to your workplace. First of all we need to proceed installation procedure which you can find on Dart Package website.
For the purpose of this article we will create simple app state which will be current Restaurant. Let’s look at restaurant model.
class Restaurant {
final String id;
final String name;
final String deliveryTime;
final List categories;
final bool isNew;
const Restaurant({
@required this.id,
@required this.name,
@required this.deliveryTime,
@required this.categories,
this.isNew = false,
});
}
First file which we will implement is the app_state.dart.
class AppState {
final Restaurant restaurant;
AppState({this.restaurant});
AppState copyWith({Restaurant restaurant}) {
return AppState(restaurant: restaurant ?? this.restaurant);
}
}
Function copyWith could be unclear, but it is just returning new state by substituting properties in the previous one. Then we need to create two actions and put it to the restaurant_actions.dart.
class SetRestaurantAction {
final Restaurant restaurant;
SetRestaurantAction(this.restaurant);
}
class ClearRestaurantAction {}
Than we can use them to define our restaurant Reducer:
import 'package:lunching/redux/restaurant/restaurant_actions.dart';
import 'package:lunching/redux/app/app_state.dart';
import 'package:redux/redux.dart';
AppState appReducer(AppState state, dynamic action) => AppState(
restaurant: restaurantReducer(state.restaurant, action),
);
final restaurantReducer = combineReducers<List<Restaurant>>([
TypedReducer<Restaurant, SetRestaurantAction>(_setRestaurant),
TypedReducer<Restaurant, ClearRestaurantAction>(_clearRestaurant),
]);
List<Restaurant> _setRestaurant(_, SetRestaurantAction action) =>
action.restaurant;
List<Restaurant> _clearRestaurant(_, __) => [];
Than we can wrap the Redux architecture part in a store.dart file:
import 'package:lunching/redux/app/app_state.dart';
import 'package:lunching/redux/app/app_reducer.dart';
import 'package:redux/redux.dart';
Future<Store<AppState>> createStore() async {
return Store(
appReducer,
initialState: AppState(),
middleware: [],
);
}
In the end we need to modify main.dart file to adopt our Redux implementation.
import 'package:flutter/material.dart';
import 'package:lunching/redux/app/app_state.dart';
import 'package:lunching/redux/store.dart';
import 'package:redux/redux.dart';
void main() async {
final store = await createStore();
runApp(LunchingApp(store));
}
class LunchingApp extends StatefulWidget {
final Store<AppState> store;
const LunchingApp(this.store);
@override
State<StatefulWidget> createState() => _LunchingAppState();
}
class _LunchingAppState extends State<LunchingApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
//UI part
);
}
}
Usage
To set current restaurant we need to connect UI widget to our Redux architecture and we can do this by using StoreConnector widget.
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:lunching/models/restaurant.dart';
import 'package:lunching/redux/app/app_state.dart';
import 'package:lunching/redux/restaurant/restaurant_actions.dart';
@override
Widget build(BuildContext context) {
Restaurant sampleRestaurant = Restaurant(
id: '1',
name: "Italiano",
categories: ["LUNCH SETS", "PASTA"],
deliveryTime: "12:30 – 13:00",
isNew: true,
);
return StoreConnector<AppState, Function(Restaurant)>(
converter: (store) =>
(restaurant) => store.dispatch(SetRestaurantAction(restaurant)),
builder: (context, callback) {
return MaterialApp(
home: Scaffold(
body: RaisedButton(
onPressed: () => callback(sampleRestaurant),
child: Text('Add restaurant'),
),
),
);
});
}
To clear restaurant state we can...
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, Function()>(
converter: (store) => () => store.dispatch(ClearRestaurantAction()),
builder: (context, clearAction) {
return MaterialApp(
home: Scaffold(
body: RaisedButton(
onPressed: () => clearAction(),
child: Text('Clear restaurant'),
),
),
);
});
}
To see current restaurant name:
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, String>(
converter: (store) => store.state.restaurant.name,
builder: (context, restaurantName) {
return MaterialApp(
home: Scaffold(
body: Text(restaurantName),
),
);
});
}
Useful libraries
In the introduction to the Redux architecture I mentioned that any impurity needs to be handled in Middleware. It can be difficult to reason about at the beginning, so you need to get the concept well.
Redux Thunk is basically a middleware that allows you to create asynchronous side-effects in your actions, as well as invoke other store actions.
Redux Persist will help you with persisting the data from the Store.
Reselect defines functions that accept the store object and adds memoization to avoid computing the store data if it hasn't changed.
Summary
That was just plain example, but that might be your very first step to base an app state on Redux solution in Flutter application. Our experience with this architecture is great, code can be well tested. With the same actions in the same order, we are going to end up in the same state, which can be achieved because of the predictability through abandonment of the mutations and encapsulation of the side effects in the Middleware.
I want to thank Krzysztof Kraszewski and Piotr Sochalewski, because most of the code in this article is based on their solutions.
Photo by Tyler Lastovich on Unsplash