Flutter promises a lot. It has also quite a few new interesting characteristics that are attractive to a CTO. When is it production-ready for your next app and how does it stack up against native apps and React Native? Will it survive and thrive over time? And for what type of projects is it suitable already now?
Jan 22, 2020
Since this post was written, I have updated and improved the starter app quite a bit to evolve with the Flutter eco-system best practices and close some gaps I felt was there. There is still lots of value in this article, so I recommend you to read it first, and then move on to the new article describing the updates I have done!
When Flutter first came out of beta, I started playing with it and quickly achieved a lot and had great fun. But I couldn’t find much evidence and code that showed how to bring a large Flutter app into production and maintain it over time. As a CTO, I love playing with new technology, but I also want to understand maturity when it comes to flexibility, maintainability, support for features typically needed for large-scale production applications, and life-time cost vs other alternative eco-systems. I ended up pulling together my own Flutter starter app that includes some of the important production app features, just to see how to do it and what the challenges are. In this post, I present what I did. If the interest is big enough, I may cover specific features of the app in follow-up posts.
The resulting starter app can be found at https://github.com/gregertw/actingweb_firstapp. The README describes how to run it and start playing with it. It does not cover all the things I wanted to include (and that are needed in a production app), but it is a good start. Here’s a quick summary of what the app covers in a running starter app:
- Separation of business logic in models and providers, and UI in a separate folder structure
- Use of a global UI theme and custom icons for both iOS and Android
- Use of scoped_model for app state management
- Simple widget framework for handling logged-in, expired, and logged-out states
- State management of login and login token using Auth0 including permanent storage for restarts
- Localisation using i18n and the Android Studio/IntelliJ flutter i18n plugin to generate boilerplate
- Testing using unit test framework and mocking
- Use of a plugin that is not published (flutter_auth0)
- Use of a OS native capability (location tracking) using a published plugin (geolocator)
- Use of Firebase Analytics for usage tracking
- Use of Firebase Crashlytics for crash reporting
Obviously, different people value different things, so let me be explicit on what I was trying to achieve. First of all, separation of code related to UI and business logic makes it easier to get good feature velocity as developers can work on the UI and business logic independently. This is not enforced by Flutter, so you need to be careful when building up the app structure. I created a global UI theme (good), created a provider for authentication using a separate folder with code files for business logic for Auth0 authentication (also good), but copied code from geolocator’s example code where widgets and logic were in a single file (bad).
State management is super-important to avoid high complexity, errors in the UI, slow rendering of the app, and the need to re-write state code as the application evolves. I tried out setState, inheritedWidget, BLOC, Redux, and scoped model (overview of state management approaches here). Of these, I evaluated setState and inheritedWidget to be too simple for a larger project, while BLOC and Redux are both viable choices for applications with lots of data. BLOC fits better in with the Flutter language constructs, but if you are used to Redux, that is probably your preferred choice. For this not-so-data-heavy app, I chose scoped model as a simple, but very powerful state library that fits nicely into Flutter thinking.
There are lots of great posts on Flutter widgets and how to build up your app, but very few that shows how to pull together login screens, storing tokens (also permanently), and handling logged in/out state. A mobile app typically needs an access token to connect to backend APIs. I explored using Firebase as an authentication service, but decided to go for Auth0 as it was easier to show a more generic approach to handling logins and access tokens. Using shared_preferences, I could store the access token across app and device restarts.
There are two things everybody knows should be part of any project, but that you typically add too late: testing and localisation. I wanted to test basic unit testing, mocking, as well as UI widget testing. I played with http_server to set up a mock web server (code is still in the test/ folder), however, I managed to do what I wanted with mocking of Auth0. The UI widget testing is still missing (PRs welcome!), but I added simple app state testing, as well as mocking of login and logout.
Localisation is in its basic form a mess in Flutter, with lots of boilerplate. The only viable option I could find was to use an Android Studio/IntelliJ plugin called flutter_i18n. It uses .arb files with translations for each language and auto-generates the boilerplate necessary to inject a localisation class S into the widgets.
One of the concerns about Flutter is how the platform-independent overlay interacts with the iOS and Android platforms to make apps that can run on both platforms. I thus wanted to both use an OS-native capability in my app (device geo location), as well as test out how to integrate code into the app that needs OS-specific code. The flutter-auth0 plugin is not published and the project was somewhat dormant, but had what I needed for Auth0 authentication and authorisation functionality, so I forked the project and used that plugin for Auth0 support. Callbacks are needed for the authn/authz flows, and this is handled with OS-specific code. Also, the geolocator plugin uses OS-specific location code, but is a published and supported library/plugin, so I could then test both.
Finally, I wanted to add analytics and crash reporting capabilities, as well as test out how to integrate with one of the backend-as-a-service offerings. I explored AWS Amplify and Google Firebase, but from a library support, only Firebase had mature enough libraries, so Firebase realistically was the only option (about six months ago). Any production-grade app needs this support, and while I have not actually put this app into production, the integration was simple enough and with Google having bought Fabric, the Crashlytics part “came for free” (though with a bit too complicated configuration).
Overall, I came out with a positive view on the readiness for production for Flutter apps. Not all apps can easily be implemented in Flutter, but if your app does not have fancy and very custom UI requirements and only uses standard OS-level functionality, you can get a LOT of velocity by having a single code base and a single team. Investing in Flutter is still a bet on the future.
On the downside, you still work in two eco-systems in parallel. Upgrading Xcode/iOS, to new libraries (e.g. AndroidX), and build system changes requires an understanding of the eco-systems and preferably Swift/Kotlin(Java). You need to continuously update to the latest technologies and libraries to avoid that you get stuck in incompatibilities between components. The library/plugin system is powerful and you can lock the dependencies, which is a good thing, but you need to investigate the maturity and history of what you choose to dependent on. If not, you may end up stuck with an important component that is not actively maintained. This means that your team should have a decent level of iOS and Android eco-system expertise in addition to Flutter to make sure you navigate well.
Comparing with React Native, it is an eco-system that has more maturity (but less focused direction), but with similar dependencies on the underlying OS. Flutter has quickly become a viable alternative. At this point, the choice is probably less about technology and more about strategy, people, and skills. If you want your frontend developers to develop mobile apps and they are already using React for web, then React Native is a logical choice. If you see mobile apps more in the context of desktop apps/unified web/desktop development, if you have more business logic, and you want UI/UX to be shared across web, mobile apps, and desktop applications, your business logic developers may be more comfortable in a language like Dart that is very similar to Java.
Many technology choices are of such a nature that engineers tend to forget that often the biggest concern is people and competences, not technical capabilities!
There is still one big missing piece in my evaluation: deployment pipeline and lifecycle management. Since I haven’t put the app into production, I have not tested code signing, beta testing, A/B testing, as well as upgrading the app. There is very limited information to be found on these aspects. However, as with the evaluation on AWS Amplify, I suspect that the only real choice for now (unless you like pain) is to go for Firebase and the eco-system around Google. The same holds for app messaging, a feature that I would love to the app, but I haven’t had the chance to do.
Feel free to interact and help me improve the starter app at https://github.com/gregertw/actingweb_firstapp! I would also love feedback if you are using it for your projects and get interesting experiences!