React Native vs. Flutter—a Biased Opinion
React Native vs. Flutter—a Biased Opinion
What is better?
This eons-old question in one of its myriad variations rears its head yet again, this time pertaining to the choice of (cross/multi)platform framework for mobile development, the one ring to rule them all.
Should I choose React Native or Flutter?
The conflict rages, both sides have their supporters and, as always on the Internet, there are hundreds of answers that contradict each other and offer you no help in making the decision.
Heck, why not throw in another?
- Source: XKCD.com *
Should you believe me?
I mostly develop for Android, which I like even though it has its quirks, I like tooling dedicated to that, and I love Kotlin. That said, I’ve had my share of romancing with (cross/multi)platform frameworks. I’ve worked on React Native projects, both from an existing base and from scratch. I participated in an AR Unity project. I’ve experimented with creating Flutter UIs and have recently worked on a Flutter library, which had to use the system APIs for hardware handling.
I tend to be skeptical and try not to get hyped about things, though I sometimes fail in the latter.
While I can’t answer the question should you believe me, I hope that those few words of introduction will let you form an opinion on that.
Well, let’s get to it, I imagine you don’t have all day!
The idea to write once and run everywhere isn’t even that fresh, and the two competitors from the title emerged long after their precursors, but because of that, their authors had time to learn from past mistakes and do things differently enough to gain significant traction in the mobile world.
Both solutions are based on similar concepts, both value composition over inheritance, and use declarative UIs. They are also both supported by tech giants and open-source, which gives hope for long term support and evolution.
This point might seem weird since both frameworks use different languages, but there’s one important common element between them—they’re both not native to either of the platforms. This means that if you have an existing mobile team, its members will most probably have to learn a new language, and they might not be happy about it.
Both solutions are organized around components (or widgets—different name, same difference, at least at this level), which are composed of smaller components and yet smaller components until you get to the most basic ones, which are provided by the framework itself. These components are different from native views on a conceptual level, which might make the switch a little more painful, but the philosophy behind them just makes sense. It might be worth getting acquainted with it, especially with Swift UI and Jetpack Compose following suit.
Communication with native APIs
Since both frameworks run beside the native stack, they use mechanisms for asynchronous communication with the native stacks. In React Native, that would be methods annotated with
@ReactMethod that accept
Promise types or emitting events through
RCTDeviceEventEmitter, while on Flutter those are
MethodChannel with its
The advantage of Flutter here is that
MethodChannel works both ways and lets you call Dart methods from native code as well as native methods from Dart, which makes the contract between the two a little cleaner.
Both depend on strings for passing method names.
If you ever worked on a native mobile app, you must know the pain of waiting precious minutes for it to compile and checking how that one input box looks like after moving it a bit. And then making another small change again. And again. And again.
Well, hot reload takes care of that. It’s only seconds from making the change to seeing it on a screen, though it increases the start up time of the app. Benefits definitely outweigh the costs.
As always the devil’s in the details, and while both are similar in broad strokes, it’s the minutiae that matters most.
But you have listed language in similarities!
Static type system
Since at least some of the dependencies, you decide to use will use pure JS, you will either have to come to terms with the risks they introduce concerning type safety or create wrappers for them yourself.
This summer, Facebook introduced Hermes, a new open-source engine optimized for React Native that uses ahead-of-time (AOT) compilation to bytecode that is then interpreted in runtime, increasing its performance.
While Flutter’s Dart can be run in JIT (just-in-time) mode on its Virtual Machine (and actually is in debug mode, which allows for the hot reload), it also supports ahead-of-time (AOT) compilation to machine code, making it lightning fast on production.
Dart doesn’t support overloading, for example. JS, on the other hand, has bizarre handling of
thisand closures and, in general, happily lets you type any rubbish and shoot yourself in the foot with a typo in a method name, which you’ll find only in the runtime. This, in turn, will nudge you into rethinking your entire career.
Flexibility in creating components
Flutter supports only one way of creating new widgets. From my perspective, that is a good thing.
While instantly loading code changes without waiting for the whole app to recompile is a great advantage over native in itself, it also had a pain point—the loss of state.
Until very recently, Flutter had a clear advantage here, since it was the only one to support stateful hot reload. So, you are not forced to retrace your steps to see that one little view ten screens into the flow of your application or tamper with screen order just to shorten the development cycle.
This has changed in September with the release of React Native 0.61 with a new feature called Fast Refresh. I did not have the chance to use it. It doesn’t support class components, which is a bad thing for legacy code, but probably a good idea to encourage moving to much clearer function components (with possible hooks).
This is one of the most important differences, both on conceptual and execution levels. While React Native approaches the UI by wrapping the native views from both platforms into JS components, Flutter wants nothing to do with that. Instead, it bundles its own rendering engine. By doing so, Flutter makes sure it can optimize redrawing performances and that the UI looks the same on each supported device and even lets you switch “platform” on the fly, as I’m sure you’ve already seen in one presentation or another. If not, you might want to read this introduction to UI testing in Flutter.
While this makes Flutter faster and lets you worry less about the widget rebuilds, since they’re optimised under the hood, this might cause it to lag behind on platform novelties, or even skip them entirely.
React Native, on the other hand, is using those native views internally anyway, and it is fairly easy to wrap a native view in a React component on your own.
This has two aspects:
The execution speed
- As I’ve mentioned above, the Dart code runs faster, because it doesn’t introduce interpretation overhead.
- Since Flutter is using its own renderer, it avoids additional bridge–passthroughs.
The development speed
- Since views rendered by Flutter are platform agnostic, the developer knows that the layout will look the same on similar screens, no matter the platform.
- Type system lets you move faster with greater certainty, making less silly mistakes on the way.
- Type system and IDE support allow you to more easily get acquainted with widgets, without external help from docs site.
- First-class IDE support and generally better tooling lets you focus more on the problem at hand.
Both use a similar system with a file in a widely-used data-definition format defining dependencies. However, thanks to scripting magic, Flutter doesn’t require additional install steps when importing dependencies with native code. Nothing big, but a nice touch.
The last time I checked, an empty React Native application created through its generator installed over 800 modules to
Dart, on the other hand, is younger and less popular, but ever since Flutter’s official release, it is catching up. pub.dev, the Flutter’s dependency go-to place, has a built-in analysis tool that lets you see any issues it found in the package.
I must admit I haven’t used many tools concerning React Native. I’ve used debugging and logging via Chrome a few times. However, it’s not perfectly dependable, since it switches the execution engine to that inside of the browser. This, in specific cases, might make tracking down bugs harder. I haven’t used a dedicated profiler, only the native ones. I’ve developed from Visual Studio Code, since it worked best for me, but each time I wanted to track where a function is used, I had to do a project–wide string search—far from ideal.
Flutter, on the other hand, has support for every Android developer’s favorite, the Android Studio, with its shortcuts, navigation, hints. It even lets you start iOS Simulator and run the app on it straight from IDE! There are context-aware actions, like wrap in
Not everything works as well as with native Android, but nevertheless, it is much better than my experience with React Native.
Since Dart is statically typed (with the addition of
dynamic type, which can be anything and is useful in specific cases), its bridging supports more granular types, making your life easier. On the other hand, React Native provides more flexibility by supporting passing callbacks, which Flutter does not.
Because React Native does not guarantee that views look the same on both platforms, you actually need experts for all of the platforms, and now you have three (!) of them, though if you meet that condition, the development does seem to proceed at a faster pace.
Both solutions run, as a general principle, on one thread, and the bridging should/must happen on the main (UI) thread, which might cause throttling issues and impact performance if you must send over large quantities of data.
Flutter has isolates, which are physical threads to which you can move heavy processing to avoid hiccups on the user interface. Asynchronous operations on both can be done coroutine–style with async/yield/await trio or using
React Native has a more standardized approach to state, with Redux and its store/action/reducer system holding the trophy. On Flutter, there are at least three ways of managing state competing, ie. Redux port, Business Logic Component (BLoC) concept using built-in or RxDart streams, and passing local state down through widgets provided by Flutter framework.
Redux makes tracking causality in code harder, BLoC is harder to read, and the passing of state might cause redundant rebuilds of the tree.
I’ve once integrated a Unity-based AR into a React Native application and its performance was… poor. Frames skipped, virtual objects jumped from time to time, animations were slow, though camera feed worked fine.
I’ve also once encountered an application where Redux state was treated like permanent storage (think: database). In the default implementation of the Redux store on Android, an attempt to write 6 or more megabytes of data cause the store to fail, dropping all of the stored state to disappear permanently. This turned out to be a serious problem that was hard to identify.
What to choose then?
The sun’s coming down, the show’s ending and I still haven’t answered the question. Even though the title suggests only two choices, remember that there are actually more. Without further ado—the summary.
When to choose React Native
I don’t like React Native, and I’d like that to make it crystal clear. That said, I do think it might be a good idea if certain conditions are met, and those are:
You already have a strong web team
Chances are your Front End and Full Stack developers already know React, and they’re pretty much the same with a bit changed names of the components on the far bottom. This means that you already got someone with the necessary knowledge that can teach the rest of the team how to avoid common pitfalls, knows how to organize architecture and how to use npm/yarn and manager dependencies.
Be aware, you still need at least one Android and one iOS developer in the team for their knowledge of their platforms.
Your app is porting a web app
In this case, you might be better off with w Progressive Web App, but nevertheless, you could create a common module for components used on both web app and the React Native app.
Your app is a content browser
If your application is not really using sensor data or the native APIs from the platforms and the only permission you require is internet access—this might be the way to go. Especially if it’s light on animations and you don’t mind an occasional drop in the performance.
Your app is not processing a lot of data
Any I/O operation you want to do has to pass through the bridge. If you’re planning to make a lot of round trips, you should look elsewhere, but if that’s not the main feature of your application, you’re good to go!
When to choose Flutter
If I were to go cross/multi–platform, this would be my recommendation as from my perspective it wins with React Native in almost every department.
You already have a mobile team
Your app doesn’t depend too heavily on the platform
If you want to create AR/VR, then you’ll probably be better off with native. You only want Internet access, Bluetooth, or something like that? I’d say you’re set!
You don’t care much about platform UI
You’re making a beautiful custom UI with pretty and smooth animations? Go Flutter! Performance is great, and they’re going to look the same anywhere you launch. It is possible to maintain two UIs that feel the iOS or Android, but it’s not the point of this framework (of course I don’t mean things like the position of the navigation bar, since it’s super easy to write it once and have it change position depending on the platform).
You want to save money, but performance matters
Let’s face it: you’re thinking about crossplatform, because it saves money—shared logic, smaller team for the same scope. Flutter has a web spin-off called Hummingbird that lets you create web apps, but I have neither used it nor researched it—just letting you know it’s there.
The hidden options
Remember that you always have other options.
One would be going native with two teams. If your app does some heavy processing, you might consider going with native, accepting the increased cost and synchronization overhead, as it might give you the best performance.
Another such solution is to use Kotlin Multiplatform for shared logic between the two platforms. This still requires you to keep two separate native mobile teams, but since Kotlin is very similar to Swift, both teams should be able to easily write new logic and sharing it means lower cost of fixing bugs and evolving requirements.
I’d like to thank Piotr Dubiel for bringing me up to speed on the latest news about React Native
If you have a project, but you are not sure which way to go, don’t hesitate to contact us, and we will help you with the decision.
You might also like
LeakCanary—Deobfuscation Feature Explained
LeakCanary—Deobfuscation Feature Explained
Learn how to detect and debug memory leaks during app deobfuscation with LeakCanary—an open-source project, to which our Senior Software Engineer, Michał, contributed.