When it comes to creating mobile applications, there is an unprecedented variety of tools we can use. The native software development kits (SDKs), provided and maintained by Apple and Google, are as feature-rich and performant as ever.
Some cross-platform app building frameworks, like Flutter, React Native, Xamarin and others, are trying to bridge the gap between iOS and Android and help build amazing applications from a single codebase.
When choosing the correct tech stack for your next project, it is important to be aware of the advantages and disadvantages of each approach and make an informed decision based on your use case.
This blog post will help you choose the most suitable app building framework by comparing native, Kotlin Multiplatform (KMP) and Flutter, three of the most popular options. We are going to compare and contrast them to explore their strengths and weaknesses from both a developer’s and a user’s perspective and understand how efficiently they perform.
The task: compare Flutter vs Kotlin Multiplatform vs native frameworks
We wanted to compare building a mobile app using three different approaches – the native SDKs for iOS and Android, Kotlin Multiplatform (KMP) and Flutter. We would then look at the different problems faced during the implementation, compare the implementation time and ask users for some feedback, gathered after a blind test of the different apps.
The requirements were the following:
- Build a simple blog app with a career section;
- Blog posts are fetched from a remote API;
- Careers are fetched from a remote API;
- Use paging to optimise the data consumption of the app;
- Cache data locally to improve the loading times;
- Overall test coverage greater than 75% for application logic;
- Overall test coverage greater than 75% for the app UI.
A couple of important considerations:
- Large parts of the design were actually done during implementation of the first iteration of the apps (native), so this might contribute to a longer implementation time;
- Since there was only one person building all the apps (lucky me) there was a lot of “knowledge bias”: I was exploring the API and dealing with some issues while implementing the first apps, gaining knowledge that I would then transfer to the building of the next apps. This has, essentially, reduced the implementation time.
When it comes to building mobile applications, the native SDKs are considered the gold standard. They provide rich functionality and are optimised for working on the targeted hardware. Therefore, it is considered that both the developers and users get the best experience and the best performance when implementing native apps.
Nevertheless, the native SDKs for Android and iOS are vastly different and there is no built-in way to share logic between them. So, when we want to build native apps, we need to build two separate applications using different tools, get people with different skills on the team and, overall, align and synchronise two different code bases to produce the same outcome.
Here is the tech stack used for the native Android app we built:
- Ktor for networking;
- Kotlinx.serialization for JSON parsing;
- Room for the local database;
- Jetpack Compose for the UI;
- Paging3 for paging;
- Mockk and Turbine for testing.
The frameworks and SDKs used are fairly standard for Android and work very well together. The Paging3 library from Google makes the implementation of pagination from a local and a remote data source quick and easy, and handles all possible edge cases.
The thing that took the most time was finalising and polishing the design that later became the standard for all the other apps.
The tech stack for the native iOS app included:
- Alamofire for networking;
- CoreData for the local database;
- Combine to facilitate the communication between the presentation and business layer;
- SwiftUI for the UI;
- ViewInspector for easier SwiftUI testing.
It should be noted that, at least for now, I am not perfectly familiar and experienced with the iOS ecosystem as a developer. I am learning and I really like SwiftUI because building screens is fast and you can do a lot with a single line of code, which is awesome. It also works really well with Combine, which can make the former a very powerful tool.
The most challenging part of implementing the app was dealing with paging from multiple data sources (remote and local). I could not find any official library or recipe for this (looking at you, Apple).
I first tried to implement it on my own, but as there were glitches I was not happy with, I opted for a third-party library. As it turned out, it also had issues, so I needed to modify it.
Could I have done a better job? Probably, but common sense dictates that something as widely used as paging should have proper first-party support.
The other significant problem I encountered was a strange behaviour in the SwiftUI navigation that caused views to be initialised more than once. Debugging and fixing this cost me over 20 hours.
Summary of native frameworks' performance
All in all, building the native apps was a positive experience from the developer’s point of view. The different modules would click together well; the integrations were good for the most part. It felt a very “native” and “natural” process indeed.
Here are the total build times:
- Android – 39 hours
- iOS – 93 hours (yes, that much)
As I mentioned earlier, I am fairly new to iOS development and it showed. The time spent on implementing the paging and fixing the navigation bug took a huge toll on the overall build time.
To make things more realistic, I decided to take the Android time as a base, since implementing the same app should take roughly the same time for developers if the skill level of the developers is comparable.
The overall experience can be summarised as follows:
- Pros: native frameworks and SDKs work well, providing great developer experience;
- Cons: two different apps, two (possibly diverging) business logic implementations, two different skill sets required;
- Total implementation time: 78 hours.
Kotlin Multiplatfrom (KMP) provides a way to share logic between Android and iOS applications. It capitalises on Kotlin’s ability to be compiled for different platforms, producing native libraries that can be used in native applications.
By default, KMP does not offer a way to share UI, so the business logic (API calls, database caching, business rules and validation) is written once and shared between Android and iOS, while the interface is built natively.
The creators of KMP, JetBrains, have been working hard on a multiplatform UI framework that can be paired up with KMP and help you build cross-plafrom applications in Kotlin. The result of this effort is Compose Multiplatform - it is still not mature enough for production use, but it will support Android (full support, since it’s based on Jetpack Compose), desktop, web and iOS (still in alpha).
Learn more about leveraging Kotlin Multiplatform to share application logic.
Shared business logic
The shared part implemented in KMM may depend on other multiplatform libraries and also call native code via the expect-actual mechanism.
Here is the tech stack:
- Ktor for networking;
- Kotlinx.serialization for JSON parsing;
- SQLDelight for the local database;
- Paging3 multiplatform for pagination;
- Mockk for unit testing.
As you can see, most of the tech stack here overlaps with the tech stack used for the Android native app. This is because most of the core Kotlin libraries are actually multiplatform, which can give you an edge when you have more Android devs than iOS devs on your team and you want to maximise their output.
The biggest challenge around shared business logic was related to pagination. I opted for Paging3, a multiplatform port of the first-party Android paging library, and I had to make sure it was easy to consume from a SwiftUI based interface. That required some wrappers and additional code but I got it working in the end.
Android user interface
The Android user interface used the same tech and architecture as the native Android app. Since they are both Kotlin-bases, it is easy to integrate it to a shared KMM library.
iOS user interface
KMM shared libraries are packaged as Objective-C frameworks for iOS. I decided to hook them up to a SwiftUI interface (again, same in terms of tech and architecture as the native app).
Kotlin and Swift have a different way of dealing with concurrency and streams, so we needed some wrappers to make the whole thing feel seamless. Once these have been implemented, they can quickly be adopted in all parts of the app, so this should not affect the overall implementation significantly.
Summary of Kotlin Multiplatform performance
Using KMM to share the business logic of the app felt like the right thing to do. All API calls, the database structure and the flow of the apps were the same, so having them implemented once and not dealing with two separate codebases is a major advantage from a developer’s point of view.
Integration with Android has been seamless, and when it comes to the integration with iOS, it was easy as well although it requires a bit more work. We can only expect the latter to improve in the future.
Here is the condensed version:
- Pros: the business logic is implemented and tested only once and consumed by a native UI, providing the best user experience;
- Cons: additional work required to provide an idiomatic Swift interface for the shared logic;
- Total implementation time: 56 hours.
Flutter is a cross platform app development framework, created and maintained by Google. It is based on the Dart programming language and provides a way to share UI definitions and business logic across Android and iOS, and can even be leveraged to build web and desktop apps.
When building a Flutter app, the Dart code gets compiled into native C code, which is then run on the target device inside a native runner app (generated when the Flutter project is created).
After some research, I chose the following tech stack:
- Dart.http for networking;
- Floor for the local database;
- Riverpod for DI and notifiers;
- Go Router for navigation;
- Infinite Scroll Pagination for pagination;
- Mockito for unit testing.
Flutter is already quite mature, provides great documentation and has an active community. Developing apps is quick and easy, there are a lot of first party and third party libraries that can help with specific tasks.
Flutter and UI
The only potential issue when using Flutter is the UI. Flutter does not rely on the native system widgets. Instead, it draws all of its UI on a canvas, provided by the native app.
Support for Material Design, Google’s design system, widely used on Android, is built in. There is also support for Apple’s Human Design, which is called Cupertino widgets in Flutter, but there is no easy way to switch between the two depending on the target platform. This means that Flutter will be most useful when we seek a unified look and feel across all the platforms we want to support. If this is the case, Flutter development can be a breeze.
Working in Flutter is fast and efficient. The documentation is great and the community is very strong. The first- and third-party libraries are working well too. The only potential issue, as mentioned earlier, is the unified UI that Flutter promotes.
In a nutshell:
- Pros: fast development, unified UI (if this is what is required), good libraries and docs;
- Cons: UI can perform poorly in certain situations, unified UI (diverging from native look and feel), accessing the host platform can require additional platform-specific development;
- Total implementation time: 41 hours.
All great apps are built with one thing in mind - making the user happy. Thus, the implications that the choice of technology might have on the user experience should play a key role in the final decision.
As a last part of the experiment, the apps were put to the test - they were sent to users. All users received three versions of the same app without knowing the tech stack of each version. They had some time to play around with them and had to fill out a form with their overall impressions. Let’s see how each app performed on the various criteria.
A good-looking UI is a must for all apps. It’s vital for that all-important first impression that the user gets. Making a compromise in that department might be justified in some cases, but not recommended overall.
So, how did the apps do? Here are the results:
The standout here is Flutter. This comes as something of a surprise, because I was a bit worried about iOS users reacting to the Material Design interface of the Flutter app. Maybe the fact that I don’t have heaps of experience with Human Design (Apple’s design system) attributed to the native interface not being on par with user expectations.
One key thing to note is that the UI of the native and KMP apps is identical (as is zero difference), just the data is being delivered in a different way. So the difference in user perception is probably due to some other subjective factor (responsiveness, smoothness, etc).
With user expectations being at an all time high, the performance of your app can give you a competitive advantage. If your app is responsive, delivers information in a fast and reliable way, it will be more engaging for the user base. So staying on top in this category is paramount.
What was the user’s verdict on the experimental apps?
The actual results from this test clearly show that no particular technology has got an edge when it comes to performance. Native and Flutter are most often chosen as both the fastest and slowest, while KMP is firmly in the middle of the pack, providing a stable experience altogether.
This goes to show that modern cross-platform development tools do not necessarily mean a performance downgrade for the particular use case we are experimenting with. Due to each platform’s features though, this might change if the requirements are different.
The concept of smoothness is highly subjective, and though difficult to define, can make users love or hate their experience while using an application. But smoothness is often cited as the factor that makes the difference between one UX and another. That is why I decided to see how the different approaches stack up in that category as well.
So, how did the test audience feel about the apps? I was as curious as you are.
Again, as was the case with performance, there is no clear winner in this category. I did not implement any additional animations or micro-interaction effects on any of the apps, and rolled with the defaults provided by the UI frameworks.
The interfaces of the native and KMP application were the same and they have very similar results. Flutter has a bit more people claiming it was the smoothest option, and this may be due to the fact that the default UI components that come with it have neat built-in animations and transitions that can get you a nice effect with no effort at all.
Yet, Flutter was also named the least smooth option more times than native and KMP. Overall, there is not much to separate those technologies when it comes to smoothness.
So, which app did the best overall? I bet you are asking yourself the same question. Without further ado, let’s dive into the results:
Flutter has an advantage here but it is not a unanimous winner, with native apps coming in second place. I think this is mainly due to the combination of a good-looking interface and speed that Flutter provides.
An important thing to note here is the very specific set of requirements that we had for the experiment. Under different circumstances, the results might have been different.
Cross-platform solutions, KMP and Flutter in particular, have come a long way since first being introduced.
The inconclusive results of the user testing show that there is no actual compromise in the quality of the app if built with KMP, Flutter or in a native way. If utilised correctly, all tools can yield fantastic results.
Each different way of building a mobile app has its strengths and weaknesses:
- Flutter is a good combination of rapid development speed and a good UX if a unified look and feel is an option;
- KMP offers the flexibility of having a native interface with the stability and performance of a natively shared app logic;
- Native apps are not dead. They can be the best fit in certain situations and can give your users the most complete native experience.
When choosing the right solution for building a mobile application, there are multiple factors to consider, including the specifics of the technology, the project requirements, the composition and skills of the team, the roadmap, to name just a few.
Having an experienced partner to guide you through the decision process is key to building a stable, performant, user-friendly application to help you drive business value. Get in touch with the Infinite Lambda team and we will help you weigh the options, build a roadmap and deliver a custom solution based on your business case.