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

Про Flutter, коротко: Основи
19 хв. читання
24 листопада 2019

Більшість статей по цій темі добре написані і не є складними. Проблема полягає в тому, що більшість з них передбачають наявність знань  які вважаються базовими основами, які, тим не менш, не згадуються в інших статтях що описують основи. У даній серії статей я хочу виправити цю ситуацію. Почнемо з нуля і, не залишаючи нічого з цього без уваги, запустимо одне або кілька застосунків. В процесі навчимося будемо користуватися усіма основними компонентами, створювати унікальний інтерфейс, працювати з рідними модулями, і, звичайно ж, збирати свій застосунок під обидві платформи.

Писати я буду з перспективи веб-розробника. Більшість з вас швидше за все знайома зі стеком веба, а аналогія зі знайомою платформою краще аналогії з будівництвом будинків або чого там ще, Animal, Dog, Foo Bar...

Викладати постараюся коротко, щоб не затягувати. А для найдопитливіших буду залишати корисні посилання з обговорюваних тем.

Про платформу

Flutter — молода, але дуже перспективна платформа, вже привернула до себе увагу великих компаній, які запустили свої програми. Цікава ця платформа своєю простотою в порівнянні з розробкою веб-застосунків, і швидкістю роботи на рівні з рідними додатками. Висока продуктивність програми та швидкість розробки досягається за рахунок декількох технік:

  • На відміну від багатьох відомих на сьогоднішній день мобільних платформ, Flutter не використовує JavaScript ні в якому вигляді. В якості мови програмування для Flutter вибрали Dart, який компілюється в бінарний код, за рахунок чого досягається швидкість виконання операцій порівнянна з Objective-C, Swift, Java, або Kotlin.
  • Flutter не використовує рідні компоненти, знову ж, ні в якому вигляді, тому не доводиться писати ніяких прошарків для комунікації з ними. Замість цього, подібно ігровим рушіям (а ви ж знаєте що UI ігор дуже динамічний ), він малює весь інтерфейс самостійно. Кнопки, текст, медіа-елементи, фон — все це малюється всередині графічного рушія в самому Flutter. Після вищесказаного варто відзначити, що "Hello World" додаток на Flutter займає зовсім небагато місця: iOS ≈ 2.5 Mb і Android ≈ 4Mb.
  • Для побудови UI під Flutter використовується декларативний підхід, натхненний веб-фреймворком ReactJS, на основі віджетів (в світі веба іменованих компонентами). Для ще більшого приросту в швидкості роботи інтерфейсу віджети перемальовуються за необхідності — тільки коли в них щось змінилося (подібно до того як це робить Virtual DOM в світі веб-фронтенда).
  • На додаток до всього, в фреймворк вбудований Hot-reload, такий звичний для вебу, і досі відсутній в рідних платформах.

Про практичну користь цих факторів я дуже рекомендую прочитати статтю Android розробника, який переписав свій застосунок Java на Dart і який поділився своїми враженнями. Сюди я лише винесу названу ним кількість файлів/рядків коду до (написаний на Java) — 179/12176, і після (переписане на Dart) — 31/1735. В документації можна знайти докладний опис технічних особливостей платформи. А ось ще посилання, якщо цікаво подивитися інші приклади працюючих застосунків.

Про Dart

Dart — мова програмування, на якій нам необхідно писати програми під Flutter. Вона дуже проста, і якщо у вас є досвід роботи з Java або JavaScript, ви швидко освоїте її.

Про підготовку

Ця тема, як і Dart, дуже добре описана в офіційному керівництві. 

Нічого не чекаючи, йдемо на сторінку керівництва по установці, вибираємо платформу і по кроках виконуємо інструкцію для установки платформи на нашу систему. У своєму редакторі обов'язково підключаємо плагіни. У тому ж керівництві є інструкція по налаштуванню VS Code і IntelliJ. Для вашого редактора теж знайдуться плагіни для Dart і Flutter (зазвичай потрібно ставити два). Запускаємо застосунок і перевіряємо його працездатність.

Підказка для користувачів OSX. Мені шкода місця що займається намальованими рамками телефону в емуляторі iOS, тому я їх відключив і переключився на iPhone 8 (він не такий "довгий"):

  • Hardware → Device → iOS # → iPhone 8
  • Window → Show Device Bezels

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

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

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

Про структуру

Зайдемо в папку зі згенерованим застосунком і розберемося що у нас там є. Не з усім, але з потрібним:

  • lib/ — За принципами pub (менеджер пакетів Dart'а) весь код лежить в цій папці;
  • pubspec.yml — сюди записуються залежності програми, які потрібно встановити для його запуску, точно як package.json, але є нюанс, встановлювати їх потрібно не через стандартну утиліту Dart'а, про яку йшлося вище, а через команду Flutter'а: flutter pub get ;
  • test/ — ви ж знаєте, що там? Запустити їх можна викликавши flutter test;
  • ios/ & android/ — папки з налаштуваннями для кожної з платформ, там вказується які права потрібні для запуску програми (доступ до локації, bluetooth), иконочки і все що специфічно для платформи.

Зі структурою розібралися, заходимо в теку lib/ де нас чекає main.dart файл. Це, як ви можете здогадатися, той самий файл в якому ми повинні запускати наш застосунок. А запускається він подібно як у мові C (і ще тонни інших) викликом функції main().

Про віджети (Hello World тут)

У Flutter'е все побудовано на Widget'ах: тут і в'юшки, та стилі з темами, і стан у віджетах зберігається. Є два основних типи віджетів: зі стейтом і без, але поки що не про це. Давайте з простого.

Видаляємо все з main.dart. Вставляємо наступний код уважно вчитуючись в коментарі:

import 'package:flutter/widgets.dart'; // підключаємо базовий набір віджетів

// Коли Dart запускає він викликає функцію main()
main() => runApp( // а функція runApp запускає Flutter
    Text( // цей віджет, він малює текст, такий собі  
        'Hello, World!!!', // перший аргумент — текст, який потрібно відобразити
        textDirection: TextDirection.ltr, // а тут ми вказуємо напрямок тексту
    ),
);

runApp(...) приймає єдиний аргумент — віджет, який буде кореневим для всього проекту. До речі, його зміни Hot-reload підхопити не може, так що потрібно буде перезапускати програму.
Text(...) — Flutter не може просто відобразити рядок на екрані. Для виводу тексту необхідно вказати Text. textDirection. І це не вирівнювання тексту на зразок text-align, якщо порівнювати з вебом, то це аналог direction. Частина API для інтернаціоналізації програми. Text не запрацює, поки не буде знати напрямок, але вказувати його скрізь не доведеться — далі ми розберемо як налаштувати напрямок тексту для всієї програми.

Вже запустили програму? "Hello, World!" вивівся! Начебто... Так? Але щось явно пішло не так.

Скріншот запущеного додатка

Текст перекритий системною інформацією. У нашому розпорядженні весь простір екрану, і ми вивели віджет в самому його початку, де в тому числі виводиться системна інформація. Давайте спробуємо кудись посунути наш текст.

import 'package:flutter/widgets.dart';

main() => runApp(
    Center( // віджет, який вирівнює вміст по центру
        child: Text(
            'Hello, World!',
            textDirection: TextDirection.ltr,
        ),
    ),
);

Center(...) — це віджет, який дозволяє розмістити інший віджет, переданий у рядку child, у центрі по горизонталі і вертикалі. Ви часто будете зустрічати child та children в додатках Flutter, так як практично всі віджети використовують ці імена для передачі віджетів, які повинні бути намальовані всередині віджета що викликається.

Композиції віджетів використовуються в Flutter для відтворення UI, зміни зовнішнього вигляду, і навіть для передачі даних. Приміром віджет Directionality(...) визначає напрямок тексту для всіх дочірніх віджетів:

import 'package:flutter/widgets.dart';

main() => runApp(
  Directionality(
    textDirection: TextDirection.ltr,
    child: Center(
      child: Text('Hello, World!'),
    ),
  ),
);

Подивимося на ще один дуже важливий віджет і заодно перетворимо зовнішній вигляд нашої програми:

import 'package:flutter/widgets.dart';

main() => runApp(
  Directionality(
    textDirection: TextDirection.ltr,
    child: Container( // новий віджет!
у світі Flutter'а // Для віджета Container значення color означає колір фона color: Color(0xFF444444), child: Center( child: Text( 'Hello, World!', style: TextStyle( // а в тексті з'явився віджет який його стилізує color: Color(0xFFFD620A), // задаємо йому колір тексту fontSize: 32.0, // і розмір шрифта ), ), ), ), ), );

Скріншот HelloWorld додатки


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

TextStyle(...) — ще цікавіший віджет, з його допомогою можна задати колір, розмір, товщину, міжрядковий інтервал, додати підкреслення та інше.

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

Про Stateless віджети

Як використовувати віджети — ми розібралися, тепер давайте розбиратися як їх створювати. Вище вже згадувалося, що є віджети у яких є стан, і у яких його немає. Досі ми використовували тільки віджети без стану. Це не означає, що його зовсім немає, адже віджети це просто класи, і їх властивості можуть бути змінені. Просто після того, як віджет буде намальований — зміни його стану не призведуть до оновлення цього віджета у UI. Наприклад, якщо нам потрібно змінити текст на екрані, потрібно буде створити інший віджет Text і вказати новий вміст який ми хочемо відобразити. Такі віджети можна назвати константними, якщо ви розумієте про що я. І вони прості, тому з них і почнемо.

Щоб створити Stateless віджет, потрібно:

  1. Придумати гарне ім'я для нового класу;
  2. Успадкувати клас від StatelessWidget;
  3. Реалізувати метод build(), який бере BuildContext як аргумент і повертає який-небудь Widget.
import 'package:flutter/widgets.dart';

main() => runApp(
  Directionality(
    textDirection: TextDirection.ltr,
    child: Center(
      child: MyStatelessWidget()
    ),
  ),
);

class MyStatelessWidget extends StatelessWidget {
// анотація @override потрібна для оптимізації, буде описано пізніше,
// що перевизначений метод батьківського класу ми використовувати
// не будемо, тому компілятор може викинути його
  @override
  Widget build(BuildContext context) { // [context] буде описаний пізніше
    return Text('Hello!');
  }
}

Приклад віджета з одним аргументом:

class MyStatelessWidget extends StatelessWidget {
  // Всі властивості Stateless віджета повинні бути оголошені з final, або з const
  final String name; // звичайна властивість
  MyStatelessWidget(this.name); // звичайний конструктор

  @override
  Widget build(BuildContext context) { // [context] буде описаний ще нижче
    return Text('Hello, $name!');
  }
}

Про Stateless більше і додати нічого...

Про Hot Reload

Зверніть увагу, що при зміні вмісту нашого віджету застосунок буде автоматично перемальовуватися. Після того, як ми винесли віджет з функції main() Hot-reload став нам допомагати.

Важливо також розуміти, що через запущений модуль для гарячої заміни застосунок працює на порядок повільніше.

Про GestureDetector

GestureDetector віджет в дії

В наступній секції ми будемо розбиратися зі StatefulWidget (з віджетами які змінюються при зміні їх стану). Для того щоб це було цікаво, нам потрібно цей стан якось змінювати, згодні? Ми будемо змінювати стан віджета реагуючи на дотики по екрану. Для цього ми будемо використовувати GestureDetector(...) — віджет, який нічого не малює, але стежить за торканнями на екрані смартфона і повідомляє про це викликаючи передані йому функції.

Створимо кнопку в центрі екрану, при натисканні на яку в консоль буде виводитися повідомлення:

import 'package:flutter/widgets.dart';

main() => runApp(
  Directionality(
    textDirection: TextDirection.ltr,
    child: Container(
      color: Color(0xFFFFFFFF),
      child: App(),
    ),
  ),
);

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector( // використовується як звичайний віджет
        onTap: () { // одне з властивостей GestureDetector
          // Цей метод буде викликаний, коли дочірній елемент буде натиснутий
          print('You pressed me');
        },
        child: Container( // нашою кнопкою буде контейнер
          decoration: BoxDecoration( // стилізуємо контейнер
            shape: BoxShape.circle, // задамо йому круглу форму
            color: Color(0xFF17A2B8), // і пофарбуємо його в синій
          ),
          width: 80.0,
          height: 80.0,
        ),
      ),
    );
  }
}

Натискаємо на синю кнопку і бачимо повідомлення у консолі. Натискаємо ще раз і знову бачимо повідомлення у консолі. 

Про Stateful віджети

StatefulWidget — прості, навіть простіше ніж StatelessWidget'и. Але є нюанс: вони не існують самі по собі, для їх роботи потрібен ще один клас який буде зберігати стан цього віджета. При цьому, його візуальна частина (віджети з яких він складається) також стають його станом.

Для початку подивимося на клас віджета:

class Counter extends StatefulWidget {
    // Змінний стан зберігається не в віджеті, а всередині об'єкта особливого класу,
    // створюваного методом createState()
    @override
    State createState() => _CounterState();
    // Результатом функції є не просто об'єкт класу State,
    // а обов'язково State runApp(App());

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Directionality(
      textDirection: TextDirection.ltr,
      child: Container(
        padding: EdgeInsets.symmetric(
          vertical: 60.0,
          horizontal: 20.0,
        ),
        color: Color(0xFFFFFFFF),
        child: Content(),
      ),
    );
  }
}

class Content extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Counter('Manchester United'),
        Counter('Juventus'),
      ],
    );
  }
}

class Counter extends StatefulWidget {
  final String _name;
  Counter(this._name);

  @override
  State createState() => _CounterState();
}

class _CounterState extends State {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(bottom: 10.0),
      padding: EdgeInsets.all(4.0),
      decoration: BoxDecoration(
        border: Border.all(color: Color(0xFFFD6A02)),
        borderRadius: BorderRadius.circular(4.0),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          // widget — це властивість класу State в якому зберігається 
          // посилання на об'єкт що створим поточний state, тобто наш віджет
          _CounterLabel(widget._name),
          _CounterButton(
            count,
            onPressed: () {
              setState(() {
                ++count;
              });
            },
          ),
        ],
      ),
    );
  }
}

class _CounterLabel extends StatelessWidget {
  static const textStyle = TextStyle(
    color: Color(0xFF000000),
    fontSize: 26.0,
  );

  final String _label;
  _CounterLabel(this._label);

  @override
  Widget build(BuildContext context) {
    return Text(
      _label,
      style: _CounterLabel.textStyle,
    );
  }
}

class _CounterButton extends StatelessWidget {
  final _count;
  final _onPressed;
  _CounterButton(this._count, {@required this._onPressed});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        _onPressed();
      },
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 6.0),
        decoration: BoxDecoration(
          color: Color(0xFFFD6A02),
          borderRadius: BorderRadius.circular(4.0),
        ),
        child: Center(
          child: Text(
            '$_count',
            style: TextStyle(fontSize: 20.0),
          ),
        ),
      ),
    );
  }
}

Ще одне Counter додаток на Flutter

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

Про домашнє завдання

Якщо вам хочеться почитати що-небудь ще на дозвіллі, ось список цікавих посилань:

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація