A Redux Implementation With the New Kid on the Block: Flutter
By: El Niño
Tags: App Development, Flutter App Development, Flutter, Google

A while ago my colleague, Victor, took an initial look at Flutter, a new framework for mobile development from Google. At the time Flutter was still in its early stages, the first beta having just been released. Here, we will look into how to extend an app with a Redux store, Thunk future actions, and the Redux Persistor.

What is Redux?

Redux is a container for a store of information for applications. It started with Redux for the React.js framework, where it alleviated a lot of the issues with passing information around the app tree. In React, just like in Flutter, an application is composed of components (in Flutter called Widgets), which are related to each other in a parent-child structure. The data flow is then top-down: the parent provides its children with prop(ertie)s and each component can have its own state.

For small applications this is not a big problem — the data can be centralised at the root node, or just passed around with props. However, with any application that has any degree of complexity, this gets rather tricky. Take as an example, an online shopping app and its integral part — the shopping cart. The cart has to follow the user around any screen that they might navigate to, and sometimes even share its information with other components like a checkout page. Passing that data around, with easy access to its modification would be a lot of work. And here comes in Redux — a single source of truth that is accessible from any component within the app, no matter where it is in the tree.

A Redux store is therefore mostly just an object that holds information about the state of the app and handles its delivery and changes. Any component connected to the store can emit Actions — think API calls or reactions to user input — that take a copy of the current state and modify it. That new state is then committed to the store through the Reducers. After the commit, the changes are visible to every component that relies on them, triggering a re-rendering to reflect the shift in state.

Redux store

In this example, we will use the structure supplied by Android Studio when making a new project. The app will have a list of movies in its store, allowing any component to connect to that piece of data, display it and modify it.

First let us make the Movie object. It will have some basic properties such as title, year of release and so on.

class Movie {

final String title;

final int year;

final String image;

final double rating;



Movie(this.title, this.year, this.image, this.rating);

}

With the movie class defined, we can make the Redux store that holds a movie list and the currently active movie.

class AppState {

final List movieList;

final Movie currentMovie;



AppState({

this.movieList,

this.currentMovie,

});



factory AppState.initial({movieList = const [], currentMovie}) => AppState(

movieList: movieList,

currentMovie: currentMovie,

);



AppState copyWith({

List movieList,

Movie currentMovie,

}) {

return AppState(

movieList: movieList ?? this.movieList,

currentMovie: currentMovie ?? this.currentMovie,

);

}

}

Then let us make an action that adds a new movie to the store.

class MovieSetSuccessAction {

Movie currentMovie;



MovieSetSuccessAction(payload) {

this.currentMovie = payload;

}

}



MovieSetSuccessAction setMovieAction(Movie movie) {

return new MovieSetSuccessAction(movie);

}

And reducer that then handles such a change to the store.

AppState _movieSetSuccessReducer(AppState state, MovieSetSuccessAction action) {

return state.copyWith(currentMovie: action.currentMovie);

}



final movieReducer = combineReducers<AppState>([

TypedReducer<AppState, MovieSetSuccessAction>(_movieSetSuccessReducer),

]);

To connect a component, such as the text in the main page, to the store we will construct a ViewModel. It allows us to specify exactly which parts of the store the component uses, so that changes unrelated to it, do not cause it to re-render.

class _MyHomePageViewModel {

final Movie currentMovie;



_MyHomePageViewModel({

this.currentMovie

});



static _MyHomePageViewModel fromStore(Store<AppState> store) {

return _MyHomePageViewModel(

currentMovie: store.state.currentMovie,

);

}

}

With all the parts of the store ready, we can now add the store to our main app:

void main() {

final Store<AppState> _store = new Store<AppState>(

movieReducer,

initialState: AppState.initial(),

);



runApp(MyApp(

store: _store,

));

}



class _MyHomePageState extends State<MyHomePage> {

@override

Widget build(BuildContext context) {

return Scaffold(

body: Center(

child: new StoreConnector<AppState, _MyHomePageViewModel>(

converter: (store) => _MyHomePageViewModel.fromStore(store),

builder: (context, viewModel) {

return Column(

children: [

Text(viewModel.currentMovie != null ? viewModel.currentMovie.title : "No movie selected"),

],

);

},

),

),

);

}

}

We will also connect the FloatingButton to the store:

floatingActionButton: new StoreConnector<AppState, VoidCallback>(

converter: (store) {return () => store.dispatch(setMovieAction(new Movie("A nice movie", 2020, "img/poster.jpg", 0.0)));},

builder: (context, setMovieFunc) {

return new FloatingActionButton(

onPressed: setMovieFunc,

tooltip: 'Set movie',

child: new Icon(Icons.movie),

);

},

),

ReduxThunk

When dispatching actions, a lot of times we want to use an asynchronous function, such as a call to an API. As the basic implementation of Redux does not support async actions, we require the Thunk middleware for that.

Firstly we will change the main() method to introduce the middleware to the store:

void main() {

final Store<AppState> _store = new Store<AppState>(

movieReducer,

initialState: AppState.initial(),

middleware: [thunkMiddleware, ]

);



runApp(MyApp(

store: _store,

));

}

When fetching data from an API, the most used type of encoding is a JSON object. We therefore need to extend the Movie model to allow for serializing to and from JSON

class Movie {

final String title;

final int year;

final String image;

final double rating;



Movie(this.title, this.year, this.image, this.rating);



static Movie fromJson(Map<String, dynamic> json) {

if (json != null && json.length > 0) {

return new Movie(json['title'], int.parse(json['year']), json['image'], double.parse(json['imDbRating']));

} else {

return null;

}

}



dynamic toJson() {

return {

"title": this.title,

"year": this.year.toString(),

"image": this.image,

"imDbRating": this.rating.toString(),

};

}

}

Then we can make an asynchronous action that, say, fetches a list of movies from the API

ThunkAction fetchMoviesRequest() {

return (Store store) async {

List<Movie> result = [];



apiClient.apiCall(API_GET_TOP_MOVIES)

.then((res) {

for (dynamic item in res['items']) {

result.add(Movie.fromJson(item));

}

store.dispatch(

new MoviesFetchSuccessAction(result));

});

};

}



class MoviesFetchSuccessAction {

List<Movie> movieList;



MoviesFetchSuccessAction(payload) {

this.movieList = payload;

}

}

And extend the reducer to allow for this action

final movieReducer = combineReducers<AppState>([

// (...)

TypedReducer<AppState, MoviesFetchSuccessAction>(_moviesFetchSuccessReducer),

]); AppState _moviesFetchSuccessReducer(AppState state, MoviesFetchSuccessAction action) {

return state.copyWith(movieList: action.movieList);

}

We will also extend the _MyHomePageViewModel to provide the dispatch function and the movie list to the text in the home page

class _MyHomePageViewModel {

final List movieList;

final Movie currentMovie;



final Function() getMovies;



_MyHomePageViewModel({

this.movieList,

this.currentMovie,

this.getMovies

});



static _MyHomePageViewModel fromStore(Store<AppState> store) {

return _MyHomePageViewModel(

movieList: store.state.movieList,

currentMovie: store.state.currentMovie,

getMovies: () {

store.dispatch(fetchMoviesRequest());

},

);

}

}

And then use those hooks to display the data:

class _MyHomePageState extends State<MyHomePage> {

@override

Widget build(BuildContext context) {

return Scaffold(

body: Center(

child: new StoreConnector<AppState, _MyHomePageViewModel>(

converter: (store) => _MyHomePageViewModel.fromStore(store),

builder: (context, viewModel) {

return Column(

children: [

Text("Number of movies in list ${viewModel.movieList.length}"),

MaterialButton(onPressed: viewModel.getMovies, child: Text("Fetch movies")),

Text(viewModel.currentMovie != null ? viewModel.currentMovie.title : "No movie selected"),

],

);

},

),

),

);

}

}

ReduxPersist

To make sure that the state can persist between user interactions for a smoother user experience, one might want to introduce the Redux Persist package to the fold. It allows us to save the state of the store as a JSON object in a couple different ways. The easiest is to save it locally to file, but it can also be sent to some backend service that allows you to restore the state across devices and to even integrate it with a Redux enabled web app. However, while it does provide some useful functionality, it requires the addition of JSON serialisability to the store, which can be a bit of a hassle.

Firstly, add the persistence functionality to the store and load a state in if its available

Future<File> get _localFile async {

final path = await getApplicationDocumentsDirectory();

return File('$path/AppState1.json');

}



void main() async {

WidgetsFlutterBinding.ensureInitialized();



final persistor = Persistor<AppState>(

storage: FileStorage(await _localFile),

serializer: JsonSerializer<AppState>(AppState.fromJson),

);



AppState initialState = await persistor.load();



final Store<AppState> _store = new Store<AppState>(

movieReducer,

initialState: initialState ?? AppState.initial(),

middleware: [thunkMiddleware, persistor.createMiddleware()]

);



runApp(MyApp(

store: _store,

));

}

And add the JSON serialize methods to the Store

class AppState {

final List movieList;

final Movie currentMovie;



AppState({

this.movieList,

this.currentMovie,

});



factory AppState.initial({movieList = const [], currentMovie}) => AppState(

movieList: movieList,

currentMovie: currentMovie,

);



static AppState fromJson(dynamic json) {

if (json == null) {

return AppState.initial(movieList: [], currentMovie: null);

} else {

return AppState.initial(movieList: parseList(json, 'movieList'), currentMovie: Movie.fromJson(json["currentMovie"]));

}

}



dynamic toJson() {

List movieList = this.movieList.map((movie) => movie.toJson()).toList();

dynamic currentMovie = this.currentMovie != null ? this.currentMovie.toJson() : null;



return {

'movieList': movieList,

"currentMovie": currentMovie

};

}



AppState copyWith({

List movieList,

Movie currentMovie,

}) {

return AppState(

movieList: movieList ?? this.movieList,

currentMovie: currentMovie ?? this.currentMovie,

);

}

}



List<Movie> parseList(dynamic json, String key) {

List<Movie> list = [];

json[key].forEach((item) {

list.add(Movie.fromJson(item));

});

return list;

}

Conclusion

Redux is a very useful tool for developing apps of significant size or complexity. It centralizes the process of accessing and modifying data, making it easier to get what information you need, wherever you need it. It is not necessary for every app out there — the additional costs of setting it up are quite high. However, it forces you into following its paradigm, which standardises the flow of data and enforces a neat separation of code.

The additions on top of Redux make it even better. ReduxThunk allows you do make the actions asynchronous, facilitating delayed responses or API calls without much work. There is no reason not to add it to any Redux project, as all you need is just the middleware declaration and you’re good to go!

ReduxPersist introduces more complexity to the application, however the gains to fluidity from having a locally stored state of the app available no matter the network are quite substantial. It is by no means a necessary addition, but in certain circumstances or in any cross-platform application, it could prove extremely useful.

Jakub Orlinski

Jakub is a front-end developer at El Niño and became quite a big fan of Docker two years ago but, sadly, never got round to part two of his blog.