Redux and epics for better-organized code in Flutter apps

Nihad Delic
8 min readSep 22, 2020

Reuse your dart code by using Flutter redux as global app state management and redux epics for handling streams.

Later equals never. ― Robert C. Martin

The first thing that you should ask yourself before starting to code is definitely how to organize the code in a way that you will not regret. That means that you should pick the architectural and structural path of your project. It can be a difficult call to make that decision, especially if you take into consideration that you are working with a new framework, a new language.

How does redux work?

There are already plenty of tutorials on how you could implement it. I would recommend this one for beginners.

What are epics?

… working with complex asynchronous operations, such as autocomplete search experiences, can be a bit tricky with traditional middleware. This is where Epics come in!

— read more about at https://pub.dev/packages/redux_epics

Other than this combination, you can use BLoC, provider state management, MobX, GetX, and others or you can just use redux with middleware functions for simple async cases.

Redux structure

First, we should create a simple redux structure. The idea is that every page has its own part of the redux implementation. Besides that, we should have one redux directory which will connect all of those related redux parts, so that we are able to have a single place of truth.

Basic redux and epic structure

I will not explain in detail which file serves which purpose, for now. But briefly, and in a readable manner,
actions are the classes, where the instances of them will help you to trigger some epic, or reducer code, by emitting them from someplace of your project. In the meantime,
epic listens for emitted actions, which will trigger your code. reducer updates the global state of the project so that you are able to use state data in many places in your project.

Example 1

Initial fetch of data

  1. Here is the perfect example of reducing possible duplication of code. At the start of the app, we will init the loading of the data by calling FetchMoviesAction.

Create actions for our needs:

Tip: Every mentioned path in this article contains a URL reference to source code.

Inside lib/pages/movies/actions.dart create actions for call, success, and failure. So the code of this file should look like this:

import 'package:redux_epic_by_example/model/movie.dart';

class FetchMoviesAction {
const FetchMoviesAction();
}

class FetchMoviesSucceededAction {
const FetchMoviesSucceededAction(this.movies) : assert(movies != null);

final List<Movie> movies;
}

class FetchMoviesFailedAction {
const FetchMoviesFailedAction(this.exception);

final Exception exception;
}

We will dispatch FetchMoviesAction every time when we want to fetch data, emit FetchMoviesSucceededAction every time our data is successfully loaded from our source, and FetchMoviesFailedAction every time we catch an exception, for example, HTTP 500.

That’s OK, but where should I call those actions?

FetchMoviesAction will be called from two places, from the initial loading of data (when the app is started), and if the user decides that he wants to refresh his list. The other two actions will be called inside epic code, but more on that later.

Initial call of FetchMoviesAction

Please take a look at the content of lib/main.dart file because this is the place where we will call the initial action.

void main() {
runApp(
StoreProvider<AppState>(
store: (AppStore.inject()..dispatchInitial()).store,
child: MaterialApp(
title: 'Movie',
home: MoviesListContainer(),
),
),
);
}

You will probably need to handle this through other widgets, but the principle is the same, you should call dispatchInitial() method at some point:

Future dispatchInitial() => store.dispatch(FetchMoviesAction());

What call of store.dispatch(...) actually means:

Runs the action through all provided [Middleware], then applies an action to the state using the given [Reducer]. Please note: [Middleware] can intercept actions, and can modify actions or stop them from passing through to the reducer.
— By redux documentation

And how is this at all connected with epics, if it says Middleware?

If you want to use redux_epics, which is very good for working with complex asynchronous operations, you now need to include them inside middleware.
Take a look at lib/redux/core.dart:

factory AppStore.inject() {
return AppStore(
store: Store<AppState>(
reducer,
initialState: _injectedState(),
middleware: [
EpicMiddleware<AppState>(epics()),
],
),
);
}

This means that we are connecting our epics with redux middleware. So if you are “unfamiliar with Streams, simple async cases are easier to handle with a normal Middleware Function”, you can use middleware (please take a look at middleware documentation inside of redux library). And continue with this article from the “User interfacechapter. In any case, bigger projects would probably need mechanisms for handling streams without using async-await every time for every function in our code. Besides epics, you can use the saga library. Anyhow, let’s continue, epics() method is basically a combination of page epics:

Epic<AppState> epics() => combineEpics<AppState>([
moviesEpics(),
]);

So, for every screen that’s using epic, you will add that one to this array.
Now, when we have established a connection between middleware and epics, every dispatch will go through all epics and find the proper one. Take a look at lib/pages/movies/epic.dart file:

Epic<AppState> _fetchMoviesEpic() =>
(Stream<dynamic> actions, EpicStore<AppState> store) {
return actions
.whereType<FetchMoviesAction>()
.switchMap((action) => _fetchMovies(action));
};

Stream<dynamic> _fetchMovies(FetchMoviesAction action) async* {
try {
final data = fetchMockDataArray();

yield FetchMoviesSucceededAction(data);
} on Exception catch (e) {
// You can log the exception, send event to you analytics..
yield FetchMoviesFailedAction(e);
}
}

Epic<AppState> moviesEpics() => combineEpics<AppState>([
_fetchMoviesEpic(),
]);

If you take a look at _fetchMoviesEpic , you will see that based on actions.whereType<FetchMoviesAction> function for fetching movies _fetchMovies will be called.

This function is responsible for fetching data, and if fetching is successful, it will emit FetchMoviesSucceededAction which will trigger reducer code. As we can see, that code is responsible for updating the state of the app.

MovieState movieReducer(MovieState state, dynamic action) {
if (action is FetchMoviesSucceededAction) {
return state.copyWith(
movies: List.from(action.movies),
);
}
return state;
}

User interface (lib/pages/movies/movies_screen.dart)

When the state is updated, it will trigger UI code, because it’s under the StoreConnector widget. This part of the code is divided into widgets tree builder functions and ViewModel class. ViewModel class is responsible for business logic like emitting actions, handling some UI logic stuff, using data from the redux store, etc. Anyhow, if we know that StoreConnector widget will refresh the UI if the state is changed, and if we know that we can access that state through ViewModel functions, we can build our list of movies. The function responsible for this is called _buildMoviesList(viewModel) .

ListView _buildMoviesList(_ViewModel viewModel) {
return ListView.builder(
padding: EdgeInsets.symmetric(vertical: 8),
itemCount: viewModel.movies.length,
itemBuilder: (BuildContext context, int index) => Card(
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Text(viewModel.movies[index].title),
Text(viewModel.movies[index].description),
Text(viewModel.movies[index].fetchTime),
],
),
),
);
}

But, now the question is, what is viewModel.movies and how did we get it. For that, we need to see the code of _ViewModel , the method that’s interesting for us is factory _ViewModel.from(Store<AppState> store) , where we will get the state from the store.

@immutable
class _ViewModel {
const _ViewModel._({
@required this.onDispatch,
@required this.movies,
}) : assert(onDispatch != null),
assert(movies != null);

factory _ViewModel.from(Store<AppState> store) {
return _ViewModel._(
onDispatch: store.dispatch,
movies: store.state.movieState.movies,
);
}

final Function(dynamic) onDispatch;
final List<Movie> movies;

void fetchData() => onDispatch(FetchMoviesAction());

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is _ViewModel &&
runtimeType == other.runtimeType &&
const ListEquality().equals(movies, other.movies);

@override
int get hashCode => movies.hashCode;
}

Finally, after a lot of initial setup work, we can reuse this code in many places in our project.

Things do not happen. Things are made to happen. — John F. Kennedy

Example 1

Refresh existing data

When we are done with the initial setup and fetching initial data, we can implement the refresh function. This basically means that the user will manually trigger some code, which will fetch data again, but this time we almost have all the code which we need. So, here is an excellent example of how we will reduce possible duplication of code. All we need to do is to dispatch FetchMoviesAction again and that’s basically it. Before we do that, there are a few more steps that need to be completed:

  1. Add a button widget to our code and implement onPressed callback
return Container(
color: Colors.grey,
child: Column(
children: [
Padding(
padding: EdgeInsets.only(top: 100),
child: FlatButton(
child: Text('Fetch again'),
onPressed: viewModel.fetchData,
color: Colors.blue,
),
),
Expanded(child: _buildMoviesList(viewModel)),
],
),
)

2. You can find the definition of viewModel.fetchData, inside _ViewModel a class, it looks like this:

void fetchData() => onDispatch(FetchMoviesAction());

And that’s it! You’ve implemented manual fetching of the movie list.

Example 2 — chaining multiple epics

It is possible to chain the execution of multiple epics. Let’s say that we want to send an event to our analytics after the data fetch is successful.

I know that momentarily this example doesn’t make sense, but you will want to send it in some situations. For example, maybe you want to send an event every time the user has tapped on one item, but also in the same epic you want to fetch some additional data.

Let’s continue with our plan, we are already emitting FetchMoviesSucceededAction inside _fetchMoviesEpic, so the only thing that we should implement is a new epic. In that newly created epic, we will print out a message, for now. Normally there would be a code for sending events to analytics.

  1. Inside lib/movies/epic.dart add a new epic: _fetchMoviesAnalyticsEpic
Epic<AppState> moviesEpics() => combineEpics<AppState>([
_fetchMoviesEpic(),
_fetchMoviesAnalyticsEpic(),
]);

2. The body of this epic will “search” for FetchMoviesSucceeded action, which means that you need to have something like this:

Epic<AppState> _fetchMoviesAnalyticsEpic() =>
(Stream<dynamic> actions, EpicStore<AppState> store) {
return actions
.whereType<FetchMoviesSucceededAction>()
.switchMap((action) => _fetchMoviesAnalytics(action));
};

3. As I mentioned, we will just print the number of elements that are fetched, but usually, you want to send events to the real server.

Stream<dynamic> _fetchMoviesAnalytics(FetchMoviesSucceededAction action) async* {
print('Total elements inside array: ${action.movies.length}');
}

Note: You can always consider to separate analytic epics and movie epics to different files.

Example 3— emit multiple actions

Of course, you can emit multiple actions inside of the same epic. So let’s say that we want to trigger an epic that will automatically refresh our data every 5 minutes.

  1. Again, add three new actions inside lib/pages/movies/actions.dart: AutomaticRefreshDataAction , AutomaticRefreshDataSucceededAction , AutomaticRefreshDataFailedAction
  2. Time to add one more epic to our lib/pages/movies/epic.dart:
Epic<AppState> _timeDataRefreshEpic() =>
(Stream<dynamic> actions, EpicStore<AppState> store) {
return actions.whereType<AutomaticRefreshDataAction>().switchMap(
(
_) => Stream.periodic(const Duration(minutes: 5))
.switchMap((_) => _autoRefreshData()),
);
};

Stream<dynamic> _autoRefreshData() async* {
yield FetchMoviesAction();
}

Epic<AppState> moviesEpics() => combineEpics<AppState>([
_fetchMoviesEpic(),
_fetchMoviesAnalyticsEpic(),
_timeDataRefreshEpic(),
]);

As you can see, we once again reduced potentially redundant code by emitting the same action for fetching movies. We’ve done this in three places by now. Stream.periodic(const Duration(minutes: 5) is the only new thing in this epic, which creates a stream that repeatedly emits events.

3. You need to emit AutomaticRefreshDataAction from _fetchMovies() method:

Stream<dynamic> _fetchMovies() async* {
try {
...

yield AutomaticRefreshDataAction();
} on Exception catch (e) {
...

}
}

Everything that we’ve implemented in the example 3 can be seen in this data flow graph to get the bigger picture.

The end

That’s it for now. I haven’t yet explained how you can use the power of redux_epics; this will be covered in one of the follow-up articles. Until then, you can read more about it here. The source code from this example can be found here. Please feel free to post your comments in the comments section.

Master programmers think of systems as stories to be told rather than programs to be written.
― Robert C. Martin

--

--