Введение в виджеты

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

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

Hello world

Минимальное приложение Flutter просто вызывает функцию runApp() с виджетами:

import 'package:flutter/material.dart';

void main() {
  runApp(
    Center(
      child: Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}

Функция runApp() принимает данный виджет и делает его корнем дерева виджетов. В данном примере дерево виджетов состоит из двух виджетов — Center widget и его дочернего виджета Text widget. Фреймворк заставляет корневой виджет перекрывать экран, что означает, что текст » Hello, world » заканчивается по центру экрана. В данном случае необходимо указать направление текста; при использовании виджета MaterialApp об этом позаботятся, как будет показано ниже.

При написании приложения вы обычно создаете новые виджеты, которые являются подклассами либо StatelessWidget, либо StatefulWidget, в зависимости от того, управляет ли ваш виджет каким-либо состоянием. Основная работа виджета заключается в реализации функции build(), которая описывает виджет в терминах других виджетов нижнего уровня. Фреймворк строит эти виджеты по очереди до тех пор, пока процесс не завершится виджетами, представляющими собой базовый объект RenderObject, который вычисляет и описывает геометрию виджета.

Основные виджеты

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

Text
Виджет «Text» позволяет создавать стилизованный текст внутри приложения.

Row, Column
Эти гибкие виджеты позволяют создавать различные компоновки как в горизонтальном (Row), так и в вертикальном направлении (Column). Дизайн этих объектов основан на модели размещения флексбоксов в веб-приложениях.

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

Ниже приведены некоторые простые виджеты, которые сочетают в себе эти и другие виджеты:

import 'package:flutter/material.dart';

class MyAppBar extends StatelessWidget {
  MyAppBar({this.title});

  // Поля подкласса виджетов всегда помечены как " final " (окончательные).

  final Widget title;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 56.0, // в логических пикселях
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      decoration: BoxDecoration(color: Colors.blue[500]),
      // Row - это горизонтальная, линейная компоновка.
      child: Row(
        // <Widget> - это тип элементов в списке.
        children: <Widget>[
          IconButton(
            icon: Icon(Icons.menu),
            tooltip: 'Navigation menu',
            onPressed: null, // ноль отключает кнопку
          ),
          // Expanded расширяет child, чтобы заполнить имеющееся пространство.
          Expanded(
            child: title,
          ),
          IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
    );
  }
}
class MyScaffold extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Material - это концептуальный лист бумаги, на котором появляется пользовательский интерфейс.
    return Material(
      // Column - это вертикальная, линейная компоновка.
      child: Column(
        children: <Widget>[
          MyAppBar(
            title: Text(
              'Example title',
              style: Theme.of(context).primaryTextTheme.headline6,
            ),
          ),
          Expanded(
            child: Center(
              child: Text('Hello, world!'),
            ),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    title: 'My app', // используемый переключателем задач операционной системы
    home: MyScaffold(),
  ));
}

Убедитесь в том, что в файле pubspec.yaml в разделе flutter присутствует строка use-material-design: true. Это позволяет использовать предопределенный набор иконок Material. Обычно, если вы используете библиотеку Materials (Материалы), хорошо бы включить эту строку:

name: my_app
flutter:
  uses-material-design: true

Многие виджеты Material Design должны быть внутри MaterialApp для правильного отображения, чтобы унаследовать данные темы. Поэтому запустите приложение с MaterialApp.

Виджет MyAppBar создает Container высотой 56 не зависящих от устройства пикселей с внутренним отступом 8 пикселей, как слева, так и справа. Внутри контейнера MyAppBar использует компоновку Row для организации children. Средний child, виджет заголовка title, помечен как Expanded, что означает, что он расширяется, чтобы заполнить любое оставшееся свободное место, которое не было занято другими children. Вы можете иметь несколько дочерних процессов Expanded и определить соотношение, в котором они потребляют доступное пространство, используя аргумент flex для Expanded.

Виджет MyScaffold организует своих children в вертикальном столбце. В верхней части колонки он помещает экземпляр MyAppBar, передавая в панель приложения текстовый виджет Text для использования в качестве заголовка. Передача виджетов в качестве аргументов другим виджетам является мощной техникой, позволяющей создавать общие виджеты, которые могут быть повторно использованы самыми разными способами. Наконец, MyScaffold использует Expanded, чтобы заполнить оставшееся пространство своим телом, которое состоит из центрированного сообщения.

Дополнительную информацию см. в разделе Разметки.

Использование Material компонентов

Flutter предоставляет ряд виджетов, которые помогают вам создавать приложения, которые следуют из Material Design. Приложение «Material» начинается с виджета MaterialApp, который строит ряд полезных виджетов в корне приложения, в том числе Navigator, который управляет пакетом виджетов, идентифицируемых по строкам, также известным как маршруты «routes». Navigator позволяет плавно переключаться между экранами вашего приложения. Использование виджета MaterialApp является полностью необязательным, но является хорошей практикой.

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Flutter Tutorial',
    home: TutorialHome(),
  ));
}

class TutorialHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Scaffold является разметкой для основных компонентов Material.
    return Scaffold(
      appBar: AppBar(
        leading: IconButton(
          icon: Icon(Icons.menu),
          tooltip: 'Navigation menu',
          onPressed: null,
        ),
        title: Text('Example title'),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
      // body - это большая часть экрана.
      body: Center(
        child: Text('Hello, world!'),
      ),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Add', // используются вспомогательные технологии
        child: Icon(Icons.add),
        onPressed: null,
      ),
    );
  }
}

Теперь, когда код переключился с MyAppBar и MyScaffold на виджеты AppBar и Scaffold, а также с material.dart, приложение начинает немного больше походить на Material. Например, в панели приложения есть тень, а заголовочный текст автоматически наследует правильный стиль. Также добавляется плавающая кнопка действия.

https://api.flutter.dev/flutter/material/AppBar-class.html#titleОбратите внимание, что виджеты передаются в качестве аргументов другим виджетам. Виджет Scaffold принимает в качестве аргументов несколько различных виджетов, каждый из которых помещается в макет Scaffold в соответствующем месте. Аналогично, виджет AppBar позволяет передавать виджеты для ведущего виджета, а также действия заголовочного виджета title. Этот шаблон повторяется по всему фреймворку и может быть учтен при проектировании собственных виджетов.

Дополнительные сведения см. в разделе виджетов Material Components.

Примечание: Material является одним из 2-х комплектов дизайна, поставляемых с Flutter. Для создания дизайна, ориентированного на iOS, см. пакет компонентов Cupertino, имеющий собственные версии CupertinoApp, и CupertinoNavigationBar.

Устранение неполадок

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

class MyButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        print('MyButton was tapped!');
      },
      child: Container(
        height: 36.0,
        padding: const EdgeInsets.all(8.0),
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(5.0),
          color: Colors.lightGreen[500],
        ),
        child: Center(
          child: Text('Engage'),
        ),
      ),
    );
  }
}

Виджет GestureDetector не имеет визуального представления, а вместо этого обнаруживает жесты, сделанные пользователем. Когда пользователь нажимает на контейнер Container, GestureDetector вызывает его обратный вызов onTap(), в данном случае выводит сообщение на консоль. GestureDetector можно использовать для обнаружения различных входных жестов, включая касания, перетаскивание и масштабирование.

Многие виджеты используют GestureDetector для обеспечения дополнительных обратных вызовов для других виджетов. Например, виджеты IconButton, RaisedButton и FloatingActionButton имеют обратные вызовы onPressed(), которые срабатывают при нажатии пользователем виджета.

Для получения дополнительной информации см. раздел «Жесты в Flutter».

Изменение виджетов в ответ на ввод

До сих пор на этой странице использовались только stateless виджеты. Stateless виджеты получают аргументы от своего родительского виджета, которые они хранят в конечных членах-переменных. Когда виджету предлагается собрать build(), он использует эти хранимые значения для получения новых аргументов для виджетов, которые он создает.

Для того, чтобы построить более сложный опыт, например, чтобы более интересным образом реагировать на пользовательский ввод, приложения, как правило, переносит некоторый state (состояние). Flutter использует StatefulWidgets для захвата этой идеи. StatefulWidgets — это специальные виджеты, которые знают, как генерировать объекты State, которые затем используются для удержания состояния. Рассмотрим этот основной пример, используя RaisedButton, упомянутый выше:

class Counter extends StatefulWidget {
    // Этот класс является конфигурацией состояния.
    // В нем хранятся значения (в данном случае ничего), 
    // предоставленные родителем и используемые 
    // методом сборки build состояния State. 
    // Поля в подклассе Виджетов всегда помечены как final (конечные).

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

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      // Этот вызов для setState говорит фреймворку Flutter, что
      // что-то изменилось в этом состоянии State, что заставляет его повторять
      // нижеприведенный метод построения build, чтобы на дисплее можно было отобразить
      // обновленные значения. Если вы измените _counter (счетчик) без вызова
      // setState(), то метод сборки больше не будет вызываться,
      // ...и, похоже, ничего не случится.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // Этот метод повторяется каждый раз при вызове SetState,
    // например, как сделано методом _increment выше.
    // Фреймворк Flutter был оптимизирован для того, чтобы сделать перезапуск
    // build методов быстро, так что вы можете просто перестроить все, что
    // нуждается в обновлении, а не в индивидуальном изменении
    // экземпляров виджетов
    return Row(
      children: <Widget>[
        RaisedButton(
          onPressed: _increment,
          child: Text('Increment'),
        ),
        Text('Count: $_counter'),
      ],
    );
  }
}

Вы можете задаться вопросом, почему StatefulWidget и State являются отдельными объектами. В режиме Flutter эти два типа объектов имеют различные жизненные циклы. Виджеты — это временные объекты, используемые для построения представления приложения в его текущем состоянии. Объекты State, с другой стороны, являются постоянными между вызовами функции build(), позволяя им запоминать информацию.

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

В Flutter, изменение потока уведомлений «поднимается» вверх по иерархии виджетов посредством обратных вызовов, в то время как текущее состояние потоков «спускается» вниз к stateless виджетам, которые делают презентацию. Общим родителем, который перенаправляет этот поток, является State. Следующий немного более сложный пример показывает, как это работает на практике:

class CounterDisplay extends StatelessWidget {
  CounterDisplay({this.count});

  final int count;

  @override
  Widget build(BuildContext context) {
    return Text('Count: $count');
  }
}

class CounterIncrementor extends StatelessWidget {
  CounterIncrementor({this.onPressed});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: onPressed,
      child: Text('Increment'),
    );
  }
}

class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

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

  @override
  Widget build(BuildContext context) {
    return Row(children: <Widget>[
      CounterIncrementor(onPressed: _increment),
      CounterDisplay(count: _counter),
    ]);
  }
}

Обратите внимание на создание двух новых виджетов без содержания, чисто разделяющих задачи отображения счетчика (CounterDisplay) и изменения счетчика (CounterIncrementor). Хотя чистый результат такой же, как и в предыдущем примере, разделение ответственности позволяет инкапсулировать большую сложность в отдельные виджеты, сохраняя при этом простоту в родительском интерфейсе.

Для получения дополнительной информации см:

Соединяя все вместе

Далее приводится более полный пример, объединяющий эти понятия: Гипотетическое торговое приложение отображает различные продукты, предлагаемые для продажи, и обслуживает корзину для предполагаемых покупок. Начните с определения класса презентации, ShoppingListItem:

class Product {
  const Product({this.name});
  final String name;
}

typedef void CartChangedCallback(Product product, bool inCart);

class ShoppingListItem extends StatelessWidget {
  ShoppingListItem({this.product, this.inCart, this.onCartChanged})
      : super(key: ObjectKey(product));

  final Product product;
  final bool inCart;
  final CartChangedCallback onCartChanged;

  Color _getColor(BuildContext context) {
    // Тема зависит от BuildContext, потому что разные части
    // дерева могут иметь разные темы.
    // Тема BuildContext указывает на то, где происходит сборка и,
    // следовательно, какую тему использовать.


    return inCart ? Colors.black54 : Theme.of(context).primaryColor;
  }

  TextStyle _getTextStyle(BuildContext context) {
    if (!inCart) return null;

    return TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
      onTap: () {
        onCartChanged(product, inCart);
      },
      leading: CircleAvatar(
        backgroundColor: _getColor(context),
        child: Text(product.name[0]),
      ),
      title: Text(product.name, style: _getTextStyle(context)),
    );
  }
}

Виджет ShoppingListItem следует общему шаблону для stateless виджетов. Он хранит полученные значения в своем конструкторе в конечных переменных-членов, которые затем использует в своей функции build(). Например, булевы inCart переключаются между двумя визуальными эффектами: один использует основной цвет из текущей темы, а другой — серый.

Когда пользователь нажимает на элемент списка, виджет не изменяет свое значение inCart напрямую. Вместо этого виджет вызывает функцию onCartChanged, полученную от родительского виджета. Этот паттерн позволяет хранить состояние выше в иерархии виджетов, что приводит к более длительному сохранению состояния. В крайнем случае, состояние, сохраненное на виджете, переданном в runApp(), сохраняется на протяжении всего времени жизни приложения.

Когда родитель получает обратный вызов onCartChanged, родитель обновляет свое внутреннее состояние, что приводит к тому, что родитель перестраивает и создает новый экземпляр ShoppingListItem с новым значением inCart. Хотя родитель создает новый экземпляр ShoppingListItem при пересборке, эта операция обходится дешево, так как фреймворк сравнивает только что построенные виджеты с ранее построенными и применяет только различия к базовому RenderObject.

Приведем пример родительского виджета, который сохраняет измененное состояние:

class ShoppingList extends StatefulWidget {
  ShoppingList({Key key, this.products}) : super(key: key);

  final List<Product> products;

  // Вызовы фреймворка создают createState в первый раз, когда виджет
  // появляется в определенном месте дерева.
  // Если родитель перестраивает и использует тот же тип
  // виджета (с тем же самым ключом), фреймворк повторно использует объект State
  // вместо создания нового State объекта.

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

class _ShoppingListState extends State<ShoppingList> {
  Set<Product> _shoppingCart = Set<Product>();

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {

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

      if (!inCart)
        _shoppingCart.add(product);
      else
        _shoppingCart.remove(product);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Shopping List'),
      ),
      body: ListView(
        padding: EdgeInsets.symmetric(vertical: 8.0),
        children: widget.products.map((Product product) {
          return ShoppingListItem(
            product: product,
            inCart: _shoppingCart.contains(product),
            onCartChanged: _handleCartChanged,
          );
        }).toList(),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    title: 'Shopping App',
    home: ShoppingList(
      products: <Product>[
        Product(name: 'Eggs'),
        Product(name: 'Flour'),
        Product(name: 'Chocolate chips'),
      ],
    ),
  ));
}

Класс ShoppingList extends (расширяет) StatefulWidget, что означает, что этот виджет хранит измененное состояние. Когда виджет ShoppingList впервые вставляется в дерево, фреймворк вызывает функцию createState() для создания свежего экземпляра _ShoppingListState для связи с этим местом в дереве (обратите внимание, что подклассы State обычно именуются с передними подчеркиваниями, чтобы показать, что они являются частными деталями реализации). Когда родительский виджет перестраивается, родитель создает новый экземпляр ShoppingList, но фреймворк использует экземпляр _ShoppingListState, который уже находится в дереве, вместо того, чтобы снова вызывать createState.

Для доступа к свойствам текущего ShoppingList’а _ShoppingListState может использовать свойство виджета. Если родитель перестраивает и создает новый ShoppingList, то _ShoppingListState перестраивается со значением нового виджета. Если вы хотите получать уведомления об изменении свойства виджета, переопределите функцию didUpdateWidget(), которой передается oldWidget, чтобы можно было сравнить старый виджет с текущим.

При обработке обратного вызова onCartChanged, _ShoppingListState изменяет свое внутреннее состояние, добавляя или удаляя продукт из _shoppingCart. Чтобы сигнализировать фреймворку, что он изменил своё внутреннее состояние, он обёртывает эти вызовы в вызов setState(). Вызов setState помечает этот виджет как «грязный» и назначает его перестройку при следующем обновлении экрана приложения. Если вы забыли вызвать setState при изменении внутреннего состояния виджета, то фреймворк не будет знать, что ваш виджет грязный и может не вызвать функцию build() виджета, что означает, что пользовательский интерфейс может не обновиться, чтобы отразить измененное состояние. Управляя состоянием таким образом, нет необходимости писать отдельный код для создания и обновления дочерних виджетов. Вместо этого вы просто реализуете функцию build, которая обрабатывает обе ситуации.

Реагирование на события жизненного цикла виджета

После вызова createState() на StatefulWidget фреймворк вставляет новый объект состояния в дерево, а затем вызывает initState() на объекте состояния. Подкласс State может переопределить initState для выполнения работы, которая должна произойти всего один раз. Например, переопределить initState для настройки анимации или подписки на сервисы платформы. Для начала работы initState необходимо вызвать super.initState.

Когда объект состояния больше не нужен, фреймворк вызывает dispose() на объекте состояния. Для выполнения очистки необходимо переопределить функцию dispose(). Например, переопределить распоряжение, чтобы отменить таймеры или отписаться от сервисов платформы. Введение утилизации обычно завершается вызовом функции super.dispose.

Дополнительные сведения см. в разделе State (состояние).

Ключи

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

Ключи наиболее полезны в виджетах, которые собирают много экземпляров одного и того же типа виджета. Например, виджет ShoppingList, который строит достаточное количество экземпляров ShoppingListItem для заполнения своего видимого региона:

  • Без ключей первая запись в текущем билде всегда синхронизировалась бы с первой записью в предыдущем билде, даже если, по семантике, первая запись в списке просто прокручивается вне экрана и больше не отображается на видовом экране.
  • Назначив каждой записи в списке «семантический» ключ, бесконечный список может быть более эффективным, так как фреймворк синхронизирует записи с соответствующими семантическими ключами и, следовательно, похожими (или идентичными) визуальными эффектами. Более того, синхронизация записей семантически означает, что состояние, сохраненное в дочерних виджетах со статусом, остается привязанным к той же семантической записи, а не к записи, находящейся в той же числовой позиции на видовом экране.

Для получения дополнительной информации смотрите Ключевой API.

Глобальные ключи

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

Для получения более подробной информации смотрите API GlobalKey.