Виджеты описывают, как должно выглядеть их представление с учетом их текущей конфигурации и состояния. Когда состояние виджета изменяется, виджет перестраивает свое описание, которое платформа сравнивает с предыдущим описанием, чтобы определить минимальные изменения, необходимые в базовом дереве рендеринга для перехода из одного состояния в другое.
Flutter отличается от других фреймворков тем, что его пользовательский интерфейс встроен в код, а не (например) в файл XML или что-то подобное. Виджеты — это основные строительные блоки пользовательского интерфейса Flutter. По мере прохождения этой кодовой лаборатории вы узнаете, что почти все во Flutter — это виджеты. Виджет — это неизменяемый объект, описывающий определенную часть пользовательского интерфейса. Вы также узнаете, что виджеты Flutter являются составными, что означает, что вы можете комбинировать существующие виджеты для создания более сложных виджетов.
Минимальное приложение 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';classMyAppBarextendsStatelessWidget {constMyAppBar({required this.title, Key? key}) : super(key: key);// Fields in a Widget subclass are always marked "final".finalWidget title;@overrideWidgetbuild(BuildContext context) {returnContainer( height:56.0, // in logical pixels padding:constEdgeInsets.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: [constIconButton( 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, ),constIconButton( icon:Icon(Icons.search), tooltip:'Search', onPressed:null, ), ], ), ); }}classMyScaffoldextendsStatelessWidget {constMyScaffold({Key? key}) : super(key: key);@overrideWidgetbuild(BuildContext context) {// Material is a conceptual piece// of paper on which the UI appears.returnMaterial(// Column is a vertical, linear layout. child:Column( children: [MyAppBar( title:Text('Example title', style:Theme.of(context) // .primaryTextTheme .headline6, ), ),constExpanded( child:Center( child:Text('Hello, world!'), ), ), ], ), ); }}voidmain() {runApp(constMaterialApp( title:'My app', // used by the OS task switcher home:SafeArea( child:MyScaffold(), ), ), );}
Многие виджеты Material Design должны быть внутри MaterialApp для правильного отображения, чтобы наследовать данные темы. Поэтому запускайте приложение с расширением MaterialApp.
Примечание. Материал — это один из двух дизайнов, поставляемых вместе с Flutter. Чтобы создать дизайн, ориентированный на iOS, см. пакет компонентов из Купертино , в котором есть собственные версии CupertinoApp, и CupertinoNavigationBar.
Обработка жестов
Большинство приложений включают ту или иную форму взаимодействия пользователя с системой. Первым шагом в создании интерактивного приложения является обнаружение жестов ввода. Посмотрите, как это работает, создав простую кнопку:
Виджет GestureDetectorне имеет визуального представления, но вместо этого обнаруживает жесты, сделанные пользователем. Когда пользователь нажимает Container, GestureDetectorвызывается onTap()обратный вызов, в данном случае вывод сообщения на консоль. Вы можете использовать GestureDetectorдля обнаружения различных жестов ввода, включая нажатия, перетаскивания и масштабирования.
Изменение виджетов в ответ на ввод
Чтобы создать более сложный опыт — например, более интересно реагировать на пользовательский ввод — приложения обычно содержат некоторое состояние. Flutter использует StatefulWidgets, чтобы уловить эту идею. StatefulWidgets— это специальные виджеты, умеющие генерировать Stateобъекты, которые затем используются для хранения состояния. Рассмотрим этот базовый пример, используя ElevatedButtonупомянутое ранее:
import'package:flutter/material.dart';classCounterextendsStatefulWidget {// 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".constCounter({Key? key}) : super(key: key);@override_CounterStatecreateState() =>_CounterState();}class_CounterStateextendsState<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++; }); }@overrideWidgetbuild(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.returnRow( mainAxisAlignment:MainAxisAlignment.center, children:<Widget>[ElevatedButton( onPressed: _increment, child:constText('Increment'), ),constSizedBox(width:16),Text('Count: $_counter'), ], ); }}voidmain() {runApp(constMaterialApp( home:Scaffold( body:Center( child:Counter(), ), ), ), );}
Объединяя все это
Далее следует более полный пример, объединяющий эти концепции: гипотетическое приложение для покупок отображает различные продукты, предлагаемые для продажи, и поддерживает корзину покупок для предполагаемых покупок. Начните с определения класса представления ShoppingListItem:
import'package:flutter/material.dart';classProduct {constProduct({required this.name});finalString name;}typedefCartChangedCallback=Function(Product product, bool inCart);classShoppingListItemextendsStatelessWidget {ShoppingListItem({required this.product,required this.inCart,required this.onCartChanged, }) : super(key:ObjectKey(product));finalProduct product;finalbool inCart;finalCartChangedCallback 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) returnnull;returnconstTextStyle( color:Colors.black54, decoration:TextDecoration.lineThrough, ); }@overrideWidgetbuild(BuildContext context) {returnListTile( onTap: () {onCartChanged(product, inCart); }, leading:CircleAvatar( backgroundColor:Colors.greenAccent, child:Text(product.name[0]), ), title:Text(product.name, style:_getTextStyle(context)), ); }}voidmain() {runApp(MaterialApp( home:Scaffold( body:Center( child:ShoppingListItem( product:constProduct(name:'Chips'), inCart:true, onCartChanged: (product, inCart) {}, ), ), ), ), );}
Виджет ShoppingListItemследует общему шаблону для виджетов без состояния. Он сохраняет значения, полученные в своем конструкторе, в finalпеременных-членах, которые затем использует во время своей build()функции. Например, inCartлогическое значение позволяет переключаться между двумя визуальными представлениями: одно использует основной цвет из текущей темы, а другое — серый.
Вот пример родительского виджета, в котором хранится изменяемое состояние:
import'package:flutter/material.dart';classProduct {constProduct({required this.name});finalString name;}typedefCartChangedCallback=Function(Product product, bool inCart);classShoppingListItemextendsStatelessWidget {ShoppingListItem({required this.product,required this.inCart,required this.onCartChanged, }) : super(key:ObjectKey(product));finalProduct product;finalbool inCart;finalCartChangedCallback 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) returnnull;returnconstTextStyle( color:Colors.black54, decoration:TextDecoration.lineThrough, ); }@overrideWidgetbuild(BuildContext context) {returnListTile( onTap: () {onCartChanged(product, inCart); }, leading:CircleAvatar( backgroundColor:_getColor(context), child:Text(product.name[0]), ), title:Text( product.name, style:_getTextStyle(context), ), ); }}classShoppingListextendsStatefulWidget {constShoppingList({required this.products, Key? key}) : super(key: key);finalList<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_ShoppingListStatecreateState() =>_ShoppingListState();}class_ShoppingListStateextendsState<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); } }); }@overrideWidgetbuild(BuildContext context) {returnScaffold( appBar:AppBar( title:constText('Shopping List'), ), body:ListView( padding:constEdgeInsets.symmetric(vertical:8.0), children: widget.products.map((Product product) {returnShoppingListItem( product: product, inCart: _shoppingCart.contains(product), onCartChanged: _handleCartChanged, ); }).toList(), ), ); }}voidmain() {runApp(constMaterialApp( 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.