To continue from my last post we’re now two weeks into the development of our new open source Flutter mobile app for Invoice Ninja. I thought it may be helpful to share some of our early experiences with Flutter and Redux.
It seems the main entry point for Flutter/Redux are these excellent sample architectures. In this post I’ll try to detail how we’ve adapted it to support our particular use cases.
The main change we made initially was to restructure our codebase in line with the layout suggested by this project. The top level folders are data, ui and redux which can be thought of as Model, View and Controller/domain. Within the redux and ui folders are separate folders for each of the app’s features (clients, invoices, etc). Using the approach has made it easier to work with related files when digging into a particular feature.
A nice aspect of the sample architecture is that the views are split into ‘container’ and ‘presentation’ widgets (or stateful/stateless), this enables dividing the UI code into view logic and view layout. These terms come from the world of Redux however the term ‘Container’ conflicts with a ‘Container‘ in Flutter. We’re labeling Containers as ViewModels to help clarify their use in the app, we’ve also made the view model class public which enables passing it to the view rather than map each property individually.
There are two main Redux examples provided: Redux and Built Redux. My approach when working with a new technology is to start with the simplest implementation and only add in extras once I’ve felt the pain they’re designed to eliminate. Master the 4 string bass before buying the 5 string…
In the plain sample the models are manually generated. From quick research it seemed json_serializable was the common choice to auto generate JSON serialization code. While the library worked as advertised it left us needing to manually write the hash/equality code required to support detecting state changes in Redux. Changing over to the built_value library (which is also used by the Built Redux sample) solved this as it generates everything needed.
As an example, it converts a Login class from this:
error: A value of type ‘…State’ can’t be assigned to a variable of type ‘…StateBuilder’.
We were initially able to solve it using the toBuilder() method but it had a serious code smell. Thankfully the author of the library suggested a better approach using the replace method instead.
A question that came up early on was how to handle navigation with respect to the app state. As became clear from the comments here navigation already has a state which is being tracked by Flutter. Adding it to the app state would end up duplicating the state in two places. We’re currently managing the navigator in our view models but I recently saw this suggestion to use middleware which we’re going to look into. (UPDATE: to achieve full persistence we’re now storing the current route in the state).
It’s worth mentioning that although most navigation tutorials focus on pop and push, the replace methods can be extremely useful when navigating a user through your app. For example to remove the login screen from the stack once a user has successfully logged in.
In our app each user can have up to five companies under one account. To support this our main AppState class has five companyState properties along with a selectedCompanyIndex field. We then use the index in the app_reducer to only pass the action to the selected company.
Another issue we faced was how to show a snack bar message after submitting an API request from the middleware. Our initial solution was to pass the context as a parameter in the action. This worked as the middleware was able to use the snack bar however it introduced UI concerns where they didn’t belong.
A cleaner approach we’ve switched to is to send a completer with the action. This enables the view model to handle any steps required once the action completes as well as remove the view dependancies from the middleware.
The last feature I’ll review is sorting and filtering. The app needs to handle thousands of records so this can become computationally expensive. To solve this we’re using the memoize library to cache our filtered listed.
The first time the function is called the list is calculated normally. When the function is called again with the same parameters (ie, sort/filter settings) it returns the original result without needing to recalculate which greatly improves the performance.
So far we thrilled with the development experience Flutter with Redux is providing. We’re making rapid progress and should have a basic app ready in just a few more weeks.