Flutter has taken the mobile market by storm. We often hear from our clients that they are considering building an application in Flutter. Although starting a new project with Flutter is often straightforward, the decision is harder when you already have an existing iOS or Android app. There are, however, some ways to build the hybrid app and add Flutter to an existing native mobile application. In this article, we will provide:
It will be based on a real-life case from the banking industry. If you are looking for a list of enterprise Flutter app examples, we prepared an article you should check out.
Add-to-app is a Flutter feature developed by the Google team. You don’t always have to write a Flutter application from scratch. Flutter can be integrated into your existing application piecemeal as a library or module. If you have an existing native mobile application for Android and iOS, you can use Flutter to render only some views in your application or reuse some business logic in Dart between the two platforms.
Flutter add to app can be added to iOS (Flutter can be incrementally added into your existing iOS applications seamlessly with Cocoapods or with pre-generated embedded frameworks) and Android app (Flutter can be embedded into your existing Android app piecemeal, as a source code Gradle subproject or as AARs).
Cross-platform development is a very promising concept for Product Owners. It saves their time and money by maintaining one codebase between iOS and Android apps, making managing the development team with a single Sprint goal easier. Therefore cross-platform frameworks such as Flutter or React Native became the first choice for the greenfield projects like startups and new enterprise ventures.
However, things can be more complicated if there is already an existing application (iOS or Android app) and you consider changing the technology. In such a case, Flutter can be integrated, and this option is called add-to-app. It enables the mobile development team to add the Flutter features to the existing native app.
Yes, you heard it correctly, you can integrate Flutter and take the benefits of cross-platform development even if you have previously developed a native code.
Yet, there are strict cases for when this scenario is worth considering.
1. Rewriting the existing mobile application
This is the most radical approach. It means that you have decided to build the new app with Flutter, which will replace the existing native solution, but you don’t want to suspend releases of the new tasks. In that case, a Flutter add-to-app will help you add new features to the existing Android or iOS app while replacing the existing ones with the new Dart code.
Add the Flutter module scenario works best when one team is working on new features and adding them to the app, and the other mobile team is working in the background on rewriting the native part. Such an approach is recommended for existing apps with a solid user base in a highly competitive market, where it is vital to introduce new features.
2. Getting the arguments for using Flutter technology
Suppose you are a Flutter advocate looking for a way to attract business stakeholders to the idea of rebuilding your current mobile app in Flutter. In that case, it is a good idea to showcase some small, additional features developed as Flutter add to app. This will help you demonstrate the most significant advantage of this technology: ease and development speed. As a result, it increases the chances of getting approval for continuing to implement Flutter throughout the app.
If you are looking for more pros and cons of using Flutter technology, check out also this article.
3. Building proof of concept type of apps
The main point of building proof of concept is to test whether or not a particular concept is possible and beneficial from a technical point of view. This is similar to the point mentioned above. However, it means that you don’t need to release that feature to your end users but only showcase it to some internal stakeholders. This is enough to prove whether your mobile app's performance will be satisfying or not.
4. Implementing a small, isolated feature
Let’s assume that your current native mobile app is working and you don’t have the native team assembled to perform new tasks. Yet, you want to add some fairly isolated features.
Previously the only chance to do so was to consider adding the PWA component and displaying the integrated webview. However, you can stick to the components created in Dart with Flutter. This will allow you to deliver a better experience to the user on Android and iOS apps and save time on gathering the two native teams for the same task on both platforms.
5. Improving the UI of your current application
One of the strongest selling points of building applications in Flutter is the ease of implementing custom, complex user interfaces. And with Flutter add-to-app, it’s no different. This is where Flutter can spread its wings and significantly speed up development. Once you add Flutter to the existing application and starts drawing things on the screen, the UI development is the same flawless experience as with pure Flutter apps. And the app's UI in Flutter looks really well.
The con of mixing two technologies in a single app is that if you want the design consistent across all screens (which is most often the case), many components of the app's UI in Flutter need to be rewritten from scratch. It might be technically possible to reuse some native components, but it kind of breaks the purpose of using Flutter in the first place. And for later maintenance, keeping all the UI components in sync can be a significant managerial challenge.
At LeanCode, we had an opportunity to develop an add-to-app Flutter proof-of-concept for a big client from the banking sector. Our goal was to prove that Flutter was the right technology for a new mobile application that was to be written from scratch.
As part of the PoC, we verified some native features like camera, biometry, and animations. The main part was to rewrite one complex business process in an existing native app. The process involved integrating with existing native screens and APIs, feature flags mechanisms, and doing some background processing. Considering the sector, fulfilling strict security requirements was also highly important.
First of all, the official documentation leaves something to be desired. Flutter add-to-app from the start is not the default, recommended way of creating apps with Flutter. The general process is described in the docs, and it seems straightforward. When it comes to integrating with a big, long-lived native app, where many things often had been implemented in a custom, non-standard way, the integration can cause many hard-to-solve problems.
It's nearly impossible to predict all edge cases, which can cause disruptions in the entire application development flow. Having multiple engines (instances) of Flutter in more advanced integration scenarios increases the complexity. The risks are reduced when we use the more straightforward, isolated approach of adding Flutter or integrating with smaller native apps. Still, some help and engagement of native developers must be considered while planning the work.
You also need to keep in mind that add to app as a long-term solution can decrease the efficiency of your app. In most cases, you would need to maintain two UI component sets and a bridging layer with developer experience decreased due to context switching and longer build times.
When you have decided on add-to-app, we recommend isolating the Flutter module as much as possible in place of a complex integration process because it needs a lot of extra work and brings issues that don't exist in pure Flutter apps. Some complex technical topics can be researched on the side without touching the native app to avoid time spent on integrating with native parts. Remember that the development effort can vary concerning code quality and the technical debt of native applications.
The navigation is where the native and Flutter definitely need to cross. Flutter add-to-app, by definition, means native and Flutter screens work seamlessly together. In our case, though, things appeared more complex.
First, we discovered that app bars in the native iOS app used a custom animation for screen transitions. It quickly turned out that it didn’t play well with Flutter. While it seemed technically possible to handle such transitions gracefully, it would involve much work to make the animations go well together.
Due to all those issues, it turned out that it might be faster to rewrite more screens in Flutter (especially as the UI was not that complex) than to look for a workaround to keep those screens native. And in fact, we did just that - a few screens that weren’t initially meant to be rewritten in Flutter were migrated to Flutter. That way, we could keep the whole process working in Flutter and avoid extra native communication. The entire business logic stayed multiplatform in Dart.
We also discovered that the native iOS app had some generic view controllers that didn’t play well with Flutter screens. We had to develop a custom wrapper for that need which was not a lot of extra work, but it required some native knowledge, and it could be tricky for many Flutter devs.
The business process we were to develop was intrinsically asynchronous - it involved checking some data in the background and displaying dialogs when the flow changed. It meant that we had to develop a way to show Flutter dialogs anywhere in the app, both over native and other Flutter screens. It involved maintaining a couple of Flutter engines and managing their lifetime, a challenge that is strictly connected with the add-to-app integration.
If your app necessitates transitions between Flutter and native screens, every such path has to be taken care of. A bridge must be created for every such transition (in Dart, Kotlin, and Swift). You can make the bridges pretty generic, but that still adds some development overhead that wouldn’t be the case for a pure Flutter app.
Almost every app communicates with backend services of some sort. Unless the Flutter module is heavily isolated (e.g., depends on some third-party APIs that are not used by the native app), another communication point arises between the native app and Flutter.
When rewriting an existing native module in Flutter, two approaches can be taken:
One of the reasons Flutter became so popular and is considered a mobile framework of choice for many developers worldwide is the developer experience it provides.
Once you’re only using one Flutter engine in your app (i.e., the Flutter part is heavily isolated) debugging experience is the same as with pure Flutter apps. But for multiple engines, we have found issues with debugging multiple Flutter contexts. The debugger wouldn’t attach to some engines, and the hot reload would not always work.
With a pure Flutter app, due to the myriad of libraries available and the Flutter approach to rendering, one rarely needs to develop and compile native Android and iOS code (which can take quite a lot of time). Hot reload and hot refresh is very fast, and the developer can immediately see all changes in the code. With native recompilation, all screen state is lost, and a developer needs to click through all screens to retest a feature. Developers lose time recompiling native parts and often need to debug across many IDEs, which hurts productivity.
Another interesting thing is that due to the lifecycle of Flutter engines in add-to-app, the screen state gets cached. So if you’re, for example, going from a native screen to a Flutter screen, do something on it, then go back to the native screen, and then again to the Flutter screen, it will look exactly the same once you have left it.
This is often an undesired behavior. The difference in the screen lifecycle can be pretty confusing to developers. They would expect the screen code to run from square one, but it doesn’t. To properly handle that case without destroying the engine, special native bridges are necessary.
While the UI is pure reusable Flutter, almost every app needs some sort of native integrations (camera, push notifications, storage, geolocation, etc.). Those need Flutter libraries containing native code. Adding such plugins is a very straightforward process in a pure Flutter app (usually, adding it is limited to changes only in the Dart code).
With add-to-app, some native libraries may require extra configuration. It is often undocumented as those libraries are not designed and tested with add-to-app in mind.
The native app we worked on did not use Cocoapods on iOS (a package manager for which Flutter add-to-app has built-in integrations). Because of that, we were forced to embed all native frameworks in the native app manually. That meant building frameworks, opening XCode, and manually adding them for each native library, an action that is easy to forget and tedious for developers.
On Android, on the other hand, we were forced to use Flutter fragments instead of activities because of the existing native app architecture (activities and fragments are two different types of UI components on Android). While it is not a problem in itself (Flutter supports fragments), we later realized that some native libraries we had installed did not work as expected. It turned out that some extra configuration was needed, and the documentation was written only with activities in mind.
Implementing some background services in the mobile app was necessary as part of the process. We needed to check for status changes periodically, and the logic had to continue executing even when the entire application was closed.
We could simply execute the existing native background services, but our goal was different - the business logic had to be a platform-independent module in Dart. We wanted to prove a hypothesis that we can achieve high code reusability even in more complex cases.
Obviously, for all of that to work, we still had to communicate with native code. The background process had to be a separate Dart isolate (a concept similar to a thread), which meant it needed to be run in a separate Flutter engine. The engines can communicate between themselves (in pure Dart), but they consume more resources.
The Flutter team introduced a new concept called engine groups which brings huge optimizations, but it was still unstable at the time we were using it. The infrastructural logic and turning on and off different engines must remain native.
We implemented a bridge for talking with iOS and Android using Pigeon - a handy but not that well-known tool yet for generating typesafe contracts for Dart, Android, and iOS bridges. It needs to be said that code for managing engine lifecycles can be tricky, especially for developers not experienced with native development. For example, a lack of a more profound understanding of the reference counting mechanism in Swift can cause unexpected crashes.
You can find an article about implementing background services in Flutter add-to-app here.
As Flutter has its rendering engine and runtime, it will impact the app size. The increase is not dramatic, but if the app size is very important for your app, it needs to be kept in mind. Remember to measure size only on release builds. Debug build sizes are definitely not representative, as they contain a lot of tooling that only developers need.
The Flutter module we developed consisted of a few Flutter screens, some assets, and a limited number of pub libraries. The size increase for our app was 27 MB for iOS and 48 MB for Android. However, the Android app was still being deployed to Play Store in a deprecated APK format. Uploading in that format is no longer possible for new apps. It is now required to use the superior AAB (Android App Bundle) format.
The problem with APKs is that they need to contain binaries for multiple architectures. In the case of AABs, the app that the end-user downloads from the Play Store is optimized for their device. With AABs, the effective increase would be 3-4 times smaller. Changing the deployment format and, thus, optimizing the size of the app with Flutter can be an extra organizational effort.
If your app contains assets that are also to be used by Flutter, they can be shared between native code on iOS, but unfortunately, at the time of writing, this feature is not yet available on Android. It means those assets will need to be included twice in the final app bundle, and they will increase the overall app size.
When you’re adding Flutter runtime over a running native app, it is impossible not to add some extra performance overhead to the existing iOS and Android app. Flutter needs its own execution environment and even multiple environments in more complex scenarios.
The most expensive operation is starting (pre-warming) the Flutter engine. All assets and the Flutter library must be loaded, a Dart virtual machine has to be started, and the entry point code has to be executed. Depending on the use case, this can be done at app startup, after the app displays some initial data to the user, or only when the user opens the Flutter screen.
It all depends on when you’re willing to sacrifice the extra time the user has to wait. While this can mostly happen in the background, Flutter still needs to block the main thread for up to a few hundred milliseconds during initialization.
With the engine being prewarmed, rendering the first Flutter UI frame also has some latency. In our testing, it was around 100ms slower than rendering the native screen. This was completely acceptable for our case and hard to be noticed by the user. Times of subsequent renders were comparable to native views.
When you open a Flutter screen and then go back to a native screen, by default, the Flutter engine keeps running. The UI state is cached so that subsequent screen openings are faster. This keeps eating the CPU, and you need to keep an eye on RAM.
When your Flutter process is not isolated, there’s a high chance you will need multiple Flutter engines. Every Flutter engine needs resources to run, so the performance overhead increases. However, the Flutter team has recently introduced a new concept called FlutterEngineGroup. It makes it possible to share resources between multiple engines so that the overhead of running another engine is minimal. This means significant improvements for more complex add-to-app scenarios.
For native apps, you most likely have some Continuous Integration system set up so that new iOS or Android app versions are automatically published for testers. The build process can also be integrated with Google Play Store and App Store so that deployments take minimum effort. While Flutter builds to native platform code (APKs and IPAs), some changes to the CI process are required.
There are two basic approaches. Firstly, you can install the Flutter pipeline on build machines - it’s the most straightforward way. Adding a few extra steps (Flutter build, tests, etc.) will complete the Flutter add-to-app integration.
If that’s a problem in your organization, e.g., due to security concerns (as it happened in our case), there is a way around in which you can build the Flutter part to native AARs (for Android) and xcframeworks (for iOS). Then, theoretically, it’s possible to commit those built artifacts to the repository and keep the existing CI process untouched.
It will, of course, lack testing and static analysis of the Dart code, but it will suffice for publishing the app with Flutter for some quick testing. There are still problems with Android, which requires network access to some Flutter Maven repositories. We quickly found out that those were blocked in the corporate network.
Introducing Flutter in a large organization and introducing it as part of an existing native application brings a lot of challenges on the management level. Large corporations often use proxies, artifact management systems for libraries, and custom certificates. The network traffic can be heavily restricted.
Flutter development may therefore require close cooperation with other teams to resolve those issues so that developers can set up their development environment on their machines. This process can take some time, and one has to remember that it can completely block development and waste resources.
When the add-to-app idea is to rewrite an existing part of the iOS and Android app in Flutter, you have to consider whether the whole business process has up-to-date documentation. For long-running apps, the documentation is often outdated, and in the end, developers will need to analyze the native code to understand how the app works. This is much slower than working with well-described requirements, especially if the code quality is poor.
Introducing Flutter can also bring some tension with the native developers currently working on the application. They will need to install and set up Flutter locally to continue working locally. Suppose that brings problems with the environment, workstations, or security. In that case, Flutter devs can deliver pre-built native artifacts (AARs and xcframeworks) so that native devs don’t need any additional tooling for the Flutter project. The approach is suboptimal due to the extra manual work required from the Flutter team.
Understandably, if you would like to rewrite your entire application in a new trendy cross-platform framework such as Flutter right away, it could be perceived as too huge of a revolution. With Flutter, though, you can start step by step. Flutter can be added to existing native apps, even only as a single screen or even as part of the screen.
Adding Flutter in such a way can be a small proof of concept to manifest the new technology in your organization. You can also implement the same feature natively and in Flutter to compare performance and user engagement with A/B tests.
We can take several approaches when considering how to include Flutter in a native app:
You can find a handy tutorial on adding Flutter to an existing application on the official Flutter page.
First, everything written above shows that Flutter add-to-app is not the default approach for Flutter app development. The development process is less strongly supported and as well documented than for pure Flutter apps.
Although this solution might not be the eventual long-term strategy you want to take for developing your application, it is an excellent way to verify the technology and showcase it to stakeholders. Add-to-app is an excellent way to ease migration to Flutter. You can verify the new technology in isolation before making a definitive decision in favor of a complete migration.
Developing the UI and logic independent from native platforms is as quick and efficient as with pure application in Flutter. If necessary, the Flutter add-to-app module can also be moved to a pure Flutter project without modifications.
Consider ordering your current mobile app audit to have the complete picture before starting with the Flutter Add to App.
LeanCode's experts will advise if using this Flutter feature can benefit your case or if it's better to rewrite your entire application. The best way is to reach out via our form.