Why Flutter, React Native technically 2023

Fun enough frameworks

MJ Studio
MJ Studio

--

2024.3
well, it is 2024.

This article is not intended to cause a dispute between RN and Flutter.

After developing with React Native for a long time, I somewhat admit that I used Flutter to highlight the fact that it technically complements React Nativeā€™s shortcomings.

React Native is also a framework that is steadily growing and has a lot of value, and in the end, I would like to tell you that both frameworks have their own pros and cons.

I have developed a mobile application with React Native for about 3 years. From it, I experienced very deep and tough side effects of React Native like weird platform, library bugs / hard performance optimization / Interoperability with other JS contexts, and native side / Nightmare Crash/Error tracings. I tried so many famous or new paradigm technics to facilitate RN more effectively.

It was fun. Yes, RN is a cross-platform framework. I could publish two applications (Android & iOS) with a single effort.

But, I am suffering a very uncomfortable developer experience with React Native recently. I learned Flutter without thinking that it can be an alternative one for React Native, and I was wrong. The developer experience with Flutter is gorgeous. In most cases, Flutter resolves all of my concerns made by RN about developing a mobile application.

Ok, what are the problem and differences between the two? Letā€™s explore.

Framework Upgrade

Without any doubt, the version upgrade process of React Native is an entire nightmare.

You can think like that ā€œWhatā€™s the problem? Just read the docs and run the command npx react-native upgrade like flutter upgrade!"

Ok, I never succeeded like that. I guess, you too. Letā€™s find a plan B.

The React Native team(or community?) made a utility website called React Native Upgrade Helper. It is just a project file-level diff check tool between two initialized React Native projects with specific versions.

It is helpful in some cases. If a patch update is released, then we can recognize we should just change the react-native version of package.json. However, if it includes android/ ios/ directory changes, then it is more complex.

Did you try to upgrade React native recently?

Please see that

Seriously? What the hell happens between 0.67.5 ~ 0.68.0 ?

Can you track all changes and copy & paste every file to file? Why do we need to know if the name of the iOS example project of React Native is changed in this update or internal batch script is changed?

The main cause of the mess is the native configuration setup logic is almost exposed in the project naively. However, it is common in cross-platform frameworks. By their abstraction strategy of wrapped native platform, some configuration files should be messy.

But it should be separated reasonably. The importing process of React Native's new architecture from version upgrade is disappointing for me.

In my project, I created many native Kotlin, Java, and Swift files and set up for native library configuration, and managed native resources fields. Also, Android Gradle and iOS Xcode project configuration varies already from empty React Native projects.

How can I upgrade with Upgrade helper? I even already forget how my MainApplication , appDelegate.m is set up. Upgrade helper indicates that ā€œYou should change A to Bā€. What? I have just C and D. I canā€™t easily follow the guide indicating line X is changed like line Y.

Finally, I succeeded in my project from 0.67.5 to 0.70.2. Can you guess the way?

I created a new React Native project with npx react-native init AwesomeProject . It was so awesome literally. It is real!

It gave me a lot of work that should be done.

1 Copy all JS/TS/TSX files and paste
2 Setup JS environemnt(test, eslint, prettier, typesscript) again
3 Copy all Java/Kotlin/Swift/Objective-C and paste
4 Go to docs of native plugin libraries and check set up code is correct(also paste).
5 iOS scheme run setting for react-native-config
6 Set Android Splash screen, App icon
7 Set iOS Splash screen, App icon
8 Set iOS assets (color, images)
9 Info.plist
10 Podfile
11 Signing setting
12 Firebase
13 ..
14 ..
...
100 Run app with new version
101 Failed
...
...

I spent a day entirely. I really felt happy with my application running on 0.70.2 successfully. React Native upgrade process makes developers stressed.

It was a nightmare.

The Ecosystem & Libraries

What were the main pros of React Native? It is React and JavaScript. The React & JS ecosystem is so big and libraries written with JS are numerous.

But it is not a deal anymore. The Dart packages are managed in Pub.dev and already many libraries are contributed.

One of the biggest pros of Flutter is Managed plugins by the Flutter team and other reliable packages(Favorite programs & plugins). The completeness of libraries is so stable.

There are many great React Native libraries in the ecosystem and community too. However, IMO, the developer experience for the RN library was poor than Flutter. I donā€™t think it is the problem of maintainers. This is the problem of architecture and framework.

Maintaining React Native native packages is hard.

The core of React Native is stable. However, if we create a package including native side codes like one of the core, compliance requirements was hard.

1 ā€” Bonding JS, native side messy

I like to create native modules from cross-platform frameworks with my native development experiences(Android, iOS) if the feature is required for that. I created a lot of native modules (not packages) in my React Native project and also contributes to the community or packages. Even I wrote a tutorial for creating native modules with a bridge.

During learning flutter, I explored first how to create native plugins in Flutter. Also, I have created a simple iOS PencilKit plugin for Flutter.

In my comparison, Flutterā€™s native bridge API is simpler than React Native especially if you plan to write code with Swift in iOS.

The API of flutter (MethodChannel, UIKitView, ā€¦) is much simpler.

Also, the Flutter team set up a well-defined process for how to manage native plugins for each platform like federated plugins.

2 ā€” New architecture of React Native

React Native uses the legacy bridge for communication between JS and the native side. This is a great idea. But the problem is we need to migrate all our packages to support React Native new architecture using JSI(Fabric or Turbo Module).

Even if it is until experimental. Hey, I have waited about 2 years already :(

I loved the improvement blueprint of React Native and had looked forward to the new architecture of React Native.

I even posted several articles introducing RN's new architecture.

But, the usage of JSI seems to be a difficult one. Basically, we should create code with C++. It doesnā€™t mean C++ is hard. Ok, Why we should use that for creating React Native package? The main pros of React Native are it is React! and pure web-based frontend developers enjoy it easily without any other concern.

Anyway, there are many well-made packages for React Native already. However, it makes it harder to make your own package or contribute to the community.

And again, importing new architecture in a project with a manual single update, Nightmare.

React Native has more features that should be implemented in native platforms.

It is caused by the limitation of React Native architecture or API.

For example, if you want to draw your geometry(like a graph) with codes, then you should use React Native SVG written with many native classes for each platform. However, In Flutter you just can use CustomPaint . Implementation variance of abstract features in each platform leads to platform inconsistency problems more easily. ā€œWorking fine in Android, fine in iOS fine in ā€¦ā€¦ā€ I saw too many Github issues like the above during work with React Native.

I admit the architecture of Flutter is not different. Originally, Cross-platform meant the stack of abstraction layers I think. It works with the abstract, and implementation of each platform(platform embedder or so many other things layer by layer). However, the implementation of Flutter exists in the framework code in general, not in the community.

The more packages developers implement layers themselves, the more problems hard to track internally.

If we draw a linear gradient with Canvas API of Flutter, the Skia handles its implementation platform by platform(Skia backends like Metal or OpenGL in iOS or Vulkan in Android). But when I use React Native Linear Gradient, I push an additional abstraction layer to the top of the stack.

In RN Linear Gradient Android implementation, the AndroidLinearGradient class is used. in iOS, CGContextDrawLinearGradient is used. Can you convince the two things to work 100% the same? Well, because of some limitations, I am using React Native SVG to draw the linear gradients.

Google, The platform owner

Who is the owner of Android OS? It is Google. Who maintains core Material design? It is Google. I am not a developer who has blind faith in Google.

Why canā€™t other third-party app stores beat Google Play Store? There are many reasons, but definitely, Googleā€™s understanding of Android binary, security is stronger than other overwhelmingly.

Is there any app store that supports Android dynamic delivery, feature module, and extra security about binary? No isnā€™t.

I feel comfortable with this reliable support aspect. For example, I can see the ā€œFlutter is supportingā€ statement on the official Material Design component website like the following.

Material 3 Chip component support status table

In another example, I used Code push for my React Native a year ago(But I remove it from my app because of problems like the gesture handler doesnā€™t work after the JS bundle update).

Yes, I posted about that too.

The CodePush team says that the current Review guideline is no problem with CodePush. But, are you sure? The dominants of Mobile platforms are Google and Apple. They can suddenly change their store policy in a day.

Apple changed the app store policy about App tracking transparency and enforced that usersā€™ ā€œboostā€ posting feature should be managed by In-app purchases suddenly. Result? Meta(aka. Facebook) was sad.

View(Widget) architecture model

Internally, the core components of React Native is mapped with native widget one by one. This is the reason there are so many RCT prefix native widgets. Then, they are composed with a C++ layout library called Yoga.

It is not a big problem in general.

But we should avoid meaningless deep tree structures in the View hierarchy. The more flat views the faster your app in general. The Android docs describe why deep tree structure causes bad performance in the app. The view tree is traversed in a recursive manner from layout, paint, hit testing, accessibility handling, etc.

If we have a deep and complex native view structure, the parent-children communication algorithms are messy and slow. Often the traversal algorithm is slower than just O(N) because of some limitation or custom layout algorithm(e.g. Android double taxation, Flutter IntrisicHeight).

You can say ā€œThe Flutter is also a tree!ā€. Yes, definitely.

But I think the possibility of deep tree harm to application performance in Flutter is lower than in React Native. Because all Flutter widgets wonā€™t transform to something that runs the above algorithms. See the figure.

https://docs.flutter.dev/resources/architectural-overview

Something is RenderObject in Flutter. All nodes in Widget exist in the Element tree but in Render object tree, it is not a case. The ComponentElement is just a compositor for views and doesnā€™t have RenderObject internal. Therefore, the final algorithms by tree traversal will work more simply. Furthermore, Flutter ensures a single-pass layout traversal like Android Jetpack Compose.

Yes, View in Android and UIView in iOS are implemented with great algorithms too. However, the Flutter team implemented its own rendering pipeline algorithm from scratch optimized in Dart and Flutter environments inspired by React. Which should be effective?

But the widget structure described by developers using Flutter for their code can easily go deeper than React Native. However, this is just an aspect of the API mental model of Flutter. If we use Padding like a component to implement padding in React Native, it should be written with more codes too.

Animation, Gesture handler API, Crash, Threading

I must state about two performance-intensive features in frontend applications, animation, and gesture handling.

The core APIs of React Native have performance limitations. Animated & Responder API. Because these donā€™t work in the UI(main) thread of the native side. You can follow up on the reason for this on Reanimated docs motivation page. Also, Animated API doesnā€™t work with layout animation.

The situation that the core API doesnā€™t support basic application framework features completely makes developers fall into confusion. The software mansion team has contributed a huge easing of API in the RN community. Especially, the Reanimated and RNGH are nice packages and I read all the document pages of the two packages.

But again, the stack up by the community is more unstable than the original framework maintainersā€™ work. I have used two packages from about 2 years ago and have suffered uncomfortable developer experiences from them.

1 ā€” Untrackable native crash

It makes me crazy. The most stressful native crash of my app is this.

This is just an example. However, what is this? Why trouble only in Android? Why do an affected user count and crash count seem to be similar(not repeated)? How can I reproduce this? I donā€™t know. It is just indicating libreanimated.so goes wrong.

I repeat, Iā€™ve been using these packages for 2 years. I had several experiences like the above.

I am developing a big application alone. This means that time is important to me and I often should borrow technical debt. Sometimes, I need a ā€œjust workā€ and stable solution.

Yes, my thinking is selfish. This is the community and open source. We should find a way together and contribute to them.

But do you agree that we have to convince React Native beginner developers that it is not easy to change the width gradually during your JS thread is busy in a way written on official framework documents?

I love AnimationController, Animation, Animatable, Tween, GestureDetector and GestureRecognizer API in Flutter. they just work without any performance issues or weird issues in most cases. If they have a bug, then the severity level of issues will be urgent and will be managed by the framework itself.

2 ā€” Limited API usage

The above two packages have been changing their API more developer friendly. The concept of SharedValue and worklet is awesome.

But there are some limitations to API usage. For example, I recently developed a vertical paging video player like this.

This is not implemented with ScrollView or FlatList. This is a custom view that handles the swipe gesture itself. In this process, I couldnā€™t implement inserting a short gesture blocking delay after swiping with Gesture.Pan(). Why? Gesture.Pan().enabled(boolean) accepts the boolean parameter and it should be managed as JS side React state(Or any other way? I read the docs but I canā€™t get any information without reading the actual code in the package).

I tried to use runOnJS . But no luck. I canā€™t block swipe completely between very fast swipe gestures. This is caused by thread inconsistency. So I had to implement this with a hacky way to declare another boolean SharedValue<0|1>. It caused another problem but it is not scoped in this posting.

Language, Environment

Setup, Coding convention

To be honest, I donā€™t like JS/TS. Have you ever set up Eslint, Prettier, Typescript, Babel, Jest, and more? Why do we need a boilerplate CLI for the JS project really? This is the reason. I hate concepts like these. This configuration setup process makes developers very tired.

But in Flutter, we just have to match the Dart version, Flutter SDK version, and just a single analysis_options.yaml file for consistent language, and lint setup for every project. For example, we just make our Dart code pass the analysis rules to contribute to Fuchsia SDK.

Furthermore, the Dart document manages a rule for the good, and bad coding conventions like Effective Dart page itself. This helps a Dart code community manage consistent coding conventions contributed by numerous developers.

Threading

The ways of handling asynchronous tasks are similar in the two. Promise and Future. And two language has a similar concept like macro and micro-task queues. However, these are not related to multi-threading. These are just API for asynchronous tasks.

JS is a single-thread language. Yes, Dart too. However, Dart provides an Isolate concept and we can use it easily(some limitations in Flutter like canā€™t load assets in not main Isolate).

JavaScript Engine

There are several options to choose React Native JS engine. The mains are JSC(JavaScript Core) and Hermes.

If Hermes is really good, why not drive all projects to use Hermes right away from specific RN version updates? Why confuse developers by giving them a choice? The opt-in/out features given to developers cause make multiple problems and each peripheral solution.

The consistent development environment from the developer community concentrates on a major problem and makes finding a solution for that intensive.

The well-managed cross-platform framework should control smartly issues like ā€œMy code doesnā€™t work in (Hermes|JSC|Android|iOS|Real Device|Simulatorā€¦)ā€ Not all developers are can manage the environment like experts.

Layout Model

Can you implement the auto-size text feature in React Native without native code? It is impossible. Why? we donā€™t have much control over layout pass in React Native. I posted about the brightness layout model in Flutter(But it is written in Korean :))

We can participate in layout algorithm and create auto-size text in Flutter just with Dart code using LayoutBuilder, TextPainter.layout() . The API of Flutter makes easy this feature.

Another example is the following. Can you implement this in React Native? without additional build phase?

logic based on some available size

No, my implementation about this is

export type AdaptiveWidthConstriantViewProps = {
ifElement?: React.ReactElement;
elseElement?: React.ReactElement;
minWidth?: number;
maxWidth?: number;
style?: StyleProp<ViewStyle>;
};
const AdaptiveWidthConstraintView = ({
elseElement,
ifElement,
minWidth = -99999,
maxWidth = 99999,
style,
}: AdaptiveWidthConstriantViewProps) => {
const [layoutWidth, setLayoutWidth] = useState(-1);

const renderState: UNDETERMINED_BOOL = useMemo(() => {
if (layoutWidth === -1) {
return UNDETERMINED;
}
if (layoutWidth >= minWidth && layoutWidth <= maxWidth) {
return TRUE;
} else {
return FALSE;
}
}, [layoutWidth, minWidth, maxWidth]);

return (
<View
style={style}
onLayout={({
nativeEvent: {
layout: { width },
},
}) => {
setLayoutWidth(width);
}}
>
{renderState === UNDETERMINED ? null : renderState === TRUE ? ifElement : elseElement}
</View>
);
};

I delayed rendering component until onLayout event is called.

Not only is it not performant, but itā€™s not a complete solution because the View doesnā€™t handle max width and height constraints from parents correctly in some cases.

With Flutter? just use LayoutBuilder

https://docs.flutter.dev/development/ui/layout/constraints

Miscellaneous API

Interval Timer

setInterval in JS donā€™t work as expected because of JS timer model doesnā€™t work like that. I analyzed this and find a solution to this.

In Dart, the Timer.periodic works as expected. It is not 100% accurate, but it ensures that no more than n callbacks will be made in duration * n time. The document says,

The exact timing depends on the underlying timer implementation. No more than n callbacks will be made in duration * n time, but the time between two consecutive callbacks can be shorter and longer than the duration.

Navigation

In React Native, the most popular navigation solution is React Navigation. But the Flutter has built-in Navgiator API(stack-based) and route-based API (navigation 2.0). The navigation 2.0 API is so complex to use but the Flutter team managed go_router the package for use this easily.

Passing the arguments to the screen is very easy in Flutter(not in router-based API). Because the screen widget is rendered with a class syntax. We just pass any type of argument (not only the plain object but function, class instance also) as constructor parameters.

Pop and pass results can be easy too. Just pop with a result to awaiting caller pushed a screen widget. If you use React Navigation, then you should call navigation.navigate('PreviousScreen') . If your screen is just a select utility screen and cannot find who is the previous router? The situation goes complex.

I think, if you donā€™t support web, then stack-based Navigator API is enough unless you support complex deep links or website universal URLs like features.

In the dialog, the problem is more serious.

I have never seen any package control React Native dialog logic perfectly. Because the dialog in React Native wonā€™t be treated as a navigation route. It is reimplemented with a custom view that can animate up and down(show and hide). Keyboard size, iPad floating keyboard, Z-index order, and Window size changing make the implementation of dialog so hard. Just making a component animation doesnā€™t cover all the above cases.

If you see a PR that I created 2 years ago in one of the dialog packages in React Native, then you can understand it clearly.

Viewport

To be honest, I see so many codes in React Native projects like that.

const screenWidth = Dimensions.get('window').width;

export const MyComponent = () => {
const myWidth = screenWidth * ...

You can never expect the window size of your devices never changed. Those who donā€™t friendly with a mobile platform from web-based platforms can easily miss Android multi-window or iPad split screen features or device display zoom, scale, font scale settings, etc.

They think ā€œOh, I blocked rotation in my application. It is safe!ā€ No!

The Flutter BuildContext and MediaQuery API enforces that developers use screen-size and view insets, padding, and safe areas with a managed value by framework responsibly.

Outro

I have failed to control the number of contents. There are many optimization features like tree surgery or const constructor optimization in Flutter that RN doesnā€™t have. But it will be posting more boring and long.

I donā€™t know every line and mechanism in React Native and Flutter. So I could be wrong.

The above content is just my candid story about using two different most famous cross-platform frameworks.

Thank you for reading.

ā€” ā€” ā€” ā€” ā€” ā€” ā€” ā€” ā€” ā€” ā€” ā€” ā€” ā€” ā€” ā€” ā€” ā€” ā€”

--

--