Flutter StatelessWidget vs StatefulWidget: углублённое понимание
Понимание разницы между StatelessWidget и StatefulWidget фундаментально для разработки на Flutter.
В этом разделе будут глубоко проанализированы сценарии использования и внутренние механизмы работы этих двух типов виджетов.
Ключевое различие
В Flutter есть два основных типа виджетов, и их фундаментальное различие заключается в том, могут ли они хранить изменяемое состояние.
| Особенности | StatelessWidget | StatefulWidget |
|---|---|---|
| State | Неизменяемый | mutable |
| Момент перестроения | Только при перестроении родительского виджета | Когда состояние изменяется + родительский Widget перестраивается |
| Производительность | Выше (не нужно слушать состояние) | Немного ниже (требуется управление состоянием) |
| Сценарий использования | Статическое содержимое | Динамическое интерактивное содержимое |
Подробное объяснение StatelessWidget
StatelessWidget подходит для содержимого, которое не меняется со временем. После создания все его свойства являются final и не могут быть изменены.
Сценарий использования
- Отображение статического текста, изображений, иконок
- Отображение списка данных (источник данных поступает от родительского Widget)
- Контейнеры макета без взаимодействия с пользователем
- Разделы отображения только для чтения в формах
Пример: Полный пример StatelessWidget
// Компонент отображения информации о пользователе (без состояния)
class UserCard extends StatelessWidget {
// Свойства объявлены как final для обеспечения неизменяемости
final String name;
final String email;
final String avatarUrl;
// конструктор const повышает производительность
const UserCard({
super.key,
required this.name,
required this.email,
this.avatarUrl = '',
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
// Аватар
CircleAvatar(
backgroundImage: avatarUrl.isNotEmpty
? NetworkImage(avatarUrl)
: null,
child: avatarUrl.isEmpty
? const Icon(Icons.person)
: null,
),
const SizedBox(width: 16),
// Информация о пользователе
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Текст(
name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Текст(
email,
style: TextStyle(
color: Colors.grey[600],
),
),
],
),
),
],
),
),
);
}
}
Подробное объяснение StatefulWidget
StatefulWidget подходит для сценариев, в которых необходимо реагировать на взаимодействие пользователя или изменения данных. Он состоит из двух частей: сам Widget (неизменяемый) и объект State (изменяемый).
Сценарий использования
- Динамические данные, такие как счётчики, таймеры
- Ввод в форму
- Управление анимацией
- Отображение статуса сетевого запроса
- Реакция на взаимодействие пользователя
Жизненный цикл объекта State
| Методы | Описание |
|---|---|
| initState | Вызывается при первом создании Widget, используется для инициализации состояния. |
| didChangeDependencies | Вызывается при изменении зависимостей Widget. |
| build | Строит UI, вызывается при каждом изменении состояния. |
| didUpdateWidget | Вызывается, когда родительский Widget перестраивается |
| dispose | Вызывается при удалении Widget, используется для очистки ресурсов. |
Пример: Полный пример StatefulWidget
// Приложение-счётчик
class CounterApp extends StatefulWidget {
const CounterApp({super.key});
@override
State<CounterApp> createState() => _CounterAppState();
}
class _CounterAppState extends State<CounterApp> {
// Состояние счётчика
int _counter = 0;
// Timer (для демонстрации dispose)
Timer? _timer;
// 1. initState - Инициализация состояния
@override
void initState() {
super.initState();
// Инициализировать счётчик
_counter = 0;
// Запустить таймер (увеличивать каждую секунду)
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_increment();
});
}
// 2. didChangeDependencies - вызывается при изменении зависимостей
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
// 3. build - построение интерфейса
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Счётчик'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Текущее значение:',
style: TextStyle(fontSize: 20),
),
Текст(
'$_counter',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
],
),
),
// Кнопка увеличения
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: const Icon(Icons.add),
),
);
}
// Метод для увеличения счётчика
void _increment() {
// Нужно вызвать setState, чтобы инициировать обновление интерфейса
setState(() {
_counter++;
});
}
// 4. didUpdateWidget — Вызывается при перестройке родительского виджета
@override
void didUpdateWidget(CounterApp oldWidget) {
super.didUpdateWidget(oldWidget);
}
// 5. dispose — Освобождение ресурсов
@override
void dispose() {
// Отмените таймер, чтобы предотвратить утечки памяти
_timer?.cancel();
super.dispose();
}
}
Правильное использование setState
В StatefulWidget вы должны уведомлять Flutter об изменениях состояния через setState.
Правильное использование
Пример: правильное использование setState
void _increment() {
setState(() {
_counter++;
});
}
// Правильно: когда обновление состояния не связано с UI, setState не нужен
void _someInternalLogic() {
// Внутренняя логика, не влияет на UI
_calculateSomething();
}
// Правильно: обновляйте состояние после асинхронной операции
Future<void> _loadData() async {
final data = await fetchData();
setState(() {
_items = data;
});
}
Неправильное использование
Пример: Неправильное использование setState
void _increment() {
_counter++; // Состояние изменилось, но UI не обновится!
}
// Неправильно: Вызов setState в методе build
@override
Widget build(BuildContext context) {
// Никогда не вызывайте setState в build!
setState(() {
_counter++; // Это приводит к бесконечному циклу
});
return Text('$_counter');
}
setState вызовет повторный вызов метода build при отрисовке следующего кадра.
Частые вызовы setState или вызов setState внутри метода build могут привести к проблемам с производительностью.
Как выбрать
Во время разработки следует выбирать подходящий тип Widget в зависимости от фактических потребностей:
Выбирайте StatelessWidget, когда
- Внешний вид Widget не зависит от изменяемого состояния.
- Все необходимые данные передаются из родительского Widget через конструктор.
- Widget не должен реагировать на взаимодействия пользователя.
- Стремление к оптимальной производительности
Выбирайте StatefulWidget, когда
- Widget должен поддерживать внутреннее состояние.
- Должен реагировать на ввод пользователя (клики, свайпы и т. д.)
- Должен реагировать на изменения данных (например, ответы на сетевые запросы)
- Необходимо управлять анимациями