Flutter architecture: Hooks, Functional Widget and RxDart

Flutter architecture: Hooks, Functional Widget and RxDart
💡
The source code has been updated for latest flutter as of January 15th 2023. Architectural approaches have changed since this article was written, but it's still relevant today.

In 2017 I discovered flutter, the new hotness for building mobile apps that seemingly achieves the holy grail of a singular code-base shared between Android and IOS. In the latter part of 2017, I started work on a new startup, utilising flutter.

Fast-forward to 2019, I'm still working on this startup, and despite numerous setbacks with the still young platform, I've managed to achieve not only a near 100% code-share between IOS and Android, but Web also.

The App

The app will be a basic "Time Tracking App". Initially it'll feature a timer that you can start, stop or reset.

Time Tracking App

Initial Architecture

Architecture is one area where I initially struggled to settle on something that I liked the feel of. The vanilla "set state" way mixes logic with UI, is difficult to maintain, makes sharing state between different parts of the widget hierarchy problematic, and is more difficult to tune for rendering performance. Many of the alternatives such as BLoC and redux seem to require a bit too much boilerplate for my liking.

Over time, I've streamlined my flutter architectural practices. Our goal should be to produce a "reactive" UI, proper separation of logic and UI, minimise boilerplate, keep code to a minimum,  be efficient in terms of widget tree rebuilds and hence, produce a performant app.

Hooks, Functional Widget and RxDart

Flutter Hooks is an implementation of React Hooks. In all practical sense, it serves as a drop-in replacement for existing Flutter state management techniques. Hooks manage the widget lifecycle separately to the widgets themselves, and to the state data that they are bound to. HookWidget is a drop-in replacement for StatefulWidget, minus the heavy boilerplate.

Widgets are classes. UIs can be composed of multiple custom widgets, particularly where different parts of the UI are to be rebuilt independently of each other. Even though HookWidget already saves us a lot of ceremony, we can make our code even cleaner by adopting Functional Widgets.

Lastly, we use RxDart as the glue between our state objects, and the UI; more specifically, we will use BehaviorSubject, which is a kind of Broadcast StreamController that automatically sends the most recent value to any new listeners. Functionally this works like a BLoC stream/sink combo, but without all the extra ceremony and boilerplate. Combined with Hooks, it makes complex Reactive UIs easy-mode.

Okay, let's get started

I'm assuming you've got the following already:

  1. Flutter (this code is built against stable 3.3.10)
  2. Visual Studio Code
  3. The following Visual Studio Code extensions: Flutter, Dart

Setup Project

In a terminal, type the following (feel free to use your own org).

Run Project

Open the project folder you just created in Visual Studio Code and hit F5. It should launch the default flutter todo app either in chrome, or in whatever device you might have plugged in.

Now to trash all that starter code!

Add some dependencies

Open pubspec.yaml and make it look like this:

💡
The visual studio code extension: pubspec assist is useful for working with pubspec files.

You'll note that we've added rxdart, functional_widget_annotations and flutter_hooks. Additionally, we've added the code-generation bits for functional widgets under dev_dependencies.

Code-gen stuff

Before we go too far, let's update our .gitignore to ignore generated code. Add this to the end of the file: *.g.dart

Next, we want to have the functional widget package generate code for us automatically on file save. In a terminal, make sure you are in the project folder, and type the following:

Folder structure

I like the idea of keeping code organised by feature / page as opposed to into views/models folders. We'll be using a folder structure like so:

lib/main.dart

Replace main.dart with this... We'll change "home" to the the timer page shortly.

What's going on here? Importing functional_widget_annotation.dart allows us to mark functions with @hwidget. Our build runner will generate a HookWidget class from that function. The "part" line tell us that there will be a generated file at main.g.dart.

Once this file is saved, the code generator should create the widget boilerplate for us at main.g.dart.

lib/pages/timer/timer_page_model.dart

This is the first part of the 2 part puzzle re state management. Essentially, this file is like the ViewModel in MVVM architecture. Think of it as the mediator between the View (Page), and the Model (in this case, its just our simple timer data so its embedded in the viewmodel.) It contains a private stopwatch and timer for tracking internal state, and a series of public BehaviorSubjects which various parts of the UI will subscribe to.

💡
We've also added a "speed multiplier" so we can test hours in minutes if need be.

lib/pages/timer/timer_page.dart

Let's break this down... there's a lot going on.

  1. This file actually contains 3 separate classes that inherit from HookWidget. That's a lot of excess code and excess files avoided. You can find the plumbing in the timer_page.g.dart autogenerated file.
  2. You'll note judicious use of hooks. For our ViewModel, we are using "useMemoized" which basically ensures that it only gets created the first time it's built and is otherwise served from cache. Another hook, useContext gets us access to the BuildContext from anywhere within a HookWidget.
  3. The useStream hook above will rebuild the entire widget (and its widget sub-tree) when the underlying BehaviorSubject has data added to it. It is imperative that you only make use of this hook at the deepest appropriate level within the widget hierarchy. This is achieved by encapsulating that part of the widget tree with an @hwidget annotated function, and by invoking the generated class instead of the function. This is the most important rule about utilising this architecture.
💡
A couple of notes on hooks: there are some usage rules. The main usage rule is that all hooks need to be called all the time on each build, there shouldn't be conditional paths that result in more or less hooks being used. Hooks take care of their own lifecycle (dispose, etc), and do not require additional lifecycle management.

One last thing, update app/lib/main.dart

Change our empty Container to a TimerPage.

All done, what's next?

This should run just fine on whatever emulator or device you have with a press of F5 (including web).  Accompanying source code can be found here: https://github.com/paddo/flutter_architecture_part1

Whilst the code has been updated as of Jan 15th 2023, This article was written in 2019 and a lot has changed. I'll be writing some new articles on current thinking re app architecture, though a lot of the bones/concepts outlined here still ring true.