Recently I had the opportunity to work on a React Native (RN) project for a FinTech startup and I wanted to share my own thoughts on the experience whilst also comparing React Native and Flutter technologies. So collected here are some points to consider when developing cross platform Business or Enterprise apps.
Working with React Native
- VS Code for writing the code
- Flipper mobile app debugger for monitoring network calls, native and RN logs, view RN component layout hierarchy and checking the state of the Redux store via a plugin.
RN hot reload — seeing the changes in the running app as soon as you’ve saved your code in the IDE — is not to be underestimated as a significant productivity gain over the native mobile app development experience. The latter requires additional actions to restore the just-modified screen into a useable state again, or more frequently a complete restart of the whole app, followed by re-navigating multiple screens to get back to the screen you were working on!
The Flutter development workflow can be performed with VS Code or Android Studio plugins, but being an Android Studio (AS) user for many years I prefer AS to do Flutter development. AS by itself is already sufficiently functional that you can compile, debug, hot restart, view widget layout hierarchy — all from the one AS window with multiple panes. Flutter also provides hot reload.
RN has a Node project structure and tooling which is familiar to many web developers. As a first time Node user, a native mobile developer soon gets used to its quirks, in addition to those of gradle (Android) or xcodebuild (iOS). You’ll need to gain knowledge of all three to get RN apps working on both platforms.
Flutter has its pub dependency mechanism and custom tooling but there are a lot of similarities between the metadata files: package.json (RN)and pubspec.yaml (Flutter).
Both RN and Flutter provide wrapper commands to compile and run apps on the supported platforms. Both also have platform specific folders (residing under the main project folder) containing the native wrapper iOS and Android app artefacts.
After all this effort, with the runtime code still being the underlying JS, runtime object types may still be different and syntactically “correct” TS is still no guarantee of preventing runtime crashes.
So in hindsight if I had a choice between implementing the project in TS or JS, I’d sway towards JS. The payoff just isn’t there for the not-inconsiderable extra effort you put in as a developer. It’s not uncommon to have .ts (pure TypeScript) and .tsx files (TypeScript with React JSX code) where the type definitions take just under half the total number of lines of source code in those files.
Compare this to Flutter which uses Dart, a strongly typed language that now supports null safety, to virtually guarantee prevention of runtime null pointer crashes due to compile time checks. I would prefer to write in Dart all day and have the knowledge that what I write and what results I get at runtime, are more consistent that I would get from a TS / JS combination. Dart just needs to have a few niceties added, like destructuring assignments, spread syntax being more widely useable, and making semicolons optional, which are real time savers in the TS / JS world leading to less verbose and busy looking code.
After years of wrestling with Dependency Injection frameworks like Dagger, it was refreshing for me to use RN functional components with hooks. No dependency configuration and overall, much simpler code organisation. You can still partition code as you wish, but connecting back end APIs and other non UI code with UI components is simple and straightforward. With React Native and TS/JS, I found I was writing a lot less code already as compared to native Android and even Flutter code, and when you consider that RN code runs on both Android and iOS, there is a multi-fold saving of writing and debugging this code, leading to speedier screen builds and increased productivity.
Flutter had some inspiration from React (and thus React Native) so there are a lot of similarities and a lot of the previous paragraph applies here too. Having said that, one direct comparison is that the way Flutter uses Dart results in a more verbose language, with the norm of using explicitly named parameters rather than positional parameters. The upside of explicitly named parameters is easier comprehension and less bugs due to incorrect parameter positioning. Being strongly typed the IDE (eg. Android Studio), provides a lot more accurate code completion as compared to that available for RN with VS Code, so the developer won’t be typing most of it, but in the end Flutter requires more lines of widget code to achieve similar UI functionality as compared to RN components. Non-UI code is much more similar in terms of verbosity and lines of code if using positional parameters. This LoC advantage that RN has, has to be weighed against the requirement for the RN developer to have greater vigilance of parameters and data structures being passed by RN code, whereas with Flutter — if it compiles and you have null safety enabled code, you can be much more assured that it’s not going to give you any nasty surprises at run time.
For the most part, Business apps perform acceptably with traditional display of information retrieved from a back end API, however your app may have a specific need to perform more extensive in-app processing that would normally result in the screen freezing or janky, stuttering screen updates. This is where multi-threading needs to be considered.
Both RN and Flutter have single threaded execution models (unlike native iOS and Android) so there is additional complexity involved to run code off the non-UI execution thread (JS thread / main Isolate, respectively). With RN you can use worklet threads like what Reanimated and RN multi threading libraries do. Behind the scenes these libraries rely on native implementations to provide the extra threads.
With Flutter you have to use Isolates which run in its own memory space and has limited communication to the main execution thread, and also limited access to the underlying system resources like the file system or other native (plugin) libraries. You could use this library which addresses some of those limitations, or you may be need to write native code to handle the multi threading needs. Implementing the solution in native code necessarily means authoring and testing the solution twice — once per platform and in their own particular languages. Writing unit tests for native code is also very different to writing RN unit tests.
Another caveat to watch for is that the Flutter platform channel — the mechanism that allows Dart code to call native code, is required to run on the main Isolate so the transition from Dart to native code, even for background threads, always requires a little data marshalling on the main execution thread, with its possible side effect of janky screen updates.
React Native multi-threading likely also has this caveat but I didn’t have time to confirm this as documentation for this is hard to find.
If your app has a requirement to do a lot of background processing, it’s essential to do proof-of-concepts to ensure that the handoff between the respective execution threads and background threads gives you the required performance without screen jank.
Redux Toolkit over raw Redux
We used Redux Toolkit which greatly simplifies implementing your app-wide state as compared to doing the same in standard Redux. There is a considerable reduxion (sorry I just had to!) in the amount of boilerplate that you need to write to achieve most use cases, and instead of configuration that is spread out across multiple files that are hard to keep track of, the Redux Toolkit configuration is kept concise and in a single file, improving code comprehension and thus maintainability.
In Flutter Redux and Redux Toolkit is also available, however the recommended approach to Flutter state management has changed over time on this page. I had no issues using Provider but I see that other frameworks have appeared such as Riverpod. Maybe an idea to keep an eye out for updates to that recommendations page.
Native app development skills needed
Experienced native mobile app developers also become accustomed to quirky compilation and native widget behaviour issues that appear unexpectedly and are not obvious on how to fix, so again increased time may be required to diagnose and fix these for developers without a native app background.
Third party library support
NPM provides a vast number of React Native JS / TS libraries. RN itself only comes with a relatively small set of built-in components so for any UI beyond the basics you will need to import a library from NPM. A search of “react native” returns over 35K packages. We found that even fairly simple functionality — such as converting seconds to hours, minutes, seconds — there was at least one library available to save the need to reinvent the wheel (and test it). As for big ticket functionality like camera access, QR code scanning, GPS location access, push notifications, deep linking (universal / app links), audio / video playback etc, there is at least one and frequently more than one, library that’s got that need covered. Being so spoilt for choice we often had to assess several libraries before selecting one.
Flutter comes with an extensive set of Material (Android) and Cupertino (iOS) widgets out of the box, and there are over 14K packages available when searching for “flutter” in pub, Flutter’s equivalent library repository. Once again the big ticket items like camera access etc. are also covered.
Rendering differences between iOS and Android
(In this section I use the term widget to refer to specific Flutter UI components, as well as generically to refer to native iOS or Android UI components that are UIViews or Views).
RN does not directly draw the components you see on screen. It uses the underlying native iOS or Android platform’s UITextView or TextView to draw that UI element, therefore the results you see on screen can differ between the platforms. For instance on a number of screens we had to use different margin values depending on whether the platform was iOS or Android, just so that it looked consistent when the two screens were placed side by side, in order to match the UX design.
Another frustrating issue was mixing pure platform UI elements with RN assisted UI elements. One such example was where we wanted the alert dialog boxes (RN calls the underlying platform’s system alert dialogs) to appear and disappear in coordination with a semi-transparent backdrop. This custom background is not part the platform’s system alerts, so the timing of the two elements could not be precisely controlled from RN. This left a slightly jarring effect with the dialog disappearing at a slightly different moment to when the background disappeared. Synchronising the two would have needed a native code implementation (2x — once for each platform).
Contrast this with Flutter where you have very precise control over what appears on screen regardless of the platform. To the host (native) platform, the Flutter app is just a full screen canvas app, and each Flutter widget draws on that canvas directly, much like a games engine takes over and draws on the whole screen even for text. There are no platform UIViews / Views involved in rendering Flutter app content apart from the full screen canvas. For this to work, the Flutter team had to build highly accurate replicas of the standard widgets of the Android and iOS platforms and to the casual observer they look and behave like the native widgets but they’re not! This is how you can get widgets that look precisely the same on Android, iOS, the web and desktops. It also means you can render exactly the same iOS (Cupertino) widgets on an Android or web or Linux screen, if you really wanted to. So there’s no need to compensate (eg. margin tweaks) for underlying platform widget differences.
The majority of the third party library components we used in the app already had animation built-in where appropriate; however we had one particular UX design requirement that required explicit animation. I quickly found that the animation component that came with React Native would not provide a smooth animation effect of several on screen items at once.
Reanimated is a third party animation library that is widely used and provided the required smooth effects; however to achieve that smoothness the work to animate is offloaded to other threads, including JS worklet threads that are separate from the main JS thread that RN uses. The Reanimated library hides a lot of the complexity of those animation threads using babel to generate glue code that is not seen in your RN source, however one has to be aware of that extra code when calling other library code to calculate the rapidly changing derived values as well as when unit testing any screens that have Reanimated code. The library does come with unit test helpers that are supposed to help with testing animations at various stages of completion however I was unable to get it to to work so had to mock out all the Reanimated code.
Previous experiences with simple animations in Flutter proved easier to work with as compared to Reanimated and I was able to achieve the required level of smoothness without having to resort to work on other threads. This is aided by Flutter compiling to machine code for production code; however even Just-in-time compilation of development animation code was found to be smooth in Flutter debug builds.
Please also refer to the section above on animation as it has some bearing on performance too.
Flutter has no JS bridge in its architecture to slow things down, and compiles to native CPU code for production builds so performance on it is similarly a non-issue.
To see some actual benchmarks of RN vs Flutter vs Native, refer to this nicely detailed article, which arrives at similar conclusions. Performance only really starts to become an issue if your app is particularly animation heavy but otherwise it’s not a concern.
Authentication is an important part of any business app but fortunately all major authentication providers such as Okta, Auth0, Amazon Cognito (via Amplify) and many OAuth providers all have RN and Flutter libraries available so today, this is not much of an issue like it was previously when Flutter support used to be patchier.
We used React Testing Library (RTL) which allowed us to test all aspects of the lifecycle of RN components including mounting and unmounting of the component, interactions such as button presses, the component reacting to the interaction and calling the back end APIs which were mocked, then screen updates (DOM changes)once the data had been returned. In effect mini integration tests were possible with RTL. Snapshot testing also allowed components to be easily verified (DOM is converted JSON so you get verification of textual content, margins and other component styling) with minimal test code.
Unit testing Flutter widgets is more verbose than RN component unit tests, mostly because of the mocking in a strongly typed language in the former, but is still less code to write than traditional Kotlin based (non Jetpack Compose) Android unit tests.
We used the Detox UI automation test framework but it was problematic from start to end, mostly because tests that ran reliably locally, failed often and without clear explanation, out on cloud based macOS CI build agents. We also had library incompatibilities between Detox, Flipper and security libraries. So I can’t quite recommend the use of this library to automation test the app once fully integrated as an app running in an iOS simulator or Android emulator. To be fair, I’ve also used Espresso (which Detox also uses under the hood for android device testing) natively and one of the projects I contributed to, it also ran reliably on a local developer machine but sometime failed mysteriously in cloud CI builds. To date I have yet to come across an entirely reliable on device testing framework. And these are all for apps that are completely stubbed out (running local in-app server) so no external network connectivity was needed. If anyone has come across a unicorn in this on device UI automation test space please let me know!
The biggest issue in React Native continuous integration / continuous deployment we had was the very long build times (1–2 hours) for iOS release builds arising from the need to build all the imported CocoaPod libraries. This is something to be aware of in both RN and Flutter projects. Android release builds in comparison only took about 15 minutes.
A contributing factor is likely the performance specification of the cloud build agents we were provided in Azure DevOps. The need to build CocoaPod libraries doesn’t change irrespective of the build system, so it would be interesting to see whether the time taken would differ significantly in other CI/CD ecosystems.
Choosing React Native or Flutter?
From the above sections you can see that React Native and Flutter provide similar functionality and productivity gains over pure native mobile app development. There may be an advantage in one particular area but it’s also offset by advantages from the other technology in other areas. To me they are so similarly capable from a technical point of view, that I would use other criteria to help in choosing RN over Flutter or vice versa.
Web (or desktop) as a third app platform
With Flutter for web having officially gone to a production ready status earlier in 2021, if your development team wants to target web as a third platform from the mobile app codebase, then selecting Flutter could be the way to go.You’ll get a progressive web app for free after a few project configuration changes, but the PWA will behave like the mobile app design unless you add platform specific code or have screen design that is not only responsive, but changes its norms for web. For example calendar date selection already has different norms between the iOS and Android worlds, and for web you could choose to use the Android Material calendar widget. If your UX has a highly branded design where all the widgets are already customised and don’t look like their native platforms widgets, potentially little work would be required to get a web version out to users. You might need to support web specific interactions such as handling navigation in the web browser’s address bar and how that navigation may be different to the mobile UX. For instance a bottom navigation bar that makes sense on a small mobile device screen, may not on a large desktop web browser screen.
Flutter for Windows (both Win32 and UWP), Linux and macOS desktop is now also officially supported, so if you were wanting to also release a desktop app, a similar argument could be had for selecting Flutter. I have demonstrated this in my previous blogs so can attest that you can get a working app on all these platforms really quickly however the result looks like a mobile app that’s been scaled up to fit the larger windows. You have a working MVP on that new platform for little effort. This can be the basis of platform specific UI tweaks, a much smaller effort than building another app from scratch. (Disclaimer — I haven’t tried building for UWP).
One caveat to bear in mind is that many third party libraries for Flutter only support iOS and Android. Support for web and desktop platforms is improving, but can still be patchy and varies from one library to another, so you need to ensure that a particular feature that you need (such as playing sounds / video) is available for that platform before selecting Flutter, or you might find yourself writing some platform specific code.
React Native, whilst theoretically based on React, ends up being quite different in the actual components used, and our colleague who was experienced in React web development said it was an initial challenge to re-learn what was second nature in React web, for the React Native platform. Often a new RN specific library would have to be imported to do the same functionality that was built into React web. So React web and React Native code artefacts are generally not reusable across the two, although you can try to deploy your React Native artefacts to web or desktop using the following libraries:
I have no experience with the above two libraries so cannot comment on their usability.
Current or targeted development team skillset
If the current development team consists mostly of native mobile app developers, then Flutter is a more natural transition as Dart (at least the variant used in Flutter) is a strongly typed language.
Regardless of RN or Flutter being selected, there is no getting around the fact that the team are developing a mobile app (on two platforms), and that will always involve a certain level of native platform knowledge being required. This is especially true for native app and Apple developer portal / Google Play console configuration. From Google and Apple’s point of view, these are just normal apps being submitted for release to the public on their stores, so must follow the norms, checks and balances for their platform.
React Native has been surprisingly easy to pick up even for someone that hasn’t touched web development for many years. It has a vast third party library ecosystem to choose. This has been a double edged sword as the need to (re-)compile CocoaPod libraries results in multi-hour production release iOS deployments.
For the typical Business or Enterprise app use case where the requirement is to display data in list and detail screens or data entry forms all with a smattering of infographics and simple animations, React Native is easily more productive than writing pure native iOS or Android apps. Simple usage of the device camera, QR code scanner, GPS, push notifications, are all catered for via libraries, although some native app configuration is required for most of these features.
The choice of React Native or Flutter for an organisation’s next cross platform app comes down less to the technical aspects as both technologies are now capable of achieving typical feature wish lists; it comes down more to an organisation’s existing and wanted skillset moving forwards, and whether web is wanted as a third app channel as part of the mobile app build.
As for me, I would still select Flutter over React Native due to a personal preference for a strongly typed language; and also down to the more precise control of widgets I place on a page that appear exactly the same — down to pixel level — regardless of which platform the code is running on.