6 способов управления состоянием в приложениях Flutter (с примерами кода)
Как управлять состоянием в Flutter
В этой статье мы рассмотрим шесть популярных способов управления состоянием в Flutter приложений, включая реальные примеры и лучшие практики:
Управление состоянием — одна из самых важных и обсуждаемых тем в разработке на Flutter. Она определяет, как ваше приложение обрабатывает изменения данных и эффективно обновляет интерфейс. Flutter предоставляет несколько стратегий для управления состоянием — от простых до высокомасштабируемых.
Стратегии управления состоянием в Flutter:
- 🧱
setState()
— Встроенный, самый простой подход - 🏗️ InheritedWidget — Основание Flutter для распространения состояния
- 🪄 Provider — Рекомендуемое решение для большинства приложений
- 🔒 Riverpod — Современная, безопасная для компиляции эволюция Provider
- 📦 Bloc — Для масштабируемых, корпоративных приложений
- ⚡ GetX — Легковесное решение “всё в одном”
1. Использование setState()
— Основы
Самый простой способ управления состоянием в Flutter — использование встроенного метода setState()
в StatefulWidget
.
Этот подход идеален для локального состояния интерфейса — когда состояние принадлежит одному виджету и не требует распространения по всему приложению.
Пример: Счетчик с setState()
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: CounterScreen());
}
}
class CounterScreen extends StatefulWidget {
@override
_CounterScreenState createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('setState Counter')),
body: Center(
child: Text('Count: $_count', style: TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
onPressed: _increment,
child: Icon(Icons.add),
),
);
}
}
Преимущества
- Очень просто в реализации
- Отлично подходит для локального или временного состояния интерфейса
- Нет внешних зависимостей
Недостатки
- Не масштабируется для больших приложений
- Сложно делиться состоянием между виджетами
- Логика смешана с интерфейсом
Используйте, когда:
- Прототипирование или создание небольших виджетов
- Обработка изолированного состояния интерфейса (например, переключение кнопки, отображение модального окна)
2. InheritedWidget — Основание Flutter
InheritedWidget
— это низкоуровневый механизм, который Flutter использует для распространения данных вниз по дереву виджетов. Большинство решений для управления состоянием (включая Provider) построены на его основе.
Понимание InheritedWidget помогает вам понять, как работает управление состоянием в Flutter на низком уровне.
Пример: Менеджер тем с InheritedWidget
import 'package:flutter/material.dart';
// InheritedWidget, который содержит данные темы
class AppTheme extends InheritedWidget {
final bool isDarkMode;
final Function toggleTheme;
const AppTheme({
Key? key,
required this.isDarkMode,
required this.toggleTheme,
required Widget child,
}) : super(key: key, child: child);
// Метод доступа для получения ближайшего AppTheme вверх по дереву виджетов
static AppTheme? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<AppTheme>();
}
@override
bool updateShouldNotify(AppTheme oldWidget) {
return isDarkMode != oldWidget.isDarkMode;
}
}
// Stateful обертка для управления изменениями состояния
class ThemeManager extends StatefulWidget {
final Widget child;
const ThemeManager({Key? key, required this.child}) : super(key: key);
@override
_ThemeManagerState createState() => _ThemeManagerState();
}
class _ThemeManagerState extends State<ThemeManager> {
bool _isDarkMode = false;
void _toggleTheme() {
setState(() {
_isDarkMode = !_isDarkMode;
});
}
@override
Widget build(BuildContext context) {
return AppTheme(
isDarkMode: _isDarkMode,
toggleTheme: _toggleTheme,
child: widget.child,
);
}
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ThemeManager(
child: Builder(
builder: (context) {
final theme = AppTheme.of(context);
return MaterialApp(
theme: theme!.isDarkMode ? ThemeData.dark() : ThemeData.light(),
home: HomeScreen(),
);
},
),
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = AppTheme.of(context);
return Scaffold(
appBar: AppBar(title: Text('InheritedWidget Theme')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Текущая тема: ${theme!.isDarkMode ? "Темная" : "Светлая"}',
style: TextStyle(fontSize: 20),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => theme.toggleTheme(),
child: Text('Переключить тему'),
),
],
),
),
);
}
}
Преимущества
- Встроено в Flutter — нет внешних зависимостей
- Эффективные перестройки виджетов
- Основание для понимания других решений
- Прямой контроль над логикой распространения
Недостатки
- Много шаблонного кода
- Требуется обертка StatefulWidget
- Легко допустить ошибки
- Не подходит для новичков
Используйте, когда:
- Изучаете, как работает управление состоянием в Flutter внутренне
- Создаете собственные решения для управления состоянием
- Вам нужна очень специфическая контроль над распространением состояния
3. Provider — Рекомендуемое решение Flutter
Когда состояние вашего приложения нужно делиться между несколькими виджетами, Provider приходит на помощь.
Provider основан на Инверсии управления — вместо того, чтобы виджеты владели состоянием, провайдер предоставляет его для использования другими. Команда Flutter официально рекомендует его для средних приложений.
Настройка
Добавьте это в ваш pubspec.yaml
:
dependencies:
provider: ^6.0.5
Пример: Счетчик с Provider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MyApp(),
),
);
}
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: CounterScreen());
}
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = context.watch<CounterModel>();
return Scaffold(
appBar: AppBar(title: Text('Provider Counter')),
body: Center(
child: Text('Count: ${counter.count}', style: TextStyle(fontSize: 24)),
),
floatingActionButton: FloatingActionButton(
onPressed: context.read<CounterModel>().increment,
child: Icon(Icons.add),
),
);
}
}
Преимущества
- Реактивные и эффективные обновления
- Чистое разделение интерфейса и логики
- Хорошо документировано и поддерживается сообществом
Недостатки
- Немного больше шаблонного кода, чем у
setState()
- Вложенные провайдеры могут становиться сложными
Используйте, когда:
- Вам нужно общее состояние между несколькими виджетами
- Вы хотите реактивный паттерн без сложности
- Ваше приложение выходит за рамки прототипа
4. Riverpod — Современная эволюция Provider
Riverpod — это полная переработка Provider, которая устраняет зависимость от BuildContext и добавляет проверку типов на этапе компиляции. Она разработана для решения ограничений Provider, сохраняя при этом ту же философию.
Установка
Добавьте это в ваш pubspec.yaml
:
dependencies:
flutter_riverpod: ^2.4.0
Пример: Профиль пользователя с Riverpod
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Модель
class UserProfile {
final String name;
final int age;
UserProfile({required this.name, required this.age});
UserProfile copyWith({String? name, int? age}) {
return UserProfile(
name: name ?? this.name,
age: age ?? this.age,
);
}
}
// State Notifier
class ProfileNotifier extends StateNotifier<UserProfile> {
ProfileNotifier() : super(UserProfile(name: 'Гость', age: 0));
void updateName(String name) {
state = state.copyWith(name: name);
}
void updateAge(int age) {
state = state.copyWith(age: age);
}
}
// Provider
final profileProvider = StateNotifierProvider<ProfileNotifier, UserProfile>((ref) {
return ProfileNotifier();
});
// Вычисляемый provider
final greetingProvider = Provider<String>((ref) {
final profile = ref.watch(profileProvider);
return 'Привет, ${profile.name}! Вам ${profile.age} лет.';
});
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: ProfileScreen());
}
}
class ProfileScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final profile = ref.watch(profileProvider);
final greeting = ref.watch(greetingProvider);
return Scaffold(
appBar: AppBar(title: Text('Профиль Riverpod')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(greeting, style: TextStyle(fontSize: 20)),
SizedBox(height: 30),
TextField(
decoration: InputDecoration(labelText: 'Имя'),
onChanged: (value) {
ref.read(profileProvider.notifier).updateName(value);
},
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Возраст: ${profile.age}', style: TextStyle(fontSize: 18)),
SizedBox(width: 20),
ElevatedButton(
onPressed: () {
ref.read(profileProvider.notifier).updateAge(profile.age + 1);
},
child: Text('+'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
if (profile.age > 0) {
ref.read(profileProvider.notifier).updateAge(profile.age - 1);
}
},
child: Text('-'),
),
],
),
],
),
),
);
}
}
Преимущества
- Нет зависимости от BuildContext — доступ к состоянию из любого места
- Проверка типов на этапе компиляции — обнаружение ошибок до запуска
- Лучшая тестируемость
- Мощное составление состояния
- Нет утечек памяти
Недостатки
- Отличный API от традиционных паттернов Flutter
- Более крутая кривая обучения, чем у Provider
- Меньшее сообщество (но быстро растет)
Используйте, когда:
- Начинаете новый проект на Flutter
- Хотите безопасность типов и гарантии на этапе компиляции
- Сложные зависимости и составление состояния
- Работаете над приложениями среднего и крупного масштаба
5. BLoC (Business Logic Component) — Для корпоративных приложений
Паттерн BLoC — это более продвинутая архитектура, которая полностью отделяет бизнес-логику от интерфейса пользователя с использованием потоков.
Он идеален для крупномасштабных или корпоративных приложений, где предсказуемые и тестируемые переходы состояния имеют решающее значение.
Установка
Добавьте эту зависимость в ваш проект:
dependencies:
flutter_bloc: ^9.0.0
Пример: Счетчик с BLoC
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// События
abstract class CounterEvent {}
class Increment extends CounterEvent {}
// Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>((event, emit) => emit(state + 1));
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterBloc(),
child: MaterialApp(home: CounterScreen()),
);
}
}
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Счетчик BLoC')),
body: Center(
child: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Text('Счет: $count', style: TextStyle(fontSize: 24));
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(Increment()),
child: Icon(Icons.add),
),
);
}
}
Преимущества
- Масштабируемость и поддержка для сложных приложений
- Четкое разделение слоев
- Легкость модульного тестирования
Недостатки
- Больше шаблонного кода
- Крутая кривая обучения
Используйте, когда:
- Разрабатываете корпоративные или долгосрочные проекты
- Вам нужна предсказуемая и тестируемая логика
- Несколько разработчиков работают над разными модулями приложения
6. GetX — Легкое решение “все в одном”
GetX — это минималистичное, но мощное решение для управления состоянием, которое также включает маршрутизацию, внедрение зависимостей и утилиты. Оно известно тем, что имеет наименьшее количество шаблонного кода среди всех решений.
Установка
Добавьте это в ваш pubspec.yaml
:
dependencies:
get: ^4.6.5
Пример: Список покупок с GetX
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// Контроллер
class ShoppingController extends GetxController {
// Наблюдаемый список
var items = <String>[].obs;
// Наблюдаемый счетчик
var totalItems = 0.obs;
void addItem(String item) {
if (item.isNotEmpty) {
items.add(item);
totalItems.value = items.length;
}
}
void removeItem(int index) {
items.removeAt(index);
totalItems.value = items.length;
}
void clearAll() {
items.clear();
totalItems.value = 0;
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Список покупок GetX',
home: ShoppingScreen(),
);
}
}
class ShoppingScreen extends StatelessWidget {
// Инициализация контроллера
final ShoppingController controller = Get.put(ShoppingController());
final TextEditingController textController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Список покупок GetX'),
actions: [
Obx(() => Padding(
padding: EdgeInsets.all(16),
child: Center(
child: Text(
'Предметов: ${controller.totalItems}',
style: TextStyle(fontSize: 18),
),
),
)),
],
),
body: Column(
children: [
Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: TextField(
controller: textController,
decoration: InputDecoration(
hintText: 'Введите название предмета',
border: OutlineInputBorder(),
),
),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
controller.addItem(textController.text);
textController.clear();
},
child: Icon(Icons.add),
),
],
),
),
Expanded(
child: Obx(() {
if (controller.items.isEmpty) {
return Center(
child: Text('В вашем списке нет предметов'),
);
}
return ListView.builder(
itemCount: controller.items.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(controller.items[index]),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () => controller.removeItem(index),
),
);
},
);
}),
),
if (controller.items.isNotEmpty)
Padding(
padding: EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () {
Get.defaultDialog(
title: 'Очистить список',
middleText: 'Вы уверены, что хотите очистить все предметы?',
textConfirm: 'Да',
textCancel: 'Нет',
onConfirm: () {
controller.clearAll();
Get.back();
},
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
minimumSize: Size(double.infinity, 50),
),
child: Text('Очистить все'),
),
),
],
),
);
}
}
Преимущества
- Минимальный шаблонный код — самое быстрое развитие
- Решение “все в одном” (состояние, маршрутизация, внедрение зависимостей, уведомления, диалоги)
- Не требуется BuildContext
- Очень легковесный и быстрый
- Легко изучить
Недостатки
- Менее “Flutter-подобный” по сравнению с Provider/BLoC
- Может привести к тесному связыванию, если не быть осторожным
- Меньший экосистема, чем у Provider
- Некоторые разработчики находят его “слишком магическим”
Используйте, когда:
- Быстрое прототипирование или разработка MVP
- Приложения малого и среднего размера
- Хотите минимальный шаблонный код
- Команда предпочитает простоту строгой архитектуре
Выбор подходящего решения для управления состоянием
Решение | Сложность | Шаблонный код | Кривая обучения | Лучше всего для |
---|---|---|---|---|
🧱 setState() |
Низкая | Минимальный | Легко | Простое локальное состояние, прототипы |
🏗️ InheritedWidget |
Средняя | Высокий | Средний | Изучение внутренностей Flutter, пользовательские решения |
🪄 Provider |
Низкая-Средняя | Низкий | Легко | Большинство приложений, общий доступ к состоянию |
🔒 Riverpod |
Средняя | Низкая-Средняя | Средний | Современные приложения, безопасность типов |
📦 BLoC |
Высокая | Высокая | Крутая | Корпоративные приложения, сложная бизнес-логика |
⚡ GetX |
Низкая | Минимальный | Легко | Быстрое разработка, MVP |
Быстрое руководство по принятию решений:
Сложность приложения | Рекомендуемый подход | Пример использования |
---|---|---|
🪶 Простое | setState() или GetX |
Переключение кнопок, анимации, небольшие виджеты |
⚖️ Среднее | Provider или Riverpod |
Общие темы, настройки пользователя, кэширование данных |
🏗️ Сложное | BLoC или Riverpod |
Интернет-магазины, чат-приложения, финансовые дашборды |
Итоговые мысли
Нет универсального подхода к управлению состоянием в Flutter.
Вот практический подход:
- Начните с малого с
setState()
для простых виджетов и прототипов - Изучите основы с InheritedWidget, чтобы понять, как работает Flutter изнутри
- Переходите к Provider, когда ваше приложение растет и требует общего состояния
- Рассмотрите Riverpod для новых проектов, где важны безопасность типов и современные паттерны
- Примените BLoC для крупных корпоративных приложений со сложной бизнес-логикой
- Попробуйте GetX, когда приоритетом являются быстрая разработка и минимальный шаблонный код
Основные выводы:
✅ Не переусложняйте — используйте setState()
до тех пор, пока не потребуется больше
✅ Provider и Riverpod — отличные выборы для большинства приложений
✅ BLoC преуспевает в крупных командах и сложных приложениях
✅ GetX отлично подходит для скорости, но будьте осторожны с связыванием
✅ Понимание InheritedWidget помогает освоить любое решение
Ключ в балансе простоты, масштабируемости и поддерживаемости — и выборе правильного инструмента для ваших конкретных потребностей, экспертизы команды и требований проекта.
Ссылки на библиотеки управления состоянием Flutter
- Официальная документация Flutter по управлению состоянием
- Пакет Provider
- Документация Riverpod
- Библиотека BLoC
- Документация GetX