
Управление состоянием — один из ключевых аспектов разработки приложений на Flutter. Часто для этой задачи выбирают тяжелые и многофункциональные решения вроде BLoC, Riverpod или GetX. Однако во многих проектах подобная инфраструктура избыточна: не каждое приложение требует сложной архитектуры и дополнительного уровня абстракции.
В данной статье мы расскажем про встроенные инструменты Flutter, которые позволяют реализовать надежный и предсказуемый state-менеджмент без сторонних фреймворков. Вы узнаете, как использовать ValueNotifier и Provider для удобной работы с состоянием и когда такой подход является оптимальным.
Ключевые инструменты
Для реализации простого управления состоянием во Flutter можно использовать инструменты, которые уже доступны в стандартной экосистеме. В данном подходе основную роль выполняют два механизма, обеспечивающие обновление данных и их связь с интерфейсом.
• ValueNotifier — инструмент для хранения одного значения (числа, строки или объекта) и уведомления подписчиков при его изменении. Работает по принципу обновления значения и автоматической передачи этого события слушателям.
• ValueListenableProvider — компонент из пакета provider, который подписывается на ValueNotifier и перестраивает зависимые виджеты при каждом изменении значения. Это позволяет поддерживать интерфейс в актуальном состоянии без дополнительного кода.
Другие простые встроенные варианты
После рассмотрения подхода на основе ValueNotifier и Provider полезно упомянуть и другие встроенные механизмы, которые Flutter предоставляет для управления состоянием. Они решают более узкие задачи и подходят в ситуациях, когда требуется минимальное вмешательство в архитектуру и простая реакция интерфейса на изменения данных.
Управление локальным состоянием с помощью setState()
Самый простой способ управления состоянием — встроенный метод setState() во Flutter. Он идеально подходит для управления локальным состоянием одного виджета.
dart class SimpleCounter extends StatefulWidget { @override _SimpleCounterState createState() => _SimpleCounterState(); } class _SimpleCounterState extends State<SimpleCounter> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Text('$_counter'), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, child: Icon(Icons.add), ), ); } }
Преимущества:
• Предельно простой и интуитивно понятный механизм.
• Не требует подключения сторонних пакетов.
• Подходит для небольших виджетов с минимальной логикой.
Недостатки:
• Не предназначен для передачи состояния между разными виджетами.
• Частые обновления могут вызывать полное перестроение виджета, что снижает эффективность.
Передача данных по дереву через InheritedWidget
InheritedWidget — это базовый механизм Flutter для эффективной передачи данных вниз по дереву виджетов. Provider построен на его основе.
dart class MyInheritedData extends InheritedWidget { final int counter; final Function() increment; MyInheritedData({ required this.counter, required this.increment, required Widget child, }) : super(child: child); @override bool updateShouldNotify(MyInheritedData oldWidget) { return counter != oldWidget.counter; } static MyInheritedData of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<MyInheritedData>()!; } } // Использование в виджете Text('${MyInheritedData.of(context).counter}')
Когда использовать:
Для простой передачи данных вглубь дерева виджетов
Когда нужно избежать добавления сторонних зависимостей
Для изучения того, как работают более высокоуровневые решения
Управление сложным состоянием через ChangeNotifier
Если ValueNotifier работает с одним значением, то ChangeNotifier может управлять состоянием целого объекта с множеством полей.
dart class UserSettings extends ChangeNotifier { String _username = 'Гость'; ThemeMode _themeMode = ThemeMode.light; bool _notificationsEnabled = true; String get username => _username; ThemeMode get themeMode => _themeMode; bool get notificationsEnabled => _notificationsEnabled; void updateUsername(String newName) { _username = newName; notifyListeners(); // Уведомляем всех слушателей } void toggleTheme() { _themeMode = _themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light; notifyListeners(); } void toggleNotifications() { _notificationsEnabled = !_notificationsEnabled; notifyListeners(); } } // В приложении ChangeNotifierProvider( create: (_) => UserSettings(), child: MyApp(), );
Пример базового применения: счетчик на основе ValueNotifier
Традиционно знакомство с управлением состоянием во Flutter начинают с простого счетчика. В данном разделе рассмотрим, как реализовать этот пример с использованием ValueNotifier, отделив логику от интерфейса и обеспечив корректное обновление данных.
Создание сервиса состояния
В файле counter_service.dart создаём класс, который будет содержать состояние и логику его изменения. Он выступает в роли хранилища, к которому будет обращаться интерфейс.
import 'package:flutter/foundation.dart'; // Используется ValueNotifier<int>, где хранимым значением является целое число. // Это значение отслеживается и при его изменении автоматически уведомляются слушатели. class CounterService extends ValueNotifier<int> { // В конструкторе вызывается базовый конструктор ValueNotifier // и задаётся начальное значение состояния (0). CounterService() : super(0); // Методы для изм��нения текущего состояния. void increment() { value++; // Изменение значения вызывает автоматическое уведомление слушателей. } void decrement() { value--; } // Так как класс наследуется от ValueNotifier, вызов notifyListeners() // осуществляется автоматически при обновлении value. }
Обеспечиваем доступ к сервису через Provider
На верхнем уровне нашего приложения (обычно в main.dart или app.dart) размещаем созданный сервис в дереве виджетов с помощью Provider. Это позволит любому виджету ниже по дереву получать к нему доступ.
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'counter_service.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { // Используется ChangeNotifierProvider, так как ValueNotifier // наследуется от ChangeNotifier и поддерживает механизм уведомлений. return ChangeNotifierProvider( create: (_) => CounterService(), // Инициализация экземпляра сервиса состояния. child: const MaterialApp( home: MyHomePage(), ), ); } }
Используем и отслеживаем состояние в интерфейсе
Теперь любой виджет ниже по дереву получает доступ к CounterService и может реагировать на изменения состояния.
Создадим файл my_home_page.dart:
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'counter_service.dart'; class MyHomePage extends StatelessWidget { const MyHomePage({super.key}); @override Widget build(BuildContext context) { // Используем Consumer. // Consumer<T> отслеживает изменения в провайдере типа T (в данном случае CounterService). // При изменении значения в CounterService перестраивается только участок интерфейса внутри билдера Consumer. return Scaffold( appBar: AppBar(title: const Text('Простой счетчик')), body: Center( child: Consumer<CounterService>( builder: (_, counterService, __) { // Внутри билдера доступен актуальный экземпляр CounterService. // Получаем текущее значение и отображаем его. return Text( '${counterService.value}', // Текущее значение из ValueNotifier. style: Theme.of(context).textTheme.headlineMedium, ); }, ), ), floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ FloatingActionButton( onPressed: () { // Получаем сервис и вызываем метод изменения состояния. // Provider.of с listen: false не вызывает перестроение текущего виджета, // поскольку здесь требуется только выполнение действия. Provider.of<CounterService>(context, listen: false).increment(); }, child: const Icon(Icons.add), ), const SizedBox(height: 10), FloatingActionButton( onPressed: () { Provider.of<CounterService>(context, listen: false).decrement(); }, child: const Icon(Icons.remove), ), ], ), ); } }
Практический пример: переключение темы
Рассмотрим реализацию переключения между светлой и тёмной темой с использованием того же подхода к управлению состоянием.
theme_service.dart
Сервис, отвечающий за хранение и переключение темы приложения.
import 'package:flutter/material.dart'; class ThemeService extends ValueNotifier<ThemeMode> { ThemeService() : super(ThemeMode.light); void toggleTheme() { value = value == ThemeMode.light ? ThemeMode.dark : ThemeMode.light; } // Геттер для определения текущего режима темы. bool get isDarkMode => value == ThemeMode.dark; }
main.dart
Подключение ThemeService к дереву виджетов и использование его в MaterialApp.
void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (_) => ThemeService(), // Передаём ThemeService в дерево виджетов. child: Consumer<ThemeService>( // Consumer оборачивает MaterialApp, чтобы приложение обновлялось при смене темы. builder: (context, themeService, child) { return MaterialApp( theme: ThemeData.light(), darkTheme: ThemeData.dark(), themeMode: themeService.value, // Текущее значение темы из ThemeService. home: const MyHomePage(), ); }, ), ); } }
Кнопка переключения темы в MyHomePage
Кнопка, вызывающая смену темы через ThemeService.
// ... inside MyHomePage's build method ... floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ // Кнопка смены темы. FloatingActionButton( onPressed: () { Provider.of<ThemeService>(context, listen: false).toggleTheme(); }, child: const Icon(Icons.brightness_6), ), // ... остальные кнопки ... ], ),
Комбинированный пример: список задач (To-Do List)
Давайте создадим более практичный пример — простое приложение для управления списком задач, используя только ValueNotifier и Provider.
Модель данных и сервис состояния
dart // todo_model.dart class TodoItem { final String id; final String title; bool completed; TodoItem({ required this.id, required this.title, this.completed = false, }); } // todo_service.dart import 'package:flutter/foundation.dart'; class TodoService extends ValueNotifier<List<TodoItem>> { TodoService() : super([]); void addTodo(String title) { value = [ ...value, TodoItem( id: DateTime.now().millisecondsSinceEpoch.toString(), title: title, ), ]; } void toggleTodo(String id) { value = value.map((todo) { if (todo.id == id) { return TodoItem( id: todo.id, title: todo.title, completed: !todo.completed, ); } return todo; }).toList(); } void removeTodo(String id) { value = value.where((todo) => todo.id != id).toList(); } int get pendingCount => value.where((todo) => !todo.completed).length; int get completedCount => value.where((todo) => todo.completed).length; }
Интерфейс приложения
dart // todo_screen.dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'todo_service.dart'; class TodoScreen extends StatelessWidget { final TextEditingController _controller = TextEditingController(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Список задач'), actions: [ Consumer<TodoService>( builder: (context, service, child) { return Chip( label: Text('${service.pendingCount}'), backgroundColor: Colors.blue, labelStyle: TextStyle(color: Colors.white), ); }, ), SizedBox(width: 10), ], ), body: Column( children: [ Padding( padding: const EdgeInsets.all(16.0), child: Row( children: [ Expanded( child: TextField( controller: _controller, decoration: InputDecoration( hintText: 'Добавить новую задачу...', border: OutlineInputBorder(), ), ), ), SizedBox(width: 10), ElevatedButton( onPressed: () { if (_controller.text.isNotEmpty) { Provider.of<TodoService>(context, listen: false) .addTodo(_controller.text); _controller.clear(); } }, child: Text('Добавить'), ), ], ), ), Expanded( child: Consumer<TodoService>( builder: (context, service, child) { if (service.value.isEmpty) { return Center( child: Text( 'Нет задач', style: TextStyle(fontSize: 18, color: Colors.grey), ), ); } return ListView.builder( itemCount: service.value.length, itemBuilder: (context, index) { final todo = service.value[index]; return ListTile( leading: Checkbox( value: todo.completed, onChanged: (_) { Provider.of<TodoService>(context, listen: false) .toggleTodo(todo.id); }, ), title: Text( todo.title, style: TextStyle( decoration: todo.completed ? TextDecoration.lineThrough : null, color: todo.completed ? Colors.grey : null, ), ), trailing: IconButton( icon: Icon(Icons.delete, color: Colors.red), onPressed: () { Provider.of<TodoService>(context, listen: false) .removeTodo(todo.id); }, ), ); }, ); }, ), ), ], ), ); } }
Оптимизация производительности
Оптимизация обновлений интерфейса играет важную роль при работе с состоянием во Flutter. Даже при использовании простых механизмов важно контролировать, какие именно части дерева виджетов должны перестраиваться при изменении данных. Flutter и Provider предлагают инструменты, позволяющие минимизировать лишние перестроения и повысить производительность приложения.
Selector вместо Consumer для точных обновлений
Consumer перестраивает виджет при любом изменении состояния. Если в вашем объекте состояния много полей, но виджет зависит только от одного из них, используйте Selector:
dart // Вместо Consumer<UserProfile>: Selector<UserProfile, String>( selector: (context, profile) => profile.username, builder: (context, username, child) { return Text('Привет, $username!'); }, ) // Виджет перестроится только когда изменится username, // даже если другие поля UserProfile изменятся.
Кеширование виджетов с помощью child
Используйте параметр child в Consumer и Selector для кеширования статических частей виджета:
dart Consumer<MyService>( builder: (context, service, child) { return Column( children: [ // Эта часть не будет перестраиваться при каждом обновлении child!, // Эта часть перестраивается Text('${service.value}'), ], ); }, child: const HeaderWidget(), // Статический виджет )
Когда какой подход использовать?
Выбор подхода к управлению состоянием зависит от конкретной задачи и масштаба вашего приложения. Вот подробное текстовое руководство по выбору.
1. Для локального состояния одного виджета
Используйте setState() — это самый простой и прямой способ. Он идеально подходит, когда состояние нужно только внутри одного виджета и нигде больше не используется. Например, для отслеживания, раскрыт ли аккордеон или активно ли переключатель. Не усложняйте архитектуру без необходимости.
2. Для связи нескольких виджетов на одном экране
Используйте ValueNotifier с ValueListenableBuilder — когда несколько виджетов на одном экране зависят от одного и того же значения. Например, если у вас есть счетчик, который должен отображаться в заголовке и в теле страницы одновременно. Этот подход создает легкую реактивную связь без лишних абстракций.
3. Для состояния, используемого в разных частях приложения
Используйте ValueNotifier или ChangeNotifier вместе с Provider — когда ваше состояние должно быть доступно из различных экранов или глубоко вложенных виджетов. Классические примеры: сервис аутентификации (информация о пользователе), корзина покупок в интернет-магазине или настройки темы приложения. Provider обеспечивает четкое и централизованное управление таким состоянием.
4. Для управления сложным объектом состояния
Используйте ChangeNotifier — когда ваше состояние представляет собой не просто число или строку, а целый объект с множеством полей и методов (например, профиль пользователя с именем, email, аватаркой и методами для обновления). ChangeNotifier позволяет инкапсулировать всю логику и уведомлять об изменениях единым вызовом notifyListeners().
5. Для учебных целей и минимальных зависимостей
Используйте базовый InheritedWidget — если вы хотите глубоко понять, как работают механизмы передачи данных во Flutter, или если в проекте критически важно избегать любых сторонних пакетов. Этот подход является фундаментом, на котором построены все высокоуровневые решения, включая Provider.
Практические рекомендации по выбору
1 Начинайте с самого простого решения, которое решает вашу текущую задачу. Не прогнозируйте избыточную сложность.
2 Эволюционируйте постепенно. Начните с setState() для виджета. Если состояние «переросло» его, поднимите его с помощью ValueNotifier и Provider на уровень выше.
3 Логически группируйте состояние. Не создавайте один огромный сервис на все приложение. Лучше иметь несколько небольших и отвественных сервисов: AuthService, CartService, SettingsService.
4 Избегайте глубокой вложенности Provider'ов в дереве виджетов. Если вам нужно предоставить несколько сервисов, используйте MultiProvider для чистоты кода.
5 Помните, что многие приложения успешно живут на ValueNotifier/ChangeNotifier и Provider. Переход на BLoC, Riverpod или GetX оправдан только тогда, когда вы реально упираетесь в ограничения более простых инструментов, например, при очень сложной асинхронной бизнес-логике или в крупной команде, где нужна строгая архитектура.
Краткий итог: Используйте setState для изолированного, ValueNotifier для общего на экране, а Provider — для общего в приложении. Переходите к более сложным фреймворкам только тогда, когда исчерпали возможности этой простой и эффективной связки.
Выводы: Дальнейшие шаги
Когда текущий стек перестаёт покрывать задачи, можно перейти к более продвинутым инструментам:
Riverpod — современная альтернатива Provider с лучшей безопасностью типов.
BLoC — строгая архитектура для больших команд и сложной бизнес-логики.
GetX — минималистичный и быстрый вариант, когда важна лаконичность.
Но важно помнить: большинство приложений не достигают уровня сложности, требующего этих инструментов. Стек ValueNotifier + Provider остаётся оптимальным для широкого спектра задач.