I’ve recently been approached by a young start-up because they needed an app for their delivery business. Time to market was crucial and the app was supposed to be available for both Android and iOS right from the beginning. Having done native development for both platforms in the past, I knew that this wasn’t going to work this time. At least not if we wanted to keep the cost down to a reasonable level. The app had to be cross-platform, meaning one code base for both platforms. Here’s why I chose Flutter and how it turned out.
There are multiple technologies for creating cross-platform mobile applications. I have built apps using Ionic which allows for rapid prototyping and can certainly be used for polished production apps. Yet having the whole app running inside a web view has its drawbacks and I’m personally not a fan of it. Having done a lot of React web development, the natural thing for me would be to turn to React Native for app development then. To me, React never really felt quite right on mobile platforms, though. I can’t really put a finger on it and I may just need to gain more experience with React Native in order to fully appreciate it.
Flutter caught my attention by providing several benefits, one of which being that it compiles to machine code while coming with its own render engine. This promises a consistent cross-platform user experience as well as good performance. In theory, neither Ionic nor React Native can compete with this.
Dart vs. Swift, Kotlin, and TypeScript
Native iOS development is done using Swift, and for native Android development I’d strongly suggest using Kotlin. While the two seem different on the surface, they share a lot of the same concepts. Ionic projects are usually written in TypeScript, and for React Native development I’d also use TypeScript. I like all three languages very much as they seem mature and modern enough to efficiently create robust applications.
Flutter projects are written in Dart, a language more or less unique to Flutter development. Dart is a pretty modern language, too, and it fits mobile development quite well. The only crucial thing it was lacking at the beginning of this project was sound null-safety, as it was only available on Dart’s beta channel and a lot of packages did not yet support it. As of this writing, null-safety for Dart is stable and a lot of third-party packages have already adopted it.
UI and UX
Probably the best part about Flutter is user interface and user experience design. This is important to me, and it’s usually important to my clients, too. Flutter is very strong with Material Design and there are ready-to-use Material widgets to help create a good user experience with minimal effort. It’s all customizable with your fonts, colors, spacings, and a lot more, of course.
Have a look at these screenshots of our MVP app:
You can see a custom app bar, buttons, text, images, a navigation bar, and a Google Maps integration with custom map markers.
The thing that seems messy at first and takes some getting used to is that in Flutter everything is a widget. There are widgets for transformations, for animations, for user interaction, as well as for layout and Material components. This results in a hugely hierarchical widget tree that can only be managed efficiently with the help of good tooling. Fortunately, Flutter has great integrations for common IDEs. They help a lot with refactoring and rearranging the widget tree.
While I’m still not sure whether I like the widget approach in terms of the code it creates, it has proven to be very powerful and hassle-free in practice. I never felt that I had to compromise significantly layout-wise which in turn allowed for the MVP to already look and feel quite polished with reasonable effort.
The business logic comes down to state management, data modeling, and APIs to external systems.
The list of possible state management approaches for Flutter is long and even includes familiar names like Redux. After some consideration I decided to use BLoC. Writing BLoC code is pretty verbose and it helps to use a code generation package like freezed in order to create immutable classes for states and events. Keeping your business logic inside multiple Business Logic Components helps keeping things predictable and testable.
For working with REST APIs, retrofit is a great package that generates a lot of the code you would otherwise have to write yourself.
As you can see, code generators are used extensively to aid with the business logic side. They can take a lot of repetitive and tedious tasks off your hands.
An important part of software development is quality assurance and especially regression testing. You don’t want to find out that features you once tested suddenly stopped working and you don’t even know when. You also want to make sure that your application can handle edge cases that don’t get tested all the time.
One the one hand, you can spend a lot of time writing automated tests and that’s usually time well spent. On the other hand, it’s not always easy for your client to see why their money should be spent on writing tests instead of adding new features. Fortunately, adding tests to our Flutter application was simple enough to achieve a level of testing I’m comfortable with without going overboard in terms of cost.
BLoCs can easily be tested using the bloc_test package. Best case scenario: You test all your BLoCs and since they’re deterministic you tested all of your business logic.
On top of testing BLoCs, UI testing can be achieved with little effort using the golden_toolkit package. It allows you to render parts of your UI (your widget tree) to so called Goldens. These define how your UI is supposed to look given a specific state. If anything in that UI is going to change you’ll catch that with the Golden test. If the change is intended you simply update your Golden. That way you can be sure your UI doesn’t change without you noticing it.
Together, BLoC tests and Golden tests are pretty powerful when it comes to regression testing.
Communication With the Host Platform
Even with the goal of having one code base for all platforms there are often times use cases that require some platform-specific implementation. In our case, we needed to include a third-party library that was available for native Android and iOS apps only. Flutter provides so called Method Channels to communicate with native code. These are straight forward and easy to implement if you know your way around the native side of Android and iOS. We had no issue using the required native library with our Flutter app. If needed, Flutter views and native views can live side by side and communicate with each other.
Building and Deployment
There are a couple of good solutions out there for building and deploying your apps. I chose a semi-automatic variant and used appcenter for our builds and internal distribution. We then take these same builds and submit them manually to the Apple App Store and Google Play Store in order to retain full control over the process.
This has proven quite efficient so far, as automatic builds include running tests and distribute new versions quickly to internal testers. For releases it’s just a matter of taking the desired builds and uploading them to the stores.
Flutter clearly exceeded my expectations of what’s possible for app development. Over the years I’ve learned to always expect at least one unforeseen hurdle with every project. Flutter has been the cause of not a single one of these during the development of this MVP. My client and I not only managed to release the app in time, we also did a backend and a second app for delivery drivers at the same time. It’s been a productive three months.
Of course, it’s not all sunshine. In order to move forward quickly, I had to use quite a lot of external packages. This has so far prevented me from migrating the codebase to the newly introduced null-safety feature mentioned before. Keeping track of all the dependencies will become increasingly challenging as the project grows and this will need to be addressed in more detail at some point.