Flutter, Redux and Firebase Cloud Firestore — in sync
Cloud Firestore is a great persistence option to keep your data in sync across mobile and web clients. Redux is a great solution to manage your application’s state in a way that’s easier to reason about.
Things get a bit hairy when you try to integrate an external event driven service, like Firestore or Firebase Realtime Database, into the synchronous world of redux. You need to figure out:
- how to maintain the connection between Redux and Firestore
- where to START listening for data change events
- where to STOP listening for those events
In this article I’ll showcase one way of solving this problem. I’m using Flutter as the context, but the ideas I’m presenting could be used in other environments as well, like react/react-native.
What are we doing today?
We’ll make a simple counter app with redux, add Cloud Firestore to it and keep the redux store in sync with data from Firestore.
How are going to do that?
Using redux.dart, flutter_redux and redux_epics.
How exactly? Keep reading… :)
Do you need to know some stuff & things?
- basic understanding of Firebase Cloud Firestore for Android/iOS/web
- be able to make a simple Flutter app
- basic concepts of Redux
- basic Rx* concepts / Dart Streams
Project setup
We’re going to keep it simple and use the default counter app that gets created when you make a new project in Flutter.
So make a new project using either Android Studio, Intellij Idea or the command line with flutter create
.
Right now, the app uses a StatefulWidget
to save the counter. Let's switch it to redux!
Reduxing our counter
I’ll assume you already know how redux works so I won’t go too much into detail how to implement it for the counter app.
Add redux dependencies to your pubspec.yaml
file:
The redux package is a great Dart port by Brian Egan and John Ryan. It’s a plain Dart package, so you can use it for a command line application as well, not only Flutter.
The flutter_redux package (say thanks to Brian!) gives us the glue we need to marry together redux and Flutter.
Implement redux, already!
First, we need to define the AppState
that will be saved in the redux store.
We have one action that can change the state: a simple IncrementCounterAction
.
The reducer is pretty straightforward.
We can put everything together in main.dart
.
The redux implementation is pretty straightforward:
- the store is just a final field of
MyApp
widget; - we use a
StoreProvider
from the flutter_redux package to expose the store to the widget tree; - down in the widget tree,
StoreBuilder
gets theAppState
and renders the counter; - when pressed, the + action button dispatches an
IncrementCounterAction
to the store.
Adding Firebase into the mix
If you need help with Firebase integration, there is a great codelab that does just that. The only difference is that we’ll be using only cloud_firestore
for this demo.
Add firestore dependency to your pubspec.yaml
file:
Again, what do we want to accomplish?
- persist our application state to Firestore;
- each time Firestore emits a data change event we want the redux store to get updated with the latest value.
If we do this right, we’ll be able to count clicks from multiple devices and see changes happening in real time.
“Firestore is our real truth!” Amen!
MyHomePage
is the widget that gets the counter from the store and renders it. As long as this widget is being displayed to the user we want the redux store to be in sync with Firestore.
Flutter offers a very easy way to init subscriptions when a widget is displayed for the first time and dispose them when the widget is gone: we override initState()
and dispose()
methods from the State
class.
But we no longer have a
State<MyHomePage>
, you might say...
No worries, both StoreBuilder
and StoreConnector
have 2 callbacks, onInit
and onDispose
, that we can use.
From flutter_redux:
Perfect! Now we know when to start and stop the Firestore connection.
Let’s add the actions and dispatch them.
We need another action, let’s call it CounterOnDataEventAction
, that will be dispatched each time Firestore fires a data change event for our counter value. This will come in handy in the next section.
Next we need to create a middleware that will watch for our request and cancel actions and manage the connection to Firestore.
Middleware madness with redux_epics & RxDart
redux_epics intro
With redux.dart, a middleware is just a function that receives the store, the dispatched action and a dispatcher (should you choose to let the action flow through).
What redux_epics brings to the table are Dart Streams
, and Streams are awesome for event driven systems like Firestore & Firebase Realtime DB.
An Epic is a function that receives a Stream of actions, handles some of those actions and returns a new Stream of actions.
That’s it. Actions go in, actions come out.
The actions you emit from your Epics are dispatched to your store, so writing an Epic that simply returns the original actions Stream will result in an infinite loop.
Do not do this!
epic dependencies!
Ok, now let’s add the redux_epics dependency to pubspec.yaml
:
redux_epics comes packed with RxDart.
RxDart gives us a Stream on steroids, called Observable. Observable is a subclass of Stream, so we can use it whenever we need a Stream.
sync. what you came for.
Now everything comes together nicely!
Let’s see what’s happening in counterEpic
, step by step:
- we create a new Observable from the actions Stream, so we’ll get operators like
map()
,ofType()
andflatMapLatest()
; ofType()
operator filters the Observable, letting pass only actions of typeRequestCounterDataEventsAction
and casts those actions fromdynamic
toRequestCounterDataEventsAction
;flatMapLatest()
(in RxJava2 it's calledswitchMap
) takes our request action and returns a new Observable that will emitCounterOnDataEventAction
s and dispose the previously created Observable;
(for more details about flatMap, switchMap & co)- each time a document changes on Firestore,
document.snapshots
Stream emits a newDocumentSnapshot
with those changes - make a new Observable out of the
snapshots
Stream; - extract the counter value from the document snapshot;
- wrap each counter value into a
CounterOnDataEventAction
that will be dispatched to our store; takeUntil()
is where we close the connection to Firestore (remember the snapshots Stream?) when the input actions stream emits aCancelCounterDataEventsAction
.
Now we need to update our counterReducer
to handle CounterOnDataEventAction
.
That’s it! We have an Epic that will keep our redux store in sync with Firestore.
There’s only one thing missing: we can no longer increment our counter!
Let’s fix that with another Epic.
Each time IncrementCounterAction
is dispatched, we increment the counter value by 1 and return a new Observable that will emit CounterDataPushedAction
if Firestore updated our counter successfully, or CounterOnErrorEventAction
if something went wrong.
We don’t really need CounterDataPushedAction
for anything, but we should fulfil the Epics contract: actions go in, actions come out, not nulls. :)
I’ll leave it up to you to handle the error action (maybe display a Toast).
wiring up
The only thing left is wiring our middleware to the store.
Both EpicMiddleware
and combineEpics
are provided by redux_epics.
Wrap-up
This pattern allows us to keep the widgets simple and testable and offers an efficient way to hook-up into an external event driven system like Cloud Firestore, or Firebase Realtime Database.
This was a long read, I know. Thanks for hanging in there!
You can find the entire project on github. Also, please give some love to the awesome packages we used in this article.
Leave your questions and feedback in the comments, I’ll do my best to reply.
Note: If want to use Cloud Firestore in your projects please vote for this pull request and the covered issues. I need those timestamps :).
About us
Shift STUDIO is a development studio that works with great companies to create amazing apps. We have been building native apps for Android and iOS ever since mobile became a thing… but Flutter got as all wet and excited like never before!
If you wanna see more articles like this, follow us on twitter @shiftstudiodevs .
Contact us about your digital project.