
После доклада Юры Лучанинова, я решил для себя попробовать Flutter. Чтобы мозг размять, и чтобы было о чем похоливарить с мужиками на кухне. Дело пошло. Я начал смотреть, потом читать, потом писать. И вроде все получается, приложения запускаются, и то что объясняют — понятно, все просто. Но не без “но” — объясняют не все. А поскольку платформа, ЯП, подходы и даже предметная область для меня новые, то подобное вызывает раздражение, ведь у тебя “не запускается”, а ты даже не знаешь что гуглить: Dart/Flutter/Window/Screen/Route/Widget?
Перечитывать всю документацию Dart, Flutter и его виджетов конечно же не хотелось, ведь времени у меня не много, и мне лишь хотелось присмотреться к Flutter. Вот было бы здорово, если бы был небольшой гайд, в котором описано все нужное, но не больше, для понимая и написания не слишком сложных приложений на Flutter!
Про гайд
Большинство статей по этой теме хорошо написаны и не являются сложными. Проблема заключается в том, что большинство из них предполагают наличие знаний считающихся базовыми основами, которые, тем не менее, не упоминаются в других статьях описывающих основы. В данной серии статей я хочу исправить эту ситуацию. Начнем с нуля и, не оставляя ничего из упомянутого без внимания, запустим одно или несколько приложений. В процессе научимся пользоваться всеми основными компонентами, создавать уникальный интерфейс, работать с нативными модулями, и, конечно же, собирать свое приложение под обе платформы.
Писать я буду с перспективы веб-разработчика. Большинство из вас скорее всего знакомо со стэком веба, а аналогия со знакомой платформой лучше аналогии с постройкой домов или чего там еще, Animal, Dog, Foo, Bar…
Излагаться постараюсь кратко, чтобы не затягивать. А для самых любознательных буду оставлять полезные ссылки по обсуждаемым темам.
Про платформу
Flutter — молодая, но очень многообещающая платформа, уже привлекшая к себе внимание крупных компаний, которые запустили свои приложения. Интересна эта платформа своей простотой сравнимой с разработкой веб-приложений, и скоростью работы на равне с нативными приложениями. Высокая производительность приложения и скорость разработки достигается за счет нескольких техник:
- В отличии от многих известных на сегодняшний день мобильных платформ, Flutter не использует JavaScript ни в каком виде. В качестве языка программирования для Flutter выбрали Dart, который компилируется в бинарный код, за счет чего достигается скорость выполнения операций сравнимая с Objective-C, Swift, Java, или Kotlin.
- Flutter не использует нативные компоненты, опять же, ни в каком виде, так что не приходится писать никаких прослоек для коммуникации с ними. Вместо этого, подобно игровым движкам (а вы ведь знаете что у игр очень динамичный UI), он отрисовывает весь интерфейс самостоятельно. Кнопки, текст, медиа-элементы, фон — все это отрисовывается внутри графического движка в самом Flutter. После вышесказанного стоит отметить, что “Hello World” приложение на Flutter занимает совсем немного места: iOS ≈ 2.5Mb и Android ≈ 4Mb.
- Для построения UI во Flutter используется декларативный подход, вдохновленный веб-фреймворком ReactJS, на основе виджетов (в мире веба именуемых компонентами). Для еще большего прироста в скорости работы интерфейса виджеты перерисовываются по необходимости — только когда в них что-то изменилось (подобно тому как это делает Virtual DOM в мире веб-фронтенда).
- В дополнение ко всему, в фреймворк встроен Hot-reload, такой привычный для веба, и до сих пор отсутствовавший в нативных платформах.
О практической пользе этих факторов я очень рекомендую прочитать статью Android разработчика, который переписал свое приложени�� с Java на Dart и поделившегося своими впечатлениями. Сюда я лишь вынесу названное им количество файлов/строк кода до (написанное на Java) — 179/12176, и после (переписанное на Dart) — 31/1735. В документации можно найти подробное описание технических особенностей платформы. А вот ещё ссылка, если интересно посмотреть другие примеры работающих приложений.
Про Dart
Dart — язык программирования на котором нам предстоит писать приложения под Flutter. Он очень прост, и если у вас есть опыт работы с Java или JavaScript, вы быстро его освоите.
Я пытался написать обзорную статью о Dart, стремясь описать лишь необходимый минимум для изучения Flutter. Но в этом языке столько нюансов, что несмотря на несколько попыток написать такую статью, у меня так и не удалось сделать ее достаточно полной и в то же время короткой. С другой стороны, авторы A Tour of the Dart Language отлично справились с этой задачей.
Про подготовку
Эта тема, как и Dart, очень хорошо описана в официальном гайде. Я мог бы разве что скопировать ее сюда, но делать этого не стану.
Ничего не дожидаясь, идем на страницу гайда по установке, выбираем платформу и по шагам выполняем инструкцию для установки платформы на нашу систему. В своем редакторе обязательно подключаем плагины. В том же гайде есть инструкция по настройке VS Code и IntelliJ. Для вашего редактора тоже найдутся плагины для Dart и Flutter (обычно нужно ставить два). Запускаем приложение и проверяем его работоспособность.
Подсказка для пользователей OSX. Мне жалко места занимаемого нарисованными рамками телефона в эмуляторе iOS, поэтому я их отключил и переключился на iPhone 8 (он не такой “длинный”):
Hardware → Device → iOS # → iPhone 8Window → Show Device Bezels
Без кнопок жить можно, ведь есть хоткеи: Shift + Cmd + H — это домой, Cmd + Right — а это перевернуть телефон, остальное можно найти в меню Hardware. А вот экранную клавиатуру я советую включить, ведь важно понимать можно ли работать с приложением когда половина экрана регулярно перекрывается клавиатурой: Cmd + K (работает когда фокус находится на каком-то поле ввода).
iPhone 8 & iPhone X с рамками

iPhone 8 & iPhone X без рамок

Про структуру
Зайдем в папку со сгенерированным приложением и разберемся что у нас там есть. Не со всем, но с нужным:
lib/— По принципам pub (менеджер пакетов Dart’а) весь код лежит в этой подпапке;pubspec.yml— сюда записываются зависимости приложения, которые нужно установить для его запуска, точно какpackage.json, но есть нюанс, устанавливать их нужно не через стандартную утилиту Dart’а, о которой говорилось выше, а через команду Flutter’а:flutter pub get <package_name>;test/— вы ведь знаете что там? Запустить их можно вызвавflutter test;ios/&android/— папки с настройками для каждой из платформ, там указывается какие права нужны для запуска приложения (доступ к локации, bluetooth), иконочки и все что специфично для платформы.
Со структурой разобрались, заходим в папку lib/ где нас ждет main.dart файл. Это, как вы можете догадаться, тот самый файл в котором мы должны запускать наше приложение. А запускается оно подобно как в языке C (и еще тонны других) вызовом функции main().
Про виджеты (Hello World здесь)
Во Flutter’е все построено на Widget’ах: тут и вьюшки, и стили с темами, и состояние в виджетах хранится. Есть два основных типа виджетов: со стейтом и без, но пока не об этом. Давайте с простого.
Удаляем все из main.dart. Вставляем следующий код внимательно вчитываясь в комментарии:
import 'package:flutter/widgets.dart'; // подключаем базовый набор виджетов // Когда Dart запускает приложение он вызывает функцию main() main() => runApp( // а функция runApp запускает Flutter Text( // этот виджет, он отрисовывает текст, такой себе <span> 'Hello, World!!!', // первый аргумент — текст который нужно отобразить textDirection: TextDirection.ltr, // а здесь мы указываем направление текста ), );
runApp(…) принимает единственный аргумент — виджет, который будет корневым для всего проекта. Кстати, его изменения Hot-reload подхватить не может, так что нужно будет перезапускать приложение.
Text(…) — Flutter не может просто отобразить строку на экране. Для вывода текста необходимо указать Text. textDirection. И это не выравнивание текста вроде text-align, если сравнивать с вебом, то это аналог direction. Часть API для интернационализации приложения. Text не заработает, пока не будет знать направление, но указывать его везде не придется — дальше мы разберем как настроить направление текста для всего приложения.
Уже запустили приложение? “Hello, World!” вывелся! Вроде бы… Да? Но что-то явно пошло не так.

Текст перекрыт системной информацией. В нашем распоряжении все пространство экрана, и мы вывели виджет в самом его начале, где в том числе выводится системная информация. Давайте попробуем куда-то подвинуть наш текст.
import 'package:flutter/widgets.dart'; main() => runApp( Center( // виджет, который выравнивает содержимое по центру child: Text( 'Hello, World!', textDirection: TextDirection.ltr, ), ), );
Center(…) — это виджет который позволяет разместить другой виджет, переданный в аргументе child, в центре по горизонтали и вертикали. Вы часто будете встречать child и children в приложениях Flutter, так как практически все виджеты используют эти имена для передачи виджетов, которые должны быть отрисованы внутри вызываемого виджета.
Композиции виджетов используются в Flutter для отрисовки UI, изменения внешнего вида, и даже для передачи данных. К примеру виджет Directionality(…) задает направление текста для всех дочерних виджетов:
import 'package:flutter/widgets.dart'; main() => runApp( Directionality( textDirection: TextDirection.ltr, child: Center( child: Text('Hello, World!'), ), ), );
Посмотрим на еще один очень важный виджет и заодно преобразим внешний вид нашего приложения:
import 'package:flutter/widgets.dart'; main() => runApp( Directionality( textDirection: TextDirection.ltr, child: Container( // новый виджет! <div> в мире Flutter'а // Для виджета Container свойство color означает цвет фона color: Color(0xFF444444), child: Center( child: Text( 'Hello, World!', style: TextStyle( // а у текста появился виджет, который его стилизует color: Color(0xFFFD620A), // задаем ему цвет текста fontSize: 32.0, // и размер шрифта ), ), ), ), ), );

Color(…) — цвет. В документации указаны разные способы его задания, но основным является просто передача числа в конструктор класса. В примере выше мы передаем конструктору число, записанное в шестнадцетиричной форме, что очень похоже на HEX, только вначале у нас добавилось еще два знака, означающих степень прозрачности цвета, где 0x00 — это абсолютно прозрачный, а 0xFF — это совсем не прозрачный.
TextStyle(…) — еще более интересный виджет, с его помощью можно задать цвет, размер, толщину, межстрочный интервал, добавить подчеркивание и прочее.
Приложение на Flutter написано, дело сделано! В доках можно почитать как его собрать под Android и iOS, там же есть ссылочки чтобы вы узнали как его отправить в нужный Store. Кому этого мало, я ниже накидал еще пару строк про Flutter, может больше…
Про Stateless виджеты
Как использовать виджеты — мы разобрались, давайте теперь разбираться как их создавать. Выше уже упоминалось, что есть виджеты у которых есть состояние, и у которых его нет. До сих пор мы использовали только виджеты без состояния. Это не значит, что у них его совсем нет, ведь виджеты это просто классы, и их свойства могут быть изменены. Просто после того, как виджет будет отрисован — изменения его состояния не приведет к обновлению этого виджета в UI. К примеру, если нам нужно поменять текст на экране, нужно будет сгенерировать другой виджет Text и указать новое содержимое которое мы хотим отобразить. Такие виджеты можно назвать константными, если вы понимаете о чем я. И они простые, поэтому с них и начнем.
Чтобы создать Stateless виджет, нужно:
- Придумать красивое имя для нового класса;
- Унаследовать класс от
StatelessWidget; - Реализовать метод
build(), который принимаетBuildContextв качестве аргумента и возвращает какой-нибудьWidget.
import 'package:flutter/widgets.dart'; main() => runApp( Directionality( textDirection: TextDirection.ltr, child: Center( child: MyStatelessWidget() ), ), ); class MyStatelessWidget extends StatelessWidget { // аннотация @override нужна для оптимизации, используя ее мы говорим, // что переопределенный метод из родительского класса мы использовать // не будем, так что компилятор может его выбросить @override Widget build(BuildContext context) { // [context] будет описан позже return Text('Hello!'); } }
Пример виджета с одним аргументом:
// … class MyStatelessWidget extends StatelessWidget { // Все свойства Stateless виджета должны быть объявлены с final, или с const final String name; // обычное свойство MyStatelessWidget(this.name); // обычный конструктор @override Widget build(BuildContext context) { // [context] будет описан еще ниже return Text('Hello, $name!'); } }
Про Stateless больше и добавить нечего…
Про Hot Reload
Обратите внимание, что при изменении содержимого нашего виджета приложение будет автоматически перерисовываться. После того, как мы вынесли виджет из функции main() Hot-reload стал нам помогать.
Важно также понимать, что из-за запущенного модуля для горячей замены приложение работает на порядок медленнее.
Про GestureDetector

В следующей секции мы будем разбираться с StatefulWidget (с виджетами которые изменяются при изменении их состояния). Для того чтобы это было интересно, нам нужно это состояние как-то изменять, согласны? Мы будем изменять состояние виджета реагируя на касания по экрану. Для этого мы будем использовать GestureDetector(…) — виджет, который ничего не отрисовывает, но следит за касаниями на экране смартфона и сообщает об этом вызывая переданные ему функции.
Создадим кнопку в центре экрана, при нажатии на которую в консоль будет выводиться сообщение:
import 'package:flutter/widgets.dart'; main() => runApp( Directionality( textDirection: TextDirection.ltr, child: Container( color: Color(0xFFFFFFFF), child: App(), ), ), ); class App extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: GestureDetector( // используется как обычный виджет onTap: () { // одно из свойств GestureDetector // Этот метод будет вызван, когда дочерний элемент будет нажат print('You pressed me'); }, child: Container( // нашей кнопкой будет контейнер decoration: BoxDecoration( // стилизуем контейнер shape: BoxShape.circle, // зададим ему круглую форму color: Color(0xFF17A2B8), // и покрасим его в синий ), width: 80.0, height: 80.0, ), ), ); } }
Нажимаем на синюю кнопку и видим сообщение в консоли. Нажимаем еще раз и снова видим сообщение в консоли. Еще раз… Ладно, хватит залипать.
Про Stateful виджеты
StatefulWidget — простые, даже проще чем StatelessWidget'ы. Но есть нюанс: они не существуют сами по себе, для их работы нужен еще один класс который будет хранить сос��ояние этого виджета. При этом, его визуальная часть (виджеты из которых он состоит) также становятся его состоянием.
Для начала, посмотрим на класс виджета:
// … class Counter extends StatefulWidget { // Изменяемое состояние хранится не в виджете, а внутри объекта особого класса, // создаваемого методом createState() @override State<Counter> createState() => _CounterState(); // Результатом функции является не просто объект класса State, // а обязательно State<ИмяНашегоВиджета> }
Выше мы создали “пустой” виджет, который реализовал очень простой метод createState(). Такое разделение презентации и состояния позволяет Flutter’у сильно оптимизировать работу приложения.
Объект состояния совершенно не сложный. Более того, он практически идентичен StatelessWidget'ам написанным нами выше. Его основное отличие — родительский класс.
// … class _CounterState extends State<Counter> { // Внутри него мы наконец-то можем объявить динамические переменные, // в которых мы будем хранить состояние. // В данном случае, это счетчик количества нажатий int counter = 0; // А дальше все очень просто, мы имплементируем точно такой же метод // для отрисовки виджетов, который мы использовали в классе Stateless виджета. @override Widget build(BuildContext context) { // И тут практически ничего не изменилось с нашего последнего примера, // а то что изменилось — я прокомментировал: return Center( child: GestureDetector( onTap: () { // В момент, когда кнопка нажата, мы увеличиваем значение // перменной counter. setState(() { // setState() необходим для того, чтобы вызвать методы // жизненного цикла виджета и сказать ему, что пора обновиться ++counter; }); }, child: Container( decoration: BoxDecoration( shape: BoxShape.circle, color: Color(0xFF17A2B8), ), width: 80.0, child: Center( child: Text( // выводим значение свойства counter '$counter', // чтобы следить за его изменением style: TextStyle(fontSize: 30.0), ), ), ), ), ); } }

Обратите внимание, что имя класса начинается с нижнего подчеркивания. В Dart’е все имена начинающиеся с нижнего подчеркивания идентифицируют приватные значения. А состояние виджетов, в Flutter’е, принято оставлять приватными, хотя это не обязательно.
Какое замечательное приложение мы с вами сделали! Это отличный результат. Но перед тем как закончить эту часть курса, давайте рассмотрим еще пару интересных виджетов. Только в этот раз мы напишем больше кода, просто, чтобы было интереснее. Большая часть приложения должна быть вам знакома, а остальное вы уже должны были научиться понимать:
import 'package:flutter/widgets.dart'; main() => runApp(App()); class App extends StatelessWidget { @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.ltr, child: Container( padding: EdgeInsets.symmetric( vertical: 60.0, horizontal: 20.0, ), color: Color(0xFFFFFFFF), child: Content(), ), ); } } class Content extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ Counter('Manchester United'), Counter('Juventus'), ], ); } } class Counter extends StatefulWidget { final String _name; Counter(this._name); @override State<Counter> createState() => _CounterState(); } class _CounterState extends State<Counter> { int count = 0; @override Widget build(BuildContext context) { return Container( margin: EdgeInsets.only(bottom: 10.0), padding: EdgeInsets.all(4.0), decoration: BoxDecoration( border: Border.all(color: Color(0xFFFD6A02)), borderRadius: BorderRadius.circular(4.0), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // widget — это свойство класса State, в котором хранится // ссылка на объект создавший текущий стейт, то есть на наш виджет _CounterLabel(widget._name), _CounterButton( count, onPressed: () { setState(() { ++count; }); }, ), ], ), ); } } class _CounterLabel extends StatelessWidget { static const textStyle = TextStyle( color: Color(0xFF000000), fontSize: 26.0, ); final String _label; _CounterLabel(this._label); @override Widget build(BuildContext context) { return Text( _label, style: _CounterLabel.textStyle, ); } } class _CounterButton extends StatelessWidget { final _count; final _onPressed; _CounterButton(this._count, {@required this._onPressed}); @override Widget build(BuildContext context) { return GestureDetector( onTap: () { _onPressed(); }, child: Container( padding: EdgeInsets.symmetric(horizontal: 6.0), decoration: BoxDecoration( color: Color(0xFFFD6A02), borderRadius: BorderRadius.circular(4.0), ), child: Center( child: Text( '$_count', style: TextStyle(fontSize: 20.0), ), ), ), ); } }

У нас появилось два новых виджета: Column() и Row(). Попробуйте сами догадаться, что они делают. А в следующей статье мы рассмотрим их подробнее, а также посмотрим еще не один виджет позволяющий компоновать вместе другие виджеты, и создадим симпатичное приложение используя Flutter библиотеку называющуюся Material.
Про домашнее задание
Если вам хочется почитать что-нибудь еще на досуге, вот список интересных ссылок:
- https://flutter.io/docs/get-started/flutter-for/web-devs
- Работа с кодом из VS Code и из InteliJ
- Подробнее о виджетах
- Про Hot reload тоже нужно почитать, чтобы понимать когда код может не перезагрузится