Le cheval a labouré le plus, mais le président de la ferme collective n'est pas devenu



Récemment, dans la communauté mobile, on peut souvent entendre parler de Flutter, React Native. J'étais intéressé à comprendre le profit de ces pièces. Et combien ils changeront vraiment des vies lors du développement d'applications. En conséquence, 4 applications (identiques du point de vue des fonctions exercées) ont été créées: Android natif, iOS natif, Flutter, React Native. Dans cet article, j'ai décrit ce que j'ai appris de mon expérience et comment des éléments similaires d'applications sont implémentés dans les solutions envisagées.

Commentaires: l'auteur de l'article n'est pas un développeur professionnel multiplateforme. Et tout ce qui est écrit sur le look d'un développeur novice pour ces plates-formes. Mais je pense que cette revue sera utile aux personnes qui utilisent déjà l'une des solutions envisagées et qui envisagent d'écrire des applications pour deux plates-formes ou d'améliorer le processus d'interaction entre iOS et Android.

En tant qu'application développée, il a été décidé de créer un «chronomètre sportif», qui aidera les personnes impliquées dans le sport lors de l'entraînement par intervalles.

L'application se compose de 3 écrans.


Écran de fonctionnement de la minuterie


Écran Historique d'entraînement


Écran Paramètres du minuteur

Cette application m'intéresse en tant que développeur, car les composants suivants qui m'intéressent seront affectés par sa création:
- Disposition
- Vue personnalisée
- Travailler avec des listes d'interface utilisateur
- multithreading
- Base de données
- Réseau
- stockage clé-valeur

Il est important de noter que pour Flutter et React Native, nous pouvons créer un pont (canal) vers la partie native de l'application et l'utiliser pour implémenter tout ce que le système d'exploitation fournit. Mais je me demandais quels cadres donner de la boîte.

Sélection d'outils de développement


Pour une application native pour iOS - j'ai choisi l'environnement de développement Xcode et le langage de programmation Swift. Pour Android natif - Android Studio et Kotlin. React Native développé dans WebStorm, le langage de programmation JS. Flutter - Android Studio et Dart.

Un fait intéressant lors du développement sur Flutter m'a semblé que depuis Android Studio (le principal IDE pour le développement Android), vous pouvez exécuter l'application sur un appareil iOS.



Structure du projet


Les structures des projets natifs iOS et Android sont très similaires. Il s'agit d'un fichier de mise en page avec les extensions .storyboard (iOS) et .xml (Android), les gestionnaires de dépendances Podfile (iOS) et Gradle (Android), les fichiers de code source avec les extensions .swift (iOS) et .kt (Android).


Structure de projet Android


Structure du projet IOS

Les structures natives Flutter et React contiennent les dossiers Android et iOS, qui contiennent les projets natifs habituels pour Android et iOS. Flutter et React Native se connectent à des projets natifs en tant que bibliothèque. En fait, lorsque vous lancez Flutter sur votre appareil iOS, l'application iOS native habituelle se lance avec la bibliothèque Flutter connectée. Pour React Native et pour Android, tout est pareil.

Flutter et React Native contiennent également des gestionnaires de dépendances et fichiers source package.json (React Native) et pubspec.yaml (Flutter) avec les extensions .js (React Native) et .dart (Flutter), qui contiennent également la mise en page.


Structure du projet Flutter


Réagir à la structure du projet natif

Disposition


Pour iOS et Android natif, il existe des éditeurs visuels. Cela simplifie considérablement la création d'écrans.


Éditeur visuel pour Android natif


Éditeur visuel pour iOS natif

Il n'y a pas d'éditeurs visuels pour React Native et Flutter, mais la fonction de rechargement à chaud est prise en charge, ce qui simplifie au moins en quelque sorte le travail avec l'interface utilisateur.


Redémarrage à chaud dans Flutter


Redémarrage à chaud dans React Native

Sous Android et iOS, la mise en page est stockée dans des fichiers séparés avec les extensions .xml et .storybord, respectivement. Dans React Native et Flutter, la mise en page est dérivée directement du code. Un point important dans la description de la vitesse de l'interface utilisateur doit être noté que Flutter a ses propres mécanismes de rendu, avec lesquels les créateurs du framework promettent 60 fps. Et React Native utilise des éléments d'interface utilisateur natifs qui sont construits à l'aide de js, ce qui conduit à leur imbrication excessive.

Sous Android et iOS, pour modifier la propriété View, nous utilisons un lien vers celle-ci à partir du code et, par exemple, pour changer la couleur d'arrière-plan, nous provoquons directement des modifications sur l'objet. Dans le cas de React Native et Flutter, il existe une autre philosophie: nous modifions les propriétés à l'intérieur de l'appel setState, et la vue elle-même est redessinée en fonction de l'état modifié.

Exemples de création d'un écran de minuterie pour chacune des solutions sélectionnées:


Disposition de l'écran du minuteur sur Android


Disposition de l'écran du minuteur sur iOS

Disposition de l'écran du minuteur 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, ), ) ], )); } 

Disposition de l'écran du minuteur React Native

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

Vue personnalisée


Se familiariser avec les solutions, il était important pour moi qu'il était possible de créer absolument n'importe quel composant visuel. Autrement dit, dessinez ui au niveau des carrés, des cercles et des chemins. Par exemple, un indicateur de minuterie est une telle vue.



Il n'y avait aucun problème pour iOS natif, car il y a accès à Layer, sur lequel vous pouvez dessiner tout ce que vous voulez.

  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 

Pour Android natif, vous pouvez créer une classe qui hérite de View. Et redéfinissez la méthode onDraw (Canvas canvas), dans le paramètre dont l'objet Canvas est dessiné dessus.

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

Pour Flutter, vous pouvez créer une classe qui hérite de CustomPainter. Et remplacez la méthode paint (Canvas canvas, Size size), qui dans le paramètre transmet l'objet Canvas - c'est-à-dire une implémentation très similaire à celle d'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); } 

Pour React Native, aucune solution prête à l'emploi n'a été trouvée. Je pense que cela s'explique par le fait que la vue n'est décrite que sur js, et elle est construite par des éléments ui natifs. Mais vous pouvez utiliser la bibliothèque react-native-canvas, qui donne accès au canevas.

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

Utilisation des listes d'interface utilisateur




L'algorithme pour Android, iOS, Flutter - les solutions sont très similaires. Nous devons indiquer le nombre d'éléments dans la liste. Et donnez par le numéro de l'élément la cellule que vous voulez dessiner.
IOS utilise UITableView pour dessiner des listes, dans lesquelles vous devez implémenter des méthodes DataSource.

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

Pour Android, ils utilisent RecyclerView, dans l'adaptateur dont nous implémentons des méthodes iOS similaires.

 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 } 

Pour flutter, ils utilisent un ListView, dans lequel des méthodes similaires sont implémentées dans le générateur.

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

React Native utilise un ListView. L'implémentation est similaire aux solutions précédentes. Mais il n'y a aucune référence au nombre et au nombre d'éléments dans la liste, dans le DataSource nous définissons la liste des éléments. Et dans renderRow, nous implémentons la création d'une cellule en fonction de l'élément venu.

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

Multithreading, asynchronie


Quand j'ai commencé à m'occuper du multithreading, de l'asynchronie, j'ai été horrifié par la variété des solutions. Dans iOS - c'est GCD, Operation, dans Android - AsyncTask, Loader, Coroutine, dans React Native - Promise, Async / Await, dans Flutter-Future, Stream. Les principes de certaines solutions sont similaires, mais la mise en œuvre est toujours différente.
Le bien-aimé Rx est venu à la rescousse. Si vous n'êtes pas déjà amoureux de lui, je vous conseille d'étudier. C'est dans toutes les solutions que je considère sous la forme: 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'); }) 

Comme vous pouvez le voir, le code est très similaire dans les quatre langues. Qui à l'avenir, si nécessaire, facilitera votre transition d'une solution de développement mobile à une autre.

Base de données


Dans les applications mobiles, la norme est la base de données SQLite. Dans chacune des solutions envisagées, un wrapper est écrit pour travailler avec. Android utilise généralement la salle ORM.

Sur iOS, Core Data. Flutter peut utiliser le plugin sqflite.

Dans React Native - react-native-sqlite-storage. Toutes ces solutions sont conçues différemment. Et pour que les applications ressemblent, vous devez écrire des requêtes Sqlite manuellement, sans utiliser de wrappers.

Il est probablement préférable de regarder vers la bibliothèque de stockage de données de Realm, qui utilise son propre noyau pour le stockage de données. Il est pris en charge sur iOS, Android et React Native. Flutter n'a actuellement pas de support, mais les ingénieurs de Realm travaillent dans cette direction.

Royaume sur Android

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

Royaume sur iOS

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

Royaume dans React Native

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

Stockage de valeurs-clés


IOS natif utilise UserDefaults. Dans Android natif, les préférences. Dans React Native et Flutter, vous pouvez utiliser des bibliothèques qui regroupent des magasins de valeurs-clés natifs (SharedPreference (Android) et 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')}); 

Réseau


Pour travailler avec le réseau dans iOS et Android natif, il existe un grand nombre de solutions. Les plus populaires sont Alamofire (iOS) et Retrofit (Android). React Native et Flutter ont leurs propres clients indépendants de la plate-forme pour se connecter. Tous les clients sont conçus de manière très similaire.

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()) 

Temps de développement




Il est probablement incorrect de tirer des conclusions en fonction de mon temps de développement, car je suis développeur Android. Mais je pense que pour le développeur iOS, entrer dans la technologie Flutter et Android semblera plus facile que dans React Native.

Conclusion


Commençant à écrire un article, je savais ce que j'écrirais dans la conclusion. Je vais vous dire quelle solution je préfère, quelle solution ne pas utiliser. Mais ensuite, après avoir parlé avec des gens qui utilisent ces solutions en production, j'ai réalisé que mes conclusions étaient incorrectes, car je regarde tout du côté de mon expérience. L'essentiel, j'ai réalisé que pour chacune des solutions envisagées, il existe des projets pour lesquels il est idéalement adapté. Et parfois, il est vraiment plus rentable pour une entreprise de créer une application multiplateforme, plutôt que de labourer le développement de deux applications natives. Et si une solution ne convient pas à votre projet, ne pensez pas qu'elle est mauvaise en principe. J'espère que cet article vous sera utile. Merci d'être arrivé au bout.

En ce qui concerne les modifications d'articles, veuillez écrire dans un e-mail personnel, je réparerai tout avec plaisir.

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


All Articles