Flutter StatelessWidget vs StatefulWidget: углублённое понимание

Понимание разницы между StatelessWidget и StatefulWidget фундаментально для разработки на Flutter.

В этом разделе будут глубоко проанализированы сценарии использования и внутренние механизмы работы этих двух типов виджетов.


Ключевое различие

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

ОсобенностиStatelessWidgetStatefulWidget
StateНеизменяемыйmutable
Момент перестроенияТолько при перестроении родительского виджетаКогда состояние изменяется + родительский Widget перестраивается
ПроизводительностьВыше (не нужно слушать состояние)Немного ниже (требуется управление состоянием)
Сценарий использованияСтатическое содержимоеДинамическое интерактивное содержимое

Подробное объяснение StatelessWidget

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

Сценарий использования

  • Отображение статического текста, изображений, иконок
  • Отображение списка данных (источник данных поступает от родительского Widget)
  • Контейнеры макета без взаимодействия с пользователем
  • Разделы отображения только для чтения в формах

Пример: Полный пример StatelessWidget

import 'package:flutter/material.dart';

// Компонент отображения информации о пользователе (без состояния)
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

import 'package:flutter/material.dart';

// Приложение-счётчик
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

// Правильно: обновляйте состояние в setState
void _increment() {
  setState(() {
    _counter++;
  });
}

// Правильно: когда обновление состояния не связано с UI, setState не нужен
void _someInternalLogic() {
  // Внутренняя логика, не влияет на UI
  _calculateSomething();
}

// Правильно: обновляйте состояние после асинхронной операции
Future<void> _loadData() async {
  final data = await fetchData();
  setState(() {
    _items = data;
  });
}

Неправильное использование

Пример: Неправильное использование setState

// Неправильно: Изменение состояния без вызова 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 должен поддерживать внутреннее состояние.
  • Должен реагировать на ввод пользователя (клики, свайпы и т. д.)
  • Должен реагировать на изменения данных (например, ответы на сетевые запросы)
  • Необходимо управлять анимациями
Другие расширения