Flutter Redux Tutorial – A simple practical Flutter Redux example

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.

flutter-redux-tutorial-overview

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,
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}
0 0 votes
Article Rating
Subscribe
Notify of
guest
77 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments