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

в 16:57, , рубрики: android, flutter, iOS, react native, мобильная разработка, разработка мобильных приложений, Разработка под android, разработка под iOS

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

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

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

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

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

Больше всех пахала лошадь, но председателем колхоза так и не стала - 2
Экран работы таймера

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

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

Мне это приложение интересно как разрабу, потому что его создании будут затронуты следующие интересующие меня компоненты:
— Верстка
— 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 устройстве.

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

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

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

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

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

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

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

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

Больше всех пахала лошадь, но председателем колхоза так и не стала - 9
Структура проекта React Native

Верстка

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

Больше всех пахала лошадь, но председателем колхоза так и не стала - 10
Визуальный редактор для нативного Android

Больше всех пахала лошадь, но председателем колхоза так и не стала - 11
Визуальный редактор для нативного iOS

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

Больше всех пахала лошадь, но председателем колхоза так и не стала - 12
Горячая перезагрузка во Flutter

Больше всех пахала лошадь, но председателем колхоза так и не стала - 13
Горячая перезагрузка в React Native

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

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

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

Больше всех пахала лошадь, но председателем колхоза так и не стала - 14
Верстка экрана таймера на Android

Больше всех пахала лошадь, но председателем колхоза так и не стала - 15
Верстка экрана таймера на iOS

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

Верстка экрана таймера 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.
Больше всех пахала лошадь, но председателем колхоза так и не стала - 16

Для нативного 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 списками

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

Алгоритм работы для 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.
В iOS — Core Data. Во Flutter можно воспользоваться плагином sqflite.
В React Native — react-native-sqlite-storage. Все эти решения спроектированы по-разному. И чтобы приложения выглядели похоже, придется писать Sqlite запросы вручную, без использования оберток.
Наверное, лучше посмотреть в сторону библиотеки для хранения данных Realm, которая использует своё ядро для хранения данных. Она поддерживается на iOS, Android и React Native. Во 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())

Скорость разработки

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

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

Заключение

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

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

Автор: ivanlardis

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js