На работе я занимаюсь разработкой мобильных приложений на 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)
tmnhy
07.10.2019 10:15+1Хелловорды работаю везде хорошо. Но не всем нужно писать приложения в духе «фронт как на вебе, только на мобилке».
Как flutter интегрируеся с нативным кодом, в том же андроиде?san-smith Автор
07.10.2019 10:24Справедливости ради, React Native — это тот же «фронт как на вебе, только на мобилке».
Ну и если вам нужно не оно, то вам оно не нужно — ваш кэп.
Что касается интеграции с нативным кодом — не знаю, я не настолько хорош во Flutter.tmnhy
07.10.2019 10:26Раз уж вы сами упоминули реакт нативе, там всё это есть и не только на уровне нативных компонент.
san-smith Автор
07.10.2019 10:29Я знаю, что в React Native это есть, и предполагаю, что такое есть и во Flutter, но я с этим не работал.
Акцент был на ваш комментарий про то что «не всем нужно» — тем, кому не нужен «фронт как на вебе, только на мобилке» не нужны ни React Native, ни Flutter.
RevanScript
07.10.2019 22:12И во флаттере с этим проблем нет:
flutter.dev/docs/development/platform-integration/platform-channels
irbis_al
07.10.2019 10:44Хелловорды работаю везде хорошо. Но не всем нужно писать приложения в духе «фронт как на вебе, только на мобилке».
Ну вот у нас "мобильный Торговый представитель создан на Flutter Это ж уже не helloword
Видео части работы(из обучающей серии)
https://cloud.mail.ru/public/4EK8/AAaoC6BPf
rstrelba
07.10.2019 15:31Никак, приложение на flutter компилируется в нативный код, в debug режиме в код встраивается виртуальная машина, которая позволяет делать мгновенный hot reload, а в release режиме это нативный код.
aaabramenko
07.10.2019 16:25Есть библиотека ffi и PlatformChannels
https://flutter.dev/docs/development/platform-integration/c-interop
https://flutter.dev/docs/development/platform-integration/platform-channels
Magister7
07.10.2019 11:14Интересно, они уже сделали возможность корректно обработать оповещение о нехватке памяти на Android? Корректно в данном случае — это синхронно, т.к. если обрабатывать асинхронно то система вполне может убить процесс — т.к. вызов обработчика завершился, а памяти больше не стало.
fokhunov
07.10.2019 15:30Спасибо за статью.
Любопытство потянуло на Flutter и переписал свое Native Android приложение.
Результат: iOS и Android.
ИТОГО:
+ очень легко и быстро
— пока сыроватоko11ega
07.10.2019 17:03— пока сыровато
Могли бы вы раскрыть это конкретными примерами? Что именно вызвало у вас затруднения?
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 Но все же, за скорость реализации огромный плюс.ko11ega
08.10.2019 22:57- Тут нечего пугаться, посмотрите на размеры приложений установленных на вашем телефоне. Flutter добавляет фикс размер, а не увеличивает размер приложения в разы. На более серъёзных приложениях довесок несущественный.
- https://habr.com/ru/company/funcorp/blog/442432/#widget_layout надо всего лишь освоить MediaQuery
- Чем больше конкретики в запросе тем проще приходит ответ..
- Анализатор можно под себя настроить
irbis_al
09.10.2019 09:50Насчет размера… Не знаю в курсе Вы или нет(я вначале работы с flutter был не в курсе)
Есть дебажная зборка… и она тяжелая, ибо отягощается обратной связью со средой разработки.Вы поменяли код и тут же runtime на вашем устройстве произошли изменения.(Это ещё одна фишка за которую я люблю flutter).И есть релизная сборка (из неё выхолощены обратная связь с IDE)
https://flutter.dev/docs/testing/build-modes
И она намного меньше весит и намного быстрее.
QtRoS
07.10.2019 18:01Интересно, код разметки UI чем-то напоминает QML. По моему личному мнению это правильный путь, в котором есть компромисс между декларативностью и явностью разметки.
GroGo
Flutter — отличный инструмент. Полностью подтверждаю. Спасибо за заметку.