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

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

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

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

Hello World

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

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

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

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

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

TextВиджет Textпозволяет вам создать серию стилизованного текста в вашем приложении.

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

StackВместо линейной ориентации (горизонтальной или вертикальной) Stackвиджет позволяет размещать виджеты друг над другом в порядке рисования. Затем вы можете использовать Positionedвиджет для дочерних элементов, Stackчтобы расположить их относительно верхнего, правого, нижнего или левого края стека. Стеки основаны на модели макета абсолютного позиционирования в Интернете.

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

Ниже приведены несколько простых виджетов, объединяющих эти и другие виджеты:

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

class MyAppBar extends StatelessWidget {
  const MyAppBar({required this.title, Key? key}) : super(key: key);

  // Fields in a Widget subclass are always marked "final".

  final Widget title;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 56.0, // in logical pixels
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      decoration: BoxDecoration(color: Colors.blue[500]),
      // Row is a horizontal, linear layout.
      child: Row(
        // <Widget> is the type of items in the list.
        children: [
          const IconButton(
            icon: Icon(Icons.menu),
            tooltip: 'Navigation menu',
            onPressed: null, // null disables the button
          ),
          // Expanded expands its child
          // to fill the available space.
          Expanded(
            child: title,
          ),
          const IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ),
        ],
      ),
    );
  }
}

class MyScaffold extends StatelessWidget {
  const MyScaffold({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // Material is a conceptual piece
    // of paper on which the UI appears.
    return Material(
      // Column is a vertical, linear layout.
      child: Column(
        children: [
          MyAppBar(
            title: Text(
              'Example title',
              style: Theme.of(context) //
                  .primaryTextTheme
                  .headline6,
            ),
          ),
          const Expanded(
            child: Center(
              child: Text('Hello, world!'),
            ),
          ),
        ],
      ),
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      title: 'My app', // used by the OS task switcher
      home: SafeArea(
        child: MyScaffold(),
      ),
    ),
  );
}
Добавлены виджеты: AppBar, IconButton

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

Примечание. Материал — это один из двух дизайнов, поставляемых вместе с Flutter. Чтобы создать дизайн, ориентированный на iOS, см. пакет компонентов из Купертино , в котором есть собственные версии CupertinoApp, и CupertinoNavigationBar.

Обработка жестов

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

import 'package:flutter/material.dart';

class MyButton extends StatelessWidget {
  const MyButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        print('MyButton was tapped!');
      },
      child: Container(
        height: 50.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: const Center(
          child: Text('Engage'),
        ),
      ),
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: MyButton(),
        ),
      ),
    ),
  );
}

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

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

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

import 'package:flutter/material.dart';

class Counter extends StatefulWidget {
  // This class is the configuration for the state.
  // It holds the values (in this case nothing) provided
  // by the parent and used by the build  method of the
  // State. Fields in a Widget subclass are always marked
  // "final".

  const Counter({Key? key}) : super(key: key);

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

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

  void _increment() {
    setState(() {
      // This call to setState tells the Flutter framework
      // that something has changed in this State, which
      // causes it to rerun the build method below so that
      // the display can reflect the updated values. If you
      // change _counter without calling setState(), then
      // the build method won't be called again, and so
      // nothing would appear to happen.
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called,
    // for instance, as done by the _increment method above.
    // The Flutter framework has been optimized to make
    // rerunning build methods fast, so that you can just
    // rebuild anything that needs updating rather than
    // having to individually changes instances of widgets.
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        ElevatedButton(
          onPressed: _increment,
          child: const Text('Increment'),
        ),
        const SizedBox(width: 16),
        Text('Count: $_counter'),
      ],
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: Counter(),
        ),
      ),
    ),
  );
}
При нажатии на кнопку инкремент увеличивает число на 1

Объединяя все это

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

import 'package:flutter/material.dart';

class Product {
  const Product({required this.name});

  final String name;
}

typedef CartChangedCallback = Function(Product product, bool inCart);

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

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

  Color _getColor(BuildContext context) {
    // The theme depends on the BuildContext because different
    // parts of the tree can have different themes.
    // The BuildContext indicates where the build is
    // taking place and therefore which theme to use.

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

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

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

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

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: Center(
          child: ShoppingListItem(
            product: const Product(name: 'Chips'),
            inCart: true,
            onCartChanged: (product, inCart) {},
          ),
        ),
      ),
    ),
  );
}

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

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

import 'package:flutter/material.dart';

class Product {
  const Product({required this.name});

  final String name;
}

typedef CartChangedCallback = Function(Product product, bool inCart);

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

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

  Color _getColor(BuildContext context) {
    // The theme depends on the BuildContext because different
    // parts of the tree can have different themes.
    // The BuildContext indicates where the build is
    // taking place and therefore which theme to use.

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

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

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

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

  final List<Product> products;

  // The framework calls createState the first time
  // a widget appears at a given location in the tree.
  // If the parent rebuilds and uses the same type of
  // widget (with the same key), the framework re-uses
  // the State object instead of creating a new State object.

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

class _ShoppingListState extends State<ShoppingList> {
  final _shoppingCart = <Product>{};

  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      // When a user changes what's in the cart, you need
      // to change _shoppingCart inside a setState call to
      // trigger a rebuild.
      // The framework then calls build, below,
      // which updates the visual appearance of the app.

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Shopping List'),
      ),
      body: ListView(
        padding: const 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(const MaterialApp(
    title: 'Shopping App',
    home: ShoppingList(
      products: [
        Product(name: 'Eggs'),
        Product(name: 'Flour'),
        Product(name: 'Chocolate chips'),
      ],
    ),
  ));
}

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

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

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

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

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

Last updated