Flutter is a cross-platform UI framework that quickly gained traction. Back in mid-2018, it was made available as a release preview. Since then it has moved from an immature technology to a fairly robust foundation for building cross-platform apps. The starter app I tinkered with back then to explore the limitations has become a source for many new (and some experienced) Flutter developers as a starting point for a new Flutter project. Listed on Awesome Flutter and with thousands of reads on Medium and here, I have done a full rewrite of the original blog post to reflect the many changes that the starter app has seen since then.
The original article can be found here (for historic reasons), while the article describing the updates (included in this rewrite) explains some of the details of my experiences and may be of interest to some.
When Flutter first came out of beta as a release preview in June 2018, 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.
Later, I open-sourced the code and shared the original version of this post for others who maybe were looking for the same. I didn’t expect the overwhelming interest, so I started updating what was meant to be a one-off project with new improvements and to new Flutter versions.
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 still does not cover absolutely all the things that are needed in a production app, but it should be a very 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 provider for app state management
- Authentication and authorisation using the https://appauth.io/ OpenID Connect and OAuth2 library
- State management of login and login token including permanent storage for restarts
- Simple widget framework for handling logged-in, expired, and logged-out states
- Testing using unit test framework and mocking
- Localisation using i18n and the Android Studio/IntelliJ flutter i18n plugin to generate boilerplate
- Use of a global UI theme
- Custom icons for both iOS and Android
- Use of Firebase Analytics for usage tracking
- Use of Firebase Crashlytics for crash reporting
- Use of a OS native capability (location tracking) using a published plugin (geolocator)
- Use of Google Maps to present a map of the current location
- Use of an independently defined new widget type called AnchoredOverlay to overlay a map widget
Below are some snapshots from the application.
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. Making changes is also easier as the application grows, because you have less to understand and less options for introducing new bugs. This separation is not enforced by Flutter, so you need to be careful when building up the app structure. I created a global UI theme, created a provider (not to be confused with provider used for state) for authentication using a separate folder with code files for business logic for Auth0 authentication, but in the first version just copied code from geolocator’s example code where widgets and logic were in a single file (bad). In a later refactoring, I rewrote the location state logic into its own model (see lib/model/locstate.dart
).
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 (they are basically low-level building blocks), 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 originally chose scoped model as a simple, but very powerful state library that fitted nicely into Flutter thinking. Google later recommended using provider, and I then refactored everything into using provider for propagation of state changes, but only simple classes to manage state. For more complex state, you probably need something more advanced for state management, like MobX, in combination with provider.
Remi Rousselet, the maker of provider, has also made a newer alternative called river_pod, that you may want to check out. It is not well-proven enough to replace provider yet.
If you are super-confused about state management, simplified it’s really about solving two problems: 1. how to group different state data based on it’s life-cycle (when it’s updated e.g. from your backend or UI interactions) and what belongs together to avoid that the UI is updated when it’s not needed, and 2. how to actually propagate the changes throughout the widget hierarchy and make your buckets of data state available to the right widgets.
There are lots of great posts on Flutter widgets and how to build up the structure of 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. An important element of first_app is thus one way of handling this with shared_preferences, as well as how you can trigger a login using a webview, catch the result asynchronously, and then store the access token ++ in the app’s state. Of course, once the app becomes more complex and with more dynamic state, you will want to capture more of the app’s state and persist it. This requires quite a bit of plumbing right now. Ideally, the state models should be automatically persisted if the app is terminated (unless you can quickly re-build the state).
I explored using Firebase as an authentication service. Firebase is fine if you primarily intend to use the Firebase backends, but if you have your own back-end or need to support various identity providers, then Firebase does not fit the bill. I decided in the end to go for Auth0 as it was easier to show a more generic approach to handling logins and access tokens. This turned out to be a bad choice over time. Auth0 did not show any interest in supporting Flutter, and although the unpublished flutter_auth0 package was later published, it eventually caused conflicts with other packages, did not fully support Auth0 functionality, and I ended up refactoring to use standard OpenID Connect and OAuth0 flows using the appauth package and demo.identityserver.io as an example Identity Provider. There is more information on this in the README.
Authentication and authorisation can be confusing as they are two separate things, but often mixed in implementation. First_app uses such a combined flow where OpenID Connect is used to establish the identity and as part of the same flow, an OAuth2 access token is retrieved to get access to a test API hosted by identityserver.io. If you have your own back-end API you want to grant access to, you could still use a 3rd party identity provider (like the demo server) to establish the identity, and your back-end service would then interact with the identity provider to issue an access token to your back-end API. The highest voted answer to this StackOverflow question shows how this works.
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 the various external services (see mockmap.dart
in lib/mock/
). Later, I added more widget testing, and also integration testing (read more about that in a previous post), so there should be a good starting point in place for building out tests as you go.
Localisation is in its basic form a mess in Flutter, with lots of boilerplate (official docs). 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. This makes first_app wired up for supporting locales and translations of your app into various languages by just following the established pattern.
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 appauth package requires a callback registered in iOS and Android. This was very smooth (see README for details). Also, the geolocator package uses OS-specific location code, something that also went pretty smoothly, even with the interaction with underlying OS permissions to get location (pop-up to request access from the user).
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. This may have changed, however, Firebase offers a lot of things out of the box for app developers and is a safe choice as back-end “to-go”. Any production-grade app needs analytics and crash reporting (though Flutter rarely crashes from what I have seen). First_app is thus wired for both and after having used CodeMagic as a build pipeline and submitted the app to both Google Play and Apple Appstore, I can highly recommend having that kind of visibility into usage!
Using CodeMagic for building and deploying the app turned out to be quite simple, and more about Apple and Google policies, review process, and technology choices for code signing. It is quite a bit of work to submit and get an app through the review process. You need screenshots, policy statements, and make changes to your app to satisfy policies. In the end, Apple did not approve first_app, they didn’t find the functionality valuable enough.
Overall, already back in 2018, I came out with a positive view on the readiness for production for Flutter apps. I then concluded that: “Investing in Flutter is still a bet on the future.” Since then, the Flutter project and community has taken off. By adding support for web apps, a single code base can now support any platform. My conclusion now is that any mobile first project should think seriously through a decision NOT to adopt Flutter.
You still need to work in two eco-systems in parallel. Upgrading Xcode/iOS, to new libraries (e.g. AndroidX), and build system changes requires a basic understanding of the eco-systems and preferably some Swift/Kotlin. 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 be 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 basic level of iOS and Android eco-system expertise in addition to Flutter to make sure you navigate well.
Comparing with React Native, an eco-system that has more maturity (but less focused direction), it has similar dependencies on the underlying OS. Flutter quickly became a viable alternative, and the choice is 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 (though I would recommend a project to evaluate what you could gain by using Flutter). 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!
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!
[…] Готовый шаблон приложения на Flutter […]