Введение в Dart для Java-программистов

Введение в Dart для Java-программистов

1. Вступление

Dart это язык программирования, используемый во Flutter, мобильном SDK от Google. Сегодня вы освоите язык Dart, при этом особый упор будет сделан на вещах, неочевидных для Java-разработчика.

Вы сможете писать функции в Dart уже через 1 минуту, скрипты через 5 минут, а приложения через 10 минут!

Что вы узнаете

  • Как создавать конструкторы
  • Способы передачи параметров
  • Когда и как создавать геттеры (getters) и сеттеры (setters)
  • Приватность (privacy) в Dart
  • Как создавать фабрики (factories)
  • Функциональное программирование в Dart
  • Другие основные концепции Dart

Что вам понадобится

Для этого урока вам нужен только браузер!

Все примеры вы будете писать и запускать на выполнение в DartPad, интерактивной веб-утилите, позволяющей использовать всю функциональность языка Dart и основных его библиотек. Но вы можете использовать IDE, например, WebStorm, IntelliJ с плагином Dart, или Visual Studio Code с расширением Dart Code.

2. Создаём класс

Начнём с создания простого класса Dart с функционалом, подобным Bicycle class из учебника Java. Класс Bicycle содержит несколько приватных переменных с геттерами и сеттерами. Метод main() создаёт экземпляр Bicycle и выводит его в консоль.

Введение в Dart для Java-программистов

Открываем DartPad

В этом уроке для каждого набора упражнений создаётся новый экземпляр DartPad. Ссылка ниже откроет экземпляр с примером по-умолчанию. Вы можете использовать одну и ту же вкладку с DartPad на весь урок, но помните, что при нажатии кнопки Reset DartPad вернёт всё к началу, затерев ваш код.

Открыть DartPad

Класс Bicycle

Создадим класс Bicycle (выше метода main()), содержащий три переменных. Удалите из main() всё содержимое, как в этом примере:

class Bicycle {
  int cadence;
  int speed;
  int gear;
}

void main() {
}

Наблюдения

  • Главный метод Dart называется main() или (если вам нужен доступ к аргументам командной строки) main(List<String> args).
  • Метод main() находится на самом верхнем уровне. В Dart вы можете размещать код вне классов. Переменные, функции, геттеры и сеттеры — все они могут быть вне классов.
  • В оригинальном примере из Java были приватные переменные, 
    заданные тегом private , которого нет в Dart. О приватности будет написано детально в разделе «Добавим переменную только для чтения».
  • Ни main(), ни Bicycle не указаны как public, потому что все идентификаторы публичны по-умолчанию. В Dart просто нет ключей public, private или protected.
  • Dart по стандарту использует отступ в 2 символа, вместо 4. Но вам не следует об этом беспокоится благодаря утилите dartfmt. Как сказано в соглашении (Effective Dart), «Официальное правило по количеству пробелов в Dart — такое, какое делает dartfmt

Конструктор Bicycle

Добавьте следующий конструктор в класс Bicycle:

Bicycle(this.cadence, this.speed, this.gear);

Наблюдения

  • У этого конструктора нет тела, это допустимо в Dart.
  • Если вы забудете поставить точку с запятой (;) в конце такого конструктора, DartPad выведет ошибку: «A function body must be provided.» (тело функции отсутствует).
  • Слово this в параметрах конструктора — это удобный способ присвоения значений переменным экземпляра.
  • Код выше полностью эквивалентен следующему:
Bicycle(int cadence, int speed, int gear) {
  this.cadence = cadence;
  this.speed = speed;
  this.gear = gear;
}

Форматируем код

Отформатировать код Dart можно нажатием на Format сверху в DartPad. Форматирование бывает полезно при вставке стороннего кода.

Нажмите Format.

Инициализируем и выведем на печать экземпляр bicycle

Добавьте следующий код в функцию main():

void main() {
  var bike = new Bicycle(2, 0, 1);
  print(bike);
}

Удалите необязательный ключ new :

var bike = Bicycle(2, 0, 1);

Наблюдения

  • Ключевое слово new стало опциональным в Dart 2.
  • Если вы уверены в том, что значение переменной не изменится, можете использовать final вместо var.

Запускаем пример

Нажмите кнопку Run сверху окна DartPad для запуска на выполнение. Если Run недоступна, читайте раздел Проблемы ниже.

Вы должны увидеть следующий вывод:

Instance of 'Bicycle'

Наблюдения

  • Если нет ошибок, значит анализатор типов правильно определил, что var bike = ... это экземпляр Bicycle.

Улучшаем вывод

Вывод «Instance of ‘Bicycle'» корректен, но малоинформативен. Все классы Dart имеют метод toString(), который вы можете переопределить для получения лучшего вывода.

Добавьте следующий метод toString() в любое место класса Bicycle:

@override
String toString() => 'Bicycle: $speed mph';

Наблюдения

  • Аннотация @override сообщает анализатору, что вы хотите переопределить какой-то метод. Анализатор проверит, что вы корректно реализовали переопределение.
  • Dart поддерживает одиночные или двойные кавычки для определения строк.
  • Используйте строковую интерполяцию для указания значения выражения внутри строкового литерала: ${expression}. Если выражение это идентификатор, вы можете обойтись без скобок: $variableName.
  • Однострочные методы используют стрелочную (=>) нотацию.

Запускаем пример

Нажмите Run.

Теперь вы должны увидеть следующее:

Bicycle: 0 mph

Проблемы?
Проверьте свой код

Добавим переменную только для чтения

В оригинальном примере на Java переменная speed — только для чтения — она приватная и у неё есть только метод геттер. Сделаем то же самое на Dart.

Откройте bicycle.dart в DartPad (или продолжайте с вашим кодом)

Чтобы сделать идентификатор в Dart приватным, напишите его имя  начинающимся с символа подчёркивания (_). Преобразуем speed в переменную «только для чтения», изменив её имя, затем добавим геттер.

В конструкторе Bicycle удалите параметр speed:

Bicycle(this.cadence, this.gear);

В main() удалите второй параметр (speed) из вызова конструктора:

var bike = Bicycle(2, 1);

Замените все speed на _speed. (в 2 местах)

Подсказка: Если вы используете JetBrains IDE, вы легко сможете переименовать все вхождения переменной, кликнув правой кнопкой на её имени, затем выбрать Refactor > Rename… из появившегося меню.

Инициализируем _speed равной 0:

int _speed = 0;

Добавим следующий геттер в класс Bicycle:

int get speed => _speed;

Наблюдения

  • Неинициализированные переменные (и даже числа) имеют значение null.
  • Компилятор Dart делает приватными все переменные, начинающиеся с подчёркивания.
  • По-умолчанию, Dart предлагает неявные геттеры и сеттеры для всех публичных экземпляров переменных. Вам не потребуется создавать свои геттеры/сеттеры, пока не будет необходимости сделать это для переменных «только для чтения» или «только для записи», рассчитать или сверить значение переменной и т.п.
  • Начните с простого поля, типа bike.cadence, затем отрефакторите его с применением геттеров и сеттеров. API при этом останется прежним. Другими словами, путь от поля до геттера/сеттера очень прост в Dart.

Добавьте следующие методы в класс Bicycle:

void applyBrake(int decrement) {
  _speed -= decrement;
}

void speedUp(int increment) {
  _speed += increment;
}

Окончательный пример на Dart выглядит схоже с оригинальным на Java, но более компактный — всего 23 строки вместо 40:

class Bicycle {
  int cadence;
  int _speed = 0;
  int get speed => _speed;
  int gear;

  Bicycle(this.cadence, this.gear);

  void applyBrake(int decrement) {
    _speed -= decrement;
  }

  void speedUp(int increment) {
    _speed += increment;
  }

  @override
  String toString() => 'Bicycle: $_speed mph';
}

void main() {
  var bike = Bicycle(2, 1);
  print(bike);
}

Проблемы?
Проверьте свой код

3. Используем опциональные параметры (вместо перегрузки)

Следующий урок посвящён классу Rectangle.

Код Java часто содержит перегружаемые конструкторы, такие, в которых конструкторы имеют одинаковое имя, но отличаются количеством или типом параметров. Dart не поддерживает такие конструкторы, но предлагает разные подходы к этой задаче.

Откройте пример Rectangle в DartPad

Добавляем конструктор Rectangle

Мы добавим одиночный пустой конструктор, полностью заменяющий все четыре конструктора в примере на Java:

Rectangle({this.origin = const Point(0, 0), this.width = 0, this.height = 0});

В этом конструкторе используются опциональные именные параметры.

Наблюдения

  • this.origin, this.width и this.height используются для присвоения значений переменным экземпляра внутри конструктора.
  • this.origin, this.width и this.height — опциональные именные параметры. Именные параметры заключены в фигурные скобки ({}).
  • Синтакс this.origin = const Point(0, 0) указывает значение по-умолчанию, равное Point(0,0) для переменной origin. Это значение должно быть константой времени компиляции (compile-time constant). В этом конструкторе значения по-умолчанию указаны для всех трёх переменных.

Улучшаем вывод

Добавим следующую функцию toString() в класс Rectangle:

@override
String toString() =>
      'Origin: (${origin.x}, ${origin.y}), width: $width, height: $height';

Используем конструктор

Чтобы проверить создание класса только с нужными нам параметрами, заменим main() следующим кодом:

main() {
  print(Rectangle(origin: const Point(10, 20), width: 100, height: 200));
  print(Rectangle(origin: const Point(10, 10)));
  print(Rectangle(width: 200));
  print(Rectangle());
}

Наблюдения

  • Конструктор класса Rectangle на Dart это всего одна строка, в отличие от 16 строк кода в примере на Java.

Запускаем пример

Вы должны увидеть следующее:

Origin: (10, 20), width: 100, height: 200
Origin: (10, 10), width: 0, height: 0
Origin: (0, 0), width: 200, height: 0
Origin: (0, 0), width: 0, height: 0

Проблемы?
Проверьте свой код

4. Создаём фабрику

Фабрики (Factories), популярный шаблон проектирования в Java, имеют некоторые преимущества перед прямым созданием объекта, такие как сокрытие деталей создания, возможность возвращения подтипа фабричного типа, а также возможность возвращения существующего экземпляра вместо нового.

Рассмотрим два способа реализации фабрики:

  • 1: Создадим функцию высшего порядка
  • 2: Создадим конструктор фабрики

Для примера используем класс Shape, создающий фигуры и выводящий информацию об их площадки:

import 'dart:math';

abstract class Shape {
  num get area;
}

class Circle implements Shape {
  final num radius;
  Circle(this.radius);
  num get area => pi * pow(radius, 2);
}

class Square implements Shape {
  final num side;
  Square(this.side);
  num get area => pow(side, 2);
}

main() {
  final circle = Circle(2);
  final square = Square(2);
  print(circle.area);
  print(square.area);
}

Открыть пример в DartPad

В консоли вы должны увидеть рассчитанную площадь круга и квадрата:

12.566370614359172
4

Наблюдения

  • Dart поддерживает абстрактные классы.
  • В одном файле можно создать множество кллассов.
  • dart.math — одна из основных библиотек в Dart. Также там есть dart:core, dart:async, dart:convert и dart:collection.
  • В Dart 1.x константы основной библиотеки были в верхнем регистре (PI); в Dart 2 они в нижнем регистре (pi).
  • В этом коде два геттера, рассчитывающих значения:
    num get area => pi * pow(radius, 2); // Круг
    num get area => pow(side, 2); // Квадрат

Создаём функцию высшего порядка

Создадим фабрику как функцию высшего порядка, добавив следующую функцию на верхний уровень (вне классов):

Shape shapeFactory(String type) {
  if (type == 'circle') return Circle(2);
  if (type == 'square') return Square(2);
  throw 'Can\'t create $type.';
}

Вызовем фабричную функцию, заменив первые две строки метода main():

  final circle = shapeFactory('circle');
  final square = shapeFactory('square');

Запускаем пример

Вывод должен быть таким же, как раньше.

Наблюдения

  • Если функцию вызвать с параметрами, отличающимися от 'circle' или 'square', она выбросит исключение.
  • Dart SDK имеет классы для большинства типовых исключений, также вы можете расширить класс Exception для создания своих исключений.
  • Когда встречается исключение, DartPad выводит Uncaught. Для более детальной информации об ошибке, используйте блоки try-catch с выводом исключения. Также дополнительно можно посмотреть в этом примере DartPad.
  • Для использования одиночных кавычек внутри строки, экранируйте их обратной косой чертой ('Can\'t create $type.') или создавайте строку в двойных кавычках  ("Can't create $type.").

Проблемы?
Проверьте свой код

Фабричный конструктор

Ключевое слово factory создаёт фабричный конструктор в Dart.

Добавим его в абстрактный класс Shape:

abstract class Shape {
  factory Shape(String type) {
    if (type == 'circle') return Circle(2);
    if (type == 'square') return Square(2);
    throw 'Can\'t create $type.';
  }
  num get area;
}

Замените первые две строки в main() на:

  final circle = Shape('circle');
  final square = Shape('square');

Удалите функцию shapeFactory(), добавленную ранее.

Наблюдения

  • Код в фабричном конструкторе идентичен коду в функции shapeFactory().

Проблемы?
Проверьте свой код

5. Реализуем интерфейс

В языке Dart не нужно ключевое слово interface потому что каждый класс создаёт интерфейс.

Откройте пример Shapes в DartPad (или продолжайте в своей копии)

Добавим класс CircleMock, расширяющий класс Circle:

class CircleMock implements Circle {}

Вы должны увидеть ошибку «Missing concrete implementations» (отсутствует реализация). Уберём эту ошибку, создав переменные area и radius:

class CircleMock implements Circle {
  num area;
  num radius;
}

Наблюдения

  • Несмотря на то, что класс CircleMock не описывает никакого поведения, он корректен в Dart — анализатор не выводит ошибки.

Проблемы?
Проверьте свой код

6. Функциональное программирование в Dart

В функциональном программировании вы можете делать такие вещи как:

  • Передавать функции как аргументы.
  • Присваивать функции переменным.
  • Деконструировать функции, принимающие множество аргументов, в последовательность функций, каждая из которых принимает единственный аргумент.
  • Создавать безымянные функции, которые можно использовать как значение констант (их также называют лямбдами).

Dart поддерживает всё это. В Dart даже функции это объекты (имеющие тип Function) и это значит, что функции могут быть присвоены переменным или переданы как аргументы, другим функциям. Также вы можете вызвать класс Dart как функцию, как в этом примере.

Следующий пример описан в императивном (не функциональном) стиле:

String scream(int length) => "A${'a' * length}h!";

main() {
  final values = [1, 2, 3, 5, 10, 50];
  for (var length in values) {
    print(scream(length));
  }
}

Откройте пример в DartPad

Его вывод должен быть таким:

Aah!
Aaah!
Aaaah!
Aaaaaah!
Aaaaaaaaaaah!
Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah!

Наблюдения

  • При использовании интерполяции строк, строка ${'a' * length} читается как «символ 'a' повторить length раз.»

Конвертируем императивный код в функциональный

Удалите императивный цикл for() {...} в main() и замените его таким однострочным выражением:

  values.map(scream).forEach(print);

Запустите пример

Функциональный подход также выведет эти же строки.

Проблемы?
Проверьте ваш код

Больше итераторов

Списки и итераторы из dart:collection поддерживают методы fold, where, join, skip и другие. В Dart также есть Map-ы и Set-ы.

Замените строку values.map() в main() следующей:

  values.skip(1).take(3).map(scream).forEach(print);

Запустите пример

Вывод должен быть примерно такой:

Aaah!
Aaaah!
Aaaaaah!

Наблюдения

  • skip(1)пропускает первое значение, 1, в списке values .
  • take(3)берёт следующие 3 значения — 2, 3 и 5 — в списке values.
  • Остальные значения списка проигнорированы.

Проблемы?
Проверьте свой код

7. Поздравляем!

По завершении этого урока вы должны были узнать большинство отличий между Java и Dart. Dart лёгкий в освоении, у него очень хорошие основные библиотеки и огромная база пакетов. Dart хорошо масштабируется. Сотни инженеров Google используют Dart для создания бизнес-критичных приложений компании.

Дальнейшие шаги

За 20 минут невозможно показать все отличия между Java и Dart. К примеру, мы не рассмотрели следующие темы:

  • async/await, что позволит писать асинхронный код так, как синхронный. Откройте пример в DartPad с анимацией расчёта первых пяти чисел π.
  • Каскадный метод, где всё — builder!
  • Null-безопасные операторы

Источник

Подписывайтесь на новости Flutter! https://t.me/flutterdaily

Flutter: реализация BottomAppBar с FAB

Flutter: BottomAppBar с FAB

Сегодня мы узнаем, как добавить FloatingActionButton (FAB) к BottomAppBar в Flutter. В итоге должно получиться что-то такое:

Flutter: реализация BottomAppBar с FAB

Наша задача — сделать BottomAppBar, который будет работать так же, как BottomNavigationBar. В нём будет несколько вкладок и только одна из них активная.

Если вы только начинаете разрабатывать на Flutter, обратите внимание на статью Создание первого приложения Flutter.

Вы можете удивиться, почему нельзя использовать сразу BottomNavigationBar? Несмотря на то, что технически можно вставить FloatingActionButton в BottomNavigationBar, на практике это решение полно недостатков.

Добавляем отцентрированный FAB

После создания нового проекта Flutter, в нём уже есть FloatingActionButton для увеличения счётчика.

Мы можем добавить BottomAppBar в наш Scaffold.bottomNavigationBar вот так:

return Scaffold(
  appBar: AppBar(
    title: Text(widget.title),
  ),
  floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
  floatingActionButton: FloatingActionButton(
    onPressed: () { },
    tooltip: 'Increment',
    child: Icon(Icons.add),
    elevation: 2.0,
  ),
  bottomNavigationBar: BottomAppBar(
    child: Row(
      mainAxisSize: MainAxisSize.max,
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: <Widget>[],
    ),
    notchedShape: CircularNotchedRectangle(),
    color: Colors.blueGrey,
  ),
);

Обратите внимание, как мы установили положение Scaffold.floatingActionButtonLocation в FloatingActionButtonLocation.centerDocked для того, чтобы вставить FAB в центр BottomAppBar.

Также мы добавили notchedShape: CircularNotchedRectangle() для создания выемки в BottomAppBar для FAB.

Если мы запустим приложение сейчас, вот что должно получиться:

Flutter: реализация BottomAppBar с FAB

Добавляем вкладки

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

  • он вмещает 2 или 4 вкладки (из-за симметрии).
  • подсвечивает и отслеживает выбранную вкладку.
  • делает callback при выборе вкладки, чтобы приложение могло обновить нужную страницу.

Сделаем набросок класса FABBottomAppBar:

class FABBottomAppBarItem {
  FABBottomAppBarItem({this.iconData, this.text});
  IconData iconData;
  String text;
}

class FABBottomAppBar extends StatefulWidget {
  final List<FABBottomAppBarItem> items;
  final ValueChanged<int> onTabSelected;
  
  @override
  State<StatefulWidget> createState() => FABBottomAppBarState();
}

class FABBottomAppBarState extends State<FABBottomAppBar> {
  int _selectedIndex = 0;

  _updateIndex(int index) {
    widget.onTabSelected(index);
    setState(() {
      _selectedIndex = index;
    });
  }
  
  // TODO: build method here
}

Вначале мы создали класс FABBottomAppBarItem, содержащий переменные IconData и String. Так мы описали вкладку.

Затем мы создали виджет FABBottomAppBar. Это необходимо для создания списка вкладок и задания callback (onTabSelected).

FABBottomAppBar это StatefulWidget, так как он должен отслеживать выбранную вкладку и обновлять её.

В классе FABBottomAppBarState мы задали _selectedIndex, обновляемый при вызове _updateIndex.

Создадим метод build:

@override
Widget build(BuildContext context) {
  List<Widget> items = List.generate(widget.items.length, (int index) {
    return _buildTabItem(
      item: widget.items[index],
      index: index,
      onPressed: _updateIndex,
    );
  });

  return BottomAppBar(
    child: Row(
      mainAxisSize: MainAxisSize.max,
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: items,
    ),
  );
}

Здесь:

  • Строки с 3 по 9: здесь мы используем генератор списка для создания вкладок, передавая нужный элемент, индекс и ширину.
  • Строки с 11 по 17: создаём BottomAppBar, содержащий Row с заданными элементами. Мы используем MainAxisSize.max и MainAxisAlignment.spaceAround для корректного размещения элементов в ряду в полную ширину.

Реализуем метод _buildTabItem:

Widget _buildTabItem({
  FABBottomAppBarItem item,
  int index,
  ValueChanged<int> onPressed,
}) {
  Color color = _selectedIndex == index ? widget.selectedColor : widget.color;
  return Expanded(
    child: SizedBox(
      height: widget.height,
      child: Material(
        type: MaterialType.transparency,
        child: InkWell(
          onTap: () => onPressed(index),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Icon(item.iconData, color: color, size: widget.iconSize),
              Text(
                item.text,
                style: TextStyle(color: color),
              )
            ],
          ),
        ),
      ),
    ),
  );
}

В строке 6 мы проверяет соответствие выбранного индекса индексу текущей вкладки, затем выбираем цвет вкладки в соответствии с этим.

В строках с 13 по 26 мы создаём InkWell внутри виджета Material. Таким образом мы получаем распознавание жеста onTap и специальный эффект при нажатии.

Потомком InkWell является Column, содержащий Icon и Text, оба настроенные на приём данных из FABBottomAppBarItem.

И всё это завёрнуто внутрь виджета Expanded. Таким образом каждый элемент имеет одинаковую ширину внутри родительского Row.

Протестируем FABBottomAppBar, указав его как bottomNavigationBar в Scaffold, прописав четыре элемента:

bottomNavigationBar: FABBottomAppBar(
  onTabSelected: _selectedTab,
  items: [
    FABBottomAppBarItem(iconData: Icons.menu, text: 'This'),
    FABBottomAppBarItem(iconData: Icons.layers, text: 'Is'),
    FABBottomAppBarItem(iconData: Icons.dashboard, text: 'Bottom'),
    FABBottomAppBarItem(iconData: Icons.info, text: 'Bar'),
  ],
),

Результат:

Flutter: реализация BottomAppBar с FAB

И всё работает, вкладки переключаются, при выборе происходит вызов callback.

Подводя итоги

Итак, прогресс определённо есть, компонент работает как задумано. Осталось немного доработать FABBottomAppBar.

В примере выше мы захардкодили такие вещи, как:

  • ПараметрыBottomAppBarheight (высота), backgroundColor (цвет фона) и notchedShape (форма выемки)
  • Размер иконки
  • Цвет активной/неактивной вкладок

Также иконки на вкладках посередине находятся слишком близко к FAB. Нужно добавить небольшой отступ для них.

Финальная версия FABBottomAppBar с указанными доработками:

import 'package:flutter/material.dart';

class FABBottomAppBarItem {
  FABBottomAppBarItem({this.iconData, this.text});
  IconData iconData;
  String text;
}

class FABBottomAppBar extends StatefulWidget {
  FABBottomAppBar({
    this.items,
    this.centerItemText,
    this.height: 60.0,
    this.iconSize: 24.0,
    this.backgroundColor,
    this.color,
    this.selectedColor,
    this.notchedShape,
    this.onTabSelected,
  }) {
    assert(this.items.length == 2 || this.items.length == 4);
  }
  final List<FABBottomAppBarItem> items;
  final String centerItemText;
  final double height;
  final double iconSize;
  final Color backgroundColor;
  final Color color;
  final Color selectedColor;
  final NotchedShape notchedShape;
  final ValueChanged<int> onTabSelected;

  @override
  State<StatefulWidget> createState() => FABBottomAppBarState();
}

class FABBottomAppBarState extends State<FABBottomAppBar> {
  int _selectedIndex = 0;

  _updateIndex(int index) {
    widget.onTabSelected(index);
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    List<Widget> items = List.generate(widget.items.length, (int index) {
      return _buildTabItem(
        item: widget.items[index],
        index: index,
        onPressed: _updateIndex,
      );
    });
    items.insert(items.length >> 1, _buildMiddleTabItem());

    return BottomAppBar(
      shape: widget.notchedShape,
      child: Row(
        mainAxisSize: MainAxisSize.max,
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: items,
      ),
      color: widget.backgroundColor,
    );
  }

  Widget _buildMiddleTabItem() {
    return Expanded(
      child: SizedBox(
        height: widget.height,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            SizedBox(height: widget.iconSize),
            Text(
              widget.centerItemText ?? '',
              style: TextStyle(color: widget.color),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildTabItem({
    FABBottomAppBarItem item,
    int index,
    ValueChanged<int> onPressed,
  }) {
    Color color = _selectedIndex == index ? widget.selectedColor : widget.color;
    return Expanded(
      child: SizedBox(
        height: widget.height,
        child: Material(
          type: MaterialType.transparency,
          child: InkWell(
            onTap: () => onPressed(index),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Icon(item.iconData, color: color, size: widget.iconSize),
                Text(
                  item.text,
                  style: TextStyle(color: color),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Обратите внимание, как мы задаём centerItemText, который будет размещён прямо под FAB. Если он пустой или null, мы отобразим пустой Text.

Вот что получится в итоге:

Flutter: реализация BottomAppBar с FAB

Что же с BottomNavigationBar?

Я пробовал расположить FAB внутри BottomNavigationBar, но у меня были такие проблемы:

  • Невозможно добавить шаблонный или пустой текст под FAB, кроме как создать BottomNavigationBarItem. Но это нежелательно, так как BottomNavigationBarItem это вкладка и может быть сама по себе задействована.
  • BottomNavigationBar не поддерживает notchedShape.

В целом BottomAppBar требует написания большего количества кода, но в результате получается лучше из-за использования более удобного в настройке Row.

Исходный код

Полный исходный код приложения вы можете скачать на GitHub

Хорошей разработки!

По материалам поста «Flutter: BottomAppBar Navigation with FAB» by Andrea Bizzotto.

Подписывайтесь на новости Flutter! https://t.me/flutterdaily

Аутентификация в приложении Flutter

Аутентификация в приложении Flutter

Сегодня мы сделаем небольшое приложение, в котором покажем, как сделать авторизацию, выход из сеанса и удаление профиля в Firebase. И мы будем делать это в Android Studio. Кликнув по ссылке, вы узнаете как установить Flutter для Android Studio. Если вы используете другую IDE, не волнуйтесь, код не будет отличаться.

Если вы только начинаете разрабатывать на Flutter, обратите внимание на статью Создание первого приложения Flutter и продолжение.

Наше приложение будет выглядеть как на скриншоте ниже. Оно сможет авторизовать пользователя в Firebase через Google, завершать сеанс, удалять пользователя в том случае, если он когда-либо авторизовывался в приложении. Также мы добавим круглый индикатор прогресса внизу, чтобы показать, что приложение занято.

Аутентификация в приложении Flutter

Подготовка

Вначале необходимо настроить Google Firebase.

Добавим Firebase в проект

Аутентификация в приложении Flutter
скопируйте файл .json в папку Android/app 

Перейдите в консоль Firebase, создайте новый проект. Затем скачайте файл google-services.json и скопируйте его в папку Android/app/ (как на скриншоте). Не забудьте добавить SHA-1, он понадобится для аутентификации. Кликните здесь, если не знаете, где взять SHA-1.По дальнейшей настройке Flutter документация здесь.

Добавим метод входа (Sign-in)

Аутентификация в приложении Flutter
включим Google Sign-in

Затем в Firebase Console перейдите в Authentication (слева на панели). Здесь есть список Провайдеры авторизации. Для нашего проекта достаточно будет только Google.  

Добавляем зависимости

Откройте файл pubspec.yaml в папке проекта. Добавьте следующие строки ниже dependencies:

      // ./pubspec.yaml
      dependencies:
      //...
      #AUTHENTICATIN
      firebase_auth: ^0.6.2+1
      google_sign_in: ^3.2.2

Нажмите кнопку ‘Packages get’ сверху. Супер, теперь можно перейти к разработке. 

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

Аутентификация в приложении Flutter
структура проекта

Я создал папку /scr в /lib. В /scr создал папку /auth. Добавил файл authHelper.dart в /auth, а home.dart и startApp.dart в /src. В принципе, вы можете создать любую структуру, однако, не забудьте поменять пути до файлов в этом случае.

Main.dart

Рассмотрим этот кусок кода. Это точка входа в приложении Flutter. Здесь мы просто запускаем StartApp().

    // ./lib/main.dart
    import 'package:flutter/material.dart';
    import 'src/startApp.dart';
    void main() => runApp(StartApp());

StartApp.dart

В startApp.dart мы пропишем тему приложения. Импортируем сюда home.dart, создадим StatelessWidget, определим цвета темы и основной экран методом Home().

    // ./lib/scr/startApp.dart
    import 'package:flutter/material.dart';
    import 'home.dart';
    class StartApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Auth Tutorial',
          theme: new ThemeData(
            // theme of our application.
            primaryColor: Colors.lightBlue,
            primaryColorDark: Colors.blue,
            accentColor: Colors.orange,
          ),
          home: new Home(),      
        );
      }
    }

Home.dart

Здесь мы импортируем flutter/material.dart и наш вспомогательный модуль authHelper.dart.Затем создаём StatefulWidget. 

    // ./lib/scr/home.dart
    import 'package:flutter/material.dart';
    import 'auth/authHelper.dart';
    class Home extends StatefulWidget {
      @override
      _Home createState() => new _Home();
    }

_Home() будет таким: создаём панель app bar с надписью ‘Flutter Authentication Tutorial’. Ниже у нас будет текстовое поле и три кнопки, ‘Login’, ‘Logout’ и ‘Delete’. Можете разместить их как угодно. Я даже присвоил разные цвета. Также мы добавим круглый индикатор прогресса.

    // ./lib/scr/home.dart
    //...
    class _Home extends State<StatefulWidget> {
      @override
      Widget build(BuildContext context) {    
        return new Scaffold(
          appBar: new AppBar(
            title: new Text('Flutter Authentication Tutorial'),
          ),
          body: new Center(
              child: new Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              Padding(
                padding: EdgeInsets.symmetric(vertical: 9.0, horizontal: 18.0),
                child: Text('User: $mUserName'),
              ),
              Padding(
                padding: EdgeInsets.symmetric(vertical: 9.0, horizontal: 18.0),
                child: RaisedButton(
                    color: Colors.green,
                    textColor: Colors.white,
                    splashColor: Colors.greenAccent,
                    child: const Text('LOGIN'),
                    onPressed: isLoggingIn ? null : clickLogin),
              ),
              Padding(
                padding: EdgeInsets.symmetric(vertical: 9.0, horizontal: 18.0),
                child: RaisedButton(
                    color: Colors.blue,
                    textColor: Colors.white,
                    splashColor: Colors.blueAccent,
                    child: const Text('LOGOUT'),
                    onPressed: isLoggingIn ? null : clickLogout),
              ),
              Padding(
                padding: EdgeInsets.symmetric(vertical: 9.0, horizontal: 18.0),
                child: RaisedButton(
                  color: Colors.red,
                  textColor: Colors.white,
                  splashColor: Colors.redAccent,
                  child: const Text('DELETE'),
                  onPressed: mUser == null || isLoggingIn ? null : clickDelete,
                ),
              ),
              new Opacity(
                opacity: cpbOpacity,
                child: new Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    new CircularProgressIndicator(),
                    new SizedBox(width: 20.0),
                    new Text('Please wait...')
                  ],
                ),
              ),
            ],
          )),
        );
      }
    }

Добавим несколько переменных и функций. Для начала, mUser и mUserName. А также булеву isLoggingIn. При каждом успешном входе она будет true. И если она true, кнопки будут неактивны (если onPressed равен null, кнопка неактивна). При клике на кнопку Login будет вызван loginWithGoogle из модуля authHelper. Здесь же мы проверяем ошибки, и если всё хорошо, установим isLoggingIn и выведем имя пользователя (username), которое получим после авторизации.

    // ./lib/scr/home.dart
    class _Home extends State<StatefulWidget> {
      var mUser;
      var mUserName;
      var isLoggingIn = false;
      Future clickLogin() async {
        //установим isLoggingIn в true при клике на Login
        setState(() {
          isLoggingIn = true;
        });
        mUser = await loginWithGoogle().catchError((e) => setState(() {
              isLoggingIn = false;
            }));
        assert(mUser != null);
        setState(() {
          isLoggingIn = false;
          mUserName = mUser.displayName;
        });
      }
      @override
      Widget build(BuildContext context) {
      //...
      }
    }

Создадим методы clickLogout и clickDelete. В clickLogout мы вызываем logoutWithGoogle из authHelper. При успешном выполнении выводим в текстовом поле ‘No user is logged in’. При клике clickDelete, пользователь будет удалён из Firebase.

    // ./lib/scr/home.dart 
    class _Home extends State<StatefulWidget> { 
      //...
      void clickLogout() {
        setState(() {
          isLoggingIn = true;
        });
        logoutWithGoogle().whenComplete(() => setState(() {
              isLoggingIn = false;
              mUserName = 'No user is logged in!';
            }));
      }
      void clickDelete() {
        setState(() {
          isLoggingIn = true;
        });
        deleteUserWithGoogle(mUser).whenComplete(() => setState(() {
              isLoggingIn = false;
              mUser = null;
              mUserName = 'No user is logged in!';
            }));
      }
      @override
      Widget build(BuildContext context) {
        // ...
      }
    }

Допишем home.dart, добавьте эти строки в начале build. Здесь устанавливаем значение непрозрачности, в том случае, когда пользователь аутентифицирован.

    // ./lib/scr/home.dart 
    class _Home extends State<StatefulWidget> { 
      //...
      @override
      Widget build(BuildContext context) {
        var cpbOpacity;
        isLoggingIn ? cpbOpacity = 1.0 : cpbOpacity = 0.0;
        return new Scaffold(
          //...
        );
      }
    }

AuthHelper.dart

Почти закончили. Добавим немного кода в файл authHelper.dart. firebase_auth и google_sign_in необходимы для процесса аутентификации. При вызове loginWithGoogle сперва пытаемся получить пользователя. При неудаче пробуем дальнейшую авторизацию. Если и тут неудача, вызовем функцию signIn. Она откроет меню Google sign-in и если его отменят, получим ошибку. Ждём аутентификацию и пробуем получить пользователя Firebase. Если всё получилось, у нас будет пользователь, при этом он неанонимен.

    // ./lib/scr/auth/authHelper.dart
    import 'dart:async';
    import 'package:firebase_auth/firebase_auth.dart';
    import 'package:google_sign_in/google_sign_in.dart';
    final FirebaseAuth _auth = FirebaseAuth.instance;
    final GoogleSignIn _googleSignIn = new GoogleSignIn();
    Future<FirebaseUser> loginWithGoogle() async {
      GoogleSignInAccount currentUser = _googleSignIn.currentUser;
      if (currentUser == null) {
        currentUser = await _googleSignIn.signInSilently();
      }
      if (currentUser == null) {
        currentUser = await _googleSignIn.signIn();
        if (currentUser == null) {
          throw('Login Canceled');
        }
      }
      final GoogleSignInAuthentication auth = await currentUser.authentication;
      final FirebaseUser user = await _auth.signInWithGoogle(
        idToken: auth.idToken,
        accessToken: auth.accessToken,
      );
      assert(user != null);
      assert(!user.isAnonymous);
      return user;
    }
    //...

Допишем оставшиеся методы logoutWithGoogle и deleteUserWithGoogle. При выходе из сеанса, вызовем signOut из Firebase и signOut из googleSignIn. При удалении пользователя, сперва завершим его сеанс и затем полностью удалим.

    // ./lib/scr/auth/authHelper.dart
    //...
    Future<Null> logoutWithGoogle() async {
      await _auth.signOut();
      await _googleSignIn.signOut();
    }
    Future<Null> deleteUserWithGoogle(FirebaseUser user) async {
      await logoutWithGoogle();
      await user.delete();
    }

Вот и всё. Если у вас есть вопросы/уточнения, задавайте их в комментариях.

Вы пришли с Xamarin? Тогда почитайте Flutter или Xamarin: сравнение инструментов кросс-платформенной мобильной разработки.

Перевод «Flutter Authentication Tutorial»

Подписывайтесь на новости Flutter! https://t.me/flutterdaily

Создание первого приложения Flutter, часть 2

Создание первого приложения Flutter

Первая часть.

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

Если вы пропустили первый урок, лучше начать с него.

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

В этом GIF показан финальный вид приложения:

Создание первого приложения Flutter, часть 2

1. Добавим иконки в список

Добавим иконки в виде сердечка в каждую строку. Чуть позже сделаем их кликабельными с сохранением выбранного элемента в избранное.

Добавьте набор (Set) _saved в RandomWordsState. Этот Set будет хранить список выбранных пользователем пар слов. Set был выбран из-за того, что не допускает дублей.

class RandomWordsState extends State<RandomWords> {
  final List<WordPair> _suggestions = <WordPair>[];
  final Set<WordPair> _saved = new Set<WordPair>();   // Добавьте эту строку.
  final TextStyle _biggerFont = const TextStyle(fontSize: 18.0);
  ...
}

В функции _buildRow добавим проверку alreadySaved для того, чтобы быть уверенным, что ранее уже не сохранили такую пару.

Widget _buildRow(WordPair pair) {
  final bool alreadySaved = _saved.contains(pair);  // Добавьте эту строку.
  ...
}

В _buildRow() добавим иконку-сердечко к объектам ListTile для включения в избранное. Пока это статичные картинки, далее мы добавим в них интерактив:

Widget _buildRow(WordPair pair) {
  final bool alreadySaved = _saved.contains(pair);
  return new ListTile(
    title: new Text(
      pair.asPascalCase,
      style: _biggerFont,
    ),
    trailing: new Icon(   // Добавьте строки отсюда... 
      alreadySaved ? Icons.favorite : Icons.favorite_border,
      color: alreadySaved ? Colors.red : null,
    ),                    // ... до сюда.
  );
}

Перезапустите приложение. Вы должны увидеть незаполненные сердечки в каждой строке.

Проблемы?

Здесь вы можете скачать рабочий код, если вдруг что-то пошло не так:

2. Добавляем интерактивность

Сделаем сердечки кликабельными. При клике на них в списке, у выбранного элемента переключается свойство «избранный», при этом пара слов попадает в набор с избранным.

Для этого доработаем функцию _buildRow. Если запись уже есть в избранном, при повторном клике она удаляется из избранного. При клике на строку списка, вызовется setState() для уведомления фреймворка о смене состояния.

Добавьте onTap, как указано ниже:

Widget _buildRow(WordPair pair) {
  final alreadySaved = _saved.contains(pair);
  return new ListTile(
    title: new Text(
      pair.asPascalCase,
      style: _biggerFont,
    ),
    trailing: new Icon(
      alreadySaved ? Icons.favorite : Icons.favorite_border,
      color: alreadySaved ? Colors.red : null,
    ),
    onTap: () {      // Добавим 9 строк отсюда...
      setState(() {
        if (alreadySaved) {
          _saved.remove(pair);
        } else { 
          _saved.add(pair); 
        } 
      });
    },               // ... до сюда.
  );
}

Замечание: Во Flutter при вызове setState() срабатывает вызов к методу build() для объекта Состояния, в результате перерисовывая UI.

Перезапустите приложение. При клике на строку списка она должна попадать в избранное или удаляться из него. Также в месте клика появляется анимация заполнения.

Проблемы?

Здесь вы можете скачать рабочий код, если вдруг что-то пошло не так:

3. Переход на новый экран

Сейчас мы добавим новую страницу для вывода избранного. Вы узнаете как переходить между основным (home route) и новым роутом.

Стеком, содержащим все роуты приложения, управляет Navigator. Добавление роута в стек Navigator-а, делает переход на этот роут. Удаление роута из стека Navigator-а, возвращает на предыдущий роут.

Добавим иконку списка к AppBar в методе build класса RandomWordsState. По клику на эту иконку, в Navigator добавится новый роут, содержащий элементы избранного.

Добавьте иконку и соответствующее ей действие в метод build:

class RandomWordsState extends State<RandomWords> {
  ...
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Startup Name Generator'),
        actions: <Widget>[      // Добавьте 3 строки отсюда...
          new IconButton(icon: const Icon(Icons.list), onPressed: _pushSaved),
        ],                      // ... до сюда.
      ),
      body: _buildSuggestions(),
    );
  }
  ...
}

Замечание: Некоторые свойства виджетов принимают одиночный виджет (child), а другие, такие как действия, принимают массив виджетов (children), это указывается квадратными скобками ([]).

Добавим функцию _pushSaved() в класс RandomWordsState.

  void _pushSaved() {
  }

Перезапустим приложение. Иконка списка появится в верхней панели. Нажатие на неё ни к чему пока не приводит, потому что функция _pushSaved пуста.

Теперь создадим роут и положим в стек Navigator-а. Это действие отобразит новый роут на экране. Содержимое новой страницы построено в MaterialPageRoute builder, в анонимной функции.

Вызовем Navigator.push, как показано ниже. IDE покажет ошибку в коде, но мы это почти сразу поправим.

void _pushSaved() {
  Navigator.of(context).push(
  );
}

Добавим MaterialPageRoute и его построитель (builder). Код ниже создаёт список рядов ListTile. Метод divideTiles() добавляет горизонтальный разделитель между элементами. Переменная divided хранить финальный вид рядов, сконвертированный в список функцией toList().

Добавьте следующий код:

void _pushSaved() {
  Navigator.of(context).push(
    new MaterialPageRoute<void>(   // Добавьте 20 строк отсюда...
      builder: (BuildContext context) {
        final Iterable<ListTile> tiles = _saved.map(
          (WordPair pair) {
            return new ListTile(
              title: new Text(
                pair.asPascalCase,
                style: _biggerFont,
              ),
            );
          },
        );
        final List<Widget> divided = ListTile
          .divideTiles(
            context: context,
            tiles: tiles,
          )
          .toList();
      },
    ),                           // ... до сюда.
  );
}

В коде ниже свойство builder возвращает Scaffold, содержащий верхнюю панель нового роута с названием «Saved Suggestions.» Тело роута представлено ListView с рядами ListTiles; между рядами есть разделители:

void _pushSaved() {
  Navigator.of(context).push(
    new MaterialPageRoute<void>(
      builder: (BuildContext context) {
        final Iterable<ListTile> tiles = _saved.map(
          (WordPair pair) {
            return new ListTile(
              title: new Text(
                pair.asPascalCase,
                style: _biggerFont,
              ),
            );
          },
        );
        final List<Widget> divided = ListTile
          .divideTiles(
            context: context,
            tiles: tiles,
          )
              .toList();

        return new Scaffold(         // Добавьте 6 строк отсюда...
          appBar: new AppBar(
            title: const Text('Saved Suggestions'),
          ),
          body: new ListView(children: divided),
        );                           // ... до сюда.
      },
    ),
  );
}

Перезапустите приложение. Пометьте избранным какое-нибудь словосочетание в списке и затем нажмите иконку списка в верхней панели. Откроется новый экран с избранным. Navigator сам добавил кнопку «Назад» в панель. Вам даже не потребуется реализация метода Navigator.pop — просто нажав на неё, вы попадёте на основной экран.

Проблемы?

Здесь вы можете скачать рабочий код, если вдруг что-то пошло не так:

4. Смена UI через тему

Сейчас мы изменим тему приложения. Параметр theme отвечает за внешний вид вашего приложения. Вы можете использовать как тему по-умолчанию (зависит от устройства), так и изменить её по вашему желанию.

Тему настроить очень легко, для этого нужно сконфигурировать класс ThemeData. Изменим главный цвет нашего приложения на белый.

Смените цвет в классе MyApp:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Startup Name Generator',
      theme: new ThemeData(          // Добавьте 3 строки отсюда... 
        primaryColor: Colors.white,
      ),                             // ... до сюда.
      home: new RandomWords(),
    );
  }
}

Перезапустите приложение. Общий фон теперь белый, даже верхняя панель.

Как упражнение для практики, воспользуйтесь ThemeData для изменения других элементов UI. Класс Colors в библиотеке Material предоставляет множество цветов, а «горячий» перезапуск делает эксперименты с UI очень лёгкими.

Проблемы?

Здесь вы можете скачать рабочий код, если вдруг что-то пошло не так:

5. Готово!

Вы создали интерактивное приложение Flutter, работающее под iOS и Android и научились:

  • Писать код на Dart.
  • Использовать «горячий» перезапуск приложения для ускорения разработки.
  • Создавать виджеты с изменяемым состоянием (stateful), добавлять интерактивность в приложение.
  • Создавать роуты (route) и логику для перемещений между основным и новым роутами.
  • Изменять интерфейс приложения с помощью тем оформления.

Почитайте также Stateful-виджеты должны исчезнуть: Stateful Builder.

Перевод «Write Your First Flutter App, part 2»

Подписывайтесь на новости Flutter! https://t.me/flutterdaily

Flutter или Xamarin: сравнение инструментов кросс-платформенной мобильной разработки

flutter против xamarin

За последнее десятилетие мы наблюдали огромный рост мобильной индустрии, особенно в отношении разработки приложений. По данным Statista Reports, в мире тогда насчитывалось более 2 миллиардов пользователей смартфонов, а к концу 2022 года их число увеличится до более чем 5 миллиардов. Из этих смартфонов почти 100 процентов работают на трех популярных платформах: Android от Google, iOS от Apple и Windows Mobile, разработанной Microsoft. Долгое время разработчики мобильных приложений зависели от инструментов, привязанных к одной платформе. Например, Kotlin и Java в основном используются для разработки нативных мобильных приложений под Android, в то время как разработчики iOS используют Objective-C, а с недавних пор и Swift.

Недостатки нативной разработки  мобильных приложений

Традиционная разработка мобильных приложений была медленной и дорогой, поскольку компаниям приходилось разрабатывать отдельные приложения для каждой платформы, нанимать разные команды разработчиков для своего инструмента разработки. Сегодня у нас появились кросс-платформенные средства мобильной разработки, которые помогают компаниям сократить время на разработку, затраты на обслуживание, привлечь больше пользователей. Упрощённо: кросс-платформенная мобильная разработка это создание мобильных приложений, которые могут работать на нескольких платформах. Сегодня разработчикам доступно несколько кросс-платформенных средств, в том числе Intel XDK, Xamarin, Cordova и Flutter. В этой статье мы сфокусируемся на двух — Xamarin и Flutter — путем сравнения характеристик, сильных сторон и недостатков каждой, чтобы помочь разработчикам выбрать лучший инструмент.

Если вы только начинаете разрабатывать на Flutter, обратите внимание на статью Создание первого приложения Flutter и продолжение.

Обзор фреймворков Xamarin и Flutter 

Xamarin от Microsoft, возможно, является одной из ведущих технологий кросс-платформенной разработки с открытым исходным кодом. Он использует язык C# для разработки мобильных приложений под Android, iOS и Windows Mobile. Xamarin позволяет разработчикам получать доступ к нативным API под Android и iOS, предоставляет общую кодовую базу на C#, а также тестирование приложений на разных устройствах с Xamarin Testing Cloud. Xamarin был создан в 2011 году разработчиками Mono, которые использовали CLI (Common Language Infrastructure) и Common Language Specification, также известную как Microsoft .NET. Microsoft приобрела Xamarin в 2016 году, а затем сделала Xamarin SDK платформой с открытым исходным кодом, которая стала неотъемлемой частью Xamarin Visual Studio IDE. Чтобы использовать весь потенциал Xamarin, разработчикам понадобятся знания iOS и Android, кроме, собственно, языка C#.

Flutter также является бесплатным кросс-платформенным инструментом с открытым исходным кодом, разработанным Google, и позволяет разработчикам создавать высокопроизводительные мобильные приложения для Android и iOS. Фреймворк использует язык программирования Dart от Google и легкий движок C++. Подобно Xamarin, он позволяет использовать единую кодовую базу для разных платформ. Фреймворк предлагает API и SDK для 2D-рендеринга, моделирования, жестов и рисования, а также позволяет использовать существующий код Swift, Objective C и Java. Он поставляется с виджетами Material Design, также продуктом Google.

Сравнение Flutter и Xamarin

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

В настоящее время Xamarin более популярен среди разработчиков: 7,2 процента опрошенных в Stack Overflow 2018 заявили, что они используют Xamarin, а Microsoft утверждает, что у них 1,4 миллиона инженеров Xamarin, в то время как Flutter не попал в список вообще. Сообщество Flutter еще недостаточно велико. Тем не менее, некоторые инженеры признают Flutter в качестве подающей надежды альтернативы Xamarin.

Ниже мы рассмотрим характеристики и основные функции обоих фреймворков.

Flutter или Xamarin: сравнение инструментов кросс-платформенной мобильной разработки

Переносимость

Flutter нацелен на разработку под Android и iOS, в то время как  Xamarin поддерживает разработку под Android, iOS, Windows (Windows 10 [UWP], устаревшие Windows-приложения [WPF]), а также MacOS. Бóльшая экосистема дает Xamarin преимущество перед Flutter. Но тот факт, что приложения Flutter не портируются на мобильную платформу Windows, не является недостатком, как утверждает недавний опрос Statista, так как на текущий момент около 98,5% смартфонов работают на Android или iOS. Но Xamarin в этом плане может стать лучшим выбором, если вы всё же хотите разрабатывать приложения и под Windows Mobile.

Flutter несовместим со старыми устройствами с 32-разрядными ОС. Поэтому, если вы планируете разрабатывать под старые телефоны, такие как, например, iPhone 5, Xamarin будет лучшим выбором.

Языки программирования

Xamarin использует C#, который популярен и широко используется разработчиками. Если у вас уже есть навыки в C# и .NET, вы можете сразу использовать Xamarin. И вы можете повторно использовать до 96 процентов своего кода на C# в Xamarin, если вы работаете с Xamarin.Forms.

Flutter использует язык Dart, который является относительно новым и непопулярным. Однако, если у вас есть опыт работы на языках ООП, таких как Java, JavaScript и C++, изучение Dart не будет проблемой, так как использует тот же подход и парадигмы.

Поддержка сообщества

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

Хотя Flutter и поддерживается Google, он относительно новый и не имеет пока широкой поддержки сообщества. Платформа также довольно молода, официального релиза пока не было, а это значит, что разработчикам еще предстоит обнаружить основные недостатки и сильные стороны платформы.

Если вы планируете долгосрочный, сложный проект, выбирайте Xamarin, чья стабильность и проблемы уже известны. Однако, сообщество Flutter очень быстро растёт, и в течение года оно уже может стать достаточно зрелым, чтобы суметь решить большинство возникающих проблем.

Доступ к нативному уровню ОС

Код Dart во Flutter компилируется в нативный код, используя компиляцию AoT (Ahead of Time), но по-прежнему требует Dart VM (виртуальная машина). Причина компиляции AoT заключается в том, что платформа iOS не поддерживает JIT или динамическую компиляцию. Flutter также позволяет создавать пользовательские плагины, которые поддерживают код, специфичный для платформы.

Flutter может получить доступ ко всем платформам и API, включая хранилище и сенсоры через механизм пакетов. Вы также можете использовать библиотеки Flutter для создания канала платформы, используемого затем для вызова нативных функций из кода Dart.

Flutter или Xamarin: сравнение инструментов кросс-платформенной мобильной разработки

В Xamarin код C# скомпилирован в машинный код, а затем упакован в .app. Генератор кода mono использует компиляцию JIT для приложений Xamarin.Android и компиляцию AoT для приложений iOS для компиляции промежуточного машинного кода (также известного как управляемый код) в нативный код платформы.

Xamarin использует .NET API и библиотеки, специфичные для платформы, посредством привязок (bindings) для доступа к нативным функциям.

Flutter или Xamarin: сравнение инструментов кросс-платформенной мобильной разработки

Дизайн UI

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

Flutter использует встроенные виджеты и не использует нативные  компоненты пользовательского интерфейса. Виджеты, как ожидается, будут оптимально настроены к платформе, для которой вы создаете пользовательский интерфейс. В настоящее время Flutter предлагает множество макетов, виджетов, платформ для создания графики и поддержки 2D-API, жестов, эффектов, анимации и других функций. Хотя функции интерфейса Flutter все еще находятся в разработке, они могут превратиться в мощную среду пользовательского интерфейса. Кроме того, Flutter поставляется с компонентами Material Design. В конце концов, это Google.

Средства разработки

Функция горячей перезагрузки в Flutter помогает разработчикам создавать пользовательские интерфейсы, экспериментировать и добавлять различные функции, а также быстро обнаруживать и исправлять ошибки без потери состояния на эмуляторах. Разработчики также могут обращаться к нативным функциям, таким как SDK сторонних разработчиков и библиотекам, повторно использовать существующий нативный код (Swift, Objective C, Java и Kotlin).

Одним из преимуществ Xamarin является то, что он позволяет разработчикам тестировать приложения на разных устройствах в Microsoft Xamarin Cloud. Однако вы должны платить абонентскую плату за доступ к этой функции. Xamarin также предоставляет функцию живой перезагрузки, такую же почти, как во Flutter, которая помогает разработчикам изменять XAML и видеть результат «вживую» без компиляции или установки приложения.

Одной из проблем Xamarin является интеграция с сторонними библиотеками, которая, по-видимому, лучше реализована во Flutter.

Опыт разработки

Xamarin позволяет переиспользовать около 96 процентов кода C#, но если вам понадобится полностью нативная производительность, вам придется использовать платформозависимый код. Таким образом, Xamarin выигрывает тогда, когда разработчики имеют опыт работы на C#, а также могут работать с такими платформами, как Java, Kotlin, Swift и Objective-C.

Для разработки приложений в Flutter вам необходимо освоить Dart, что может отнять у вас немного времени, однако, если вы знакомы с Java и C ++, это не будет проблемой. Если вы новичок в разработке мобильных приложений и не имеете опыта разработки приложений для Android или iOS, вам нужно вначале хорошо освоить Dart.

Размер приложения и APK

Согласно испытаниям Корхана Бикарна, инженера Capital One, простое приложение Flutter имеет размер бинарника 40,2 МБ, а такое же на Xamarin — 25,1 МБ. Размер APK для такого приложения Flutter занимает почти 8 Мбайт, а на Xamarin — около 7 МБ. Здесь вы можете увидеть более подробные результаты.

Использование памяти, процессора и графического процессора

Согласно тому же сравнению, инициализация адресного пространства приложения и динамическое связывание заняло 1,05 секунды в Flutter. Приложение запускается за ~220 мс и работает на 58 FPS. В Xamarin инициализация адресного пространства приложения и динамическое связывание отняли 3,2 секунды. Приложение запускается примерно за 345 мс на 53 FPS. Вы также можете узнать больше о производительности Xamarin в специальной статье.

Выводы

Хотя разработчики сейчас и выбирают чаще Flutter, это не означает, что Xamarin устарел. Имеет смысл выбрать Flutter, если вы новичок в кросс-платформенной мобильной разработке, так как эта платформа, вероятно, станет очень популярной в ближайшем будущем.

Тем не менее, платформа Xamarin в настоящее время более зрелая во многих отношениях, включая сообщество, набор инструментов и стабильность. Для сложных и долгосрочных проектов мы рекомендуем остановиться на Xamarin, особенно если у вас есть команда разработчиков на C# и .NET, и вы работаете с экосистемой Microsoft.

Перевод: «Flutter vs Xamarin Cross-Platform Mobile Development Compared»

Подписывайтесь на новости Flutter! https://t.me/flutterdaily

Создание первого приложения Flutter, часть 1

Создание первого приложения Flutter

1. Введение

Flutter это SDK для создания мобильных приложений от Google под iOS и Android. С Flutter можно переиспользовать код, им пользуются разработчики по всему миру, он имеет открытый исходный код и бесплатен.

В этом уроке вы создадите несложное приложение Flutter. Если вы знакомы с объектно-ориентированным программированием и основами разработки, такими как переменные, циклы и условия, у вас всё должно получиться. Опыта разработки на Dart или под мобильные устройства не требуется.

Что мы узнаем в этом уроке

  • Как написать приложение на Flutter, выглядящее нативно и в  iOS, и в Android.
  • Базовую структуру приложений Flutter.
  • Поиск и использование пакетов для расширения функционала.
  • Использование горячей перезагрузки (hot reload) для удобства разработки.
  • Как реализовать stateful-виджет.
  • Как создать бесконечный список с «ленивой» загрузкой.

Во второй части мы добавим интерактивность, изменим тему приложения, добавим возможность перехода на другую страницу (называемую роутами (route) во Flutter).

Что мы сделаем в первой части

Вы сделаете простое мобильное приложение, генерирующее наименования для стартапов. Пользователь сможет выбирать и отменять выбор названий, сохраняя понравившиеся. Код будет «лениво» генерировать десять названий за один раз. По мере прокрутки пользователем списка, новые названия будут создаваться в фоне и подгружаться. Таким образом, пользователь сможет прокручивать список вечно.

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

Создание первого приложения Flutter, часть 1

2. Настройте окружение Flutter

Всё, что вам нужно для этого: Flutter SDK и редактор кода. В этом уроке предполагается, что вы выберете Android Studio, но выбор редактора остаётся за вами.

Протестировать работу приложения можно будет с этими устройствами:

  • Физическое устройство (Android или iOS), подключенное к компьютеру с настроенным режимом разработчика.
  • Симулятор iOS. (Требуется установка XCode)
  • Эмулятор Android. (Требуется установленная Android Studio)

3. Создаём каркас приложения Flutter

Для этого воспользуйтесь инструкцией Getting Started with your first Flutter app (возможно, позже её перевод будет доступен). Назовите проект startup_namer (вместо myapp). В дальнейшем мы будем дорабатывать это приложение.

Совет: Если вы не видите пункт «New Flutter Project» в вашей IDE, убедитесь, что она настроена для работы с Flutter и Dart.

В этом уроке мы будем преимущественно править файл lib/main.dart, в котором находится логика приложения.

Замените содержимое файла lib/main.dart.
Для этого удалите весь код в нём. Затем вставьте в него код ниже, выводящий надпись «Hello World» по центру экрана.

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: const Text('Welcome to Flutter'),
        ),
        body: const Center(
          child: const Text('Hello World'),
        ),
      ),
    );
  }
}

Замечание: При вставке кода могут исказиться отступы. Вот как это поправить:
   — Android Studio / IntelliJ IDEA: кликните правой кнопкой на коде и выберите Reformat Code with dartfmt.
   — VS Code: кликните правой кнопкой и выберите Format Document.
   — Terminal: запустите flutter format <имя файла>.

Запустите приложение. Вы должны увидеть приметно такое, в зависимости от того, Android или iOS у вас:

Замечание:  При первом запуске на физическом устройстве может пройти много времени. После этого будет работать hot reload. При нажатии на кнопку Save также будет запущен hot reload при работающей программе.

Подытожим

  • В этом примере создаётся приложение Material. Material — это описание визуального представления, он является стандартом в web и на мобильных платформах. Flutter содержит огромный спектр виджетов типа Material.
  • Метод main использует нотацию с большой стрелкой (=>), такая нотация используется в однострочных функциях.
  • Приложение наследует StatelessWidget, следовательно всё приложение также является виджетом. Во Flutter практически всё является виджетом, включая выравнивание, отступы, макет.
  • Виджет Scaffold из библиотеки Material предоставляет панель приложения, заголовок и свойство body, которое отвечает за иерархию виджетов на основном экране. И эта иерархия может быть иногда очень огромной.
  • Главная задача виджета — вернуть метод build, который описывает как нужно вывести виджет по отношению к соседним.
  • body в этом примере состоит из виджета Center, содержащего дочерний виджет Text. Виджет Center выравнивает всю иерархию потомков по центру экрана.

4. Используем сторонние пакеты

Сейчас вы научитесь работать с внешними пакетами, на примере пакета english_words, содержащего несколько тысяч популярных английских слов и утилиты для удобной работы с ними.

Найти пакет english_words, так же, как и многие другие пакеты с открытым исходным кодом, вы можете на pub.dartlang.org.

Файл pubspec управляет списком зависимостей приложения Flutter. В конец pubspec.yaml добавим english_words: ^3.1.0 (english_words версии 3.1.0 или выше) в список зависимостей (dependencies):

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^0.1.0
  english_words: ^3.1.0   # добавьте эту строку

Если вы открыли файл pubspec в Android Studio, кликните Packages get. Эта команда загрузит пакет из сети. В консоли вы должны увидеть что-то подобное:

flutter packages get
Running "flutter packages get" in startup_namer...
Process finished with exit code 0

Откроем lib/main.dart, импортируем пакет:

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';  // добавьте эту строку.

По мере написания, Android Studio будет предлагать библиотеки для импорта. Но добавленная строка сразу окрасится серым цветом, показывая неиспользуемый (пока) импорт.

Пришло время применить пакет english_words, пусть он генерит текст вместо текущей статичной надписи «Hello World».

Сделайте следующие изменения:

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final wordPair = new WordPair.random(); // добавьте эту строку.
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(    // Замените "const" на "new".
          //child: const Text('Hello World'),   // Замените этот текст...
          child: new Text(wordPair.asPascalCase),  // этим.
        ),
      ),
    );
  }
}

Замечание: «Pascal case» (так же известный как CamelCase («ВерблюжийРегистр»)), означает, что каждое слово в строке, включая первое, начинается с большой буквы. Таким образом, «upper camel case» становится «UpperCamelCase».

Если не заменить Center на «new Center», система типов покажет предупреждение о том, что объект Center не должен быть константой (const), потому что его потомок, объект Text больше не является константой. Поэтому и Center и Text должны создавать экземпляры с помощью new.

Если приложение уже запущено, используйте для перезапуска кнопку hot reload. При каждом перезапуске или холодном запуске проекта, вы будете видеть в приложении разные пары слов, выбранные в случайном порядке. Это происходит потому, что фразы создаются внутри метода build, запускаемого каждый раз при запросе от MaterialApp на перерисовку, или же переключении Platform в Flutter Inspector.

Проблемы?

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

5. Добавим stateful виджет

Stateless-виджеты неизменяемые (immutable), это значит, что их свойства не могут меняться  — все их значения завершённые (final).

Stateful виджеты поддерживают смену состояния. Реализация такого виджета подразумевает создание минимум двух классов: 1) класс StatefulWidget создающий экземпляр класса 2) State. Класс StatefulWidget сам по себе неизменяемый (immutable), однако класс State сохраняется на протяжении всего жизненного цикла виджета.

На этом шаге вы добавите stateful-виджет RandomWords, который создаст свой State-класс, RandomWordsState. Затем вы добавите RandomWords как дочерний элемент stateless-виджета MyApp.

Создадим минимальный класс состояния (state). Он может располагаться в любом месте файла в MyApp, но в этом решении он расположен в конце файла. Добавьте эти строки:

class RandomWordsState extends State<RandomWords> {
  // TODO Добавить метод build
}

Обратите внимание на объявление State<RandomWords>. Это указывает на то, что мы используем универсальный класс State, специализированный для использования с RandomWords. Большинство логики приложения и состояние находится здесь для поддержки состояния виджета RandomWords. Этот класс сохраняет сгенерированные пары слов, количество которых растёт бесконечно по мере прокрутки пользователя, так же как и избранных пар слов (будет в части 2), которые пользователь добавляет или удаляет в списке, нажимая кнопку в виде сердечка.

RandomWordsState зависит от класса RandomWords. Сейчас добавим его.

Добавьте stateful-виджет RandomWords в main.dart. Виджет RandomWords делает немного больше, чем просто создаёт State-класс:

class RandomWords extends StatefulWidget {
  @override
  RandomWordsState createState() => new RandomWordsState();
}

После этого IDE сообщит, что в классе отсутствует метод build. Добавим метод build, создающий пары слов, переместив этот код из MyApp в RandomWordsState. Добавьте код:

class RandomWordsState extends State<RandomWords> {
  @override                                  // Добавим с этой строки ... 
  Widget build(BuildContext context) {
    final WordPair wordPair = new WordPair.random();
    return new Text(wordPair.asPascalCase);
  }                                          // ... до этой.
}

Удалим часть кода из MyApp:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final WordPair wordPair = new WordPair.random();  // Удалите эту строку.

    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(
          //child: new Text(wordPair.asPascalCase), // А эту замените... 
          child: new RandomWords(),                 // ... на эту.
        ),
      ),
    );
  }
}

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

Совет: Если вы видите следующее сообщение при горячей перезагрузке, лучше полностью перезапустить его:
Reloading…
Not all changed program elements ran during view reassembly; consider restarting.

Возможно, это и излишне, однако при перезапуске вы будете точно уверены что все изменения отразятся в интерфейсе приложения.

Проблемы?

Если ваше приложение не корректно работает, рабочий код по ссылке:

6. Создание ListView с бесконечной прокруткой

В этом шаге мы расширим RandomWordsState таким образм, чтобы выводился список пар слов. По мере его прокрутки, список (представленный виджетом ListView) будет расти бесконечно. Конструктор builder в ListView позволяет создавать списковые представления на лету, по запросу.

Добавьте список _suggestions в класс RandomWordsState для хранения предложенных пар. Также добавьте переменную _biggerFont для увеличения размера шрифта.

Замечание:  Добавление префикса в виде подчёркнутой линии к имени переменной делает её закрытой (private) в языке Dart.

class RandomWordsState extends State<RandomWords> {
  // добавьте следующие две строки.
  final List<WordPair> _suggestions = <WordPair>[];
  final TextStyle _biggerFont = const TextStyle(fontSize: 18.0); 
  ...
}

Затем добавьте функцию _buildSuggestions() в класс RandomWordsState. Этот метод создаст ListView, который выведет сгенерированные пары слов.

Класс ListView предоставляет свойство itemBuilder, это построитель и анонимная функция с обратным вызовом. Она принимает два параметра — BuildContext, и переменную-итератор i. Итератор начинается с 0 и увеличивается при каждом вызове функции, т.е. каждый раз при генерации новой пары слов. Такая модель позволяет списку предложений расти постепенно при перемотке списка пользователем.

Добавьте функцию _buildSuggestions, описанную ниже, в класс RandomWordsState (delete the comments, if you prefer):

  Widget _buildSuggestions() {
    return new ListView.builder(
      padding: const EdgeInsets.all(16.0),
      // The itemBuilder callback is called once per suggested 
      // word pairing, and places each suggestion into a ListTile
      // row. For even rows, the function adds a ListTile row for
      // the word pairing. For odd rows, the function adds a 
      // Divider widget to visually separate the entries. Note that
      // the divider may be difficult to see on smaller devices.
      itemBuilder: (BuildContext _context, int i) {
        // Add a one-pixel-high divider widget before each row 
        // in the ListView.
        if (i.isOdd) {
          return new Divider();
        }

        // The syntax "i ~/ 2" divides i by 2 and returns an 
        // integer result.
        // For example: 1, 2, 3, 4, 5 becomes 0, 1, 1, 2, 2.
        // This calculates the actual number of word pairings 
        // in the ListView,minus the divider widgets.
        final int index = i ~/ 2;
        // If you've reached the end of the available word
        // pairings...
        if (index >= _suggestions.length) {
          // ...then generate 10 more and add them to the 
          // suggestions list.
          _suggestions.addAll(generateWordPairs().take(10));
        }
        return _buildRow(_suggestions[index]);
      }
    );
  }

Функция _buildSuggestions вызывает _buildRow для каждой пары слов. Она выводит созданную пару в ListTile, и это позволит вам сделать ряды более привлекательными в части 2.

Добавьте функцию _buildRow в RandomWordsState:

  Widget _buildRow(WordPair pair) {
    return new ListTile(
      title: new Text(
        pair.asPascalCase,
        style: _biggerFont,
      ),
    );
  }

Обновите метод build в RandomWordsState с добавлением _buildSuggestions() вместо прямого вызова библиотеки генерации слов. (Scaffold реализует базовый визуальный стиль в Material Design)

  @override
  Widget build(BuildContext context) {
    //final wordPair = new WordPair.random(); // удалить эти... 
    //return new Text(wordPair.asPascalCase); // ... две строки.

    return new Scaffold (                   // Добавить отсюда... 
      appBar: new AppBar(
        title: new Text('Startup Name Generator'),
      ),
      body: _buildSuggestions(),
    );                                      // ... досюда.
  }

Обновим метод build в MyApp, сменим заголовок и основной экран на виджет RandomWords.

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Startup Name Generator',
      home: new RandomWords(),
    );
  }

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

Проблемы?

Если ваше приложение работает не так, как должно, скачайте код по ссылке ниже и замените им свой, чтобы продолжить работу.

7. Что дальше

Поздравляем!

Вы закончили первую часть! Если хотите продолжить, переходите в часть 2, где сможете доработать приложение:

  • Добавить интерактивность.
  • Добавить навигацию.
  • Сменить цвет темы.

В конце части 2 приложение будет таким:

Создание первого приложения Flutter, часть 1

Подписывайтесь на новости Flutter! https://t.me/flutterdaily

Перевод «Write Your First Flutter App, part 1»

Stateful-виджеты должны исчезнуть: Stateful Builder

Stateful-виджеты исчезнут: Stateful Builder

Как и сказано в названии, вам не всегда нужен StatefulWidget.

При разработке приложений Flutter, в зависимости от виджета, метод build может быть огромным. И использование StatefulWidget означает перестроение всей иерархии виджетов при каждом вызове setState .

Есть и более изящный способ обработки изменений состояния — использование StatefulBuilder.

Если вы только начинаете разрабатывать на Flutter, обратите внимание на статью Создание первого приложения Flutter и продолжение.

Stateful Builder

StatefulBuilderэто виджет Flutter, позволяющий встраивать состояние прямо в метод build. Этот виджет использует метод StatefulWidgetBuilder. В нём два параметра, context и setState. Параметр context это параметр типа BuildContext. Но наиболее интересен параметр setState.

typedef StatefulWidgetBuilder = Widget Function(BuildContext context, StateSetter setState);

Посмотрев в исходники StatefulWidgetBuilder, мы увидим, что setState это StateSetter. StateSetter это обратный вызов к State.setState. При его вызове, метод перестраивает иерархию виджетов с учётом их изменений.

Как использовать Stateful Builder

StatefulBuilder лучше всего использовать когда у вас средняя/большая иерархия виджетов, но состояние меняется только у небольшой его части.

import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(home: MyApp()));

class MyApp extends StatelessWidget {
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.amberAccent,
      body: SafeArea(
        child: Center(
          child: PersonCard(),
        ),
      ),
    );
  }
}

class PersonCard extends StatelessWidget {
  int counter = 0;
  @override
  Widget build(BuildContext contex) {
    return Card(
      child: SizedBox(
        width: 300.0,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: <Widget>[
                Padding(
                  padding: const EdgeInsets.all(18.0),
                  child: Text(
                    "Luke Skywalker",
                    style:
                        TextStyle(fontSize: 16.0, fontWeight: FontWeight.w700),
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.all(18.0),
                  child: Text(
                    "Age:  23",
                    style:
                        TextStyle(fontSize: 16.0, fontWeight: FontWeight.w700),
                  ),
                ),
              ],
            ),
            SizedBox(height: 12.0),
            Center(
              child: Container(
                height: 130.0,
                width: 130.0,
                child: Image.asset("assets/skywalker.png"),
              ),
            ),
            SizedBox(height: 12.0),
            StatefulBuilder(
              builder: (BuildContext context, StateSetter setState) {
                return Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: <Widget>[
                    IconButton(
                      icon: Icon(Icons.skip_previous),
                      onPressed: () {
                        setState(() {
                          counter--;
                        });
                      },
                    ),
                    Text(
                      "$counter",
                      style: Theme.of(context).textTheme.body2,
                    ),
                    IconButton(
                      icon: Icon(Icons.skip_next),
                      onPressed: () {
                        setState(() {
                          counter++;
                        });
                      },
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

Здесь показана работа StatefulBuilder с виджетом Card. Вместо того, чтобы сделать всю Card stateful, мы используем StatefulBuilder для построения только той секции дерева виджетов, в которой есть state . В этом случае мы используем его только для построения Row с кнопками и текстом. В результате, при срабатывании onPressed и вызове setState, будут перестроены только Row и его потомки.

Stateful-виджеты должны исчезнуть: Stateful Builder

Если бы мы сделали виджет полностью stateful, то вместо перестроения только Row, это затронуло бы весь Card.

Stateful-виджеты должны исчезнуть: Stateful Builder
Рис. 1: Штриховкой показана область перестроения

Видео законченного приложения:

Перевод статьи «Stateful Widgets be gone: Stateful Builder»

Подписывайтесь на новости Flutter! https://t.me/flutterdaily

Создание хорошего плагина Flutter

Создание первого приложения Flutter

Плагины — очень важная часть экосистемы Flutter. Они позволяют разработчикам взаимодействовать с платформой, на которой работают их приложения.

На текущий момент в каталоге насчитываются сотни плагинов для Flutter. Качество плагина — наиважнейшая вещь, независимо от автора плагина — это компания Google или частный разработчик. В этой статье мы рассмотрим, что такое хороший плагин и как написать такой самостоятельно.

Если вы только начинаете разрабатывать на Flutter, обратите внимание на статью Создание первого приложения Flutter и продолжение.

Сперва — функциональность, затем остальное

При написании плагина вы скорее всего попытаетесь использовать существующую нативную библиотеку. Для этого нужно взять API этой библиотеки и создать для неё эквивалентный API на Dart. Однако если API-интерфейсы существенно отличаются под разными платформами, начинаются проблемы.

При написании плагинов сначала думайте о функциональности, а не об API. К примеру, если вы пишете плагин локального хранилища, вам, вероятно, нужны API-интерфейсы, которые поддерживают хранение значений различных типов с возможностью их повторного вызова. После того, как вы определили, какую функциональность вы хотите заложить, подумайте о построение красивого Dart API:

class StoragePlugin {
  
  /// Читаем строку
  Future<String> getString(String key) async {}
  
  /// Записываем строку
  Future<void> setString(String key, String value) async {}
}

После того, как у вас будет Dart API, переходите к общему API, тогда и увидите, какие ваши вызовы API должны быть адаптированы к вызовам API библиотеки конкретной платформы:

class StoragePlugin {
  
  Future<String> getString(String key) async {
    if (Platform.isIos) {
      return await callMethodChannel('fetchValue', {'id': base64encode(key)});
    } else if (Platform.isAndroid) {
      return await callMethodChannel('fetchValue', {'id': key});
    }
  }
  
  Future<void> setString(String key, String value) async {
    if (Platform.isIos) {
      await callMethodChannel('setValue', {'id': base64encode(key), 'value': value});
    } else if (Platform.isAndroid) {
      await callMethodChannel('setValue', {'id': key, 'value': value});
    }
  }
}

Если вы хотите реализовать какую-то функциональность, которая не поддерживается какой-то платформой, совершенно нормально что этот API будет нерабочим в ней.

Вы должны следить за тем, чтобы приложение не падало в этом случае и всё было тщательно задокументировано. Также вы должны запросить отсутствующую функциональность с этой платформы, чтобы ваш плагин стал завершённым.

Избегайте использования специфичных для платформы API-методов

Flutter не привязан к какой-либо платформе, поэтому и в приложениях на Flutter не должно быть платформенных методов. Например, если у вас такой код:

if (Platform.isIos) {
  myPlugin.doIOSThing();
} else if (Platform.isAndroid) {
  myPlugin.doAndroidThing();
}

то переместите в плагин всю заботу о платформах и пишите так:

myPlugin.doThing();

Избегайте поддержки только одной платформы

Возможно, вы хотите начать с малого и написать плагин только под Android. Но к несчастью, после публикации его в каталоге, пользователи неподдерживаемых платформ будут сильно разочарованы в нём.

Однако, есть и примеры обратного: Android Intent. Intent-ы существуют только в Android, поэтому существование такого плагина совершенно нормально.

Сделайте плагин удобным для чтения и тестирования

Как правило, ваш плагин должен быть написан в основном на Dart.

  • Нативный слой как правило сложен для тестирования, т.к написан с применением библиотек, не работающих в эмуляторах.
  • Flutter написан большей частью на Dart. Приложения Flutter написаны на чистом Dart. При трассировке кода легче всего будет, если большая часть логики описана на Dart.

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

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

В идеале, нативный слой будет просто ожидать вызовов метода, пересылая их библиотеке, возвращая затем результат в Dart. Если же требуется выполнить какую-либо дополнительную работу, стремитесь описать её на стороне Dart. В конце концов, вы напишете меньше кода, так как логика, вероятно, будет одинакова на всех платформах.

Избегайте писать статические (или глобальные) методы

Вместо такого подхода:

Future<User> authenticate() async {
  // код...
}

используйте:

class AuthenticatePlugin {
  Future<User> authenticate() async {
    // код...
  }
}

это сделает плагин более удобным для тестирования на эмуляторах.

Почитайте также Stateful-виджеты должны исчезнуть: Stateful Builder

Плагины это один из лучших способов расширить функциональность  Flutter. Поделитесь в комментариях своим опытом создания плагинов!

Написано по материалам статьи «Writing a good Flutter plugin»

Подписывайтесь на новости Flutter! https://t.me/flutterdaily

Flutter и перспектива

Создание первого приложения Flutter

Немного поиграем с 3D и виджетом Transform

В этой статье мы создадим небольшое приложение, демонстрирующее работу виджета Transform по отрисовке 3D перспективы. Мы увидим, как легко это сделать с Flutter, при том, что это сделает не каждый на нативных виджетах.

Ниже скрин приложения в работе (маленький кружок показывает позицию пальца пользователя на экране):

Flutter и перспектива

Приступим

Создайте проект командой flutter create или же средствами вашей IDE. Добавьте два виджета в наше приложение:  Transform и GestureDetector.

Первый:

// v1: move default app to separate function with fixed name
// Add transform widget, rotate and perspective
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Perspective',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key); // changed

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  Offset _offset = Offset(0.4, 0.7); // new

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Transform(  // Transform widget
      transform: Matrix4.identity()
        ..setEntry(3, 2, 0.001) // perspective
        ..rotateX(_offset.dy)
        ..rotateY(_offset.dx),
      alignment: FractionalOffset.center,
      child: _defaultApp(context),
    );
  }

  _defaultApp(BuildContext context) {  // new
    return Scaffold(
      appBar: AppBar(
        title: Text('The Matrix 3D'), // changed
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }

}

Вот что должно получиться при запуске:

Flutter и перспектива

Здесь мы немного почистили типовой код, убрав ненужные комментарии и уже ненужное в Dart 2 ключевое слово new. Мы переместили часть представления (метод build из _MyHomePageState) в отдельный метод _defaultApp (строки 49–74). И для простоты  присвоили название прямо в AppBar (строка 52), вместо передачи его параметром в MyHomePage.

Виджет Transform

Добавленный виджет Transform описан в строках 39–46. Посмотрим на него внимательнее. Transform принимает 3D матрицу перехода, т.е. Matrix4. Почему 3D матрица? Разве Flutter не для двумерной графики?

Практически все современные смартфоны, за исключением самых слабых, имеют невероятно быстрый GPU, оптимизированный для 3D графики. Следовательно, практически всё, что вы видите на смартфоне, это обработка в 3D, даже изначально двумерные объекты. Немного странно, да?

Изменяя параметры матрицы, мы манипулируем отображением представления (даже в 3D!). Типичные трансформации включают перемещение, поворот, изменение масштаба и перспективы. Создаём экземпляр матрицы методом identity (строка 40), затем трансформируем его. Трансформации не коммутативны, поэтому нам необходимо определять их в правильном порядке. Законченная матрица будет отправлена GPU для трансформации затронутых ею объектов.

Трансформация это довольно непростая тема, но если вы хотите узнать больше про неё, погуглите материалы по 3D графике, а также  по матрицам перехода и однородным координатам.

Перспектива

Первая трансформация (строка 41) затрагивает изменение перспективы. Перспектива делает удалённые объекты меньше. Установим настройки матрицы так: ряд 3, колонка 2, увеличение 0.001.

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

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

Повороты

Затем в строках 42 и 43 мы задаём два поворота на основе значения переменной _offset (в стр. 29; а ниже мы используем эту же переменную для отслеживания положения пальца пользователя). И что удивительно, поворот по оси X зависит от смещения Y, а поворот по оси Y — от смещения X. Почему так?

Flutter и перспектива

Рассмотрим это изображение, на котором зелеными стрелками указаны оси X и Y. Эти оси начинаются в верхнем левом углу дисплея (именно поэтому ось Y указывает вниз), но наша программа устанавливает начало координат (в строке 44) из центра дисплея.

Поворот задаётся по отношению к оси, метод rotateX определяет поворот вокрут оси X, которая отклоняется по направлению оси Y (вверх-вниз). Точно так же, rotateY отклоняется в направлении X (лево-право) (вокруг оси Y). Поэтому rotateX контролируется с _offset.dy и rotateY с _offset.dx.

Также здесь есть ось Z, которая идёт перпендикулярно экрану, таким образом значение Z положительно при удалении от зрителя. И метод rotateZ поворачивает объект в плоскости дисплея.

Взаимодействие

Ну и наконец мы должны добавить виджет GestureDetector:

// v2: add Gesture detector
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Perspective',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key); // changed

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  Offset _offset = Offset.zero; // changed

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Transform(  // Transform widget
      transform: Matrix4.identity()
        ..setEntry(3, 2, 0.001) // perspective
        ..rotateX(0.01 * _offset.dy) // changed
        ..rotateY(-0.01 * _offset.dx), // changed
      alignment: FractionalOffset.center,
      child: GestureDetector( // new
        onPanUpdate: (details) => setState(() => _offset += details.delta),
        onDoubleTap: () => setState(() => _offset = Offset.zero),
        child: _defaultApp(context),
      )
    );
  }

  _defaultApp(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('The Matrix 3D'), // changed
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }

}

В строке 28 _offset инициализируется в значении нуля. Строки 44–48 определяют GestureDetector, отслеживающий два вида жестов: перемещение (напр., движения пальца по экрану) и двойной тап. В строке 45 увеличиваем количество движений (в пикселях) в переменной _offset. В строке 46 обнуляем _offset при двойном тапе пользователя по экрану. И для обоих жестов метод setState() перерисовывает экран.

В строках 41 и 42 смещение (в пикселях) умножается на 0.01 для удобства расчета вращения (оно указывается в радианах, где полное вращение это 2π , т.е. приблизительно 6.28  — таким образом для полного вращения потребуется перемещение на 628 пикселей).

Готово!

Конечно, ни один из этих объектов не является по-настоящему трёхмерным. Все они двумерные (плоские) (даже искусственные тени от AppBar и FAB). Однако, это не мешает нам крутить их в трёхмерном пространстве как угодно.

Ещё один пример

Flutter и перспектива

Итак, сделаем красивый псевдотрёхмерный эффект, наподобие такого:

Flutter и перспектива

Как это работает

На первый взгляд, идея проста: у нас есть набор элементов, каждый из которых разбит на половинки, может вращаться вокруг оси X, открывая следующий за ним элемент.

Как это воплотить в коде? Разобьём эту задачу на две:

  • Разрезаем элемент на две половины
  • Вращаем одну половинку вокруг оси X

Почитав документацию Flutter, я нашёл два виджета, подходящих для этой задачи: ClipRect и Transform.

Реализация

  • Разрезаем элемент на две половины:

Виджет ClipRect имеет параметр clipper для установки параметров изображения, но также есть другой способ использования ClipRect, в комбинации с Align:

class FlipWidget extends StatelessWidget {
  Widget child;

  FlipWidget({Key key, this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        ClipRect(
            child: Align(
          alignment: Alignment.topCenter,
          heightFactor: 0.5,
          child: child,
        )),
        Padding(
          padding: EdgeInsets.only(top: 2.0),
        ),
        ClipRect(
            child: Align(
          alignment: Alignment.bottomCenter,
          heightFactor: 0.5,
          child: child,
        )),
      ],
    );
  }
}

Пробуем:

Flutter и перспектива

Готово. К этому добавим, что child позволяет нам свободно управлять содержимым анимации (текстом, картинкой, да чем угодно).

  • Вращаем одну половинку вокруг оси X:

Виджет Transform имеет параметр transform типа Matrix4 для выбора вида применяемой трансформации, также Matrix4 имеет конструктор rotationX(), похоже, что мы можем его использовать для верхней половинки панели:

@override
Widget build(BuildContext context) {
   return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Transform(
          transform: Matrix4.rotationX(pi / 4),
          alignment: Alignment.bottomCenter,
          child: ClipRect(
              child: Align(
            alignment: Alignment.topCenter,
            heightFactor: 0.5,
            child: child,
          )),
        ),
        ...
      ],
    );
}

Пробуем:

Flutter и перспектива

Правда, это похоже на эффект масштабирования?

Изменим передаваемые значения в Matrix4 должно всё исправить:

...
Transform(
  transform: Matrix4.identity()..setEntry(3, 2, 0.006)..rotateX(pi / 4),
  alignment: Alignment.bottomCenter,
  child: ClipRect(
      child: Align(
    alignment: Alignment.topCenter,
    heightFactor: 0.5,
    child: child,
  )),
),
...

Пробуем:

Flutter и перспектива

Осталось добавить анимацию для нашего виджета. Здесь немного сложнее. Фактически, каждая панель содержит контент с обеих сторон (спереди и сзади), но в один момент времени видна только одна сторона. Думаю, что нам нужно создать анимацию, в которой панели поворачиваются вверх — она будет состоять из двух этапов (последовательно), первый — перевернуть нижнюю половину вверх и показать нижнюю половину следующей панели, в то же время скрыть нижнюю половину текущей панели, а второй этап — перевернуть верхнюю половину в том же направлении и открыть верхнюю половину следующего, тут же спрятав верхнюю половину текущей:

Flutter и перспектива

Полный код можно скачать здесь.

Вот что получим в итоге:

Flutter и перспектива

Написано по материалам статей «Perspective on Flutter» и «Make 3D flip animation in Flutter»

Если вы только начинаете разрабатывать на Flutter, обратите внимание на статью Создание первого приложения Flutter и продолжение.

Подписывайтесь на новости Flutter! https://t.me/flutterdaily

Сканер штрихкодов на Flutter

Создание первого приложения Flutter

Мне всегда хотелось попробовать сделать сканер штрихкодов под Android, но никогда не находилось достаточно мотивации для этого… Но не сейчас 🙂

Сканер штрихкодов на Flutter
Общий вид приложения

В поиске нужного пакета для Flutter я нашел barcode_scan

Начнём

Сканер штрихкодов на Flutter
Добавляем зависимости

Добавляем зависимость barcode_scan : “0.0.4”  в файл pubspec.yaml.

Если вы только начинаете разрабатывать на Flutter, обратите внимание на статью Создание первого приложения Flutter и продолжение.

Затем добавьте следующую строку в Android Manifest вашего приложения:

<activity android:name="com.apptreesoftware.barcodescan.BarcodeScannerActivity"/>
Сканер штрихкодов на Flutter
Измененный Android Manifest.xml

При клике на кнопку Capture Image, начнётся сканирование.

Вам должен открыться запрос на доступ к камере. В случае отказа мы вернем сообщение пользователю:

Сканер штрихкодов на Flutter
Ошибка в случае не предоставления доступа

Если разрешение получено, произойдёт считывание изображения со штрихкодом и на экран покажется сообщение с текстом кода:

Сканер штрихкодов на Flutter
Штрихкод отсканирован

Кусочек кода сканирования :

Сканер штрихкодов на Flutter
Сканирование кода

Вот и всё! Кратко о том же на видео:

Полный код программы можно скачать здесь https://github.com/AseemWangoo/flutter_programs/blob/master/barcode.dart

Как написать плагин для Flutter?

Перевод статьи Barcode Scanner in Flutter

Подписывайтесь на новости Flutter! https://t.me/flutterdaily

Adblock detector