Flutter Redux Tutorial – A simple practical Flutter Redux example
In this Flutter Redux tutorial, we’re gonna introduce main concept of Redux: what it is, how to work with Redux Store, Action, Reducers. Then we will practice to understand all of them with a simple practical Flutter Redux example.
Flutter Redux Overview
Redux
Redux is a state container that helps applications to manage state.
=> Whenever we wanna read the state, look into only one single place – Redux Store.
=> Managing the state could be simplified by dealing with simple objects and pure functions.
Redux Store
Store holds the current state so that we can see it as a single source of truth for our data.
@immutable class MyAppState { final int counter; MyAppState(this.counter); } final store = new Store(counterReducer, initialState: MyAppState(0));
Redux Action
Action is payload of information that is sent to Store using store.dispatch(action)
.
Action must have a type
property that should typically be defined as string constants. It indicates the type of action being performed:
{ 'type': Actions.INCREMENT 'number': 3 } { 'type': Actions.DECREMENT 'number': 2 }
Redux Reducer
Reducer is a pure function that generates a new state based on an Action it receives. These Actions only describe what happened, but don’t describe how state changes.
MyAppState counterReducer(MyAppState prevState, dynamic action) { switch (action['type']) { case Actions.INCREMENT: return MyAppState(prevState.counter + action['number']); case Actions.DECREMENT: return MyAppState(prevState.counter - action['number']); default: return prevState; } }
*Note: Reducer must be a pure function:
=> From given arguments, just calculate the next state and return it.
=> No side effects. No API or non-pure function calls. No mutations.
Flutter Redux
– StoreProvider
: This is the base Widget that passes the given Redux Store to all descendants that request it. So it should wrap MaterialApp or WidgetsApp.
– StoreConnector
: This is a descendant Widget that gets the Store from the nearest StoreProvider
ancestor. It:
+ converts the store
into appropriate value/object/callback with the given converter
function
+ passes the value/object/callback to a builder function.
When an action
is dispatched and run through the reducer
, reducer
updates the state
, the Widget will be rebuilt with the latest value automatically.
=> We don’t need to manually manage subscriptions or streams.
// action: { 'type': Actions.INCREMENT 'number': 3 } class MyHomePage extends StatelessWidget { final store = new Store(reducer, initialState: MyAppState(0)); @override Widget build(BuildContext context) { return new StoreProvider( store: store, child: Scaffold( ... body: Center( child: Column( ... children: [ StoreConnector ( converter: (store) => store.state.counter.toString(), builder: (context, counter) { return Text(counter); }), StoreConnector ( converter: (store) { return () => store.dispatch({ 'type': Actions.INCREMENT, 'number': 3, }); }, builder: (context, callback) { return RaisedButton( child: Text('INC'), onPressed: callback, ); }, ), ], ), ), ), ); } }
Practice
Example overview
This is a simple Flutter Redux Application that has:
– MyAppState
as the main state that is stored inside Redux Store.
– 2 types of Action: 'INCREMENT'
, 'DECREMENT'
.
– One Reducer: counterReducer
.
We can increase/decrease the Counter value. App will update UI immediately.
Setup Flutter Redux Project
Open pubspec.yaml in Flutter Project. Add a dependency for redux
and flutter_redux
:
dependencies: flutter: sdk: flutter redux: 3.0.0 flutter_redux: 0.5.2
Run command: flutter packages get
.
Full Code
lib/main.dart
import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; @immutable class MyAppState { final int counter; MyAppState(this.counter); } enum Actions { INCREMENT, DECREMENT, } MyAppState counterReducer(MyAppState prevState, dynamic action) { switch (action['type']) { case Actions.INCREMENT: return MyAppState(prevState.counter + action['number']); case Actions.DECREMENT: return MyAppState(prevState.counter - action['number']); default: return prevState; } } void main() { runApp(new MyReduxApp()); } class MyReduxApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'ozenero Demo', debugShowCheckedModeBanner: false, home: MyHomePage(), ); } } class MyHomePage extends StatelessWidget { final store = new Store(counterReducer, initialState: MyAppState(0)); @override Widget build(BuildContext context) { return new StoreProvider( store: store, child: Scaffold( appBar: AppBar( title: Text('ozenero Redux Demo'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ StoreConnector ( converter: (store) => store.state.counter.toString(), builder: (context, counter) { return Text( counter, style: TextStyle( color: Colors.blue, fontSize: 20.0, ), ); }), Padding( padding: EdgeInsets.all(20.0), child: StoreConnector ( converter: (store) { return () => store.dispatch({ 'type': Actions.INCREMENT, 'number': 3, }); }, builder: (context, callback) { return RaisedButton( child: Text('INC'), onPressed: callback, ); }, )), StoreConnector ( converter: (store) { return () => store.dispatch({ 'type': Actions.DECREMENT, 'number': 2, }); }, builder: (context, callback) { return RaisedButton( child: Text('DEC'), onPressed: callback, ); }, ), ], ), ), ), ); } }