The horse plowed the most, but did not become the chairman of the kolkhoz

Recently, in the mobile community, you can often hear about Flutter, React Native. It became interesting to me to understand the profit from these pieces. And how much they really change life when developing applications. As a result, 4 (identical in terms of functions performed) applications were created: native Android, native iOS, Flutter, React Native. In this article, I described what I learned from my experience and how similar elements of applications are implemented in the solutions under consideration.
Comments: the author of the article is not a professional cross-platform developer. And all that is written about is the look of a novice developer for these platforms. But I think this review will be useful for people who are already using one of the solutions in question, and who are looking to write applications for two platforms or to improve the process of interaction between iOS and Android.
As a developed application, it was decided to make a “Sport Timer”, which will help people involved in sports in performing interval training.
The application consists of 3 screens.

Timer work

screen Training history

screen Timer settings screen
This application is interesting to me as a developer, because the following components of interest to me will be affected by its creation:
- Layout
- Custom View
- Working with UI lists
- Multithreading
- Database
- Network
- key-value storage
It is important to note that for Flutter and React Native we can create a bridge (channel) to the native part of the application and use it to implement everything what the operating system provides. But I was wondering what the frameworks give out of the box.
Choosing development tools
For a native iOS application, I chose the Xcode development environment and the Swift programming language. For native Android, Android Studio and Kotlin. React Native developed in WebStorm, the JS programming language. Flutter - Android Studio and Dart.
An interesting fact when developing on Flutter it seemed to me that right from Android Studio (the main IDE for Android development), you can run the application on an iOS device.

Project structure
The structures of native iOS and Android projects are very similar. This is a file for layout with the extensions .storyboard (iOS) and .xml (Android), dependency managers Podfile (iOS) and Gradle (Android), source code files with extensions .swift (iOS) and .kt (Android).

The structure of the Android project

The structure of the iOS project
The Flutter and React Native structures contain the Android and iOS folders, which contain the usual native projects for Android and iOS. Flutter and React Native are connected to native projects as a library. In fact, when you start Flutter on an iOS device, the normal native iOS application starts with the Flutter library connected. For React Native and for Android, everything is the same.
Flutter and React Native also contain package.json (React Native) and pubspec.yaml (Flutter) dependency managers and source code files with the .js (React Native) and .dart (Flutter) extensions that also contain the layout.

Flutter

Project Structure React Native Project Structure
Layout
For native iOS and Android, there are visual editors. This greatly simplifies the creation of screens.

Visual editor for native Android

Visual editor for native iOS
For React Native and Flutter, there are no visual editors, but there is support for the hot-restart feature, which at least makes it easier to work with UI.

Flutter

hot reboot React Native hot reboot
On Android and iOS, layout is stored in separate files with the extensions .xml and .storybord, respectively. In React Native and Flutter, the layout comes directly from the code. An important point in describing the speed of ui is to note that Flutter has its own rendering mechanisms, with which the creators of the framework promise 60 fps. And React Native uses native ui elements that are built using js, which leads to their excessive nesting.
In Android and iOS, to change the View property, we use a link to it from the code and, for example, to change the background color we cause changes to the object directly. In the case of React Native and Flutter, there is another philosophy: we change the properties inside the setState call, and the view itself is redrawn depending on the changed state.
Examples of creating a timer screen for each of the selected solutions:

Screen layout of a timer on Android

Screen layout of a timer on iOS
Screen layout of a timer Flutter
@overrideWidget build(BuildContext context){
return Scaffold(
body: Stack(
children: <Widget>[
new Container(
color: color,
child: new Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
" ${getTextByType(trainingModel.type)}",
style: new TextStyle(
fontWeight: FontWeight.bold, fontSize: 24.0),
),
new Text(
"${trainingModel.timeSec}",
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 56.0),
),
new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
"СЕТЫ ${trainingModel.setCount}",
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 24.0),
),
Text(
"ЦИКЛЫ ${trainingModel.cycleCount}",
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 24.0),
),
],
),
],
),
padding: const EdgeInsets.all(20.0),
),
new Center(
child: CustomPaint(
painter: MyCustomPainter(
//0.0
trainingModel.getPercent()),
size: Size.infinite,
),
)
],
));
}
Screen layout timer React Native
render() {
return (
<Viewstyle={{flex:20,
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'stretch',
}}><Viewstyle={{height:100}}><Textstyle={{textAlign: 'center', fontSize:24}}>
{this.state.value.type}
</Text></View><Viewstyle={styles.container}><CanvasTestdata={this.state.value}style={styles.center}/></View><Viewstyle={{height:120}}><Viewstyle={{flex:1}}><Viewstyle={{flex:1, padding:20,}}><Textstyle={{fontSize:24}}>
Сет {this.state.value.setCount}
</Text></View><Viewstyle={{flex:1, padding:20,}}><Textstyle={{textAlign: 'right', fontSize:24}}>
Цикл {this.state.value.cycleCount}
</Text></View></View></View></View>
);
}
Custom view
Getting acquainted with the solutions, it was important for me that you could create absolutely any visual component. That is, draw ui at the level of squares, circles and paths. For example, the timer indicator is such a view.

For native iOS, there were no problems, as there is access to the Layer, on which you can draw anything.
let shapeLayer = CAShapeLayer()
var angle = (-Double.pi / 2
- 0.000001 + (Double.pi * 2) * percent)
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 100, y: 100),
radius: CGFloat(95),
startAngle: CGFloat(-Double.pi / 2),
endAngle: CGFloat(angle),
clockwise: true)
shapeLayer.path = circlePath.cgPa
For native Android, you can create a class that inherits from View. And override the onDraw (Canvas canvas) method, in the parameter of which the Canvas object is on it and draw.
@OverrideprotectedvoidonDraw(Canvas canvas){
pathCircleOne = new Path();
pathCircleOne.addArc(rectForCircle, -90, value * 3.6F);
canvas.drawPath(pathCircleBackground, paintCircleBackground);
}
For Flutter, you can create a class that inherits from CustomPainter. And override the paint method (Canvas canvas, Size size), which in the parameter passes the Canvas object - that is, a very similar implementation as in Android.
@overridevoidpaint(Canvas canvas, Size size){
Path path = Path()
..addArc(
Rect.fromCircle(
radius: size.width / 3.0,
center: Offset(size.width / 2, size.height / 2),
),
-pi * 2 / 4,
pi * 2 * _percent / 100);
canvas.drawPath(path, paint);
}
For React Native, no solution was found out of the box. I think this is explained by the fact that js is only described by the view, and is built by native ui elements. But you can use the react-native-canvas library, which gives access to the canvas.
handleCanvas = (canvas) => {
if (canvas) {
var modelTimer = this.state.value;
const context = canvas.getContext('2d');
context.arc(75, 75, 70,
-Math.PI / 2, -Math.PI / 2 - 0.000001 - (Math.PI * 2)
* (modelTimer.timeSec / modelTimer.maxValue), false);
}
}
Work with UI lists

The algorithm of work for Android, iOS, Flutter - solutions is very similar. We need to specify how many items are in the list. And give the cell number that you want to draw by the element number.
In iOS, UITableView is used to draw lists in which you need to implement DataSource methods.
func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return countCell
}
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return cell
}
For Android, they use RecyclerView, in whose adapter we implement similar IOS methods.
classMyAdapter(privatevalmyDataset: Array<String>) :
RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
override fun onBindViewHolder(holder: MyViewHolder, position: Int){
holder.textView.text = myDataset[position]
}
override fun getItemCount()= myDataset.size
}
For flutter use ListView, in which similar methods are implemented in the builder.
new ListView.builder(
itemCount: getCount() * 2,
itemBuilder: (BuildContext context, int i) {
returnnew HistoryWidget(
Key("a ${models[index].workTime}"), models[index]);
},
)
React Native uses a ListView. The implementation is similar to previous solutions. But there is no binding to the number and number of elements in the list, in the DataSource we set the list of elements. And in renderRow, we implement the creation of a cell, depending on which element has come.
<ListView
dataSource={this.state.dataSource}
renderRow={(data) => <HistoryItemdata={data}/>}
/>
Multithreading, asynchrony
When I started to deal with multithreading, asynchrony, I was horrified by the variety of solutions. In iOS, this is GCD, Operation, in Android, AsyncTask, Loader, Coroutine, in React Native, Promise, Async / Await, in Flutter-Future, Stream. The principles of some solutions are similar, but the implementation is still different.
He came to the rescue beloved Rx. If you are not yet in love with him, I advise you to study. It is in all the solutions I consider in the form: RxDart, RxJava, RxJs, RxSwift.
Rxjava
Observable.interval(1, TimeUnit.SECONDS)
.subscribe(object : Subscriber<Long>() {
fun onCompleted(){
println("onCompleted")
}
fun onError(e: Throwable){
println("onError -> " + e.message)
}
fun onNext(l: Long?){
println("onNext -> " + l!!)
}
})
Rxswift
Observable<Int>.interval(1.0, scheduler: MainScheduler.instance)
.subscribe(onNext: { print($0) })
Rxdart
Stream.fromIterable([1, 2, 3])
.transform(new IntervalStreamTransformer(seconds: 1))
.listen((i) => print("$i sec");
Rxjs
Rx.Observable
.interval(500/* ms */)
.timeInterval()
.take(3)
.subscribe(
function (x) {
console.log('Next: ' + x);
},
function (err) {
console.log('Error: ' + err);
},
function () {
console.log('Completed');
})
As you can see, the code looks very similar in all four languages. That in the future, if necessary, will facilitate your transition from one solution for mobile development to another.
Database
In mobile applications, the standard is SQLite database. In each of the solutions considered, a wrapper is written to work with it. Android usually uses ORM Room.
In iOS, Core Data. In Flutter, you can use the sqflite plugin.
In React Native - react-native-sqlite-storage. All of these solutions are designed differently. And to make the applications look similar, you will have to write Sqlite queries manually, without using wrappers.
It is probably better to look towards the Realm data storage library, which uses its own kernel for data storage. It is supported on iOS, Android and React Native. There is currently no support for Flutter, but Realm engineers are working in this direction.
Realm in Android
RealmResults<Item> item = realm.where(Item.class) .lessThan("id", 2) .findAll();
Realm in iOS
let item = realm.objects(Item.self).filter("id < 2")
Realm in React Native
let item = realm.objects('Item').filtered('id < 2');
Key-value store
In native iOS, UserDefaults are used. In the native Android - preferences. In React Native and Flutter, you can use libraries that are wrappers for native key-value repositories (SharedPreference (Android) and UserDefaults (iOS)).
Android
SharedPreferences sPref = getPreferences(MODE_PRIVATE);
Editor ed = sPref.edit();
ed.putString("my key'", myValue);
ed.commit();
iOS
let defaults = UserDefaults.standard
defaults.integer(forKey: "my key'")
defaults.set(myValue, forKey: "my key")
Flutter
SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.getInt(my key')
prefs.setInt(my key', myValue)
React native
DefaultPreference.get('my key').then(function(value) {console.log(value)});
DefaultPreference.set('my key', myValue).then(function() {console.log('done')});
Network
There is a huge amount of solutions for working with the network in native iOS and Android. The most popular are Alamofire (iOS) and Retrofit (Android). React Native and Flutter have written their own platform-independent clients to surf the net. All customers are designed very similarly.
Android
Retrofit.Builder()
.baseUrl("https://timerble-8665b.firebaseio.com")
.build()
@GET("/messages.json")
fun getData(): Observable<Map<String,RealtimeModel>>
iOS
let url = URL(string: "https://timerble-8665b.firebaseio.com/messages.json")
Alamofire.request(url, method: .get)
.responseJSON { response in …
Flutter
http.Response response = await
http.get('https://timerble-8665b.firebaseio.com/messages.json')
React native
fetch('https://timerble-8665b.firebaseio.com/messages.json')
.then((response) => response.json())
Development time

Probably incorrect to draw any conclusions based on my development time, since I am an Android developer. But I think for the iOS developer the entrance to the Flutter and Android technology will seem easier than in React Native.
Conclusion
Starting to write an article, I knew what I would write in the output. I'll tell you what solution you liked more, which solution you should not use. But then, after talking with people who use these solutions in production, I realized that my conclusions are incorrect, because I look at everything from the side of my experience. Most importantly, I realized that for each of the solutions under consideration, there are projects for which it is ideal. And sometimes it's really more profitable for a business to do a cross-platform application, and not to plow over the development of two native ones. And if some decision is not suitable for your project, you should not think that it is bad in principle. I hope the article will be useful. Thanks for getting to the end.
Regarding the editing of the article, please write in a personal, I will gladly correct everything.