Введение в 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, часть 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»

Разработка кросс-платформенного мобильного приложения на голом JSON

Разработка: Разработка кросс-платформенного мобильного приложения на голом JSON

Предыдущие несколько месяцев я посвятил работе над совершенно новым способом разработки нативных приложений для iOS и Android и назвал его Jasonette.

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

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

В видео ниже кратко описан весь процесс (на англ.):

Jasonette состоит из многих компонентов. Там есть функции, шаблоны, стили и многое другое, и всё это в JSON разметке. Вы можете писать супер-изощрённые нативные приложения в стиле [simple_tooltip content=’Model-View-Controller (Модель-Представление-Контроллер) — схема разделения данных приложения, пользовательского интерфейса и управляющей логики на три отдельных компонента: модель, представление и контроллер — таким образом, что модификация каждого компонента может осуществляться независимо.’]MVC[/simple_tooltip].

Сегодня мы разберём только «Представления»:

  1. Как Jasonette выражает различные кросс-платформенные шаблоны UI в JSON.
  2. Как реализованы внутренние преобразования JSON-в-Native.

Базовая структура

Издалека можно подумать, что Jasonette работает подобно браузеру. Но вместо того, чтобы работать с HTML и отрисовывать web-view, Jasonette загружает JSON и «на лету» собирает нативное представление.

Разметка JSON здесь самая обычная, но при этом она строится на нескольких стандартах. Во-первых, структура начинается с ключа $jason, который имеет двух потомков: head и body. Выглядит она так:

{
  "$jason": {
    "head": {
      .. метаданные документа ...
    },
    "body": {
      .. содержимое, выводимое на экран ..
    }
  }
}

Философия проектирования

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

  1. Список с вертикальной прокруткой
  2. Список с горизонтальной прокруткой
  3. Абсолютное позиционирование
  4. Сетка

Взглянем на первые три, ибо они самые распространённые.

1. Секции — описание списка с прокруткой

Списки с прокруткой это самый популярный способ построения интерфейса приложений. В Jasonette мы называем их sections.

Они бывают двух видов: вертикальные и горизонтальные.

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON


Реализация — Вертикальные секции

Под iOS Jasonette реализует их с помощью UITableView. Под Android — с RecyclerView.


{
  "body": {
    "sections": [{
      "items": [
        {"type": "label", "text": "Item 1"},
        {"type": "label", "text": "Item 2"},
        {"type": "label", "text": "Item 3"}
      ]
    }]
  }
}

Под iOS эта JSON разметка создаст UITableView с тремя UITableViewCells, каждая из которых содержит UILabel, с соответствующими атрибутами.
Под Android будет создано RecyclerView с тремя элементами, каждый из которых это TextView, выводящий соответствующий элемент.
Всё это будет сконструировано программно, без какого-либо использования Storyboards (в iOS) или файлов макета XML (в Android).

Реализация — Горизонтальные секции

Синтаксически нет разницы с вертикальными секциями. Единственное, что мы изменили, это type в “horizontal”.


{
  "body": {
    "sections": [{
      "type": "horizontal",
      "items": [
        {"type": "label", "text": "Item 1"},
        {"type": "label", "text": "Item 2"},
        {"type": "label", "text": "Item 3"}
      ]
    }]
  }
}
2. Элементы — Описываем макет каждого элементы прокрутки

Теперь мы понимаем, как работают представления верхнего уровня, перейдём к items. Каждая секция может состоять из множества прокручиваемых элементов, items. Помните, что каждый элемент имеет фиксированный размер, и внутри самого элемента нет других прокручиваемых элементов.

Элемент может быть:

  • Единичным компонентом типа label (метка), image (картинка), button (кнопка), textarea (текстовое поле) и т.д.
  • Комбинацией любых этих элементов

К счастью, iOS и Android имеют похожие системы построения представлений, UIStackView и LinearLayout, соответственно. И эти системы в свою очередь похожи на CSS Flexbox, что облегчает нам работу. И вдобавок к этому, нативные системы представлений бесконечно компонуемы — как показано ниже, вы можете создать вертикальный макет, горизонтальный макет, а также скомпоновать и горизонтальный и вертикальный в одном, и так до бесконечности.

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON

Разработка кросс-платформенного мобильного приложения на голом JSON

Для создания вертикального макета, выставим type как vertical, затем настроим остальные компоненты:


{
  "items": [{
    "type": "vertical",
    "components": [
      {
        "type": "label",
        "text": "First"
      }, 
      {
        "type": "label",
        "text": "Second"
      }, 
      {
        "type": "label",
        "text": "Third"
      }
    ]
  }]
}

Здесь то же самое. Просто установим type в horizontal:


{
  "items": [{
    "type": "horizontal",
    "components": [
      {
        "type": "image",
        "url": "http://i.giphy.com/LXONhtCmN32YU.gif"
      }, 
      {
        "type": "label",
        "text": "Rick"
      }
    ]
  }]
}

Встроить один макет в другой так же просто:


{
  "items": [{
    "type": "horizontal",
    "components": [
      {
        "type": "image",
        "url": "http://i.giphy.com/LXONhtCmN32YU.gif"
      }, 
      {
        "type": "vertical",
        "components": [{
          "type": "label",
          "text": "User"
        }, {
          "type": "label",
          "text": "Rick"
        }]
      }
    ]
  }]
}

Чтобы не усложнять понимание, я пока не упоминал про стилизование элементов приложения, но это делается очень просто. Всё, что вам нужно для этого, это добавить объект style с описанием атрибутов font (шрифт), size (размер), width (ширина), height (высота), color (цвет), background (фон), corner_radius (угловой радиус), opacity (прозрачность) и т.п.

3. Слои — абсолютное позиционирование

Иногда вам может понадобиться разместить элементы в определённой части экрана без прокрутки и перемещений. Jasonette поддерживает такое размещение в layers.

На текущий момент в слое можно разместить только два типа дочерних объектов: image и label. Вы можете разместить их в любой части экрана. Ниже пример этого:

Разработка кросс-платформенного мобильного приложения на голом JSON

В этом примере у нас созданы две метки (для температуры и состояния погоды) и картинка (иконка камеры). Они размещены в своих координатах и не двигаются:


{
  "$jason": {
    "body": {
      "style": {
        "background": "camera"
      },
      "layers": [
        {
          "type": "label",
          "text": "22°C",
          "style": {
            "font": "HelveticaNeue-Light",
            "size": "20",
            "top": "50",
            "left": "50%-100",
            "width": "200",
            "align": "center"
          }
        },
        {
          "type": "label",
          "text": "few clouds",
          "style": {
            "font": "HelveticaNeue",
            "size": "15"
          }
        },
        {
          "type": "image",
          "url": "https://s3.amazonaws.com/.../camera%402x.png",
          "style": {
            "bottom": "100",
            "width": "30",
            "color": "#ffffff",
            "right": "30"
          }
        }
      ]
    }
  }
}

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

Вот ещё примеры того, что построено в Jasonette:

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON
За пределами представлений

Прочитав всё это, вы можете подумать:

  • «Ух ты, круто! Я должен это попробовать!» или
  • «Да, я конечно могу сделать приложение на поиграться, но реальное — никогда!»

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

Вы можете соединить действия с элементами UI и они сработают, как только пользователь кликнет на них. Также вы можете вызывать действия одно за другим, основываясь на возвращаемых ими успехах/ошибках. А можно даже автоматически вызывать действия на основе происходящих событий с устройством или приложением.

Что ещё возможно?

Вам обязательно нужен сервер, хранящий JSON, а в остальном Jasonette совершенно автономна. И этот JSON может прилетать отовсюду: с локального устройства, с удаленных серверов, да хоть с raspberry pi!

  1. У вас есть веб-приложение? Тогда вы с лёгкостью построите мобильное приложение, вызывая серверный API
  2. Вам можно вообще не думать о сервере. Храните JSON файл на Pastebin или Github!
  3. Сконвертируйте любой веб-сайт в приложение. В Jasonette есть мощный парсер HTML-в-JSON на базе библиотеки cheerio, которая позволяет преобразовать любой HTML в объект JSON. Ну и, конечно, вы можете сами сформировать нужный JSON.

Ещё немного примеров:

Разработка кросс-платформенного мобильного приложения на голом JSON

Фото-приложение, делающее снимок камерой устройства и выкладывающая его в S3, затем она создаёт запись в ленте новостей на своём сервере:

Разработка кросс-платформенного мобильного приложения на голом JSON

Приложение Eliza Chatbot для iOS и Android на базе Node.js:

Приложение для микро-блоггинга:

Разработка кросс-платформенного мобильного приложения на голом JSON
Разработка кросс-платформенного мобильного приложения на голом JSON

Приложение, конвертирующее сайт HTML в JSON структуру, а затем в мобильное приложение:

Разработка кросс-платформенного мобильного приложения на голом JSON
Заключение

Jasonette это пока молодой проект. Версия для iOS вышла в конце 2016 г, а для Android ещё немного позже.

Но уже сейчас у неё есть огромное сообщество разработчиков и она активно развивается.

Звучит круто? Тогда Jasonette будет очень рад вам!

Источник

Создание кроссплатформенного плеера для SoundCloud® с Fuse

Создание кроссплатформенного плеера для SoundCloud® с Fuse

Мы постоянно получаем запросы от наших пользователей, которые хотят увидеть, как выглядят “реальные программы”, сделанные с Fuse. Наш учебник специально предназначен для быстрого начала работы с Fuse, но при этом он пока не содержит описаний сложных задач.

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

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

Как мы видим, SoundCloud® имеет всё необходимое:

  • Они предоставляют REST API (и он бесплатен)
  • Есть много контента для работы (картинки и музыка)
  • Нашему приложению понадобятся нативные компоненты (контролы для управления музыкой и т.п.)

Перед тем, как начать…

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

Внимание: приложение FuseCloud это неофициальный плеер для SoundCloud, и никаким образом не связано с SoundCloud. Оно просто использует SoundCloud API.

Реализация

Есть три главных подзадачи: пользовательский интерфейс, написанный на UX и JavaScript; обёртка вокруг SoundCloud REST API; нативный музыкальный плеер.

Навигация

Я использовал компоненты Router и Navigator для построения большей части навигации, с единственным исключением для PageControl в главном представлении, в котором вы можете переключаться между тремя табами (лента новостей, поиск, избранное). После создания каждой страницы приложения отдельным компонентом, навигация будет примерно такой:

// fusecloudnavigationstructure.ux
<Navigator DefaultTemplate="main">
    
    <FuseCloud.MainPage ux:Name="main">
        <PageControl ux:Name="pageControl" Active="searchPage">
            <Page ux:Name="newsFeedPage" />
            <Page ux:Name="searchPage" />
            <Page ux:Name="favoritesPage" />
        </PageControl>
    </FuseCloud.MainPage>
    
    <FuseCloud.CommentsPage ux:Template="comments" router="router" />
    
    <FuseCloud.TrackDetailsPage ux:Name="track" router="router"/>
    
</Navigator>
Бесконечная прокрутка

Благодаря новым возможностям Fuse, я легко создал плавную бесконечную прокрутку для отображения всех комментариев к каждой композиции. Вставляя отдельные комментарии в блок Deferred, я защищён от глюков при автоматической подгрузке новых элементов. Ниже вы можете увидеть пример UX-кода для создания такой прокрутки:

// fusecloudendlessscroller.ux
<ScrollView ClipToBounds="False">
    <StackPanel>
        <Each Items="{comments}">
            <Deferred>
                <FuseCloud.DividerLine Alignment="Top"/>
                <FuseCloud.Comment ux:Name="comment" ThumbnailUrl="{avatar_url}" Username="{username}" Body="{body}" />
            </Deferred>
        </Each>
    </StackPanel>
    <Scrolled To="End" Within="100">
        <Callback Handler="{showMoreComments}" />
    </Scrolled>
</ScrollView>

Прокрутка на 100 точек от нижнего края приложения вызывает JavaScript-функцию, которая подгружает следующие комментарии:

// fusecloudendlessscroller.js
function showMoreComments() {
    if (nCommentsShowing < allComments.length) {
        nCommentsShowing += nCommentsPerPage;
        while (comments.length < nCommentsShowing && comments.length < allComments.length - 1) {
            comments.add(allComments.getAt(comments.length));
        }
    }
}

Замутнение фона

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

// fusecloudscaledblur.ux
<FuseCloud.AlbumArt Width="20%" Height="20%">
    <Blur Radius="2"/>
    <Scaling Factor="5" />
</FuseCloud.AlbumArt>

 

Здесь я уменьшаю размер изображения до 20% от оригинального и замутняю уменьшенное изображение, затем увеличиваю получившееся изображение до нормального размера.

Создание кроссплатформенного плеера для SoundCloud® с Fuse

Работаем с SoundCloud API

Вообще это несложная задача и в ней нет никаких специфичных для Fuse техник. Я структурировал обёртку, поэтому каждый запрос возвращает promise. Модель приложения была использована как интерфейс к API через набор функций-геттеров, возвращавших promise-ы в Observable. Код ниже наглядно иллюстрирует этот подход:

Функция, используемая для получения статуса лайка для трека, возвращает promise:

// fusecloudislikingtrackfetch.js
function isLikingTrack(trackId) {
    return Auth.getAccessToken()
        .then(function(token) {
            return FuseCloudGet("me/favorites/" + trackId, {}, token);
        });
}

 

Модель превращает этот promise в observable, используя удобную функцию (DelayedObservable):

// fusecloudislikingtrackobservable.js
function GetIsLikingTrack(trackId) {
    return DelayedObservable(function(obs) {
        FuseCloud.isLikingTrack(trackId)
            .then(function(result) {
                obs.add(result);
            });
    });
}

 

Это реально удобно, учитывая что возвращённый observable будет заполнен сразу при получении данных. Затем мы можем сделать привязку к нему и не беспокоиться об обратных вызовах или обновлениях интерфейса приложения.

Функция DelayedObservable работает как мост между API, основанной на promise-ах, и API, основанной на Observable:

// fuseclouddelayedobservable.js
function DelayedObservable(getter) {
    var ret = Observable();
    getter(ret);
    return ret;
}

 

Эта функция отвечает за обновление Observable при загрузке данных.

OAuth 2.0

SoundCloud API позволяет авторизоваться с помощью протокола OAuth 2.0. Используя модуль InterApp, я легко перекидываю пользователя на авторизацию с помощью нативного браузера:

// fusecloudlaunchuri.js
var uri = "https://soundcloud.com/connect?client_id=" + clientId
        + "&display=popup"
        + "&response_type=code"
        + "&redirect_uri=fuse-soundcloud://fuse";
InterApp.launchUri(uri);

 

В URL выше передаётся URI обратного вызова, который SoundCloud использует для возврата токена. Fuse позволяет зарегистрировать свою URI-схему в файле проекта:

// fusecloudcustomurischeme.json
"Mobile":{
    "UriScheme": "fuse-soundcloud"
}

 

Таким образом, SoundCloud API автоматически вернётся в нашу программу, как только токен доступа будет готов.

Создание кроссплатформенного аудиоплеера

Реализация обёртки для нативных плееров была наиболее интересной частью процесса. Частично из-за того, что API для этого различны у Android и iOS, но также по причине монолитной природы медиа-плееров. Я начал с минимального набора требований.

Наш StreamingPlayer должен:

  • Транслировать аудио по URL
  • Продолжать играть, когда приложение уходит в фон
  • Позволять переключаться между треками, пока приложение находится в фоне (используя нативные контролы на экране блокировки)
  • Отображать обложку альбома на экране блокировки

Создание кроссплатформенного плеера для SoundCloud® с Fuse

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

Прежде всего, подключение к нативному аудиоплееру для проигрывания URL было суперпростым. API у Android MediaPlayer-а и AVPlayer у iOS предлагают это прямо из коробки. Моей начальной задумкой было использовать минимальную обёртку вокруг обоих этих API и просто делать остальную работу (типа управления плейлистами и состоянием) в JavaScript. Но ограничение на фоновое выполнение JS на этих платформах поставило крест на этом (одно из наших требований — возможность использовать контролы на экране блокировки).

Это означало, что я должен реализовать работу с плейлистами в нативном коде, при этом учитывая особенности Android и iOS. К счастью, всё оказалось гораздо проще, так как возможности внешнего кода Fuse позволяют вам легко интегрировать код на Java и Objective-C в проекты Fuse. Это очень удобно!

Другой интересной задачей было получение текущего состояния плеера для обоих компонентов, MediaPlayer и AVPlayer. Оба этих API имеют разные модели состояния и разные пути управления ими, но я нашёл универсальный способ.

И, наконец, работа с экраном блокировки. В iOS это крайне просто; достаточно зарегистрировать несколько системных вызовов. В Android же это не такая простая задача. В API, начиная с уровня 21, Android может получать медиа-нотификации, которые замещают обычные контролы на экране блокировки. Но нужно копать в сторону системы intent-ов для настройки коммуникации между нотификациями и фоновой службой.

Возможности

В приложение FuseCloud встроены очень большие возможности и механизмы, и так как я люблю перечисления, вот небольшой список фич, заложенных в этой программе (и исходном коде):

  • Аутентификация в SoundCloud® по протоколу OAuth 2.0
  • Использование пакета InterApp для запуска url во внешнем браузере и передача отклика по URI
  • Автоматическое обновление некорректных токенов
  • Получение данных по REST API
  • Лента новостей, поиск треков, избранное
  • Возможность поставить лайк и дизлайк треку
  • Обложки треков
  • Отображение комментариев к треку
  • Размещение комментариев
  • Статистика пользователя
  • Смахивание влево/вправо для переключения дорожки
  • Потягивание экрана для обновления
  • Бесконечный список прокрутки
  • Смахивание для показа действий с элементом (дизлайк в избранном)
  • Сохранение состояния UI с использованием Storage API (приветственная информация показывается только один раз при начале работы с программой)®
  • HTTP Audio StreamingPlayer для iOS и Android
  • Трансляция музыки из SoundCloud®
  • Настраиваемая панель перемотки
  • Фоновое проигрывание
  • Контролы на экране блокировки в iOS и Android
  • iOS: следующий, предыдущий, играть/пауза, перемотка на экране блокировки
  • Обложка альбома на экране блокировки
  • Нотификации в Android: следующий, предыдущий, играть/пауза
  • Показ обложки альбома в нотификации и в фоне
  • Плейлисты
  • Автопроигрывание следующего при окончании трека

Выводы и загрузки

Было реально классно работать над этим проектом. Я необъективен, но Fuse реально впечатлила меня  — в который раз.

И механизмы внешнего кода Fuse оказались действительно хорошим способом создания нативных компонентов. Они позволяют использовать документацию к API каждой платформы на своём языке, где это возможно, и без обёрток на JavaScript.

Вы можете скачать приложение FuseCloud для Android и iOS в Apple App Store, в Google Play, и исходный код на Github.

Внимание: ещё раз о создании “реальной” программы (с нативными компонентами и интеграциями с бекендом) — вы почти наверняка столкнетесь с некоторыми трудностями. Мы постоянно улучшаем нашу документацию, но если всё-таки встретите такой случай, дайте знать об этом нам и сообществу, и мы с радостью вам поможем 🙂

Узнать больше о Fuse можно посмотрев постоянно растущий список примеров (с исходным кодом, конечно), вступайте в наше сообщество (у нас есть классный форум и группа в Slack) или подписывайтесь на нас в Twitter или Facebook.

Автор оригинала Kristian Hasselknippe, Software Engineer at Fuse

Adblock detector