App architecture: MVVM in Flutter using Dart Streams
Since there are almost no constraints to your architecture in Flutter, it’s fairly easy to run into this problem. Some developers write all of their code in the Widget until they realize the mess they produced. Reusing code in other Projects seems impossible and in the end, you write most of your code twice. MVVM tries to solve that by splitting up business logic and view details.
In this article, we show you how MVVM with Flutter could look like. We’ll create a functional reactive ViewModel using Darts Stream API.
MVVM
Before we look at any code, we should get a basic understanding of MVVM (Model-View-ViewModel). If you’re familiar with MVVM you can skip this part.
The main goal behind MVVM is to move as much of the state and logic from the View into a separate entity called the ViewModel. The ViewModel also contains the business logic and serves as the mediator between the View and the Model.
The ViewModel has basically two responsibilities:
- it reacts to user inputs (e.g. by changing the model, initiating network requests or routing to different screens)
- it offers output data that the View can subscribe to
The View, on the other hand, does not contain any business logic. These are the responsibilities of the view:
- it reacts to new output states of the ViewModel and renders them accordingly (e.g. by showing a String in a text field)
- it tells the ViewModel about new user inputs (e.g. button-clicks, text-changes, screen touches)
In contrast to popular MVC approaches the Fragment/Activity/UIViewController does not contain business logic in MVVM. It is a humble view that simply renders the ViewModel’s output states. The ViewModel does NOT KNOW the View (a difference to forms of MVP and MVC). It simply offers output states that the View observes:
MVVM in Flutter
In Flutter, the Widget represents the View of MVVM. The business logic sits in a separate ViewModel-class. The ViewModel is totally platform-independent. It contains no dependencies to Flutter and can, therefore, be easily reused e.g. in a web project.
That is one of MVVM’s biggest powers. We can create a Mobile App and a website that both share the same ViewModel. You don’t need to reinvent and write the logic twice.
Example: Email Subscription Widget
Let’s look at an example – We’ll implement a Newsletter signup-form with an email textfield and a submit button. The button is disabled and the user sees an error if the email is invalid:
The ugly way
Without any specific architecture, the business logic and the current state are part of the widget. It could look something like this:
The problem is that view logic, view state, and business logic are mixed up. That leads to a few problems:
1. It’s hard to unit test
2. Other dart projects cannot reuse the business logic since it’s intertwined with Flutter-dependent View logic
3. This style gets messy very soon and you end up with huge Widget classes
Let’s see how we can improve this…
Solution with MVVM
As explained above, the ViewModel has Input and Output parameters. We will add an ‚input‘ or ‚output‘ prefix for the sake of clarity.
All Inputs are Sinks. The View can use those to insert data into the ViewModel.
All Outputs are Streams. The View can listen for changes by subscribing to the Streams. The interface for our ViewModel looks like this:
We are using a StreamController as an input Sink. This StreamController provides a stream that we can use internally to handle those input events.
Binding a View to the ViewModel
So how to supply inputs and handle output events?
To supply input values to the ViewModel we insert them into the ViewModel’s Sinks. We’ll bind a Widget to the ViewModel. In this case, we insert the TextField’s text whenever it changes.
You listen to the ViewModel Outputs by subscribing to the Output-Streams.
Flutter provides a really cool Widget called StreamBuilder that will update whenever a Stream provides a new value. We won’t call „setState“ ever again!
The StreamBuilder‘s “builder” method gives you a snapshot whenever it builds. This snapshot contains information about the stream, its data, and its errors. If our stream did not emit any value, “snapshot.data” will be null. So, be careful.
QUICK TIP: Try to help the Dart compiler when working with streams, add all the needed generic types to avoid runtime errors.
Here you can see the whole picture:
As you can see, the View’s only responsibility is rendering Outputs and supplying Inputs to the ViewModel. Our Widget is therefore super slim and easy-to-read.
Conclusion
We started out with MVVM in the native world and wondered if it would also work with Flutter. After trying it out, we can say: MVVM is a great fit for Flutter as well. We love it how nicely the View-logic is separated from the business logic, how easy ViewModels can be unit-tested, and how seemingly Dart ViewModels can be shared with other platforms that are using Dart. The Stream-API takes some time to get used to but afterward, it feels very smooth. For more complicated tasks we used RxDart which adds functionality to the standard Stream-API.
If you’re just hacking a small app, then the normal “put-everything-in-one-class” approach might be more straightforward. If you plan to build a bigger app though, MVVM might be the architecture for you.