Ongoing adventures with Flutter and Redux…

We’re now about 3 months into the project (with a short detour to work on itsallwidgets.com), I thought it may be helpful to share some more thoughts on using Redux to manage state in Flutter.

What about BLoC?

I don’t believe there are ‘better’ architectures, selecting the right one for your app comes down to your specific needs and background. Redux has been around long enough that it’s become a well understood architecture, most topics are covered by detailed tutorials.  They’re typically for React/JavaScript but it’s straightforward to port the ideas to Flutter/Dart.

While I haven’t used it myself developers are clearly very happy with the BLoC pattern. My sense is the differences between Redux and BLoC are similar to Redux and MobX. I think Dart’s first class support for streams combined with the reactive nature of Flutter make the BLoC pattern a great fit. It’s worth noting that under the hood the flutter_redux library is just providing a wrapper for StreamBuilder, you don’t have to feel like you’re missing out using streams with Redux.

I find it interesting that in the JavaScript/React world you have to convince developers not to use Redux whereas with Flutter it seems to be the opposite, that said I really like this tweet from Dan Abramov the co-creator of Redux.

I’m endorsing MobX because I hear its happy users and I hate to see FUD against new approaches. But I still prefer immutability 😉

Another valid criticism of Redux is that debugging the code can require a lot of searching for matching actions. I don’t have any experience developing plugins for an IDE but I’m curious if it’s possible to build a plugin which understands the app state and reducers, enabling you to trace the Redux flow the same way you can drop down into the source code.

Don’t rebuild too much

The main challenge we faced initially was preventing too much of the app from getting rebuilt when the state changed. For example, we weren’t able to keep a bottom sheet open if a setting was selected. The problem was that we were using a StoreBuilder in main.dart, this meant any change rebuilt most of the app. The solution was to move the rebuilding further down the widget tree limiting it to just the parts of the app which needed to be updated.

If you’re basing your app of off Brian Egan’s excellent Redux architecture sample take note that this approach is currently used. It can work for some apps but it’s definitely something to be aware of.

Related to this it’s important to note that all widgets, including non-visible widgets further down the navigation stack, are rebuilt when the state changes. In our app we simply keep the navigation stack short. but you can set distinct to true to only update the view when the state is updated. Note: this also requires overriding == and hashCode on the view model.

Boilerplate Code

I think the main criticism of Redux is that small changes to the functionality can require large changes to the code. In my experience this is accurate. These metrics are from Brian’s comment on the Reddit post linked above.

At this point, scoped_model requires the least amount of code at ~800 LOC. Next best is simple_bloc at around ~900. Finally Redux was ~1200.

It was clear from the outset that app would require a lot of boilerplate code, rather than write it by hand we created a library to help generate it for us.

While I agree less code is generally better than more code (every one less line of code we write is one less line for a bug to hide in), our main objective is to build the best app with the fewest bugs possible. Redux provides a form of state machine for your UI/UX making it far more predictable, that benefit though does require paying a Redux tax in the form of additional lines of code.

Built Value

I mentioned this in the last post but it’s too important not to mention again. If you’re building a Redux app strongly consider using built_value to handle immutability. It’s possible to build a Redux app without it however it would require writing a ton of extra code by hand.

A great feature is that it automatically adds support for object equality, this is extremely useful when managing forms. One challenge however is if the app supports creating multiple blank records they’ll end being equivalent. Our solution is to use a static counter to set a negative id for new records, once they’re saved to the server the ids are replaced.

Although built value supports the @nullable annotation I’d advise avoiding it if possible. To support creating new records in the app we use a factory constructor which sets default blank values. One of the many problems with using the @nullable annotation is a new record will appear changed by the form because the field will be set to an empty string rather than null.

It’s worth noting if you’re making a lot of changes consider using the watch command rather than the build command, it’s much quicker to update the files.

Forms

We created this sample app which demonstrates our approach to forms. To persist state while typing we listen for changes to the TextEditingController, when they’re detected we use the view model to update the store. When the user clicks ‘Save’ we run the form validation, if it passes we persist to the server.

To support localization (ie, number or date formatting) we initially passed all of the supporting data to the forms in the view model however found it required a fair amount of extra code mapping each of the fields and then passing it down. Instead we now just pass the context to a formatting utils class and use StoreProvider.of<AppState>(context).state; to access the state.

If you use this approach it’s important to move any initialization code from initState to didChangeDependencies. This is spelled out clearly in the docs for the State class.

Subclasses of State should override didChangeDependencies to perform initialization involving InheritedWidgets.

Gotchas

Like most design patterns done right Redux can be amazing, however if done wrong it can become painful to say the least. One critical aspect is separating application state from transient UI state. We’ve found we use a combination of dispatching actions alongside some calls to setState() for cases where we’re managing transient state.

To support changing top level settings such as the application theme or locale we use a custom AppBuilder widget, this enables to on-demand rebuild the entire app. You can read more about it here. Our top level widgets are ReduxStore > AppBuilder > MaterialApp, this enables us to rebuild the MaterialApp without affecting the store.

If you’re using a form in a dialog and the state changes the form will rebuild with the latest values from the state, this may or may not be the desired behavior. We prevent it by checking if the form has already been initialized.

In middleware functions take note of whether next(action); is at the beginning or end of the function, it will determine whether or not the state is updated before/after the code is executed.

Debugging

If you’re using the flutter_redux package I’d highly recommend installing redux_logging, it automatically prints the Redux actions to the console. This is really helpful when trying to debug the app.

By default the output will include the entire state (which can get pretty big), you can override the toString method on the state class to just print the relevant info.

Wrap Up

Flutter is amazing… whether you use Redux, BLoC, ScopedModel or something else you’ll be able to quickly build a great app. In my time with Redux I’ve found it works incredibly well for our use case (a CRUD style app). I think a key metric for these choices is whether development speeds up or slows down over time, we’ve found we’re constantly accelerating.

If you’d like to learn more I’ve also written about our overall architecture, managing complex forms and other Redux topics. You can follow @hillelcoren for my latest thoughts on Flutter. If anything’s unclear please let me know.

flutter-logo-sharing

Flutter: How to rebuild the entire app to change the theme or locale

Settings which are defined in the MaterialApp widget, such as the theme or locale, require the entire widget tree to be rebuilt when changed.

This article (and package) by Norbert Kozsir make it easy to set the app’s theme however in our case we need to manage additional top level properties including the locale. All of the settings are stored in our central data store, we just need to tell the app to rebuild after the settings are changed.

This AppBuilder widget (which is a stripped down version of Norbert’s solution) makes it possible for any part of the widget tree to request the app to be rebuilt. I believe the key aspect which makes it work is that the build method calls a builder function rather than directly returning a child widget.

import 'package:flutter/material.dart';

class AppBuilder extends StatefulWidget {
  final Function(BuildContext) builder;

  const AppBuilder(
      {Key key, this.builder})
      : super(key: key);

  @override
  AppBuilderState createState() => new AppBuilderState();

  static AppBuilderState of(BuildContext context) {
    return context.ancestorStateOfType(const TypeMatcher<AppBuilderState>());
  }
}

class AppBuilderState extends State<AppBuilder> {

  @override
  Widget build(BuildContext context) {
    return widget.builder(context);
  }

  void rebuild() {
    setState(() {});
  }
}

You can use this class by wrapping your MaterialApp with it, for example:

Widget build(BuildContext context) {
  return AppBuilder(builder: (context) {
    return MaterialApp(
       ...
    );
  });
}

Finally, you can tell the app to rebuild using:

AppBuilder.of(context).rebuild();

I should note this is probably a bit of an anti-pattern in Flutter. In our app I’ve found it extremely useful as it compliments our Redux architecture. I plan to discuss this in greater detail in the next post…

flutter-logo-sharing

An architectural review of the Invoice Ninja Flutter app

We’ve been working on our Flutter mobile app for a few months now, I thought it may helpful to share some of the techniques we’re using to help keep our code maintainable.

OpenSourcedResized

Create widgets!

Keep your code DRY (don’t repeat yourself). Refactoring widgets is just like refactoring standard code, look for patterns of duplicate code and refactor it out to a widget. Widgets can be incredibly small but if used throughout the app they both reduce code and make it easier to apply changes later on.

For example we created an ElevatedButton widget which wraps a standard RaisedButton class so it always has a consistent elevation and enables setting the color or applying an icon. Another example is this IconText widget which just makes it easier to show an icon and text together.

Wrapping/extending widgets is a great way to customize the framework to suit your needs. We try to follow the convention where the name of the widget is the concatenation of the child widgets it combines.

View models

One of the core principles of software architecture is SRP (Single Responsibility Principle). As the views get more complicated a great way to implement separation of concerns is to create a view model which backs the view.

This enables the view to focus on the layout of the UI while the view models manage the view logic. For example, the view model would handle preparing data (ie, caching using memoize) from the central store to the format needed by the view and provide methods to dispatch actions to update the store.

Completers

In the app it’s common for the user to trigger an action in the UI which depends on completing a successful request on the server. For example, saving a new record.

Initially we passed the context in the action and had the middleware use the context, however we’ve found using completers provides much cleaner code in the UI layer.

We use a utility class to create common completer types. For example. a snackBarCompleter will show a SnackBar message if the request completes successfully or a modal ErrorDialog if the request fails.

final completer = snackBarCompleter(context, localization.archivedProduct)
store.dispatch(ArchiveProductRequest(product.id, completer));

There’s also a popCompleter which is used by actions dispatched from dialogs which will automatically close the dialog and return the response message in the call to pop().

Enums

Our app has many modules, for example: clients, products, invoices, tasks, expenses, … They all provide similar functionality (list, view, edit, archive, delete, restore) and then some provide other actions such as invoice or email.

To support this we use built_value enums. In this case we have an EntityType enum and an EntityAction enum. A nice aspect of this solution is it’s automatically serialized/deserialized when the state is persisted.

Our custom widgets then can accept an EntityType parameter which can be used to configure itself from the store. We’ve also added a lookup function in the AppLocalization class to help with translations.

Persistence

Our solution to persistence is to split up the core parts of the store (data, UI and auth) to persist each part separately. As a user makes changes to a new record we can constantly persist the UI store without needing to persist the data store which could potentially have tens of thousands or records.

If a user starts to create a new record and then quits the app we’re able to present the partially completed record when the app is relaunched. This is handled in the code by having any action which requires persistence (such as when data is loaded or modified) implement the PersistData or PersistUI abstract classes.

class SortProducts implements PersistUI {
  final String field;
  SortProducts(this.field);
}

Abstract classes

We have two types of data in the app: editable entities which the user can create and edit (such as clients and invoices) and static data which can be referenced but not changed (such as the list of languages or currencies).

To support typical interactions in the app (ie, selecting a choice from a list) we’ve created a SelectableEntity abstract class. The built_value classes then implement the class. This provide a way for each entity type to define how it should be presented and searched in a list, we use the subtitle of the ListTile to show the matching field. This class is implemented by both types of data.

The editable entities implement the BaseEntity class which provides shared functionality such as archiving and deleting and handles filtering the list by their state (active, archived or deleted) and status (ie, draft, sent, paid, …).

Clean code

If you’re just getting started I’d highly recommend using a more comprehensive analysis_options.yaml file. Our approach was to start with the Flutter project’s file and comment out if needed. This can be harder to change in an existing app as it can generate thousands of new warnings.

Hope you found this useful, if anything’s unclear or can be improved please let me know. You can follow my thoughts on Flutter on my Twitter feed or subscribe to the blog for more posts. Thanks for reading!