В предыдущем уроке мы рассмотрели пример построения интерфейса приложения с помощью виджетов, создали свой виджет MyBody наследуемый от класса StatelessWidget.
Все собственные классы-виджеты рекомендуется создавать от двух основных: StatelessWidget и StatefulWidget.
Отдельные части интерфейса удобно объединять в отдельные виджеты, созданные от этих двух суперклассов, но какой из них использовать – мы рассмотрим в этом уроке.
1. StatelessWidget
StatelessWidget – рекомендуется для неизменяемых виджетов. Это такие виджеты которые не имеют внутреннего состояния, зависят только от конфигурационных параметров и от родительских виджетов.
Вот некоторые виджеты которые наследуются от StatelessWidget:
- class Text()
- class FlatButton()
- class Container()
- и др.
Самая простая конструкция для создания виджета от суперкласса StatelessWidget:
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Text(‘my text’);
}
}
Допустим, что мы хотим выводить список анонсов новостей, каждая новость представляет из себя блок в котором есть: картинка, заголовок и текст.
Получения списка новостей с сервера мы рассмотрим позже, сейчас нас интересует отдельная новость и блок который ее будет отображать.
Поскольку все данные для каждого блока нам известны заранее и не будут изменятся, то суперкласс StatelessWidget нам отлично подходит.
Создадим класс NewsBox от суперкласса StatelessWidget и определим в нем поля:
_title – заголовок анонса новости,
_text – содержание новостного сообщения,
_imageurl – ссылка на изображение.
class NewsBox extends StatelessWidget {
final String _title;
final String _text;
String _imageurl;
Разберем строку final String _title;
title – поле нашего класса, переменная в которой мы будем хранить заголовок новостного сообщения
_ (подчеркивание перед именем переменной) – в языке Dart говорит о том что данная переменная скрыта и доступна только внутри объекта нашего класса (аналог private в других языках программирования)
String – наша переменная строкового типа
final – данная переменная должна быть объявлена при создании экземпляра класса и не может быть изменена
Можно было бы просто объявить поле в классе с помощью var title; в перспективе это плохой вариант:
- в нем нет типизации, а значит можно передать туда число или любой другой объект – в будущем такая конструкция может создать много проблем;
- открытый доступ к полю объекта в более сложном примере может неожиданно «сломать» работу вашего класса, если Вы будете менять его из вне и он будет не final, или же если нужно производить проверку входного значения;
- отсутствие final там где данные в объекте не меняются – может смутить другого разработчика, или даже Вас самих когда начнете забывать свой код, так же редактор Android рекомендует чтобы все поля от суперкласса StatelessWidget были final.
Добавим конструктор в наш класс:
NewsBox(this._title, this._text, {String imageurl}) {
_imageurl = imageurl;
}
NewsBox – название конструктора должно совпадать с именем нашего класса.
В круглых скобках () мы указываем, что при создании экземпляра ожидается два обязательных параметра значения которых будет присвоено скрытым полям _title и _text нашего класса.
В фигурных скобках {} указывается, что в конструкторе можно указать параметр imageurl со значением типа String.
Т.е. мы можем создать экземпляр нашего класса двумя способами:
- new NewsBox(‘заголовок’, ‘содержание новости’, imageurl: ‘http://url/image.jpg’) // с параметром imageurl
- new NewsBox(‘заголовок’, ‘содержание новости’) // без imageurl
А вот так вызывать наш конструктор нельзя:
- new NewsBox() // без параметров
- new NewsBox(‘заголовок’) // только с одним параметром
- new NewsBox(‘заголовок’, imageurl: ‘http://url/image.jpg’, ‘содержание новости’) // менять местами обязательные и не обязательные параметры по ключу
Приведем краткую структуру, которую мы хотим вывести:
- Если изображение задано через параметр imageurl то в функции build мы выведим такую структуру:if (_imageurl != null) return new Container(
child: new Row(
children: [
new Image.network(_imageurl),
new Column(children: [new Text(_title),new Text(_text)])
])
) - Если изображения нет, то выведем без него:return new Container(
child: new Row(
children: [
new Column(children: [new Text(_title),new Text(_text)])
])
)
Листинг программы
import ‘package:flutter/material.dart’;
class NewsBox extends StatelessWidget {
final String _title;
final String _text;
String _imageurl;
NewsBox(this._title, this._text, {String imageurl}) {
_imageurl = imageurl;
}
@override
Widget build(BuildContext context) {
if (_imageurl != null && _imageurl != ”) return new Container(
color: Colors.black12,
height: 100.0,
child: new Row(children: [
new Image.network(_imageurl, width: 100.0, height: 100.0, fit: BoxFit.cover,),
new Expanded(child: new Container(padding: new EdgeInsets.all(5.0), child: new Column(children: [
new Text(_title, style: new TextStyle(fontSize: 20.0), overflow: TextOverflow.ellipsis),
new Expanded(child:new Text(_text, softWrap: true, textAlign: TextAlign.justify,))
]
))
)
])
);
return new Container(
color: Colors.black12,
height: 100.0,
child: new Row(children: [
new Expanded(child: new Container(padding: new EdgeInsets.all(5.0), child: new Column(children: [
new Text(_title, style: new TextStyle(fontSize: 20.0), overflow: TextOverflow.ellipsis),
new Expanded(child:new Text(_text, softWrap: true, textAlign: TextAlign.justify,))
]
))
)
])
);
}
}
void main() => runApp(
new MaterialApp(
debugShowCheckedModeBanner: false,
home: new Scaffold(
appBar: new AppBar(),
body: new NewsBox(‘Новый урок по Flutter’, ”‘В новом уроке рассказывается в каких случаях применять класс StatelessWidget и StatefulWidget. Приведены примеры их использования.’”,
imageurl: ‘https://flutter.su/favicon.png’)
)
)
);
В полном листинге программы мы усложнили функцию build чтобы получить ожидаемый результат:
- if (_imageurl != null && _imageurl != ”) – в условие мы добавили проверку не только на то что адрес изображения определен, но и что он не пустой
- new Container(color: Colors.black12, height: 100.0, – в виджете контейнера мы указали цвет фона блока и его высоту. ВНИМАНИЕ: параметр height ожидает значение типа double поэтому стоит 100.0 а не 100
- new Image.network(_imageurl, width: 100.0, height: 100.0, fit: BoxFit.cover,) – для виджета изображения мы добавили параметры высоты и ширины, а также указали способ кадрирования и заполнения
- new Expanded(child: new Container(padding: new EdgeInsets.all(5.0), child: new Column(children: – виджет Column, который содержит два текстовых виджета, мы обернули двумя виджетами:
- new Expanded – используется для ограничения размеров дочерних виджетов, без него виджет Column может иметь безграничные размеры и выходить за края экрана.
- new Container – со свойством padding создает внутри контейнера отступы, таким образом текст не будет впритык к изображению или краям экрана.
- new Text(_title, style: new TextStyle(fontSize: 20.0), overflow: TextOverflow.ellipsis) – для заголовка новости мы применили стиль с увеличенным размером и свойство overflow которое позволяет обрезать слишком длинный текст
- new Expanded(child:new Text(_text, softWrap: true, textAlign: TextAlign.justify)) – к виджету текста мы применили перенос текста с помощью параметра softWrap, добавили выравнивание текста по ширине, в случае слишком длинного текста, виджет Expanded обрежет все то что не войдет по высоте.
Структура виджета NewsBox
Если задан параметр imageurl


Если параметр imageurl не задан


2. StatefulWidget
StatefulWidget – рекомендуется для изменяемых виджетов, с изменяемым внутренним состоянием.
В примере выше мы создали класс, который может выглядеть по разному если в нем есть или нет изображения, о наличии изображения известно при создании экземпляра класса.
Под изменяемым состоянием понимается изменение внутреннего состояния экземпляра класса в зависимости от какого-то события (по нажатию, времени и пр.) Для этого нужно создавать виджет от суперкласса StatefulWidget.
Вот некоторые виджеты, которые наследуются от StatefulWidget:
- class Image()
- class Form()
- и др.
Самая простая конструкция для создания виджета от суперкласса StatefulWidget:
class MyWidget extends StatefulWidget {
@override
createState() => new MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
@override
Widget build(BuildContext context) {
return new Text(‘my text’);
}
}
Конструкция изменяемого виджета отличается от той которую мы использовали раньше:
- MyWidget – наш класс должен иметь реализацию функции createState(), которая должна возвращать экземпляр класса от суперкласса State
- @override – аннотация переопределения указывает на то, что последующее описание функции заменяет функцию суперкласса от которого идет наследование.
Неиспользование данной аннотации не приводит к ошибкам, но желательно для понимания кода - createState() => new MyWidgetState(); – это краткая запись
createState() {return new MyWidgetState();} – так выглядит привычная для многих полная реализация функции (можно использовать и ее). - MyWidgetState – содержит знакомую функцию build которая аналогична тому как мы использовали раньше в виджете от суперкласса StatelessWidget
- setState(() {}); – теперь вызывая такую конструкцию при событии в нашем виджете или дочерних элементах, мы сможем вызывать перестроение нашего виджета
Возьмем наш предыдущий виджет для отображения новостных анонсов и добавим в него кнопку «избранное» и счетчик который будет указывать сколько людей добавило новость в избранное. Т.е. у нас будет два состояния: «избранное» и «не избранное».
Поскольку изменяться будет только кнопка и счетчик, то мы и обернем их в отдельный виджет class NewsBoxFavourit extends StatefulWidget.
class NewsBoxFavourit extends StatefulWidget {
final int _num;
final bool _like;
NewsBoxFavourit(this._num, this._like);
@override
createState() => new NewsBoxFavouritState(_num, _like);
}
class NewsBoxFavouritState extends State<NewsBoxFavourit> {
int num;
bool like;
NewsBoxFavouritState(this.num, this.like);
void pressButton() {
setState(() {
like = !like;
if(like) num++;
else num–;
});
}
@override
Widget build(BuildContext context) {
return new Column(children: [
new Text(‘В избранном
$num’, textAlign: TextAlign.center),
new IconButton(
icon: new Icon(like ? Icons.star : Icons.star_border, size: 30.0, color: Colors.blue[500]),
onPressed: pressButton
)
]);
}
}
В нашем классе NewsBoxFavourit мы объявили два скрытых поля _num и _like, значение которых устанавливается через конструктор NewsBoxFavourit(this._num, this._like), после они передается в класс состояния «избранное» createState() => new NewsBoxFavouritState(_num, _like).
В классе состояния class NewsBoxFavouritState extends State мы объявили одноименные поля только тут они публичные и изменяемые.
Поля num и like в классе состояния можно сделать приватными, но мы так сделали намеренно, чтобы на момент изучения данного урока не было путаницы – какие из объявленных переменных мы используем для проверки и изменения (те которые публичные, в классе состояния).
Конструктор NewsBoxFavouritState(this.num, this.like) инициализирует переменные в классе.
Передать значения из виджета без конструктора можно с помощью переменной widget, для нашего примера: widget._num.
Так как переменные _num и _like в виджете NewsBoxFavourit не изменяемые, мы не можем использовать их напрямую, и должны переопределить в NewsBoxFavouritState таким образом:
class NewsBoxFavouritState extends State<NewsBoxFavourit> {
int num;
bool like;
@override
void initState() {
num = widget._num;
like = widget._like;
super.initState();
}//…
void pressButton() { – мы вынесли реализацию функции нажатия на кнопку в класс, чтобы визуально разгрузить код программы, также это может позволить использовать одну функцию в нескольких виджетах или при разных событиях.
setState() – в качестве параметра принимает функцию, в которой мы сделали смену состояния, после выполнения переданной функции setState вызывает перестроение виджета через функцию build() в классе состояния.
Еще функцию нашей кнопки можно было описать так:
void pressButton() {
like = !like;
if(like) num++;
else num–;
setState(() {});
}
Результат работы был бы таким же.
like = !like – производит смену состояния, если у нас значение было false – то станет противоположным – true, и наоборот с true.
if(like) num++; else num–; – у нас сменилось состояние и теперь если мы добавили новость в избранное то количество точно увеличится на один, по этому мы прибавляем к счетчику единицу. И на оборот, если мы когда-то уже добавляли в избранное, а теперь убираем свой выбор – мы должны отнять единицу в переменной num.
В функции build класса NewsBoxFavouritState происходит вывод виджетов с учетом наших переменных num и like.
new Text(‘В избранном $num’, textAlign: TextAlign.center) – виджет текста выводит в две строки сообщение ( – означает перевод строки)
$num – это вызов переменной счетчика в строке, можно еще написать так ‘В избранном ${num}’
IconButton – виджет кнопки с иконкой, обязательные параметры icon и onPressed
icon: new Icon(like ? Icons.star : Icons.star_border, size: 30.0, color: Colors.blue[500]) – в виджете Icon первым параметром должен быть объект класса IconData,
like ? Icons.star : Icons.star_border – это выражение возвращает Icons.star если like == true, либо Icons.star_border если like == false
size: 30.0 – через параметр size можно увеличить или уменьшить размер иконки
color: Colors.blue[500] – устанавливает оттенок синего цвета для иконки
onPressed: pressButton – вызов функции pressButton при нажатии на кнопку из нашего класса NewsBoxFavouritState
Структура виджета NewsBoxFavourit

Листинг программы
import ‘package:flutter/material.dart’;
class NewsBoxFavourit extends StatefulWidget {
final int _num;
final bool _like;
NewsBoxFavourit(this._num, this._like);
@override
createState() => new NewsBoxFavouritState(_num, _like);
}
class NewsBoxFavouritState extends State<NewsBoxFavourit> {
int num;
bool like;
NewsBoxFavouritState(this.num, this.like);
void pressButton() {
setState(() {
like = !like;
if(like) num++;
else num–;
});
}
@override
Widget build(BuildContext context) {
return new Column(children: [
new Text(‘В избранном
$num’, textAlign: TextAlign.center),
new IconButton(
icon: new Icon(like ? Icons.star : Icons.star_border, size: 30.0, color: Colors.blue[500]),
onPressed: pressButton
)
]);
}
}
class NewsBox extends StatelessWidget {
final String _title;
final String _text;
String _imageurl;
int _num;
bool _like;
NewsBox(this._title, this._text, {String imageurl, int num = 0, bool like = false}) {
_imageurl = imageurl;
_num = num;
_like = like;
}
@override
Widget build(BuildContext context) {
if (_imageurl != null && _imageurl != ”) return new Container(
color: Colors.black12,
height: 100.0,
child: new Row(children: [
new Image.network(_imageurl, width: 100.0, height: 100.0, fit: BoxFit.cover,),
new Expanded(child: new Container(padding: new EdgeInsets.all(5.0), child: new Column(children: [
new Text(_title, style: new TextStyle(fontSize: 20.0), overflow: TextOverflow.ellipsis),
new Expanded(child:new Text(_text, softWrap: true, textAlign: TextAlign.justify,))
]
))
), new NewsBoxFavourit(_num, _like)
])
);
return new Container(
color: Colors.black12,
height: 100.0,
child: new Row(children: [
new Expanded(child: new Container(padding: new EdgeInsets.all(5.0), child: new Column(children: [
new Text(_title, style: new TextStyle(fontSize: 20.0), overflow: TextOverflow.ellipsis),
new Expanded(child:new Text(_text, softWrap: true, textAlign: TextAlign.justify,))
]
))
), new NewsBoxFavourit(_num, _like)
])
);
}
}
void main() => runApp(
new MaterialApp(
debugShowCheckedModeBanner: false,
home: new Scaffold(
appBar: new AppBar(),
body: new NewsBox(‘Новый урок по Flutter’, ”‘В новом уроке рассказывается, в каких случаях применять класс StatelessWidget и StatefulWidget. Приведены примеры их использования.’”,
imageurl: ‘https://flutter.su/favicon.png’, num: 10)
)
)
);
3. Итоги
StatelessWidget – нужен там где внутреннее состояние одно и оно сформировано параметрами и данными которые нам известны заранее.
StatefulWidget – нужен когда внутренних состояний больше одного и они могут сменять друг друга.
Виджеты от StatelessWidget и StatefulWidget могут включать друг друга.
Создатели Flutter SDK рекомендуют использовать StatelessWidget везде, где можно обойтись без StatefulWidget – связано это с тем что перестроение последнего стоит намного дороже. Так же рекомендуется сводить к миниму дочерних виджетов в StatefulWidget.