Недавно попалась на глаза новость, что вышел очередной релиз Flutter (1.9), который обещает разные вкусности и, в том числе, раннюю поддержку веб-приложений.

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

Такое вот «одно кольцо, чтобы править всеми».

Покрутив пару дней в голове мысли о том, какое приложение можно попробовать сделать, я решил выбрать задачу со звёздочкой — что нам эти проторенные дорожки? Замахнёмся на десктоп и будем героически преодолевать трудности! Забегая вперёд, скажу, что трудностей почти не возникло.

Под катом — рассказ о том как я решал привычные для React Native программиста задачи средствами Flutter, плюс общее впечатление от технологии.



Размышляя о том, какие возможности Flutter хотелось бы «пощупать», я решил, что в моём приложении должны быть:

  • запросы к удаленному API;
  • переходы между экранами;
  • анимация переходов;
  • менеджер состояния — redux или что-то подобное.

Я не умею в бэкенд, поэтому решил поискать стороннее открытое API. В итоге остановился на этом ресурсе — Курсы ЦБ РФ в XML и JSON, API. Ну и тут уже окончательно определился с функциональностью приложения: будет два экрана, на главном — список валют по курсу ЦБР, при клике на элемент списка открываем экран с детальной информацией.

Подготовка


Поскольку команда flutter create пока не умеет создавать проект для Windows/Linux (на данный момент поддерживается только Mac, для этого используйте флаг --macos), приходится использовать этот репозиторий, где имеется подготовленный пример. Клонируем репозиторий, забираем оттуда папку example, если нужно — переименовываем и дальше работаем в ней.

Так как поддержка десктоп-платформ пока находится в разработке, то нужно выполнить ещё ряд манипуляций. Чтобы получить доступ к возможностям, находящимся в разработке, выполните в терминале:

flutter channel master
flutter upgrade

Кроме того, нужно указать Flutter, что он может использовать вашу платформу:

flutter config --enable-linux-desktop

или

flutter config --enable-macos-desktop

или

flutter config --enable-windows-desktop

Если всё прошло хорошо, то выполнив команду flutter doctor вы должны увидеть похожий вывод:



Итак, декорации готовы, зрители в зале — можем начинать.

Вёрстка


Первое, что бросается в глаза после React Native — это отсутствие специального языка разметки а ля JSX. Flutter заставляет вас писать и разметку, и бизнес-логику на языке Dart. Поначалу это раздражает: взгляду не за что зацепиться, код кажется громоздким, да ещё эти скобочки в конце компонента!

Например, такие:



И это ещё не предел! Стоит удалить одну не в том месте и приятное (нет) времяпрепровождение вам гарантировано.

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

Особенность же эта заключается в том, что в Flutter стили — это такие же компоненты (если быть точнее — виджеты).

Если в React Native для расположения трёх кнопок в ряд внутри View так, чтобы они равномерно распределили пространство контейнера, мне достаточно для View в стилях указать flexDirection: 'row', а для кнопок добавить в стили flex: 1, то в Flutter есть отдельный компонент Row для расположения элементов в ряд и отдельный — для «расширяемости» элемента на всё доступное пространство: Expanded.

В результате, вместо

<View style={{height: 100, width:300, flexDirection: 'row'}}>
  <Button title='A' style={{flex:1}}>
  <Button title='B' style={{flex:1}}>
  <Button title='C' style={{flex:1}}>
</View>

нам приходится писать так:

Container(
  height: 100,
  width: 300,
  child: Row(
    children: <Widget>[
      Expanded(
        child: RaisedButton(
          onPressed: () {},
          child: Text('A'),
        ),
      ),
        Expanded(
        child: RaisedButton(
          onPressed: () {},
          child: Text('B'),
        ),
      ),
        Expanded(
        child: RaisedButton(
          onPressed: () {},
          child: Text('C'),
        ),
      ),
    ],
  ),
)

Более многословно, не правда ли?

Или, скажем, вы захотите добавить рамочку с закруглёнными краями к этому контейнеру. В React Native мы просто добавим к стилям:

borderRadius: 5, borderWidth: 1, borderColor: '#ccc'

В Flutter нам придётся добавить в аргументы контейнера что-то такое:

decoration: BoxDecoration(
  borderRadius: BorderRadius.all(Radius.circular(5)),
  border: Border.all(width: 1, color: Color(0xffcccccc))
),

В общем, поначалу моя разметка превратилась в огромные простыни кода, в которых чёрт ногу сломит. Однако не всё так плохо.

Во-первых, большие компоненты нужно конечно же разбивать — выносить в отдельные виджеты или хотя бы в методы вашего класса-виджета.

Во-вторых, очень сильно помогает плагин Flutter в VS Code — на картинке выше комменты к скобкам подписывает сам плагин (и они неудаляемые), что помогает не запутаться в скобках. Плюс средства автоформатирования — через полчаса привыкаешь периодически нажимать Ctrl+Shift+I, чтобы отформатировать код.

К тому же, синтаксис языка Dart во второй редакции стал гораздо приятнее, так что к концу дня я уже получал удовольствие от его использования. Непривычно? Да. Но не неприятно.

Запросы к API


В React Native для получения данных с какого-нибудь API мы обычно используем метод fetch, который возвращает нам Promise.

В Flutter ситуация похожая. Посмотрев примеры в документации, я добавил в pubspec.yaml (аналог package.json из мира JS) пакет http и написал примерно такую функцию:

Future<http.Response> getAnything() {
  return http.get(URL);
}

Объект Future по смыслу очень похож на Promise, поэтому здесь всё довольно прозрачно. Ну а для сериализаци/десериализации json объектов можно использовать концепцию классов-моделей со специальными методами fromJSON/toJSON. Подробнее об этом можно прочитать в документации.

Переход между экранами


Несмотря на то, что я делал десктоп-приложение, с точки зрения Flutter нет никакой разницы на какой платформе он крутится. Ну то есть, в моём случае это так, в общем — не знаю. Фактически, системное окно, в котором запускается flutter приложение — это такой же экран смартфона.

Переход между экранами выполняется достаточно тривиально: создаем класс-виджет экрана, а затем пользуемся стандартным классом Navigator.

В простейшем случае это может выглядеть как-то так:

RaisedButton(
  child: Text('Go to Detail'),
  onPressed: () {
    Navigator.of(context).push<void>(MaterialPageRoute(builder: (context) => DetailScreen()));
  },
)

Если в вашем приложении несколько экранов, то более разумно сначала подготовить словарь роутов, а затем пользоваться методом pushNamed. Небольшой пример из документации:

class NavigationApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
            ...
      routes: <String, WidgetBuilder>{
        '/a': (BuildContext context) => usualNavscreen(),
        '/b': (BuildContext context) => drawerNavscreen(),
      }
            ...
  );
  }
}

// AnyWidget
...
onPressed: () {
  Navigator.of(context).pushNamed('/a');
},
...

Кроме того, вы можете подготовить специальную анимацию для перехода между экранами и написать что-то такое:

Navigator.of(context).push<void>(ScaleRoute(page: DetailScreen()));

Здесь ScaleRoute — это специальный класс для создания анимаций перехода. Неплохие примеры таких анимаций можно найти здесь.

State managment


Бывает, что нам нужно иметь доступ к каким-нибудь данным из любой части нашего приложения. В React Native для этих целей часто (если не чаще всего) используют redux.

Для Flutter есть репозиторий, в котором приведены примеры использования различных архитектур приложения — есть там и Redux, и MVC, и MVU, и даже такие, о которых я раньше не слышал.

Покопавшись немного в этих примерах, я решил остановиться на Provider.

В целом идея достаточно проста: мы создаем специальный класс-наследник класса ChangeNotifier, в котором будем хранить наши данные, обновлять их с помощью методов этого класса и забирать их оттуда при необходимости. Подробнее — в документации пакета.

Для этого добавляем в pubspec.yaml пакет provider и готовим класс Provider. В моём случае он выглядит так:

import 'package:flutter/material.dart';
import 'package:rates_app/models/rate.dart';

class RateProvider extends ChangeNotifier {
  Rate currentrate;

  void setCurrentRate(Rate rate) {
    this.currentrate = rate;
    notifyListeners();
  }
}

Здесь Rate — это мой класс-модель валюты (с полями name, code, value и т.п.), currentrate — поле, в котором будет хранится выбранная валюта, а setCurrentRate — метод, с помощью которого обновляется значение currentrate.

Чтобы присоединить наш провайдер к приложению, изменим код класса приложения:

@override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      builder: (context) => RateProvider(), // присоединяем провайдер
      child: MaterialApp(
       ...
        ),
        home: HomeScreen(),
      ),
    );
  }

Всё, теперь если мы хотим сохранить выбранную валюту, то пишем что-то такое:

Provider.of<RateProvider>(context).setCurrentRate(rate);

А если хотим получить сохраненное значение, то такое:

var rate = Provider.of<RateProvider>(context).currentrate;

Всё достаточно прозрачно и никакого бойлерплейта (в отличие от Redux). Само собой, может быть для более сложных приложений всё окажется не так гладко, но для таких как мой пример — чистый вин.

Сборка приложения


В теории, для сборки приложения используется команда flutter build <platform>. На практике при выполнении команды flutter build linux я получил такое сообщение:



«Не больно-то и хотелось», подумал я, ужаснулся весу папки build — 287,5 МБ — и по простоте душевной удалил эту папку безвозвратно. Как оказалось — зря.

После удаления директории build проект перестал запускаться. Восстановить я её не мог, поэтому скопировал из исходного примера. Не помогло — сборщик ругался на недостающие файлы.

После проведения небольшого исследования выяснилось, что в этой папке есть файл snapshot_blob.bin.d, в котором, судя по всему, прописаны пути ко всем файлам, используемым в проекте. Я дописал недостающие пути и всё заработало.

Таким образом, на данный момент Flutter не умеет готовить релизные сборки под десктоп. Во всяком случае, для линуксов.

В целом, если закрыть глаза на этот минус, приложение получилось таким, как я и хотел и выглядит

так


Бонус


Переходим к обещанному бонусу.

Ещё на этапе написания приложения у меня возникло желание проверить, насколько трудно будет портировать его на другие платформы. Начнём с мобилки.

Наверняка есть менее варварский способ, но я решил, что самый короткий путь — прямой. Поэтому просто создал новый проект Flutter, перенёс в него файл pubspec.yaml, директории assets, fonts и lib и добавил в AndroidManifest.xml строку:

<uses-permission android:name="android.permission.INTERNET" />

Приложение запустилось с полпинка и я получил такую

картинку


С вебом поначалу пришлось повозиться. Я не знал как создавать веб-проект, поэтому воспользовался инструкциями из интернета, которые почему-то не работали. Хотел было уже плюнуть, но наткнулся на этот мануал.

В итоге, всё оказалось проще простого — нужно было всего-то включить поддержку веб-приложений. Выжимка из мануала:

flutter channel master
flutter upgrade
flutter config --enable-web
cd <into project directory>
flutter create .
flutter run -d chrome

Затем я таким же варварским способом перенёс нужные файлы в этот проект и получил такой

результат


Общие впечатления


Поначалу работать с Flutter было непривычно, я постоянно пытался применять привычные подходы из React Native и это мешало. Кроме того, немного раздражала некоторая избыточность кода на dart.

После того как немного набил руку (и шишек), мне стали видны преимущества Flutter перед React Native. Перечислю некоторые.

Язык. Dart вполне понятный и приятный язык со строгой статической типизацией. После JavaScript он был как глоток свежего воздуха. Я перестал бояться, что мой код сломается в рантайме и это было приятное ощущение. Кто-то может сказать, что есть Flow и TypeScript, но это всё не то — в наших проектах мы использовали и то, и другое и всегда где-нибудь что-нибудь ломалось. Когда я пишу на React Native, то не могу отделаться от ощущения, что мой код стоит на подпорках из спичек, которые могут сломать в любой момент. С Flutter я забыл про это ощущение, и если цена — избыточность кода, то я готов её заплатить.

Платформа. В React Native вы используете нативные компоненты и это в целом хорошо. Но из-за этого вам иногда приходится писать код, специфичный для платформы, а также отлавливать баги, специфичные для каждой из платформ. Это может быть невероятно утомительно. С Flutter вы можете забыть эти проблемы как страшный сон (хотя может быть, что в крупных приложениях всё не так гладко).

Окружение. С окружением в React Native всё грустно. Плагины vscode постоянно отваливаются, отладчик может сожрать 16 гигов оперативы и 70 гигов свопа, намертво повесив систему (из личного опыта), а самый частый сценарий исправления ошибок: «удали node_modules, установи пакеты заново и попробуй перезапустить несколько раз». Обычно это помогает, но блджад! Не так должно быть, не так.

Кроме того, вам периодически придётся запускать AndroidStudio и XCode, потому что некоторые либы ставятся только так (справедливости ради, с выходом RN 0.60 с этим стало получше).

На этом фоне официальный плагин Flutter для vscode выглядит очень недурно. Подсказки для кода позволяют знакомиться с платформой не заглядывая в документацию, автоформатирование решает проблему со стилем кодирования, нормальный отладчик и т.д.
В целом это выглядит как более зрелый инструмент.

Кроссплатформенность. React Native исповедует принцип «Learn once, write everywhere» — однажды научившись, вы сможете писать под разные платформы. Правда, под каждой платформой вы столкнётесь со специфичными для неё проблемами. Но возможно это лишь следствие незрелости React Native — на текущий момент последней стабильной версией является 0.61. Может быть с выходом версии 1.0 большинство этих проблем уйдёт.

Подход Flutter больше похож на «Write once, compile everywhere». И пусть на текущий момент десктоп не готов к продакшену, веб пока тоже в альфе, но всё идёт к этому. А возможность иметь единую кодовую базу для всех платформ — это сильный аргумент.

Само собой, Flutter тоже не лишён недостатков, но малый опыт его использования не позволяет мне их выявить. Так что если хотите более объективной оценки — смело делайте скидку на эффект новизны.

В целом, следует заметить, что Flutter оставил в основном положительные ощущения, хотя ему и есть куда расти. И следующий проект я с большей бы охотой начал на нём, а не на React Native.

Исходный код проекта можно найти на GitHub.

P.S. Пользуясь случаем, хочу поздравить всех причастных с прошедшим Днём учителя.

Комментарии (20)


  1. GroGo
    07.10.2019 09:36
    +1

    Flutter — отличный инструмент. Полностью подтверждаю. Спасибо за заметку.


  1. tmnhy
    07.10.2019 10:15
    +1

    Хелловорды работаю везде хорошо. Но не всем нужно писать приложения в духе «фронт как на вебе, только на мобилке».

    Как flutter интегрируеся с нативным кодом, в том же андроиде?


    1. san-smith Автор
      07.10.2019 10:24

      Справедливости ради, React Native — это тот же «фронт как на вебе, только на мобилке».
      Ну и если вам нужно не оно, то вам оно не нужно — ваш кэп.

      Что касается интеграции с нативным кодом — не знаю, я не настолько хорош во Flutter.


      1. tmnhy
        07.10.2019 10:26

        Раз уж вы сами упоминули реакт нативе, там всё это есть и не только на уровне нативных компонент.


        1. san-smith Автор
          07.10.2019 10:29

          Я знаю, что в React Native это есть, и предполагаю, что такое есть и во Flutter, но я с этим не работал.

          Акцент был на ваш комментарий про то что «не всем нужно» — тем, кому не нужен «фронт как на вебе, только на мобилке» не нужны ни React Native, ни Flutter.


        1. RevanScript
          07.10.2019 22:12

          И во флаттере с этим проблем нет:
          flutter.dev/docs/development/platform-integration/platform-channels


    1. irbis_al
      07.10.2019 10:44

      Хелловорды работаю везде хорошо. Но не всем нужно писать приложения в духе «фронт как на вебе, только на мобилке».

      Ну вот у нас "мобильный Торговый представитель создан на Flutter Это ж уже не helloword
      Видео части работы(из обучающей серии)
      https://cloud.mail.ru/public/4EK8/AAaoC6BPf


    1. rstrelba
      07.10.2019 15:31

      Никак, приложение на flutter компилируется в нативный код, в debug режиме в код встраивается виртуальная машина, которая позволяет делать мгновенный hot reload, а в release режиме это нативный код.



  1. Magister7
    07.10.2019 11:14

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


  1. dark_ruby
    07.10.2019 11:39

    Касательно стейт менеджмент — обратите внимание на BLOC
    это фактически де-факто эквивалент redux в flutter.


    1. san-smith Автор
      07.10.2019 15:36

      Я не специалист, но похоже это всё-таки разные вещи — в этом репозитории есть и BLOC, и redux.

      Ну и не скажу, что я в восторге от redux — слишком много шаблонного кода, который нужно написать, прежде чем оно начнёт работать. Provider выглядит как-то полегче.


  1. fokhunov
    07.10.2019 15:30

    Спасибо за статью.

    Любопытство потянуло на Flutter и переписал свое Native Android приложение.
    Результат: iOS и Android.

    ИТОГО:
    + очень легко и быстро
    — пока сыровато


    1. ko11ega
      07.10.2019 17:03

      — пока сыровато

      Могли бы вы раскрыть это конкретными примерами? Что именно вызвало у вас затруднения?


      1. fokhunov
        07.10.2019 18:08

        например,

        1. Это мой первый и единственный cross-platform, но меня размер испугал. Native Android весил около 3MB (apk). Flutter apk = 16MB, bundle = 9MB.
        2. В Native Android есть xml файлы, через которые я мог под разные экраны корректировки делать. В Flutter я не понял как это делать, если весь UI пишется в коде =)
        3. Библиотек маловато, тот же Firebase имеет лимитированную поддержку под Admob о_О
        4. Приложение запускается и работает, но всегда есть warnings (deprecated and etc).

        p.s Но все же, за скорость реализации огромный плюс.


        1. ko11ega
          08.10.2019 22:57

          1. Тут нечего пугаться, посмотрите на размеры приложений установленных на вашем телефоне. Flutter добавляет фикс размер, а не увеличивает размер приложения в разы. На более серъёзных приложениях довесок несущественный.
          2. https://habr.com/ru/company/funcorp/blog/442432/#widget_layout надо всего лишь освоить MediaQuery
          3. Чем больше конкретики в запросе тем проще приходит ответ..
          4. Анализатор можно под себя настроить


        1. irbis_al
          09.10.2019 09:50

          Насчет размера… Не знаю в курсе Вы или нет(я вначале работы с flutter был не в курсе)
          Есть дебажная зборка… и она тяжелая, ибо отягощается обратной связью со средой разработки.Вы поменяли код и тут же runtime на вашем устройстве произошли изменения.(Это ещё одна фишка за которую я люблю flutter).И есть релизная сборка (из неё выхолощены обратная связь с IDE)
          https://flutter.dev/docs/testing/build-modes
          И она намного меньше весит и намного быстрее.


          1. fokhunov
            09.10.2019 11:31

            Про билды в курсе.
            Вот тут все описано и я ему следовал при релизе.


  1. ko11ega
    07.10.2019 17:00

    del


  1. QtRoS
    07.10.2019 18:01

    Интересно, код разметки UI чем-то напоминает QML. По моему личному мнению это правильный путь, в котором есть компромисс между декларативностью и явностью разметки.