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!

Continue to part 5 >>

2 thoughts on “An architectural review of the Invoice Ninja Flutter app”

Leave a comment