Про Flutter, кратко: Основы


После доклада Юры Лучанинова, я решил для себя попробовать 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 8
  • Window → Show Device Bezels

Без кнопок жить можно, ведь есть хоткеи: Shift + Cmd + H — это домой, Cmd + Right — а это перевернуть телефон, остальное можно найти в меню Hardware. А вот экранную клавиатуру я советую включить, ведь важно понимать можно ли работать с приложением когда половина экрана регулярно перекрывается клавиатурой: Cmd + K (работает когда фокус находится на каком-то поле ввода).


iPhone 8 & iPhone X с рамками
iPhone 8 & iPhone X с рамками


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, // и размер шрифта
          ),
        ),
      ),
    ),
  ),
);

Скриншот HelloWorld приложения


Color(…) — цвет. В документации указаны разные способы его задания, но основным является просто передача числа в конструктор класса. В примере выше мы передаем конструктору число, записанное в шестнадцетиричной форме, что очень похоже на HEX, только вначале у нас добавилось еще два знака, означающих степень прозрачности цвета, где 0x00 — это абсолютно прозрачный, а 0xFF — это совсем не прозрачный.


TextStyle(…) — еще более интересный виджет, с его помощью можно задать цвет, размер, толщину, межстрочный интервал, добавить подчеркивание и прочее.


Приложение на Flutter написано, дело сделано! В доках можно почитать как его собрать под Android и iOS, там же есть ссылочки чтобы вы узнали как его отправить в нужный Store. Кому этого мало, я ниже накидал еще пару строк про Flutter, может больше…


Про Stateless виджеты


Как использовать виджеты — мы разобрались, давайте теперь разбираться как их создавать. Выше уже упоминалось, что есть виджеты у которых есть состояние, и у которых его нет. До сих пор мы использовали только виджеты без состояния. Это не значит, что у них его совсем нет, ведь виджеты это просто классы, и их свойства могут быть изменены. Просто после того, как виджет будет отрисован — изменения его состояния не приведет к обновлению этого виджета в UI. К примеру, если нам нужно поменять текст на экране, нужно будет сгенерировать другой виджет Text и указать новое содержимое которое мы хотим отобразить. Такие виджеты можно назвать константными, если вы понимаете о чем я. И они простые, поэтому с них и начнем.


Чтобы создать Stateless виджет, нужно:


  1. Придумать красивое имя для нового класса;
  2. Унаследовать класс от StatelessWidget;
  3. Реализовать метод 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


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),
            ),
          ),
        ),
      ),
    );
  }
}

Работающее Counter приложение


Обратите внимание, что имя класса начинается с нижнего подчеркивания. В 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),
          ),
        ),
      ),
    );
  }
}

Еще одно Counter приложение на Flutter


У нас появилось два новых виджета: Column() и Row(). Попробуйте сами догадаться, что они делают. А в следующей статье мы рассмотрим их подробнее, а также посмотрим еще не один виджет позволяющий компоновать вместе другие виджеты, и создадим симпатичное приложение используя Flutter библиотеку называющуюся Material.


Про домашнее задание


Если вам хочется почитать что-нибудь еще на досуге, вот список интересных ссылок: