El caballo más arado, pero el presidente de la granja colectiva no se convirtió



Recientemente, en la comunidad móvil, a menudo se puede escuchar sobre Flutter, React Native. Me interesaba entender el beneficio de estas piezas. Y cuánto realmente cambiarán vidas al desarrollar aplicaciones. Como resultado, se crearon 4 aplicaciones (idénticas desde el punto de vista de las funciones realizadas): Android nativo, iOS nativo, Flutter, React Native. En este artículo, describí lo que aprendí de mi experiencia y cómo se implementan elementos similares de aplicaciones en las soluciones bajo consideración.

Comentarios: el autor del artículo no es un desarrollador profesional multiplataforma. Y todo lo que se escribe es el aspecto de un desarrollador novato para estas plataformas. Pero creo que esta revisión será útil para las personas que ya están utilizando una de las soluciones en consideración y que buscan escribir aplicaciones para dos plataformas o mejorar el proceso de interacción entre iOS y Android.

Como una aplicación desarrollada, se decidió hacer un "temporizador deportivo", que ayudará a las personas involucradas en los deportes cuando realicen un entrenamiento de intervalos.

La aplicación consta de 3 pantallas.


Pantalla de operación del temporizador


Pantalla de historial de entrenamiento


Pantalla de configuración del temporizador

Esta aplicación es interesante para mí como desarrollador, porque los siguientes componentes que me interesan se verán afectados por su creación:
- Diseño
- Vista personalizada
- Trabajar con listas de IU
- multihilo
- Base de datos
- red
- almacenamiento de valor-clave

Es importante tener en cuenta que para Flutter y React Native podemos crear un puente (canal) a la parte nativa de la aplicación y usarlo para implementar todo lo que proporciona el sistema operativo. Pero me preguntaba qué marcos dan fuera de la caja.

Selección de herramientas de desarrollo.


Para una aplicación nativa para iOS, elegí el entorno de desarrollo Xcode y el lenguaje de programación Swift. Para Android nativo: Android Studio y Kotlin. React Native desarrollado en WebStorm, el lenguaje de programación JS. Flutter - Android Studio y Dart.

Un hecho interesante al desarrollar en Flutter me pareció que desde Android Studio (el IDE principal para el desarrollo de Android) puede ejecutar la aplicación en un dispositivo iOS.



Estructura del proyecto


Las estructuras de los proyectos nativos de iOS y Android son muy similares. Este es un archivo de diseño con las extensiones .storyboard (iOS) y .xml (Android), los administradores de dependencias Podfile (iOS) y Gradle (Android), archivos de código fuente con las extensiones .swift (iOS) y .kt (Android).


Estructura del proyecto Android


Estructura del proyecto IOS

Las estructuras Flutter y React Native contienen las carpetas Android e iOS, que contienen los proyectos nativos habituales para Android e iOS. Flutter y React Native se conectan a proyectos nativos como una biblioteca. De hecho, cuando inicia Flutter en su dispositivo iOS, la aplicación nativa de iOS se inicia con la biblioteca Flutter conectada. Para React Native y para Android, todo es igual.

Flutter y React Native también contienen package.json (React Native) y pubspec.yaml (Flutter) gestores de dependencia y archivos fuente con las extensiones .js (React Native) y .dart (Flutter), que también contienen el diseño.


Estructura del proyecto Flutter


Reaccionar estructura de proyecto nativo

Diseño


Para iOS y Android nativos, hay editores visuales. Esto simplifica enormemente la creación de pantallas.


Editor visual para Android nativo


Editor visual para iOS nativo

No hay editores visuales para React Native y Flutter, pero hay soporte para la función de recarga en caliente, que al menos de alguna manera simplifica el trabajo con la interfaz de usuario.


Reinicio en caliente en Flutter


Reinicio en caliente en React Native

En Android e iOS, el diseño se almacena en archivos separados con las extensiones .xml y .storybord, respectivamente. En React Native and Flutter, el diseño se deriva directamente del código. Un punto importante en la descripción de la velocidad de la interfaz de usuario debe tenerse en cuenta que Flutter tiene sus propios mecanismos de representación, con los cuales los creadores del marco prometen 60 fps. Y React Native utiliza elementos de interfaz de usuario nativos que se crean utilizando js, ​​lo que conduce a su anidamiento excesivo.

En Android e iOS, para cambiar la propiedad Ver, utilizamos un enlace desde el código y, por ejemplo, para cambiar el color de fondo, provocamos cambios en el objeto directamente. En el caso de React Native and Flutter, existe otra filosofía: cambiamos las propiedades dentro de la llamada setState, y la vista en sí misma se redibuja dependiendo del estado cambiado.

Ejemplos de crear una pantalla de temporizador para cada una de las soluciones seleccionadas:


Diseño de la pantalla del temporizador en Android


Diseño de la pantalla del temporizador en iOS

Diseño de la pantalla del temporizador Flutter

@override Widget 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, ), ) ], )); } 

Reaccionar el diseño de la pantalla del temporizador nativo

  render() { return ( <View style={{ flex: 20, flexDirection: 'column', justifyContent: 'space-between', alignItems: 'stretch', }}> <View style={{height: 100}}> <Text style={{textAlign: 'center', fontSize: 24}}> {this.state.value.type} </Text> </View> <View style={styles.container}> <CanvasTest data={this.state.value} style={styles.center}/> </View> <View style={{height: 120}}> <View style={{flex: 1}}> <View style={{flex: 1, padding: 20,}}> <Text style={{fontSize: 24}}>  {this.state.value.setCount} </Text> </View> <View style={{flex: 1, padding: 20,}}> <Text style={{textAlign: 'right', fontSize: 24}}>  {this.state.value.cycleCount} </Text> </View> </View> </View> </View> ); } 

Vista personalizada


Al familiarizarme con las soluciones, fue importante para mí que fuera posible crear absolutamente cualquier componente visual. Es decir, dibuje ui a nivel de cuadrados, círculos y caminos. Por ejemplo, un indicador de temporizador es tal vista.



No hubo ningún problema para iOS nativo, ya que hay acceso a Layer, en el que puede dibujar lo que quiera.

  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 

Para Android nativo, puede crear una clase que herede de View. Y redefina el método onDraw (Canvas Canvas), en el parámetro del cual se dibuja el objeto Canvas.

  @Override protected void onDraw(Canvas canvas) { pathCircleOne = new Path(); pathCircleOne.addArc(rectForCircle, -90, value * 3.6F); canvas.drawPath(pathCircleBackground, paintCircleBackground); } 

Para Flutter, puede crear una clase que herede de CustomPainter. Y anule el método de pintura (Canvas Canvas, Size size), que en el parámetro pasa el objeto Canvas, es decir, una implementación muy similar a la de Android.

  @override void paint(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); } 

Para React Native, no se encontró una solución lista para usar. Creo que esto se explica por el hecho de que la vista solo se describe en js, y está construida por elementos de interfaz de usuario nativos. Pero puede usar la biblioteca react-native-canvas, que le da acceso al lienzo.

  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); } } 

Trabajando con Listas de UI




El algoritmo para Android, iOS, Flutter: las soluciones son muy similares. Necesitamos indicar cuántos elementos hay en la lista. Y dé por el número del elemento la celda que desea dibujar.
IOS usa UITableView para dibujar listas, en las que necesita implementar métodos DataSource.

  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return countCell } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return cell } 

Para Android, usan RecyclerView, en cuyo adaptador implementamos métodos iOS similares.

 class MyAdapter(private val myDataset: Array<String>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() { override fun onBindViewHolder(holder: MyViewHolder, position: Int) { holder.textView.text = myDataset[position] } override fun getItemCount() = myDataset.size } 

Para el aleteo, utilizan un ListView, en el que se implementan métodos similares en el generador.

 new ListView.builder( itemCount: getCount() * 2, itemBuilder: (BuildContext context, int i) { return new HistoryWidget( Key("a ${models[index].workTime}"), models[index]); }, ) 

React Native utiliza un ListView. La implementación es similar a las soluciones anteriores. Pero no hay referencia al número y número de elementos en la lista, en DataSource establecemos la lista de elementos. Y en renderRow, implementamos la creación de una celda dependiendo de qué elemento vino.

 <ListView dataSource={this.state.dataSource} renderRow={(data) => <HistoryItem data={data}/>} /> 

Multithreading, asincronía


Cuando comencé a lidiar con la multiproceso, la asincronía, me horroricé con la variedad de soluciones. En iOS - esto es GCD, Operation, en Android - AsyncTask, Loader, Coroutine, en React Native - Promise, Async / Await, en Flutter-Future, Stream. Los principios de algunas soluciones son similares, pero la implementación sigue siendo diferente.
El amado Rx vino al rescate. Si aún no estás enamorado de él, te aconsejo que estudies. Está en todas las soluciones que considero en la forma: 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'); }) 

Como puede ver, el código se ve muy similar en los cuatro idiomas. Lo que en el futuro, si es necesario, facilitará su transición de una solución para desarrollo móvil a otra.

Base de datos


En aplicaciones móviles, el estándar es la base de datos SQLite. En cada una de las soluciones consideradas, se escribe un contenedor para trabajar con él. Android generalmente usa la sala ORM.

En iOS, Core Data. Flutter puede usar el complemento sqflite.

En React Native - react-native-sqlite-storage. Todas estas soluciones están diseñadas de manera diferente. Y para que las aplicaciones se vean, debe escribir consultas Sqlite manualmente, sin usar envoltorios.

Probablemente sea mejor mirar hacia la biblioteca de almacenamiento de datos de Realm, que usa su propio núcleo para el almacenamiento de datos. Es compatible con iOS, Android y React Native. Flutter actualmente no tiene soporte, pero los ingenieros de Realm trabajan en esa dirección.

Reino en Android

 RealmResults<Item> item = realm.where(Item.class) .lessThan("id", 2) .findAll(); 

Reino en iOS

 let item = realm.objects(Item.self).filter("id < 2") 

Reino en React Native

 let item = realm.objects('Item').filtered('id < 2'); 

Almacenamiento de valor clave


IOS nativo usa UserDefaults. En Android nativo, preferencias. En React Native and Flutter, puede usar bibliotecas que son un contenedor de almacenes de valores clave nativos (SharedPreference (Android) y 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") 

Aleteo

 SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.getInt(my key') prefs.setInt(my key', myValue) 

Reaccionar nativo

 DefaultPreference.get('my key').then(function(value) {console.log(value)}); DefaultPreference.set('my key', myValue).then(function() {console.log('done')}); 

Red


Para trabajar con la red en iOS y Android nativos, hay una gran cantidad de soluciones. Los más populares son Alamofire (iOS) y Retrofit (Android). React Native y Flutter tienen sus propios clientes independientes de la plataforma para conectarse. Todos los clientes están diseñados de manera muy similar.

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 … 

Aleteo

 http.Response response = await http.get('https://timerble-8665b.firebaseio.com/messages.json') 

Reaccionar nativo

 fetch('https://timerble-8665b.firebaseio.com/messages.json') .then((response) => response.json()) 

Tiempo de desarrollo




Probablemente sea incorrecto sacar conclusiones basadas en mi tiempo de desarrollo, ya que soy un desarrollador de Android. Pero creo que para el desarrollador de iOS, ingresar a la tecnología Flutter y Android parecerá más fácil que en React Native.

Conclusión


Comenzando a escribir un artículo, supe lo que escribiría en la conclusión. Te diré qué solución me gustó más, qué solución no debe usarse. Pero luego, después de hablar con las personas que usan estas soluciones en la producción, me di cuenta de que mis conclusiones son incorrectas, porque miro todo desde el lado de mi experiencia. Lo principal, me di cuenta de que para cada una de las soluciones consideradas, hay proyectos para los que es ideal. Y a veces es realmente más rentable para una empresa hacer una aplicación multiplataforma, en lugar de arar el desarrollo de dos nativas. Y si alguna solución no es adecuada para su proyecto, no piense que es mala en principio. Espero que este artículo sea útil. Gracias por llegar al final.

Con respecto a la edición de artículos, escriba un correo electrónico personal, arreglaré todo con mucho gusto.

Source: https://habr.com/ru/post/es430566/


All Articles