In the following article we are going to compare and emphasize pros and cons of three mobile app development frameworks: Ionic, Flutter and React Native basing on our experiences with creating a commercial mobile app for medical events.
The mobile application itself is not very complicated but allows to compare a few crucial different development aspects of each technology such as third party login libraries, webviews, state-management libraries, UI styling, API handling or storing data in phone memory.
App developer who had more experience in such languages as Java and Kotlin, Flutter, which is based on Dart, seems like a natural choice. Dart language was developed by Google especially for cross platform app development and is not very popular in the developers community, which at first glance may be a deterrent. However, it turns out that it meets all developers expectations:
- it is object-oriented and garbage collected,
- easy to write and similar to popular backend languages.
- it also supports interfaces, mixins, abstract classes and can be strongly or weakly typed.
Dart introduces some custom mechanisms, for example for styling, that are designed especially for mobile apps.
Ionic is especially friendly for web developers, as we are essentially creating a web application that will serve as both web, Android and IOS app. We use the same tools as for construction of a classical Single Page Application - programming language, framework, good practices, libraries, and finally the IDE. Thanks to this, even without any knowledge of mobile technologies, front-end developers can build native-looking mobile app.
Our choice for mobile app development
We decided to use Ionic for the first version of our mobile app, and it successfully went live. During the whole mobile app development process we discovered many pitfalls that one can fall into when using this technology, that will be described in the following sections. After publishing the application, we had time to build both React Native and Flutter prototypes. Both came with their own sets of problems and solutions, and finally we decided to go with the Flutter version that is currently available on the market.
When we talk about differences, we generally consider below-mentioned factors in mind:
For a very long time, React Native was in the first place in multiplatform mobile apps development and was comfortable because solutions to almost every problem could be found in Google. Recently the popularity of Flutter is growing at an enormous rate and there are currently lots of forums, facebook groups and YouTube videos about Flutter. While developing mobile app for a client - Upraisal, we haven’t encountered a problem which hasn't been described in Google for ReactNative or Flutter.
Examples with interesting business cases in mobile apps on Github:
For Ionic, developers will be in the best position if they decide to develop from scratch with this framework - and develop all three platforms at once, iOS / Android / web. It gives a lot of benefits, mainly because of one source code base. The components for creating the user interface provided by Ionic are designed primarily with mobile devices in mind - but they will also work very well for web applications. For example, you can easily change the navigation from the "hamburger menu" in mobile to the sidebar in the case of web applications. The procedure and decisions regarding UI and UX design in web applications are usually in line with the "mobile first" rule - which says that it is easier to design an interface on a smaller screen and extend it accordingly, than the other way around. It's the same with Ionic.
This open source SDK also provides some starting templates, but they are limited and can be compared to starting with a well-known create-react-app tool for React apps. They mostly focus on specifying the default layout for the application.
Flutter and React Native
Flutter and ReactNative provide only mobile development tools, so in the best case developers will have to maintain two code-bases - mobile and web. Both Fl and RN have some full-blown templates that speed up the development process (the one used in our React Native project, Ignite, claims to “save an average of two weeks on React Native development”). Indeed, the template was ready to go, but also a little bit intimidating for a developer with a little experience of ReactNative in the past. After all, we recommend creating some applications from scratch first, before continuing with such advanced tools. For Flutter, we used flutter-boilerplate-project, and we were quite pleased with it.
It is possible to install required additional libraries with Expo and in this process no more additional work is necessary. It also eliminates one of the main bottlenecks when developing iOS applications, which is having a MacOS development device. With Expo, developers can easily build iOS applications on cloud servers. Having an free Expo account makes the process of bootstrapping applications even simpler, because we can see the list of our existing projects in a clear web UI. We were amazed by the testing possibilities and ease of use that Expo provides, especially when we could send a QR code to our testers that will allow them to reinstall an updated version of the app with one click.
Debugging in Ionic is simple - by finding our emulator or connected device in the browser (in Google Chrome: chrome://inspect/#devices), we can view both console logs and network traffic. The application running with the debugger will run slowly and we have had to reboot at times - however these problems are relatively easy to work around.
In RN, there is a very nice tool called Reactotron, that gives us insight to the application. It works in a similar way to the browser extensions, popular in Vue and React web apps, but it’s a standalone application that needs a little bit of configuring to be working properly.
Developing React Native apps in Expo also allows you to debug your code using Expo IDE in your browser and no additional configurations are needed.
In Flutter, the debugging process is really comfortable because we have a few qualitative tools for it:
- DevTool - a tool running in browser, which offers performance and profiling metrics
- Android Studio or VS Code with necessary Dart and Flutter plugins. Both support a built-in source-level debugger with the ability to set breakpoints, step through code and examine values.
- Flutter Inspector - this tool allows to examine visual representations of all widgets in a widget tree or check single widget properties to verify performance issues.
In case of Ionic, building process depends on the tool that we choose - Cordova or Capacitor. Generally speaking, after the web app is built, it is wrapped up with native code that provides the native APIs (like camera access) and allows the final step - building the final .apk. This last step is done in AndroidStudio or XCode for Android/iOS respectively, which means that the whole setup can be quite complex and hard to configure properly. We need both platform SDKs, proper developer accounts etc. Luckily, the preparation of JS code and wrapping it within the native wrapper is done via ionic cli commands, and takes seconds.
Another thing worth mentioning is the difference between Capacitor and Cordova - the former solution is newer and claims to solve multiple problems that occurred in the past, however Cordova still has more plugins/packages available. One of the most important things to note is that Cordova will rebuild the whole android/ios application source on every build, which means that any changes made in the auto-generated files will be lost (this usually happens to permissions or other small but relevant changes). You can communicate with Cordova only via its extensive configuration.
On the other hand, Capacitor will create native code only once, and then will update only the relevant changes. You are even encouraged to commit this code to the repository. This opens up possibilities of writing part of the application in the native code, which can be quite interesting for engineers that already have knowledge of Java/Kotlin/Swift.
During development, Flutter apps run in a VM that offers stateful hot reload of changes without needing a full recompile. For release, Flutter apps are compiled directly to machine code, whether Intel x64 or ARM instructions.
Building a Flutter app is really simple because developers can build iOS and Android apps from the console or using XCode and Android Studio. Building an Android application using Android Studio allows us to create apk or optimized app bundle ready for Google Play release. To configure automatic signing, it’s necessary to add a few lines of code in build.gradle.
Building iOS applications using XCode is not complicated either, and users can upload new builds directly to the AppStore by archiving an app in Organiser. All signing stuff is being implemented by XCode.
Android Studio and XCode allow easy configuration of building modes for different types of operations and devices. Please pay attention that running apps in release mode is unavailable for Simulators.
The process of building depends on the specific tools at play. We used Expo, and it is quite a special case, because we not only have no access to the generated files - the whole build is generated in the cloud. This speed ups the building process, but inability to to interact with these auto-generated result files can make debugging a lot harder.
In mobile devices , the common practice is to disallow the app from integrating with too many native APIs without the user’s consent. That’s why a lot of apps are asking for specific permissions, for example to access a gallery or create push notifications. Setting up these permission requests is platform specific - looks differently in native iOS and Android apps. In Ionic, we had to write them manually for both platforms. ReactNative with Expo was a pleasant surprise - permissions are handled via project configuration in one place, and there is no need to worry about them later on. In Flutter, most of the configuration can be handled automatically, but sometimes it’s necessary to add permission’s descriptions in info.plist for iOS due to AppStore approval purposes.
Flutter introduces a new way of UI styling that is different from what frontend-developers are used to, but simple enough to learn it on the fly. Developers can wrap every widget with another styling widget such as Row, Column, Stack, Padding, Align etc., which are richly equipped with customizable properties adjusted for mobile apps look.
Widgets, which is Flutter's name for what’s known on the web as “components”, can be easily reusable in other widgets in the whole application.
One thing to consider when building apps with Flutter is that styling widgets in an inline manner creates a tight coupling between them, which can get out of hand and result with big and unreadable files, same as inline styles in web applications. Developers must remember to keep their widgets small and pluggable.
React Native contains a lot of ready components, which are also reusable and can be customizable in the whole mobile app. The core of RN’s component design are styles similar to styled-components in React - based on classic CSS. We can separate small objects with styles for every single class or component. There is also inline styling allowed in the form of style props.
In Ionic, we have absolute freedom to select whichever solution works best for our case, at least in theory. We can be focusing on native Ionic components, which are based on web-components and provide native look and feel on every application, or styling with CSS, using UI libraries, styled-components, as we would in web applications. As long as we stick to the former solution, we should be fine - in practice, designing custom components when working with Ionic goes against one of the main benefits of using this framework, and in that case we should reconsider using it at all. For Ionic components, they work like any other UI library components, but sometimes we have to implement by ourselves functionalities usually available in standard JS UI packages (e.g. v-model for Vue is missing).
By default, Ionic apps will look differently on multiple platforms - they will resemble iOS native design on iOS and use material design everywhere else. This is called Adaptive Styling and it’s the default philosophy of Ionic, but if you want to override the styles and make them look the same everywhere, it’s pretty easily done by modifying CSS classes on the main document. One thing to note is that you may then have some problems with handling the height of elements - for example status bar on iOS and Android behaves differently.
ReactNative will create differently looking apps, similar to Ionic, but easily customizable. However, forcing the components to look exactly the same is not as trivial, since there is no easily accessible global stylesheet at play.
Flutter apps are identical on all devices, because they are not using native widgets, but Dart widgets instead. This is useful in most cases, but if you want a native look and feel on iOS, you have to implement it yourself (Flutter starts with material-design as default).
In all of the solutions, there are options for global theming. In Ionic, this can be done by modifying CSS variables used in default Ionic stylesheets. Flutter has it’s own Theme widgets that can be applied to change the appearance of the UI in a global way. ReactNative, as usual, has multiple options also for theming - from Theme components to spreading default theme values into style objects.
We can include a global state with all of the solutions with well-known tools that use the Flux architecture, such as Redux, Vuex or Mobx. In our tests, we used MobX for RN and Flutter, and Vuex for Ionic.
As for Vuex, we had no trouble implementing it - worked out of the box within Ionic app, allowing us to store user data and implement a simple cache for most common requests.
MobX is a really useful state-management library that makes it simple to connect the reactive data of the application with the UI.
The way of developing global state in both ReactNative and Flutter was similar and allowed us to separate business logic from UI. With MobX, Developers can easily mark which classes, methods and variables will be observable (@observable, @action, @computed), which means that these will dynamically react to changes in global store.
Depending on developer needs the store classes can be used in single views, handling only data required in them or can store global state in the whole application using Providers. Providers allow us to pass store classes into descendant components or widgets and use them in a comfortable way.
Because of MobX, the code is much more transparent, mainly due to placing the whole business logic in separate store classes. One downside in using MobX in ReactNative could be that it introduces a new type system that is not entirely compatible with TypeScript, which may be a huge disappointment for TS fans.
This framework has an amazing and powerful HTTP client for Dart - Dio - which supports Interceptors, Global configuration, FormData, Request Cancellation, File downloading, Timeout etc. Dio automatically logs the whole request and response process, and the developer can easily observe all the most important parameters.
RN - as we started the project from a template, we continued with the default HTTP client already included - Apisauce. It’s a nice Axios wrapper with standardized errors and we had no trouble using it.
Here we used simple Axios. ReactNative and Ionic both support the JS ecosystem, so developers can choose between multiple solutions known from the web development world - this is a huge advantage for frontend developers, who can use their favourite tools out of the box.
Mobile applications should be able to handle a lot of mobile specific actions - starting from user gestures, using camera and file system, to push notifications and working in the background. ReactNative and Flutter have proven to be up to date with the current technologies - It was very easy to set up camera integration or social login. For Ionic, simple things like accessing files are easy to set up, but problems arise when we start building complex behaviours, i.e. background tasks or payment flows.
For example, while developing the app with Ionic, we encountered issues with processing in-app payments. We already used Stripe as our payment platform, and one of it’s requirements for their JS library is that the application was served via https. Unfortunately, the Ionic WebView does not provide this capability. We tried to handle the payment by different native plugins, but these were not using the payment intents, a method currently encouraged by Stripe. Ultimately we decided on a simple solution - redirect user to the web application to complete payment flow. This is a common solution that can be found in multiple applications, however it’s suboptimal when it comes to apps that rely heavily on payments.
No native support for Stripe's modern payment methods, especially in Vue + Capacitor setup is a definite downside and should be considered at the start of every bigger project. Additionally, you can forget about modern technologies like ApplePay, which are a standard when building Flutter, ReactNative or native applications.
Flutter is a special case amongst multiple mobile solutions, since it is not converted to Java/Swift upon build, is not using WebView or the OEM widgets. It uses its own rendering engine to draw components, and the Dart code is compiled into native libraries. This leads to a situation where knowledge of Dart is everything that is needed to write apps, but on the other hand it limits the ecosystem only to the Dart packages available. However, since it’s a relatively young technology, most of the commonly used packages are up-to-date. Implementing google authentication with an existing backend took literally minutes.
RN has the whole power of the JS ecosystem, with all it’s ups and downs. Most noticeably, there are a lot of not opinionated solutions for everything, just like for the web apps. But there is no way of using DOM-oriented libraries (including any component libraries) for ReactNative - it is possible only to use the packages and code not related to UI. This significantly reduces the interchangeability of code between ReactNative and React. The former have to rely on it’s own architecture when handling native APIs, and this works fine with common scenarios.
However, problems can appear when we try to build an app that is running in the background, or process complicated gestures in real time. The connection between React’s JS events and native events is handled via a bridge that translates one into another. JS is single-threaded, and this means that some long running actions on the JS side can block the UI. Some actions may also be delayed. This leaves ReactNative behind Flutter when it comes to fully using the potential of mobile.
For JS based frameworks - Ionic and ReactNative - a standard set of testing solutions is available. The most common unit test framework is Jest, as it allows tests of business logic, as well as creating component snapshots (used in pair with Enzyme) for regression testing. Using MobX for ReactNative also allows usage of snapshots to monitor application state transformations.
There is also Storybook - not exactly a testing tool, but can be thought of as such, because it allows easy manual testing of specific components disconnected from the whole application. For Ionic, there are also e2e tests available - we prefer Cypress, and these can be performed when hosting the app in the web mode.
Flutter also offers a few tools for automated testing, with standard solutions such as:
- unit tests for testing simple method, functions or classes
- widget tests for testing single widgets
- integration tests for testing a complete application
Mockito library allows us to mock classes and improve our unit tests quality. Other important testing functionalities can be found in the flutter_test library.
We believe that all of these solutions can be used to create a great mobile application, but there are business cases where we suggest using a specific one.
This is a great solution, but only if you use it’s benefits to the full extent. Need a fast development, native looking app that will work everywhere? Great, Ionic looks like a good fit. Even better if you are not planning to extensively use native APIs and don't worry too much about performance.
- A truly hybrid solution that supports web + iOs + android.
- Best for fast development of simple apps on all of the platforms using native looking components.
- Average performance and native API support
- Doesn’t make sense to choose it if you plan to have a custom design or decide not to use a web application.
- A really weak support for Vue.js
If your team of developers consist mostly of JS experts, you are looking for a good performance and your app can still be classified as typical in terms of UI - choose RN. The main selling point is that people already know React and it takes a little to no time to start coding.
- It shares at least part of a codebase with web apps. We write in JS / TS so our own abstractions (e.g. types in TS, API module, parsers) can be transferred 1:1 from the web project. This can be a very big plus when doing the backend + frontend + mobile project, as long as we separate the logic from the UI.
- It is a best fit for people already knowing React. Manipulating logic, best practices, it’s all there. I’d suggest that it will also be the best fit for any JS/frontend developers - it’s easier to get to know React than write good OOP code without previous experience.
- Is still in beta - and updates can crash your application. This probably won’t happen on a daily basis, but for a big project spanning across multiple months, this can be an issue.
- Not behaving well in certain business scenarios - i.e. working in background, handling complicated gestures. Definitely works best with typical interfaces.
Side note: the RN ecosystem, as well as Ionic and JS in general, is "unreliable" - you have to look at a lot of packages and decide yourself which one to use. Flutter has very good, opinionated common libraries, and these just "work" without trouble. On the other hand: if we were to code something by ourselves, do pull requests or forks, it’d probably be easier to write in JS than in Dart, since we are primarily JS and Python developers.
Seems like the best solution, but you need to change your mindset for it - which makes it harder to introduce it to the frontend team. It’s definitely easier to learn for backend developers, since it relies heavily on the OOP principles. Don’t expect rapid development with no previous experience, because most of the good practices need to be learnt from scratch.
- has out of the box better performance than RN (although for standard applications without animation it will be marginal).
- has the best integration with native APIs.
- Has the strongest community and a reliable library ecosystem.
- has a custom styling solution, that frontend developers must learn from scratch.
- For a project with an existing JS frontend, developers won’t have a chance to reuse code.