Flutter Redux – Part 2

This is continuation of the previous blog and in this we look at more advanced implementation of redux with flutter.

In the previous blog we understood the basic concepts of redux and implemented a simple counter. In this make a simple List app i.e to display a List of item and add new items to that list.

Let’s start, till now i have a setup a new app and install redux. This is the state of my codebase https://github.com/excellencetechnologies/flutter_redux_demo/commit/c0ff79ae1a2c32dedef1807fc192bbd319eefbf4

First lets create a UI which we need i.e a List View and a simple Form to add item to list. We will just create the UI, no state or dynamic at this stage.

Also go through this on how make a form https://flutter.dev/docs/cookbook/forms

Here is the full code for the UI https://github.com/excellencetechnologies/flutter_redux_demo/commit/16e2d82e8cc9a41095395b145b815d024b154e44 and how it looks

Since we are not focusing on UI, so i will not go through explaining the UI. But we have a basic setup now

State

Next, let’s setup our app state.

Before setting up our app state, we need to define a model for our Item.

//models/Item.dart
class Item {
  final String text;
  Item(this.text);
}
//redux/app_start.dart
import 'package:flutter_redux_demo/models/item.dart';

class AppState {
  List<Item> items;
}
//redux/reducer/dart
import 'package:flutter_redux_demo/redux/app_state.dart';

AppState appReducer(AppState state, dynamic action) {
  return state;
}

https://github.com/excellencetechnologies/flutter_redux_demo/commit/9be8e9f0ae9b586f3e9082708ddf0985a46ed05b

Next, update our reduce to make changes as per action

import 'package:flutter_redux_demo/redux/app_state.dart';
import 'package:flutter_redux_demo/redux/actions.dart';

AppState appReducer(AppState state, dynamic action) {
  if (action is AddItemAction) {
    return AppState.fromItems(List.from(state.items)..add(action.item));
  }
  return state;
}

One thing to remember in reduce is that a reducer should always return a new State object.

Also have made some updates to our AppState

import 'package:flutter_redux_demo/models/item.dart';

class AppState {
  List<Item> items;

  static var empty = AppState(new List());

  AppState(this.items);
  AppState.fromItems(this.items);
}

Next, in our main.dart file let’s initialize our redux store now

import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:flutter_redux_demo/redux/app_state.dart';
import 'package:flutter_redux_demo/screen/home.dart';
import 'package:redux/redux.dart';
import 'package:flutter_redux_demo/redux/reducer.dart';

void main() => runApp(MyApp());

final store = Store<AppState>(appReducer, initialState: AppState.empty);

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return StoreProvider(
      child: MyMaterialApp(),
      store: store,
    );
  }
}

class MyMaterialApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: HomeScreen());
  }
}

So now all connected cool!

https://github.com/excellencetechnologies/flutter_redux_demo/commit/d3bc8cbbaf9950f8551fe63b37643e9ff9190b85

Next, lets connect our ListView to redux!

import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:flutter_redux_demo/models/item.dart';
import 'package:flutter_redux_demo/redux/app_state.dart';

class ListWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, List<Item>>(
      converter: (store) => store.state.items,
      builder: (BuildContext context, List<Item> list) {
        return ListView.builder(
            itemCount: list.length,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text('${list[index].text}'),
              );
            });
      },
    );
  }
}

One important thing which i have seen if you define the types for StoreConnector properly, you can auto completed everything else easily e.g

StoreConnector<dynamic, dynamic>

if you do like this, it will not able to use your editor’s automate to fill values.

So for StoreConnector the first type is always the appstate object and second type is the return value the converter function. keep this in mind very critical.

Now, let’s add some initial state data to the our state so that we can display some data.

import 'package:flutter_redux_demo/models/item.dart';

class AppState {
  List<Item> items;

  static var empty = AppState(new List());

  AppState(this.items);

  AppState.fromList(List<String> list) {
    //added a new contructor to setup inital app state
    this.items = list.map((f) => new Item(f)).toList();
  }
  AppState.fromItems(this.items);
}
final store = Store<AppState>(appReducer,
    initialState: AppState.fromList(List.from(["Item1", "Item2"])));

Now you should be able to see ListView with data https://github.com/excellencetechnologies/flutter_redux_demo/commit/d8e4684611e777c005d5bb4bab7ce77c75e46933

Now, next connect our Form!

import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:flutter_redux_demo/models/item.dart';
import 'package:flutter_redux_demo/redux/actions.dart';
import 'package:flutter_redux_demo/redux/app_state.dart';

class AddForm extends StatelessWidget {
  final _formKey = GlobalKey<FormState>();
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, OnItemAddedCallback>(converter: (store) {
      return (text) => store.dispatch(AddItemAction(Item(text)));
    }, builder: (BuildContext context, callback) {
      return Form(
          key: _formKey,
          child: Container(
            padding: EdgeInsets.all(10),
            child: Row(
              children: <Widget>[
                Expanded(
                  flex: 3,
                  child: TextFormField(
                    controller: _controller,
                    decoration: InputDecoration(labelText: 'Enter list item'),
                    // The validator receives the text that the user has entered.
                    validator: (value) {
                      if (value.isEmpty) {
                        return 'Please enter some text';
                      }
                      return null;
                    },
                  ),
                ),
                Expanded(
                    child: RaisedButton(
                        onPressed: () {
                          // Validate returns true if the form is valid, or false
                          // otherwise.
                          if (_formKey.currentState.validate()) {
                            // If the form is valid, display a Snackbar.
                            // Scaffold.of(context).showSnackBar(
                            //     SnackBar(content: Text('Processing Data')));

                            callback(_controller.text);
                            _formKey.currentState.reset();
                          }
                        },
                        child: Text('Add')))
              ],
            ),
          ) // Build this out in the next steps.
          );
    });
  }
}

typedef OnItemAddedCallback = Function(String text);

Let’s see few important things here

typedef OnItemAddedCallback = Function(String text);

// this is a new function type we defined to pass to our StoreConverter.

Also read about TextEditingController to see how to fetch/manage a textfield value.

We can further refactor this in two components as follows

import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:flutter_redux_demo/models/item.dart';
import 'package:flutter_redux_demo/redux/actions.dart';
import 'package:flutter_redux_demo/redux/app_state.dart';

class AddFormViewModel extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, OnItemAddedCallback>(converter: (store) {
      return (text) => store.dispatch(AddItemAction(Item(text)));
    }, builder: (BuildContext context, callback) {
      return AddForm(callback);
    });
  }
}

class AddForm extends StatelessWidget {
  final _formKey = GlobalKey<FormState>();
  final _controller = TextEditingController();

  final OnItemAddedCallback callback;

  AddForm(this.callback);

  @override
  Widget build(BuildContext context) {
    return Form(
        key: _formKey,
        child: Container(
          padding: EdgeInsets.all(10),
          child: Row(
            children: <Widget>[
              Expanded(
                flex: 3,
                child: TextFormField(
                  controller: _controller,
                  decoration: InputDecoration(labelText: 'Enter list item'),
                  validator: (value) {
                    if (value.isEmpty) {
                      return 'Please enter some text';
                    }
                    return null;
                  },
                ),
              ),
              Expanded(
                  child: RaisedButton(
                      onPressed: () {
                        if (_formKey.currentState.validate()) {
                          callback(_controller.text);
                          _formKey.currentState.reset();
                        }
                      },
                      child: Text('Add')))
            ],
          ),
        ) // Build this out in the next steps.
        );
  }
}

typedef OnItemAddedCallback = Function(String text);

This way we are able to separate UI from redux.

Current source code here https://github.com/excellencetechnologies/flutter_redux_demo/commit/d8e4684611e777c005d5bb4bab7ce77c75e46933

Till this stage we have conved all basics of redux with flutter.

Let’s make further modification’s to understand this further. Let’s add a new state i.e read/unread to the list of items we have.

To do this first, we need to update our state to store this data.

//models/item.dart
class Item {
  final String text;
  bool read; //we cannot make this final, because it's value will change

  Item(this.text, {this.read = false});
  @override
  String toString() {
    //this is mainly for debugging purposes
    return "Text: $text isRead $read ";
  }
}

This is how our model looks now

//redux/actions.dart

//...

class MarkItemAction {
  final Item item;
  final bool read;
  MarkItemAction(this.item, this.read);
}

//redux/reducer.dart

AppState appReducer(AppState state, dynamic action) {
  if (action is AddItemAction) {
    return AppState.fromItems(List.from(state.items)..add(action.item));
  } else if (action is MarkItemAction) {
    return AppState.fromItems(state.items.map((item) {
      if (item.text == action.item.text) {
        //not a strong way to do this we should ideally have a unique id
        print(action.read);
        item = new Item(item.text, read: action.read);
      }
      return item;
    }).toList());
  }
  return state;
}

Next, we will make a change. We tried to seperate redux code and view logic previously, now we take it the next level!

Let’s introduce containers and viewmodels. The reason to do this, our current List Widget would require two things

a) State Data i.e items

b) Able to dispatch function to mark item

So it needs two things, so our older approach won’t work anymore.

class _ViewModel {
  final List<Item> items;
  final Function(Item, bool) markItem;

  _ViewModel({@required this.items, @required this.markItem});

  static _ViewModel fromStore(Store<AppState> store) {
    return _ViewModel(
        items: store.state.items,
        markItem: (Item item, bool read) {
          store.dispatch(MarkItemAction(item, read));
        });
  }
}

This a simple class in dart, which return both list of items and a function.

Now let’s defined a container to use this

class ListContainer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, _ViewModel>(
        converter: _ViewModel.fromStore,
        builder: (context, vm) {
          return ListWidget(vm.items, vm.markItem);
        });
  }
}

Ok so what happened! Our converter function now returns a _ViewModel which intern returns a function and list!! cool! This is a very scalable architecture for larger apps.

If we look at our actual ui widget it looks like this

class ListWidget extends StatelessWidget {
  final List<Item> list;
  final Function(Item, bool) doMarkItem;

  ListWidget(this.list, this.doMarkItem);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
        itemCount: list.length,
        itemBuilder: (context, index) {
          return ListTile(
            onTap: () {
              print(list[index]);
              doMarkItem(list[index], list[index].read ? false : true);
            },
            title: Text(
              '${list[index].text}',
              style: list[index].read
                  ? TextStyle(decoration: TextDecoration.lineThrough)
                  : TextStyle(decoration: TextDecoration.none),
            ),
          );
        });
  }
} 

So now we have a very scalable app architecture, with a consistent and optimized data <===> ui interaction.

https://github.com/excellencetechnologies/flutter_redux_demo/commit/23d7a2f18c1bde5225847663f0b6967e167838f8

excellence-social-linkdin
excellence-social-facebook
excellence-social-instagram
excellence-social-skype