Image by DigIO

React Native vs Flutter — A comparison

Gary Chang
DigIO Australia
Published in
19 min readDec 6, 2021

--

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

IDE

For React Native (RN) development, I found that Visual Studio Code (VS Code) by itself didn’t give me enough debugging capabilities so I was often switching between three windows:

  • VS Code for writing the code
  • Metro Bundler which serves up the RN JavaScript to the app (used for hot restarting the RN part of the app)
  • 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.

Tooling

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.

Choice of language: TypeScript or JavaScript?

We were mandated to use TypeScript (TS) rather than JavaScript (JS) on the RN project we created. Having developed in strongly typed languages for the majority of the mobile work I’ve done I didn’t mind TS, however one can get into a bit of a rabbit hole and take extra time trying to get type definitions right to keep the TS compiler happy. Sometimes the resulting definition you write looks obscure and non-evident and it’s arguable that it contributes to code readability / maintainability. The imported third party libraries have widely varying TS definitions, and on occasions we had to override the provided definitions because they were simply wrong. We also had to manually create TS definitions for other libraries that didn’t have any TS definitions at all! On a TS project, all libraries must have definitions provided for them.

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.

Photo by iSAW Company from Pexels

App architecture

Architectural layers

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.

Photo by Mel Poole on Unsplash

Threading

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.

Photo by Greg Galas from Pexels

Native app development skills needed

Whilst the majority of the codebase was TypeScript / JavaScript React Native code, about 5–10% of the code was native iOS (Swift / Obj-C / plist) or Android (Kotlin / Java / XML). However this does not reflect the additional effort needed to diagnose and fix the native side code / configuration. For instance SSL security pinning; app permissions (eg. access the device camera); deep linking entitlements; push notification configuration all need to be configured in a native way, and twice: once for each platform. I estimate that up to 20% of the development effort is in this native app space, and this was for a team that already had native iOS and android developers who had done this type of work before. Without such prior experience a lot more ramp up of sometimes arcane native app configuration skills would be required.

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.

Photo by Sharon McCutcheon from Pexels

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.

Photo by CHUTTERSNAP on Unsplash

Animation

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.

Depending on your animation needs you may find that libraries like Lottie may give you what you need without having to worry about threading but does require Adobe After Effects source files.

Photo by SevenStorm JUHASZIMRUS from Pexels

Performance

For a typical business app where the requirement is mostly about showing list and detail screens; data entry forms; with a sprinkling of infographics and minimal animation to keep the UI at least a little bit interesting, the performance of React Native is a non-issue. Having an embedded JavaScript bridge at runtime interpreting JS code is for the most part, fine. If there are other concerns such longer app startup times with a JS bridge, you can switch to using the Hermes JS engine, which does ahead of time compilation for production builds and we found that that was sufficient for our needs.

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

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.

Photo by Artem Podrez from Pexels

Unit testing

One of the highlights for me moving from a strongly typed language (Kotlin, Flutter) to a loosely typed language (JavaScript) for unit testing was the ease with which all code (especially library code) could be mocked. In the former languages a lot of boilerplate would be required using a library like Mockito to mock and stub out fake responses for unit tests. Android had many static methods that meant they were not easy to mock unless you used libraries like PowerMock, so one might drift towards an even more intrusive “unit” testing framework like Robolectric which resulted in considerable lock-in to that library.

In JavaScript — which our tests were written in, not TypeScript — with Jest and its mocking capabilities, I really appreciated the relatively small amount of boilerplate required to mock out any dependent library and write descriptive unit tests. It was easy to write unit tests for pure TypeScript / JavaScript code, as well as RN components because RN is DOM based.

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.

Automation testing

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!

CI/CD

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.

Photo by Karolina Grabowska from Pexels

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 has mostly a web skillset (particularly a React web skillset) or the target being aimed at is mostly web, then it makes sense to select React Native as the everyday coding will involve JavaScript / TypeScript, JSX and CSS-like styling syntax, and Node is likely something the team is already familiar with.

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.

Photo by Jaanus Jagomägi on Unsplash

Conclusion

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.

Less verbose React Native TypeScript or JavaScript code together with unit tests which are easier to write (in large part due to easier mocking of dependencies) are real productivity highlights of React Native.

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.

Originally published at https://digio.com.au on December 6, 2021.

--

--