Главная » Хабрахабр » Больше всех пахала лошадь, но председателем колхоза так и не стала

Больше всех пахала лошадь, но председателем колхоза так и не стала

Мне стало интересно понять профит от этих штук. В последнее время в мобильном сообществе часто можно услышать про Flutter, React Native. В итоге было создано 4 (одинаковых с точки зрения выполняемых функции) приложения: нативное Android, нативное iOS, Flutter, React Native. И насколько они реально изменят жизнь при разработке приложений. И все о чем написано — это взгляд начинающего разраба под эти платформы. В этой статье я описал то, что вынес из своего опыта и как реализуются схожие элементы приложений в рассматриваемых решениях.
Комментарии: автор статьи не является профессиональным кроссплатформенным разработчиком. Но думаю, этот обзор будет полезен людям, уже использующим одно из рассматриваемых решений, и которые смотрят в сторону того, чтобы писать приложения под две платформы или улучшить процесс взаимодействия iOS и Android.

В качестве разрабатываемого приложения было решено сделать “Спортивный Таймер”, который поможет людям, занимающимся спортом, при выполнении интервальных тренировок.

Приложение состоит из 3 экранов.


Экран работы таймера


Экран истории тренировок


Экран настроек таймера

Мне это приложение интересно как разрабу, потому что его создании будут затронуты следующие интересующие меня компоненты:
— Верстка
— Custom View
— Работа с UI списками
— Многопоточность
— База данных
— Сеть
— key-value хранилище

Но мне было интересно, что фреймворки дают из коробки. Важно заметить, что для Flutter и React Native мы можем создать мост (канал) в нативную часть приложения и с его помощью реализовать все то, что предоставляет операционная система.

Выбор средств для разработки

Для нативного приложения под iOS — я выбрал среду разработки XCode и язык программирования Swift. Для нативного Android — Android Studio и Kotlin. React Native разрабатывал в WebStorm, язык программирования JS. Flutter — Android Studio и Dart.

Интересным фактом при разработке на Flutter мне показалось то, что прямо из Android Studio (главной IDE для Android разработки) можно запустить приложение на iOS устройстве.

Структура проектов

Структуры нативного iOS и Android проектов очень схожи. Это файл для верстки с расширениями .storyboard (iOS) и .xml (Android), менеджеры зависимостей Podfile(iOS) и Gradle(Android), файлы исходного кода с расширениями .swift (iOS) и .kt(Android).


Структура проекта Android


Структура проекта iOS

Подключается Flutter и React Native к нативным проектам как библиотека. Структуры Flutter и React Native содержат папки Android и iOS, в которых находятся обычные нативные проекты под Android и iOS. Для React Native и под Android все аналогично. По факту, при запуске Flutter на устройстве iOS запускается обычное нативное приложение iOS с подключенной библиотекой Flutter.

Также Flutter и React Native содержат менеджеры зависимостей package.json(React Native) и pubspec.yaml(Flutter) и файлы исходного кода с расширениями .js (React Native) и .dart(Flutter) в которых находится и верстка.


Структура проекта Flutter


Структура проекта React Native

Верстка

Для нативного iOS и Android существуют визуальные редакторы. Это очень упрощает создание экранов.


Визуальный редактор для нативного Android


Визуальный редактор для нативного iOS

Для React Native и Flutter визуальных редакторов нет, но существует поддержка функции горячей перезагрузки, которая хоть как-то упрощает работу с UI.


Горячая перезагрузка во Flutter


Горячая перезагрузка в React Native

В React Native и Flutter верстка происходит прямо из кода. В Android и iOS верстка хранится в отдельных файлах с расширениями .xml и .storybord соответственно. А React Native использует нативные ui элементы, которые строятся при помощи js, что ведет к их излишней вложенности. Важным моментом при описании скорости ui нужно отметить то, что у Flutter собственные механизмы рендеринга, при помощи которых создатели фреймворка обещают 60 fps.

В случае React Native и Flutter другая философия: свойства мы меняем внутри вызова setState, а view уже сама перерисовывается в зависимости от измененного состояния. В Android и iOS для изменения свойства View мы используем ссылку на нее из кода и, например, чтобы изменить цвет фона вызываем изменения у объекта напрямую.

Примеры создания экрана таймера для каждого из выбранных решений:


Верстка экрана таймера на Android


Верстка экрана таймера на iOS

Верстка экрана таймера Flutter

@override Widget build(BuildContext context) ", 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, ), ) ], )); }

Верстка экрана таймера 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> ); }

Custom View

Знакомясь с решениями, мне было важно, чтобы можно было создать абсолютно любой визуальный компонент. То есть рисовать ui на уровне квадратов, кругов и путей. Например, индикатор таймера является такой view.

Для нативного iOS не было проблем, так как есть доступ к Layer, на котором можно нарисовать все, что угодно.

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

Для нативного Android можно создать класс, наследующийся от View. И переопределить метод onDraw(Canvas canvas), в параметре которого объект Canvas — на нем и рисуем.

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

Для Flutter можно создать класс, который наследуется от CustomPainter. И переопределить метод paint(Canvas canvas, Size size), который в параметре передает объект Canvas — то есть очень похожая реализация как в 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); }

Для React Native решение из коробки не было найдено. Думаю, это объясняется тем, что на js только описывается view, а строится уже нативными ui элементами. Но можно воспользоваться библиотекой react-native-canvas, которая дает доступ к 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); } }

Работа с UI списками

Нам нужно указать, сколько элементов в списке. Алгоритм работы для Android, iOS, Flutter — решений очень схож. И выдать по номеру элемента ту ячейку, которую нужно нарисовать.
В iOS для рисования списков используют UITableView, в котором нужно реализовать методы DataSource.

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

Для Android используют RecyclerView, в адаптере которого, мы реализуем аналогичные IOS методы.

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
}

Для flutter используют ListView, в котором в билдере реализуются аналогичные методы.

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

В React Native используют ListView. Реализация схожа с предыдущими решениями. Но здесь нет привязки к номеру и количеству элементов в списке, в DataSource мы задаем список элементов. А в renderRow реализуем создание ячейки в зависимости от того, какой элемент пришел.

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

Многопоточность, асинхронность

Когда я начал разбираться с многопоточностью, асинхронностью — то ужаснулся разнообразием решений. В iOS — это GCD, Operation, в Android — AsyncTask, Loader, Coroutine, в React Native — Promise, Async/Await, во Flutter- Future, Stream. Принципы некоторых решение схожи, но реализация все же отличается.
На спасение пришел всеми любимый Rx. Если вы еще не влюблены в него, советую изучить. Он есть во всех рассматриваемых мною решениях в виде: 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'); })

Как видите, код выглядит очень схоже на всех четырех языках. Что в будущем, в случае необходимости, облегчит вам переход с одного решения для мобильной разработки к другому.

База данных

В мобильных приложениях стандартом является SQLite база данных. В каждом из рассматриваемых решений написана обертка для работы с ней. В Android обычно используют ORM Room.

Во Flutter можно воспользоваться плагином sqflite. В iOS — Core Data.

Все эти решения спроектированы по-разному. В React Native — react-native-sqlite-storage. И чтобы приложения выглядели похоже, придется писать Sqlite запросы вручную, без использования оберток.

Она поддерживается на iOS, Android и React Native. Наверное, лучше посмотреть в сторону библиотеки для хранения данных Realm, которая использует своё ядро для хранения данных. Во Flutter на данный момент поддержки нет, но инженеры Realm работают в этом направлении.

Realm в Android

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

Realm в iOS

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

Realm в React Native

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

Key-value хранилище

В нативном iOS используется UserDefaults. В нативным Android — preferences. В React Native и Flutter можно пользоваться библиотеками, которые являются оберткой нативных key-value хранилищ (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')});

Сеть

Для работы с сетью в нативном iOS и Android есть огромное количество решений. Самые популярные — это Alamofire (iOS) и Retrofit (Android). В React Native и Flutter написаны свои собственные независимые от платформы клиенты для похода в сеть. Все клиенты спроектированы очень схоже.

Android

Retrofit.Builder() .baseUrl("https://timerble-8665b.firebaseio.com") .build() @GET("/messages.json")
fun getData(): Flowable<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())

Время разработки

Но думаю, для iOS разработчика вход в технологию Flutter и Android покажется легче, чем в React Native. Наверно некорректно делать какие-то выводы, исходя из моего времени разработки, так как я являюсь Android-разработчиком.

Заключение

Начиная писать статью, я знал, о чем напишу в выводе. Расскажу, какое решение понравилось больше, какое решения не нужно использовать. Но потом, пообщавшись с людьми, которые пользуются на продакшене этими решениями, понял, что мои выводы некорректны, потому что я смотрю на все со стороны своего опыта. Главное, я понял, что для каждого из рассматриваемых решений, есть свои проекты, для которых оно подходит идеально. И иногда для бизнеса реально выгоднее делать кроссплатформенное приложение, а не пахать над разработкой двух нативных. И если какое-то решение не подходит именно для вашего проекта, не стоит думать, что оно плохое в принципе. Надеюсь, статья будет полезна. Спасибо, что добрались до конца.

По поводу правок статьи, пожалуйста, пишите в личку, я с удовольствием все поправлю.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Расширяйте кругозор, Холмс! Или зачем физикам скрипка и кулинарные навыки

О современной литературе, философии и политике он, по-видимому, не знал почти ничего.… Однако мое изумление достигло апогея, когда я случайно обнаружил, что он не знаком с теорией Коперника и не представляет себе, как устроена Солнечная система.… «Но не знать о ...

Спам звонки. Можно ли с ними бороться?

Доброго времени суток, Хабр. Есть несколько способов бороться со спамными звонками на мобильный телефон, но бывают такие ситуации, когда это практически невозможно. Я расскажу свой случай о систематических звонках со скрытого номера.Все мы привыкли раз в какое-то время получать спамные ...