In this article, you will learn further differences between Flutter and React Native. We will take a look at the framework level how they differ aesthetically.

We could argue that React Native's popularity is due to React's popularity. This is why we will further discuss the differences aesthetically of each framework. What does it look like to create similar features in each of these frameworks? And how do the architectural decisions made by each platform impact the overall outcome?
Aesthetic Differences - Offerings & Decisions
Both allow you to construct smaller reusable pieces. Flutter calls it Widgets, React calls it Components.
Flutter uses the builder pattern. "The intent of the Builder design pattern is to separate the construction of a complex object from its representation. It is one of the Gang of Four design patterns" (1).
React Native does not enforce a design pattern. Its support for Class and Functional components allow for an array of design patterns. Arguably this flexibility can backfire with codebases diverging due to opinions. On the other hand, someone could also argue that this flexibility is what made React and React Native popular.
Flutter titles itself as a UI Toolkit. React Native does not. And why is this relevant?
We could say toolkits or Software Development Kits, SDKs, are expected to fulfill your entire needs to develop on that given platform. As a result, Flutter has out of the box support for many UI elements, animation, and navigation.
React Native, on the other hand, does not wish to be a complete UI toolkit. It does not include, for example, out of the box support for navigation. Further evidence of this is the Lean Core initiative (2) where back in August 2018, it was made clear that the intention was to support only the core elements of React Native, leaving the remaining non-core elements to the open source initiative to maintain.
Despite the Lean Core initiative, React Native's open source ecosystem is vast. Thus, you are likely to find UI components or libraries for every need you have. But, do not expect these libraries to be maintained by Facebook's Core React Native team.
On this note, we have to mention Expo. It tries to be what React Native is not - a complete SDK for cross-platform mobile development. It maintains an array of third-party modules and offers multiple services to facilitate development.
Once you understand the architectural decisions and what problems each proposes to solve, you can move onto implementation details of an app.
Building Two Apps
At the developer experience level, both frameworks support debugging with specialized tools, hot reloading of the screen, and debugging at native levels. Thus, we will focus on implementation details.
When talking about aesthetic, it is hard to come to a consensus since it is a subjective matter strongly influenced by your overall experience as a developer. Take the remaining points and arguments as the point of a view of a single developer and not the world.
There is only way one way to find out how it looks - building with it. So, we built two identical apps: one in Flutter and another in React Native.
We decided to build an app that renders a small list of random dog pictures, allows you to add a single comment on a picture, and also select that dog picture to navigate to its details. We also focused only on the iOS app due to time constraints.
This app has the basic elements that every app has, a card like UI, text, input, scroll view, data requesting, and navigation.
Demo:
Comparing Code & Implementation Differences
Building apps can be a top-down or a bottom-up approach. When building top-down, you start by structuring your navigation, your pages, your data requests, and then your smaller UI elements. The bottom-up is the opposite. For this explanation let's take a top-down approach: We will explain navigation, page rendering, data requesting, then rendering smaller UI elements.
Navigation
What does navigating and rendering pages look like in Flutter and React Native?
In Flutter
Flutter ships with all of the navigation and routing solutions you need. It is flexible in the sense that you can declare all of your routes up front, or dynamically push a new page widget to the screen.
With the named route approach, you set all of your routes up front and you can navigate to it by calling Navigator.pushNamed(context, '/second').

Named routes make it clear which routes your app has and it organizes it in a central place. Passing arguments/parameters to a named route is more work though as you can see in their documentation.
For this app, we did a dynamic route push instead. We only declared home in the main app widget:

Then when we decide to navigate, we call Navigator.push passing it a context and a Route Widget for it to build for us.

This gives us the flexibility of instantiating the widget on the spot and passing whatever arguments we want to it.
This is where you see how Dart and Flutter is object oriented. Navigator.push's second argument accepts a class that is an Abstract class for routes. MaterialPageRoute implements that abstract class.
In React Native
While Flutter ships with all you need, React Native does not. To init this React Native app I used Expo's bare workflow that outputs a TypeScript app plus some libraries installed for me like Reanimated, Gesture Handler, and React Native Screens, but it does not include a navigation library.
Thus, I had to install react-navigation for this demo, see this guide on to install their library. Expect to install a number of packages.
Once you have installed react-navigation, this is what it looks like to declare routes and navigate to it:

Declaring routes is done in a centralized place as well, in JSX.
When navigating you also have a function to call provided by the navigation prop.

To access navigator we use a custom hook provided by react-navigation:

Page Rendering
In Flutter
Everything in Flutter is a widget. Either stateful or stateless.

In React Native
In contrast, everything in React Native is a component.

Requesting Data
In Flutter
For data requests you need to include the http package and also dart:convert to use decodeJSON.
To make this network request when RandomDogCard widget mounts, we can use the initState method to run this logic:

Dart is a statically typed language and thus you need to make sure you convert your JSON response into datatypes it understands. Larger JSON objects would require you to create a class object representation of it.
In React Native
On the other side, to mimic the same behaviour of requesting data on mount, for functional components you need to use the hook API called useEffect with the following signature: useCallback(callback, []). The empty array is to tell it we only want the callback to be called once - in the first effect or mount. Plus, you don't need to include any 3rd party packages since fetch is globally available and JavaScript interops with JSON graciously.

Stateful Logic
In Flutter
Once we have data displayed, handling some logic based on state is common to all apps.
For Flutter we can achieve this with Stateful widgets. These widgets automatically update when we make changes to class fields within a callback.
In our example, we wanted to display a comment section which you can toggle with a button. When you press the add comment button, we swap the text for an input and allow you to add your comment. We also swapped the add comment button for a save comment button.
To achieve the above, we mutated a boolean value and conditionally rendered the text for the button and the widget for the input/title.
Declared our state value:

Mutated isEditing value on button press:

Then conditionally rendered the Save Comment or Edit Comment copy:

Also conditionally rendered which widget to display, Text or TextField (Input):

See full widget here.
In React Native
To achieve the same in React Native we use hooks instead of widgets.
Declared our stateful value with useState:

Mutated isEditing value on button press:

Then conditionally rendered the Save Comment or Edit Comment copy:

Also conditionally rendered which widget to display, Text or TextInput:

Extra Notes
Hot Reload
Hot reload was not working as smoothly on Flutter as it was on React Native. I think the issue has to do with Stateless widgets in Flutter not supporting hot reload.
Keyboard Avoiding View Problem
Flutter automatically solved this for us, as you can see in the demo videos, with React Native on the other hand it took some figuring it out, plus adding an extra component - KeyboardAvoidingView, and it still didn't end up as smooth as Flutter.
TypeScript Unhappy
The fruit of a large unconnected ecosystem, React Navigation's type for params weren't accurate. The project worked despite the param type definition complaints:

Cross-Platform Differences
Remember, I said I was developing only on iOS because of time constraints. But, at the end I took a look at how each app looked on Android:
As you can see, the React Native app does not look exactly the same, plus it has broken behaviour due to the KeyboardAvoidingView component not working the same as in iOS.
React Native uses the slogan "learn once write everywhere" and they mean it. With React Native you can expect to have extra work with each platform, especially Android, to adapt to the platform quirks.
Flutter, on the other hand, does a much better job at being truly cross-platform. It has some small differences like the screen navigation, but that is done on purpose by the Material Page router, which we can change.
Are We Done?
No we are not done, this is part two of a three-part series where we dig deep into understanding the differences between each solution. Read Part Three here.
We first outlined some of the fundamental architectural differences between React Native and Flutter. Now we highlighted how they are different in practice. In the last part we will outline what considerations teams should make before deciding which technology to adapt.