O cavalo arou mais, mas o presidente da fazenda coletiva não se tornou



Recentemente, na comunidade móvel, pode-se ouvir frequentemente sobre Flutter, React Native. Eu estava interessado em entender o lucro dessas peças. E quanto eles realmente mudarão vidas ao desenvolver aplicativos. Como resultado, foram criadas 4 aplicações (idênticas do ponto de vista das funções executadas): Android nativo, iOS nativo, Flutter, React Native. Neste artigo, descrevi o que aprendi com minha experiência e como elementos semelhantes de aplicativos são implementados nas soluções em consideração.

Comentários: o autor do artigo não é um desenvolvedor profissional de plataforma cruzada. E tudo o que está escrito é sobre a aparência de um desenvolvedor iniciante para essas plataformas. Mas acho que essa revisão será útil para pessoas que já estão usando uma das soluções em consideração e que desejam criar aplicativos para duas plataformas ou melhorar o processo de interação entre iOS e Android.

Como um aplicativo desenvolvido, foi decidido criar um "Temporizador de Esportes", que ajudará as pessoas envolvidas no esporte ao realizar o treinamento intervalado.

O aplicativo consiste em 3 telas.


Tela de operação do temporizador


Tela Histórico de Exercícios


Tela de configurações do temporizador

Esse aplicativo é interessante para mim como desenvolvedor, porque os seguintes componentes que me interessam serão afetados por sua criação:
- Layout
- Visualização personalizada
- Trabalhar com listas de interface do usuário
- multithreading
- Banco de Dados
- Rede
- armazenamento de valores-chave

É importante observar que, para Flutter e React Native, podemos criar uma ponte (canal) para a parte nativa do aplicativo e usá-la para implementar tudo o que o sistema operacional fornece. Mas eu queria saber o que as estruturas dão fora da caixa.

Seleção de ferramentas de desenvolvimento


Para um aplicativo nativo para iOS - escolhi o ambiente de desenvolvimento do Xcode e a linguagem de programação Swift. Para Android nativo - Android Studio e Kotlin. React Native desenvolvido no WebStorm, a linguagem de programação JS. Flutter - Android Studio e Dart.

Um fato interessante ao desenvolver no Flutter me pareceu que, a partir do Android Studio (o principal IDE para desenvolvimento do Android), você pode executar o aplicativo em um dispositivo iOS.



Estrutura do projeto


As estruturas dos projetos nativos iOS e Android são muito semelhantes. Este é um arquivo de layout com as extensões .storyboard (iOS) e .xml (Android), gerenciadores de dependência Podfile (iOS) e Gradle (Android), arquivos de código-fonte com as extensões .swift (iOS) e .kt (Android).


Estrutura do projeto Android


Estrutura do projeto IOS

As estruturas Flutter e React Native contêm as pastas Android e iOS, que contêm os projetos nativos usuais para Android e iOS. Flutter e React Native conectam-se a projetos nativos como uma biblioteca. De fato, quando você inicia o Flutter no seu dispositivo iOS, o aplicativo iOS nativo usual é iniciado com a biblioteca Flutter conectada. Para o React Native e para o Android, tudo é o mesmo.

Flutter e React Native também contêm gerenciadores de dependências package.json (React Native) e pubspec.yaml (Flutter) e arquivos de origem com as extensões .js (React Native) e .dart (Flutter), que também contêm o layout.


Estrutura do Projeto Flutter


Reagir estrutura nativa do projeto

Layout


Para iOS e Android nativos, existem editores visuais. Isso simplifica bastante a criação de telas.


Editor visual para Android nativo


Editor visual para iOS nativo

Não há editores visuais para React Native e Flutter, mas há suporte para a função hot reload, que pelo menos de alguma forma simplifica o trabalho com a interface do usuário.


Reinicialização a quente em Flutter


Reinicialização a quente no React Native

No Android e iOS, o layout é armazenado em arquivos separados com as extensões .xml e .storybord, respectivamente. No React Native e Flutter, o layout é derivado diretamente do código. Um ponto importante na descrição da velocidade da interface do usuário deve ser observado que o Flutter possui seus próprios mecanismos de renderização, com os quais os criadores da estrutura prometem 60 fps. E o React Native usa elementos de interface do usuário nativos criados com js, o que leva ao aninhamento excessivo.

No Android e iOS, para alterar a propriedade View, usamos um link para ele no código e, por exemplo, para alterar a cor do plano de fundo, causamos alterações diretamente no objeto. No caso de React Native e Flutter, existe outra filosofia: alteramos as propriedades dentro da chamada setState, e a própria exibição é redesenhada, dependendo do estado alterado.

Exemplos de criação de uma tela de timer para cada uma das soluções selecionadas:


Layout da tela do temporizador no Android


Layout da tela do timer no iOS

Layout da tela do temporizador de vibração

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

Reagir o layout da tela do timer 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> ); } 

Visualização personalizada


Familiarizando-me com as soluções, era importante para mim que era possível criar absolutamente qualquer componente visual. Ou seja, desenhe a interface do usuário no nível de quadrados, círculos e caminhos. Por exemplo, um indicador de temporizador é essa visão.



Não houve problema no iOS nativo, pois há acesso ao Layer, no qual você pode desenhar o que quiser.

  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, você pode criar uma classe que herda de View. E redefina o método onDraw (Canvas canvas), no parâmetro em que o objeto Canvas é desenhado nele.

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

Para Flutter, você pode criar uma classe que herda de CustomPainter. E substitua o método paint (Canvas canvas, Size size), que no parâmetro passa o objeto Canvas - ou seja, uma implementação muito semelhante à do 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, uma solução pronta para uso não foi encontrada. Eu acho que isso é explicado pelo fato de que o view é descrito apenas em js, e é construído por elementos nativos da interface do usuário. Mas você pode usar a biblioteca react-native-canvas, que fornece acesso à tela.

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

Trabalhando com listas de interface do usuário




O algoritmo para Android, iOS, Flutter - as soluções são muito semelhantes. Precisamos indicar quantos elementos estão na lista. E forneça pelo número do elemento a célula que você deseja desenhar.
O IOS usa o UITableView para desenhar listas, nas quais você precisa implementar os métodos DataSource.

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

Para o Android, eles usam o RecyclerView, no adaptador do qual implementamos métodos iOS semelhantes.

 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 flutter, eles usam um ListView, no qual métodos semelhantes são implementados no construtor.

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

O React Native usa um ListView. A implementação é semelhante às soluções anteriores. Mas não há referência ao número e número de elementos na lista; no DataSource, configuramos a lista de elementos. E no renderRow, implementamos a criação de uma célula, dependendo de qual elemento veio.

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

Multithreading, assincronia


Quando comecei a lidar com assincronia com multithreading, fiquei horrorizado com a variedade de soluções. No iOS - este é o GCD, Operation, no Android - AsyncTask, Loader, Coroutine, no React Native - Promise, Async / Await, no Flutter-Future, Stream. Os princípios de algumas soluções são semelhantes, mas a implementação ainda é diferente.
O amado Rx veio em socorro. Se você ainda não está apaixonado por ele, recomendo que você estude. Está em todas as soluções que considero no formato: 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 você pode ver, o código parece muito semelhante nos quatro idiomas. Que, no futuro, se necessário, facilitará sua transição de uma solução para desenvolvimento móvel para outra.

Banco de Dados


Em aplicativos móveis, o padrão é o banco de dados SQLite. Em cada uma das soluções consideradas, um wrapper é escrito para trabalhar com ele. O Android geralmente usa a sala ORM.

No iOS, Core Data. Flutter pode usar o plugin sqflite.

Em React Native - reat-native-sqlite-storage. Todas essas soluções são projetadas de maneira diferente. E para fazer com que os aplicativos pareçam, você precisa escrever consultas Sqlite manualmente, sem usar wrappers.

Provavelmente, é melhor procurar a biblioteca de armazenamento de dados do Realm, que usa seu próprio kernel para armazenamento de dados. É suportado no iOS, Android e React Native. Atualmente, o Flutter não tem suporte, mas os engenheiros do Reino trabalham nessa direção.

Reino no Android

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

Região no iOS

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

Região em React Native

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

Armazenamento de valor-chave


O iOS nativo usa os padrões de usuário. No Android nativo, preferências. Em React Native and Flutter, é possível usar bibliotecas que são um invólucro de armazenamentos de valores-chave nativos (SharedPreference (Android) e 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) 

Reagir nativo

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

Rede


Para trabalhar com a rede em iOS e Android nativos, há um grande número de soluções. Os mais populares são Alamofire (iOS) e Retrofit (Android). O React Native e o Flutter têm seus próprios clientes independentes de plataforma para ficar online. Todos os clientes são projetados de maneira muito semelhante.

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

Reagir nativo

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

Tempo de desenvolvimento




Provavelmente, é incorreto tirar conclusões com base no meu tempo de desenvolvimento, já que sou desenvolvedor do Android. Mas acho que, para o desenvolvedor do iOS, entrar na tecnologia Flutter e Android parecerá mais fácil do que no React Native.

Conclusão


Começando a escrever um artigo, eu sabia o que escreveria na conclusão. Vou dizer qual solução eu mais gostei, qual solução não deve ser usada. Mas então, depois de conversar com pessoas que usam essas soluções na produção, percebi que minhas conclusões estão incorretas, porque observo tudo do lado da minha experiência. O principal, percebi que, para cada uma das soluções consideradas, existem projetos para os quais é ideal. E, às vezes, é realmente mais lucrativo para uma empresa criar um aplicativo de plataforma cruzada, em vez de se interessar pelo desenvolvimento de dois nativos. E se alguma solução não for adequada para o seu projeto, não pense que seja ruim em princípio. Espero que este artigo seja útil. Obrigado por chegar ao fim.

Em relação às edições do artigo, escreva em um e-mail pessoal, eu consertarei tudo com prazer

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


All Articles