
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 temporizadorEsse 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 IOSAs 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 projetoLayout
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 nativoNã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 NativeNo 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 iOSLayout 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(
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 ) .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