Building Cross-Platform Mobile Apps Using Flutter
By Mike Behnke, Senior Software Engineer
Kumanu Senior Software Engineer Mike Behnke writes about the development team’s decision to switch from building our mobile applications individually in native code to using Flutter for cross-platform development from a single codebase.
Developing the Roadmap
About 6 months ago, we sat down to have a chat about our current technology stack. We had native mobile apps in Java/Kotlin for Android, Swift/ObjectiveC for iOS and were using Ember for the web app. Every time we would add a new feature, we had to build it three times, test it three times, and deploy it . . . well, you get the picture. As you could imagine, maintenance of this system was challenging since the native apps differed from each other and the bugs we discovered were not only platform specific but individual device specific as well.
Around the same time, the company embarked on a journey that would substantially change the course of the app experience for the better: providing a simpler user experience and making the app more widely accessible. We weren’t sure originally if we were going to adapt the existing mobile apps or if starting new greenfield apps was an option.
There were significant concerns among the dev team that if we tried to refactor and adapt the existing apps that we would add a number of challenges to the existing challenge of creating a new app. The code base was very specific to the application we had built, and we would need to gut a large portion of that. In addition, we were changing so much that even the logic we could potentially reuse would likely need to be modified. And lastly, reusing the existing apps would mean continuing down the path of creating two mobile native applications, and we were very interested in exploring some of the hybrid options available.
Given those challenges and the others listed earlier, our previous experience informed our first decision to start from scratch. So, we had some choices to make – we could keep using the same technologies, or we could look into using one of the available hybrid mobile technologies. It was possible we could end up reusing some existing code by sticking with the same technologies used in our existing apps, but in the end we determined that the advantages of building for both iOS and Android in the same code base outweighed the small amount of code reuse we forecasted.
After evaluating a variety of options, we ended up choosing Flutter/Dart for our mobile applications. Let’s look at the selection process and how we came to that decision.
We have a variety of expertise across our team, both mobile and web developers, and many of us have at least explored mobile-hybrid technologies. Some of us had experiences using various frameworks in the past, but the landscape in 2019 had changed since that time. We wanted to explore what we saw as the major options now without pre-judging them based on those prior experiences.
This is the proverbial elephant in the room. React Native takes the React library and compiles down to the native platform. Originally introduced in 2015, it utilizes bridge code that sits in between the native platform and the compiled application. Rather than talking to an HTML DOM like React does, it communicates across that bridge to native components on each platform.
We did do a brief spike in React Native as a proof of concept and to see what development in React Native would look like. We built the login flow UI using our existing APIs and an initial screen. It was relatively easy to get started, and development went quick. Within the 1-2 day spike we had a rough version of the login implemented, as well as 2 internal pages pulling data from our APIs. We only styled one of the pages during the spike to get a feel for how difficult matching our designers vision would be and overall it wasn’t that difficult to achieve the look we were trying to match.
The ecosystem for React Native is fairly mature given its relatively young age. There is also a large developer community both specifically for React Native but also for React itself, where the knowledge transfer is significant. For a team with a large proportion of web developers, React Native is a very viable choice. However, most of our team was coming from mobile and server development, rather than from a web development. In the end, we came to the conclusion that for our team composition, React Native might not be the best fit.
We had some concerns about performance going this route, and there are plenty of reports that despite speed increases in recent years that apps built using Cordova just don’t quite measure up to native apps. Whether it’s slightly different transitions, the look and feel not being quite the same as native or other minor differences, we were concerned that if we went down this route there would be compromises that could harm the end user experience.
We didn’t spike out Cordova as a number of us have used it in the past, and felt confident after a bit of research that things had not changed so significantly that it invalidated that experience.
Ionic was interesting as we were also considering Angular for our web app, and we could potentially find some code reuse by using Ionic. Being built on top of Cordova, Ionic makes it simple to use Angular to build a mobile hybrid app.
As much as we tried to put past biases aside, those of us that had used Cordova in the past weren’t able to give it a strong recommendation given our new mobile app would be the center of our business strategy. Similar to React Native, Cordova solutions seem aimed at web developers as opposed to those already familiar with mobile development.
Unfortunately for NativeScript, we didn’t spend a lot of time evaluating whether it was a viable alternative. We did a little bit of research, particularly since we were looking at building a web version, and were interested in whether we could benefit from code reuse between the web and mobile applications. Similar to using React Native and React, business logic and non-UI specific code could potentially be abstracted out to a common library used by both applications, just in Angular (or Vue) instead.
In the end though, a number of things worked against NativeScript. It was hard to make a case specifically for NativeScript that couldn’t then be made for React Native, at which case the larger community and contributor-base for React Native, as well as the backing of a large company like Facebook, makes it hard not to choose React Native over NativeScript. We did think NativeScript would be a good solution for those that want a web technology based mobile-hybrid solution that don’t care to work with JSX and React.
One of our existing developers introduced us to Flutter as we started having this discussion, and took some of his own time to do a proof of concept based on our existing application to demonstrate Flutter’s capabilities. It was immediately intriguing how quickly the proof of concept went.
Flutter is a UI framework built on top of Dart designed for quickly building hybrid apps for both iOS and Android mobile platforms from a single codebase, as well as potentially other platforms like desktop and web in the future. In addition, because of how Flutter renders to the Skia engine rather than compiling down to native app widgets on each platform, it promises less cross-platform and cross-device issues. The Skia engine also enables 60fps rendering and beautiful animations, both concerns we had when considering the various web technology hybrid solutions mentioned earlier.
In evaluating Flutter, we had two fairly large concerns. One, it was not even out of beta, and Google hasn’t exactly had the best track record getting things out of beta in a timely matter (see Angular 2’s pre-release cycles). In addition, the community is still relatively small, so resources and solutions aren’t always available as readily as in other, more mature frameworks.
The technology itself was very impressive. While Dart isn’t currently any of our developers’ favorite language and we gave it mixed reviews, it is a very capable language. The Flutter framework provides for very good performance, extremely fast development (particularly as compared to building two separate applications natively). Flutter’s Hot Reload significantly increases developer efficiency compared to traditional native mobile development as changes appear on the development app within seconds.
In our experience to date, Flutter has lived up to most of the promises it makes. It is very fast to develop in. Developing features consists of working up a collection of widgets and then composing those widgets together.
For state management and organizational purposes, we adopted the BLoC pattern. BLoC stands for Business Logic Component and is a common pattern in the Flutter community. Some basic considerations when using the BLoC pattern are:
- Use Stream / Sink for all input and output interfaces
- Inject dependencies into the BLoC class and ensure they are environment independent
- Do not have conditional branching in the BLoC per environment
- The implement of the BLoC pattern is up to the developers as long as it complies with the given guidelines
In the BLoC pattern, we create separate API classes and BLoC classes when setting up API access and stored state. Then the individual widgets fetch their data and state via the BLoC classes rather than accessing the API directly. The BLoC pattern utilizes streams to push data reactively down to the widgets, which then rebuild whenever new data is pushed to the stream.
Given the overall simplicity of the app currently, we didn’t feel we needed to use the Redux pattern at this time, but that it would not be difficult to adapt to it from the BLoC pattern if we decided we did need it in the future.
We extracted common UI patterns out to reusable widgets, which has helped speed development further. Whenever we need a Text or Header, for example, we use one of our library widgets which provide common styling based on the Flutter widget. That means we don’t need to set styling, colors and other UI properties individually, which enables consistency and allows us to adjust themes and styling globally by editing the code in a single place.
In the end, looking back, the question initially proposed to kick off this post:
- Was choosing Flutter was a good choice?
- Would we make the same choice again, given what we now know?
In answer to those questions, it hasn’t been without challenges. We’ve run into bugs in Flutter itself, some of the plugins we are using have had bugs we’ve had to fix, and we’ve even needed to write new plugins ourselves from scratch when things didn’t exist yet in the ecosystem. Given the open source nature not only of Flutter but of the plugin ecosystem, we’ve been able to contribute those bug fixes back to plugins to help everyone.
Flutter specifically does not follow Semantic Versioning and that has been a source of frustration. We ran into breaking changes when our continuous integration server would build using a different (newer) version than the one we tested on. Now we lock down the version of Flutter used, determine when we are ready to update the version and then ensure that we do a full build test when that happens. At times this may mean we can’t be on the latest stable version of Flutter.
When Flutter or Dart don’t have functionality built-in, we’ve almost always been able to find plugins made by the Flutter team or the Flutter community that will serve our needs. Overall, the maturity of the ecosystem was one of the pleasant surprises given the young age of Flutter overall. Some of the things that could be improved in the Flutter plugin ecosystem however is to ensure that all plugins are accessible, that is one area we have found lacking and are actively working to improve some of the plugins we use to include accessibility features that users expect in mobile apps.
Those challenges aside, using Flutter enabled us to get a polished app out into production and accepted in both app stores in about 2 to 2½ months of build time. We likely would have had to focus on a single platform in order to do the same thing in native code, and then spend additional time building for the 2nd platform. In our experience, the speed of development with Flutter may actually be faster than either one of the native platforms alone, which means we currently are achieving the mythical 2-for-1 promise that native hybrid solutions rarely deliver on.
In addition, there are some things that Flutter does really well, like handling page transitions and animations to give the app a unique flavor and professionalism that our customers are really responding well to. The ability for our designers to give the developers a Flare animation means we can integrate fluid animations into the app very quickly without needing to spend time building the animation in code.
We have seen improvements in our testing due to using Flutter. We see fewer bugs reach QA and have less device specific issues. Automated tests now cover both platforms. Widget tests don’t require spinning up a device or emulator, which allows for the tests to run fast and in isolation. When writing tests, we were able to focus on unit testing specific functionality rather than how a user would need to navigate through to a specific widget. We were able to integrate with test coverage tools to find places in our code that didn’t have any test coverage.
These improvements in testing, both automated and manual, specifically address one of the major issues we were seeing when building separate native mobile apps. In addition, since Flutter uses a flexible layout system, our app easily adjusts to multiple screen sizes and formats without a lot of additional layout work, something that was sometimes difficult to achieve in native mobile, particularly iOS.
The experience using Flutter has been an extremely positive one for our team. It is still a relatively young framework, and as such, there will be challenges. But our team found that it lives up to the promises it makes regarding fast cross-platform development as well as providing the ability to build dynamic, immersive applications without significant limitations.