I have to admit we are not early adopters…
When you build high-performance mobile apps, aiming for extremely reliable crash-free sessions (and while maintaining very good code quality), being an early adopter is not normally a smart option.
As Head of Mobile, one of my responsibilities is to make sure that our technical decisions never have any negative impact on our business. We must never be in a situation where we cannot release a feature because we took a decision that now prevents it, or slows it down because of a bad architectural decision.
The moment Swift was released publicly, we went through a quick proof of concept period. The outcome was very positive, so we decided to go all in.
File > New Project
Not very long after that, we went through a deep redesign of our client mobile application. At the same time, Kotlin was publicly announced. Again, a quick proof of concept, followed by:
File > New project
When I started in the programming world a few years ago, I took a deep dive into the well-known book “Head First Design Patterns” by O'Reilly. There, I learned a key lesson about programming: Good patterns make the difference. I also realised that you could build a project, and maintain it forever (without starting it from scratch) if you did it the right way.
As the years passed, I learned this was not so simple.
Revolution beats evolution
There are some moments in a project where a full reset is a smarter pick than a refactor. As I mentioned earlier, this happened twice already when we rewrote our apps with a new, superior language. But in-between those periods, we rewrote our apps completely with the exact same language.
Moving to Reactive programming required so many changes that it was simply quicker to do so with a completely new architecture. This was true for Java’s Rx release and then its latest Rx2 version.
One of the main reasons we are not afraid of starting the project from scratch is because of our QA team. They keep an awesome Mindmap up to date in which we can check all the features built within our products. This way we can rewrite our apps, without leaving anything behind.
So far, it has been a huge success. We released many updates for our Mobile Client App and Courier Applications for iOS and Android, including complete rewrites.
Neither clients nor couriers noticed this at all! 😁
Because of this precedent, we are confident to say: We are all-in with Flutter.
So what is Flutter?
For those who do not really know what Flutter is, I would summarise it as:
“A white canvas, with full control of all pixels in the screen, with a single codebase for iOS and Android”.
As soon as Google officially announced Flutter last December, I spent a couple of weeks investigating how it works to determine whether it would fit our needs. One member of my team found an awesome online course in Udemy from Maximilian Schwarzmüller. Soon we’d all completed it and we felt that it could really be a game changer!
We started by building a proof of concept of the “Address Search Screen” of our client mobile application, mimicking the behaviour of the native app and I was totally blown away! There were zero visual differences and the performance was still snappy.
At Stuart, we are currently undergoing a complete rewrite of our platform, with a more scalable database model, event messaging architecture, and some other technologies that will allow us to scale with ease.
Because of this big change, we also had to rewrite our courier mobile applications. We did so with native code: Kotlin and Swift, respectively. And now that we are ready for that, the whole mobile team is working on one single app for both platforms, written in Flutter.
Previously, our team was split between iOS development and Android development. Thanks to Flutter, we have effectively doubled our team capacity!
- We committed with our CTO to release a working version of the new courier mobile application without specifying if it would finally be written natively, or if it would be released with Flutter (the business never stops!).
- We opened our courier mobile application and started writing down tickets on Jira with all the features we saw on the screen. We also wrote down those features that are hidden behind the scenes, but that make our courier mobile app a very complex application.
- We created a Kanban board and started picking the features in the most natural way we could think of. First the login screen, then the networking components, then the network parsing… and so on. Merge conflicts arose quickly, during the beginning, but are now decreasing over time.
We code review via Gerrit.
- We do pair-programming when needed.
- We chat and discuss a lot.
I have to say the path has not always been a happy one. I would raise two big concerns we struggled with, which are:
Firstly, Flutter does not save states at all.
It might be more or less OK for iOS, but if you are an Android developer you might know what I am talking about. There is no onSavedInstanceState or anything similar in Flutter. So every time the app goes background, there is a high chance the app will be closed down.
Once reopened, you will be at the “main” screen again. Sadly there are no generic workarounds so we will have to write our own app state management which will somehow communicate with native code and persist that on disk for later restoration.
Secondly, our app requires to send device geolocation to our server, as well as maintaining a WebSocket connection with our servers. Again, no issues for iOS, but for Android, we have to start a sticky service to keep the code alive.
This is necessary because, when the user taps back, the Activity that holds Flutter is destroyed. When this happens, all the running Dart code is stopped (Including the WebSocket connection). But, thanks to this amazing article, we found a solution:
Run an Android sticky Service that executes Dart code in order to run our background tasks.
To execute Dart code we create a FlutterNativeView that allows us to run any Dart code with just calling “runFromBundle”. But this code is executed in a different Isolate than the one from the main app.
This introduces a new problem: How do background tasks communicate with the main app? To solve that issue, we had to make use of IsolateNameServer in order to share SendPort objects, and then enable the communication between Isolates.
I am sure we will learn a lot through this project, and what I like the most is that we will able to share with you what we learned, what we tried, where we failed and where we succeeded.
Are you all-in with Flutter as we are?
Would you like to see more? We are hiring! 🚀 Check out our open positions.