Flutter and Dartea — create mobile application with pleasure.
Part 1: introduction.
If you tired of all this state mutation mess, or feel exhausted while debugging bunch of Rx stuff, then this series of articles is for you. I’m going to introduce modern way of creating mobile applications. I believe such approach helps us to write clean, maintainable, scalable and predictable code for UI. And indeed, it makes us, as developers, a little bit happier :)
In this article I’m going to meet you with Flutter and Dartea. And, yes, we will create yet another counter app.
Let’s start with what is Flutter. As official documentations says “Flutter is Google’s mobile app SDK for crafting high-quality native interfaces on iOS and Android in record time.” Yes, one more cross-platform framework backed by Google. I’m not going to show and explain every part of basic Flutter application, there are already many blog-posts and videos on that topic. If you’re not really familiar with Flutter, then this links might be helpful:
We just glance at simple HelloWorld application and go through the main concepts and features quickly.
First of all we need to download and install Flutter itself following this instructions. When everything is successfully installed we can create new application using simple command in terminal “flutter create hello_world”.
Let’s remove everything in lib/main.dart and write simpler example.
Flutter uses main function as entry-point. Inside main we call runApp and pass an instance of HelloWorld class to it. HelloWorld is custom stateless widget, it’s basic Flutter’s component. We need to build some kind of a widgets tree to let Flutter’s engine render something on the screen. Inside build method we compose three builtin widgets: MaterialApp, Center and Text. And we should see something like this.
Much more helpful links about Flutter could be found here.
- It uses Dart as main language
- Compiles into native application
- Doesn’t rely on native UI components, render everything on Canvas via Skia instead
- Uses immutable widgets tree (aka virtual-dom) for describing what to draw on the screen.
I’m not big fan of Dart programing language, but its usage in Flutter gives additional advantages, such as hot-reload (one can just save dart file in IDE and see the results on real device in seconds, that’s awesome) and compilation into native code (unlike React Native with its JS runtime).
The most essential part of Flutter is immutable, declarative UI composition. Why does it matter? Both native platforms (iOS and Android) provide imperative approach for creating and updating UI. For those who doesn’t understand the differences between declarative and imperative paradigm:
With imperative approach we say how to do something, and in the opposite with declarative approach we say what to do.
For example if we want to hide progress indicator and show some data. Using imperative approach we can do it like this:
Here we send commands to the View part of our application. Those commands describe how exactly we want to make desired changes: hide loading indicator, show data.
And if want to do the same declarative using Flutter, then it might look like this:
Here we can see that UI part (build method) is completely declarative. It doesn’t say how to update the UI, instead it says what we want to render, and the engine knows how to handle it. Still, we have piece of imperative code — onDataLoaded method and we can get of rid of it easily, but the most important for now is how to render and update the UI using declarative approach.
Now return to our question: why does it matter? Because with declarative approach of creating UI we can decouple a View from a Model easily, and make our View as dumb as possible. This will give us cool benefits such as testable UI logic and predictable UI state. Now let’s discuss how to make it possible.
Let’s create simple counter app to demonstrate all the core parts of MVU architecture.
First of all we need to add dartea library as dependency. In pubsec.yaml file just add dartea: ^0.6.0 in the dependencies part.
Ok, now we can start from creating our model.
Model describes state of our app. It should be immutable aggregate data structure. Dart doesn’t have built-in syntax for immutable data structures like records (or data classes in Kotlin), but we can simulate it with declaring all the fields final and assign them via constructor. Also here we add copyWith method, it’s common pattern in Dart for updating just specific fields in immutable classes.
Next, we need to describe how our model can be mutated. This should be done via messages. Every event that can mutate application’s state should be described as message. Our simple counter model could be incremented or decremented.
Ideally, messages should be sort of sum type or tagged union. Since Dart doesn’t support any algebraic data types, we can simulate it via a common empty abstract class.
Next step could be a view function.
View is pure function which takes three arguments. First argument is Flutter’s context, it might be useful in some situations. The last argument is current model, I think everything is clear here. The most interesting is the second argument. Dispatch is a function which takes a message and dispatches it to the Dartea’s engine. For example here we handle button’s onPressed action and dispatch increment/decrement message. The cool thing about MVU (and all unidirectional data flow architectures) is that dispatching a message is the only way to mutate our state. We can just look at all the messages and understand what kind of mutation we have. Result of the view function is a widget. It’s simple and straightforward: map current model to UI elements tree. And indeed, it’s declarative. There is no mutations of UI elements or any side-effects. Luckily, in Flutter we use immutable widgets trees to compose UI scene.
Nice, then we need to react to messages and mutate our model. That’s what update function does.
It takes current model and a message. In that function we just check a type of the message and mutate the model accordingly. The result of that function is Upd class, but we will look at it later, in next parts of the series. Then new model goes to view function and the cycle is repeating. That’s it. Update function is also pure, it does not really update model, but create new one instead (model is immutable, remember?).
Ok, now we need to put all the pieces together.
To connect all the MVU components together we can use Program class. It’s an essential class in Dartea library, it has everything that we need to describe and run our application. We pass to the Program’s constructor three functions: init, update, view. Two of them we’ve just discussed, and here we say few words about init.
Init function is used for creating an initial state of our app, here we initialize our model with 0 counter.
Ok, we created our program instance, then we need to show something on the UI. For that purpose we create stateless widget CounterApp and pass our program to it. Then inside build we create some basic shell and in the end call program.build() method, it returns a widget which could be mounted into the tree. That’s it. Now our app is up and running.
Woohoo, it works!
Complete source code could be found here.
Let’s summarize what we’ve got.
- Model — immutable data-object. Contains all application state.
- View — pure function, which maps current model to Widgets tree.
- Update — pure function, which takes a message (‘what just has happened’) and current model and returns new model.
All core components of application are pure and immutable. Let’s count benefits:
- Clean separation of model (application state), view and state mutation logic (update). We know that our state can be changed only in update function, and our view function is responsible just for mapping current state to UI widgets tree.
- Immutable model gives us confidence that our state is valid at this or that moment.
- Pure functions are really easy to test and compose.
- Dart doesn’t support immutability out of the box. So we have to do some extra work to achieve it.
- Modeling application using messages (events) and immutable model requires some boilerplate code. We must pay this price, even though it’s not too expensive.
In next articles I’m going to create simple application for browsing GitHub repositories and we will face with basic mobile development routine such as displaying lists, making http requests, navigation and etc.
Thanks for your attention and stay tuned!