Перейти к основному содержимому

Provider и Riverpod во Flutter

Разработчику Архитектору

Во Flutter виджеты immutable (неизменяемы): при изменении данных нужно вызвать setState или передать новое состояние сверху. В экранах с формами, авторизацией и списками с API длинная цепочка параметров через десять уровней дерева становится неудобной.

Provider и Riverpod решают задачу доступа к общему состоянию из любого виджета в поддереве, с предсказуемым обновлением UI.

Практикум идёт по шагам — счётчик на Provider, корзина, async API, тот же сценарий на Riverpod, troubleshooting и упражнения.

ШагТемаЗачем
0Flutter project, pubspecБазовый каркас
1ChangeNotifier + ProviderМодель и подписка UI
2watch / read / selectПравильные rebuild
3Учебное приложение "Избранное"CRUD в памяти
4ProviderScope + NotifierRiverpod без BuildContext
5FutureProvider + APIAsyncValue и loading/error
6Тесты ProviderContainerПроверка без MaterialApp
7Маршрут выбора стекаProvider или Riverpod
ПодходИдеяПакет
setStateСостояние внутри одного StatefulWidgetВстроено
ProviderChangeNotifier + ChangeNotifierProviderprovider
RiverpodГраф провайдеров, ref.watchflutter_riverpod
МатериалЗачем
FlutterВиджеты, hot reload
Dart — типыКлассы, null safety
async/FutureЗагрузка с API
Паттерны и switch в Dart 3sealed-модели UI
Мобильные приложенияUI-паттерны

Навигация по разделу Dart

Hot reload

Запускайте flutter run в терминале или через IDE. После изменения моделей иногда нужен hot restart (R в консоли) — см. запуск приложений.


Шаг 0 — подготовка проекта

Создайте приложение:

flutter create favorites_lab
cd favorites_lab

pubspec.yaml — зависимости для обоих подходов (в учебнике можно подключать по очереди):

dependencies:
flutter:
sdk: flutter
provider: ^6.1.0
flutter_riverpod: ^2.6.0
http: ^1.2.0
flutter pub get

Структура каталогов (рекомендуемая):

lib/
main.dart
main_riverpod.dart # отдельная точка для Riverpod-ветки
models/
favorite_item.dart
provider_app/
favorites_model.dart
favorites_screen.dart
riverpod_app/
favorites_notifier.dart
favorites_screen.dart
services/
fake_api.dart

База Dart: типы, async/Future.


Шаг 1 — когда достаточно Provider

Provider — лёгкий вариант для учебных и средних приложений. Официальные codelabs Flutter исторически использовали Provider.

Модель FavoriteItem

lib/models/favorite_item.dart:

class FavoriteItem {
const FavoriteItem({required this.id, required this.title});

final String id;
final String title;
}

ChangeNotifier — FavoritesModel

lib/provider_app/favorites_model.dart:

import 'package:flutter/foundation.dart';
import '../models/favorite_item.dart';

class FavoritesModel extends ChangeNotifier {
final List<FavoriteItem> _items = [];

List<FavoriteItem> get items => List.unmodifiable(_items);
int get count => _items.length;

bool contains(String id) => _items.any((e) => e.id == id);

void add(FavoriteItem item) {
if (contains(item.id)) return;
_items.add(item);
notifyListeners();
}

void remove(String id) {
_items.removeWhere((e) => e.id == id);
notifyListeners();
}

void clear() {
_items.clear();
notifyListeners();
}
}

Разбор:

  • ChangeNotifier — mixin с подписчиками на изменения.
  • notifyListeners() сообщает Provider, что виджеты нужно обновить.
  • Геттер items возвращает неизменяемую копию — снаружи нельзя сломать инварианты.
  • Без notifyListeners() UI не перерисуется.

Подключение в дереве

lib/main.dart:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'provider_app/favorites_model.dart';
import 'provider_app/favorites_screen.dart';

void main() {
runApp(
ChangeNotifierProvider(
create: (_) => FavoritesModel(),
child: const MyApp(),
),
);
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Избранное (Provider)',
home: const FavoritesScreen(),
);
}
}

Разбор:

  • ChangeNotifierProvider создаёт модель один раз и хранит её выше дерева UI.
  • create вызывается лениво при первом watch/read.
  • Для нескольких моделей используйте MultiProvider.

Шаг 2 — чтение состояния в виджете

lib/provider_app/favorites_screen.dart:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/favorite_item.dart';
import 'favorites_model.dart';

class FavoritesScreen extends StatelessWidget {
const FavoritesScreen({super.key});

@override
Widget build(BuildContext context) {
final count = context.select<FavoritesModel, int>((m) => m.count);

return Scaffold(
appBar: AppBar(title: Text('Избранное ($count)')),
body: const _FavoritesList(),
floatingActionButton: const _AddFab(),
);
}
}

class _FavoritesList extends StatelessWidget {
const _FavoritesList();

@override
Widget build(BuildContext context) {
final items = context.watch<FavoritesModel>().items;

if (items.isEmpty) {
return const Center(child: Text('Список пуст'));
}

return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
title: Text(item.title),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => context.read<FavoritesModel>().remove(item.id),
),
);
},
);
}
}

class _AddFab extends StatelessWidget {
const _AddFab();

@override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: () {
final model = context.read<FavoritesModel>();
final id = DateTime.now().millisecondsSinceEpoch.toString();
model.add(FavoriteItem(id: id, title: 'Элемент $id'));
},
child: const Icon(Icons.add),
);
}
}
МетодКогда
context.watch<T>()Перестраивать виджет при изменении
context.read<T>()Одно действие (кнопка), без подписки
context.select<T, R>()Слушать только часть модели (здесь count)
read внутри build

Не вызывайте context.read для данных, которые должны отображаться на экране — UI не обновится. Для отображения — watch или select.


Шаг 3 — MultiProvider и разделение моделей

Если приложение растёт, разбивайте ChangeNotifier:

MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => FavoritesModel()),
ChangeNotifierProvider(create: (_) => CartModel()),
],
child: const MyApp(),
)

CartModel — пример из корзины (см. ниже). Один огромный ChangeNotifier сложно тестировать.

CartModel (краткий пример)

import 'package:flutter/foundation.dart';

class CartModel extends ChangeNotifier {
final List<String> _items = [];

List<String> get items => List.unmodifiable(_items);
int get count => _items.length;

void add(String name) {
_items.add(name);
notifyListeners();
}

void clear() {
_items.clear();
notifyListeners();
}
}

Паттерны UI — мобильные приложения, галерея виджетов (Lab).


Шаг 4 — переход на Riverpod

Riverpod (от автора Provider) убирает привязку к BuildContext для чтения состояния, упрощает тесты и композицию провайдеров (async, family, autoDispose).

ProviderScope

lib/main_riverpod.dart:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'riverpod_app/favorites_screen.dart';

void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Избранное (Riverpod)',
home: const RiverpodFavoritesScreen(),
);
}
}

Без ProviderScope вызов ref.watch завершится ошибкой.

NotifierProvider

lib/riverpod_app/favorites_notifier.dart:

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/favorite_item.dart';

class FavoritesNotifier extends Notifier<List<FavoriteItem>> {
@override
List<FavoriteItem> build() => [];

void add(FavoriteItem item) {
if (state.any((e) => e.id == item.id)) return;
state = [...state, item];
}

void remove(String id) {
state = state.where((e) => e.id != id).toList();
}

void clear() {
state = [];
}
}

final favoritesProvider =
NotifierProvider<FavoritesNotifier, List<FavoriteItem>>(
FavoritesNotifier.new,
);

Разбор:

  • build() — начальное состояние (аналог конструктора).
  • state = ... с новым списком — immutable update, Riverpod уведомляет слушателей.
  • NotifierProvider связывает класс и тип состояния.

ConsumerWidget

lib/riverpod_app/favorites_screen.dart:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/favorite_item.dart';
import 'favorites_notifier.dart';

class RiverpodFavoritesScreen extends ConsumerWidget {
const RiverpodFavoritesScreen({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final items = ref.watch(favoritesProvider);

return Scaffold(
appBar: AppBar(title: Text('Избранное (${items.length})')),
body: items.isEmpty
? const Center(child: Text('Список пуст'))
: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
title: Text(item.title),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () =>
ref.read(favoritesProvider.notifier).remove(item.id),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
final id = DateTime.now().millisecondsSinceEpoch.toString();
ref.read(favoritesProvider.notifier).add(
FavoriteItem(id: id, title: 'Элемент $id'),
);
},
child: const Icon(Icons.add),
),
);
}
}

Разбор:

  • WidgetRef ref заменяет context.watch — удобно в тестах без дерева виджетов.
  • ref.watch(favoritesProvider) подписывает виджет на изменения.
  • ref.read(favoritesProvider.notifier).add(...) — изменение без лишних rebuild всего списка в других виджетах.

Шаг 5 — async и загрузка с API

Fake API

lib/services/fake_api.dart:

import '../models/favorite_item.dart';

Future<List<FavoriteItem>> fetchCatalog() async {
await Future.delayed(const Duration(seconds: 1));
return [
const FavoriteItem(id: '1', title: 'Dart'),
const FavoriteItem(id: '2', title: 'Flutter'),
const FavoriteItem(id: '3', title: 'Riverpod'),
];
}

FutureProvider (Riverpod)

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/favorite_item.dart';
import '../services/fake_api.dart';

final catalogProvider = FutureProvider<List<FavoriteItem>>((ref) async {
return fetchCatalog();
});

UI:

class CatalogTab extends ConsumerWidget {
const CatalogTab({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncCatalog = ref.watch(catalogProvider);

return asyncCatalog.when(
data: (items) => ListView(
children: items
.map((e) => ListTile(
title: Text(e.title),
trailing: IconButton(
icon: const Icon(Icons.star_border),
onPressed: () => ref
.read(favoritesProvider.notifier)
.add(e),
),
))
.toList(),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Ошибка: $error')),
);
}
}

Разбор:

  • AsyncValue инкапсулирует loading / data / error — меньше ручных флагов.
  • ref.refresh(catalogProvider) перезапускает загрузку.
  • Реальный HTTP — пакет http, см. консоль и HTTP, REST API.

Provider + FutureBuilder

Аналог без Riverpod:

Future<List<FavoriteItem>>? _future;

@override
void initState() {
super.initState();
_future = fetchCatalog();
}

@override
Widget build(BuildContext context) {
return FutureBuilder<List<FavoriteItem>>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Ошибка: ${snapshot.error}');
}
final items = snapshot.data ?? [];
return ListView(/* ... */);
},
);
}

Примеры FutureBuilderгалерея виджетов (Lab).


Шаг 6 — family, autoDispose, select

family — параметризованный провайдер

final itemProvider = FutureProvider.family<FavoriteItem?, String>((ref, id) async {
final catalog = await ref.watch(catalogProvider.future);
return catalog.cast<FavoriteItem?>().firstWhere(
(e) => e?.id == id,
orElse: () => null,
);
});

Использование: ref.watch(itemProvider('2')).

autoDispose

final searchQueryProvider = StateProvider.autoDispose<String>((ref) => '');

Состояние сбрасывается, когда нет слушателей — удобно для полей поиска на экране.

select в Riverpod

final count = ref.watch(
favoritesProvider.select((items) => items.length),
);

Rebuild только при изменении длины списка — аналог context.select в Provider.


Шаг 7 — тестирование

Provider

import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import '../lib/provider_app/favorites_model.dart';

void main() {
testWidgets('add increases count', (tester) async {
await tester.pumpWidget(
ChangeNotifierProvider(
create: (_) => FavoritesModel(),
child: MaterialApp(
home: Builder(
builder: (context) {
context.read<FavoritesModel>().add(
const FavoriteItem(id: '1', title: 'Test'),
);
final count = context.watch<FavoritesModel>().count;
return Text('$count');
},
),
),
),
);

expect(find.text('1'), findsOneWidget);
});
}

Riverpod — ProviderContainer

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../lib/models/favorite_item.dart';
import '../lib/riverpod_app/favorites_notifier.dart';

void main() {
test('add item', () {
final container = ProviderContainer();
addTearDown(container.dispose);

container.read(favoritesProvider.notifier).add(
const FavoriteItem(id: '1', title: 'A'),
);

expect(container.read(favoritesProvider).length, 1);
});
}

Разбор:

  • ProviderContainer — тестовый контекст без ProviderScope в виджетах.
  • Тесты Riverpod часто проще и быстрее widget-тестов.

Запуск:

flutter test

Сравнение Provider и Riverpod

КритерийProviderRiverpod
Порог входаНижеЧуть выше (новые термины)
Зависимость от BuildContextДаНет (ref)
Async из коробкиFutureProvider (пакет)FutureProvider, AsyncValue
Compile-time safetyСлабееСильнее (типы провайдеров)
ТестированиеВозможноУдобнее (ProviderContainer)
Документация FlutterОфициальные codelabsАктивное community

Оба пакета дополняют архитектуру, но не заменяют её. Разделяйте UI, домен и данные — см. паттерны проектирования.

Параллель по идее "UI от состояния" — Kotlin Compose.


Sealed-модели UI (Dart 3)

Для сложных экранов комбинируйте Riverpod и sealed class:

sealed class CatalogState {}

class CatalogLoading extends CatalogState {}

class CatalogData extends CatalogState {
CatalogData(this.items);
final List<FavoriteItem> items;
}

class CatalogError extends CatalogState {
CatalogError(this.message);
final String message;
}

В build:

return switch (state) {
CatalogLoading() => const CircularProgressIndicator(),
CatalogData(:final items) => ListView(/* ... */),
CatalogError(:final message) => Text(message),
};

Исчерпывающий switch — компилятор проверит все варианты.


Типичные ошибки и troubleshooting

ОшибкаСимптомРешение
read внутри buildUI не обновляетсяИспользуйте watch или select
Забыли notifyListenersСписок застылВызов после изменения полей
Нет ProviderScopeCrash при ref.watchОбернуть runApp
Огромный ChangeNotifierСложно тестироватьРазбить на несколько моделей
watch в callbackЛишние rebuildread в onPressed
Provider not foundException при watchProvider выше по дереву
Riverpod override в testСтарый stateProviderContainer(overrides: [...])
Async gapsetState after disposeПроверять mounted или отмена
DevTools

Flutter DevTools показывает rebuild stats — полезно, когда watch стоит слишком высоко в дереве и перерисовывает весь экран.


Упражнения

  1. Добавьте переключатель темы через ChangeNotifier / StateNotifierThemeMode light/dark.
  2. Реализуйте поиск по каталогу с StateProvider.autoDispose и debounce 300 ms.
  3. Напишите widget-тест: tap FAB → элемент появился в списке (Provider).
  4. Напишите unit-тест FavoritesNotifier без MaterialApp (Riverpod).
  5. Загрузите реальный JSON с публичного API через http — обработайте 404 в AsyncValue.error.
Подсказка к упражнению 2
final debouncedSearchProvider = FutureProvider.autoDispose<String>((ref) async {
final query = ref.watch(searchQueryProvider);
await Future.delayed(const Duration(milliseconds: 300));
return query;
});

Следите за отменой через ref.onDispose при смене query.


FAQ

Provider устарел?

Пакет поддерживается. Riverpod — эволюция идей, не обязательная миграция для маленьких приложений.

Нужен ли BLoC?

BLoC — отдельный паттерн с событиями и состояниями. Provider/Riverpod проще для старта. Выбор зависит от команды и масштаба.

Riverpod 3?

Следите за changelog на pub.dev. API Notifier / NotifierProvider — современная база (Riverpod 2.x+).

Где хранить API-ключи?

Не в коде. --dart-define, env-файлы, backend proxy — конфигурации.

Можно смешивать Provider и Riverpod?

Технически да, но в одном проекте лучше один основной подход — меньше путаницы.

GetX, MobX?

Альтернативы с другой философией. Provider/Riverpod — наиболее частые в учебных материалах Flutter.


Маршрут обучения

  1. Flutter — виджеты, StatelessWidget / StatefulWidget.
  2. Provider на одном экране (счётчик, корзина, избранное).
  3. Riverpod — тот же экран + FutureProvider для API.
  4. Паттерны и switch в Dart 3 — sealed-модели состояний UI.
  5. Консоль и HTTP во Flutter/Dart — реальные запросы.
Практика

Реализуйте экран "Избранное" с ChangeNotifier и переключитесь на NotifierProvider — сравните объём boilerplate и тесты без MaterialApp.


Шаг 8 — полный пошаговый tutorial "Избранное"

Ниже — единый сценарий от пустого экрана до каталога с API. Выполняйте шаги по порядку.

8.1 Создайте модель

Файл lib/models/favorite_item.dart — см. Шаг 1.

8.2 Provider-ветка

  1. FavoritesModel extends ChangeNotifier.
  2. ChangeNotifierProvider в main.dart.
  3. FavoritesScreen с watch / read.
  4. flutter run -t lib/main.dart.

8.3 Riverpod-ветка

  1. FavoritesNotifier extends Notifier.
  2. ProviderScope в main_riverpod.dart.
  3. ConsumerWidget с ref.watch.
  4. flutter run -t lib/main_riverpod.dart.

8.4 Добавьте вкладки

DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Text('Demo'),
bottom: const TabBar(tabs: [
Tab(text: 'Каталог'),
Tab(text: 'Избранное'),
]),
),
body: const TabBarView(
children: [CatalogTab(), FavoritesTabHost()],
),
),
);

FavoritesTabHost — ваш список избранного; CatalogTabFutureProvider из Шаг 5.


Шаг 9 — навигация и передача состояния

С go_router Riverpod сочетается через ConsumerWidget на каждом экране:

GoRoute(
path: '/item/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ItemDetailScreen(itemId: id);
},
),

На экране детали:

final item = ref.watch(itemProvider(itemId));

Provider без Riverpod — передайте FavoritesModel через Provider.value или используйте ancestor context.watch.

Маршрутизация Flutter — Flutter, мобильные паттерны — навигация в приложениях.


Шаг 10 — persistence с shared_preferences

Сохранение id избранного между перезапусками (Riverpod):

dependencies:
shared_preferences: ^2.3.0
class FavoritesNotifier extends AsyncNotifier<List<FavoriteItem>> {
static const _key = 'favorite_ids';

@override
Future<List<FavoriteItem>> build() async {
final prefs = await SharedPreferences.getInstance();
final ids = prefs.getStringList(_key) ?? [];
return ids.map((id) => FavoriteItem(id: id, title: 'Saved $id')).toList();
}

Future<void> add(FavoriteItem item) async {
final current = state.value ?? [];
if (current.any((e) => e.id == item.id)) return;
final next = [...current, item];
state = AsyncData(next);
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList(_key, next.map((e) => e.id).toList());
}
}

Разбор:

  • AsyncNotifier — начальная загрузка async.
  • UI: ref.watch(favoritesProvider).when(...).
  • Для production — SQLite (drift) или Isar.

Конфигурация — конфигурации и данные.


Шаг 11 — StateNotifier и legacy Provider

Старый стиль Riverpod 1.x / Provider bridge:

class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
}

final counterProvider = StateNotifierProvider<CounterNotifier, int>(
(ref) => CounterNotifier(),
);

В Riverpod 2+ предпочтите Notifier — см. Шаг 4.


Шаг 12 — ConsumerStatefulWidget

Когда нужен lifecycle (initState, AnimationController):

class CatalogScreen extends ConsumerStatefulWidget {
const CatalogScreen({super.key});

@override
ConsumerState<CatalogScreen> createState() => _CatalogScreenState();
}

class _CatalogScreenState extends ConsumerState<CatalogScreen> {
@override
void initState() {
super.initState();
Future.microtask(() => ref.invalidate(catalogProvider));
}

@override
Widget build(BuildContext context) {
final async = ref.watch(catalogProvider);
return async.when(/* ... */);
}
}

ref доступен после super.initState() в microtask или в didChangeDependencies.


Шаг 13 — override провайдеров в тестах

testWidgets('catalog shows items', (tester) async {
final container = ProviderContainer(
overrides: [
catalogProvider.overrideWith((ref) async => [
const FavoriteItem(id: '1', title: 'Mock'),
]),
],
);
addTearDown(container.dispose);

await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: const MaterialApp(home: CatalogTab()),
),
);

await tester.pumpAndSettle();
expect(find.text('Mock'), findsOneWidget);
});

Override изолирует тест от сети — best practice для widget-тестов.


Шаг 14 — performance и rebuild

ПроблемаПричинаРешение
Весь экран мигаетwatch высоко в деревеselect, вынести списки
Лишние API callsFutureProvider без cachekeepAlive, ref.invalidate осознанно
Jank при addnotifyListeners для всей моделиМельче провайдеры

DevTools → Performance + Rebuild statsFlutter DevTools.


Шаг 15 — архитектура слоёв

UI (Widget)
↓ ref.watch / context.watch
State (Notifier / ChangeNotifier)

Repository (abstract)

API / DB (http, drift)
abstract class FavoritesRepository {
Future<List<FavoriteItem>> load();
Future<void> save(List<FavoriteItem> items);
}

final favoritesRepositoryProvider = Provider<FavoritesRepository>((ref) {
return InMemoryFavoritesRepository();
});

final favoritesProvider = AsyncNotifierProvider<FavoritesNotifier, List<FavoriteItem>>(
FavoritesNotifier.new,
);

FavoritesNotifier вызывает repository — UI не знает про HTTP.

Паттерны — проектирование.


Provider — расширенные паттерны

ProxyProvider

Когда модель зависит от другой:

MultiProvider(
providers: [
Provider(create: (_) => ApiClient()),
ChangeNotifierProxyProvider<ApiClient, CatalogModel>(
create: (_) => CatalogModel(null),
update: (_, api, model) => model!..updateApi(api),
),
],
child: const MyApp(),
)

ValueListenableProvider

Для простых значений без ChangeNotifier:

final counter = ValueNotifier<int>(0);

ValueListenableProvider.value(
value: counter,
child: ...,
);

Riverpod — Riverpod Generator (обзор)

Пакет riverpod_annotation + code generation:

@riverpod
class Favorites extends _$Favorites {
@override
List<FavoriteItem> build() => [];

void add(FavoriteItem item) {
state = [...state, item];
}
}

Команда dart run build_runner build генерирует favoritesProvider. Удобно в больших проектах — официальная документация на riverpod.dev.


Сравнение с другими подходами Flutter

ПодходСложностьКогда
setStateНизкаяОдин виджет, локальное UI
ProviderСредняяУчебные и средние apps
RiverpodСредняя+Тесты, async, масштаб
BLoCВышеСтрогие event/state потоки
GetXНизкая входМалые проекты, спорная архитектура

Выбор зависит от команды — Provider/Riverpod покрывают большинство учебных и production mobile apps.


Расширенный troubleshooting

СимптомПричинаРешение
Tried to modify provider while buildingwrite в buildПеренести в callback / post-frame
Infinite rebuildwatch + setState loopРазделить read/watch
FutureProvider каждый buildНовый FutureСтабильный provider, не inline Future
Hot reload state lostНорма для stateHot restart R
Platform channel + providerContext missingProvider выше MaterialApp
Exception in notifierStack в zoneFlutterError.onError

Дополнительные упражнения

  1. Dark theme через StateProvider<ThemeMode> + MaterialApp.themeMode.
  2. Pull-to-refresh каталога — RefreshIndicator + ref.invalidate(catalogProvider).
  3. Golden test для FavoritesScreenmatchesGoldenFile.
  4. Integration test — integration_test tap FAB, expect list tile.
  5. Migrate Provider app to Riverpod file-by-file с UncontrolledProviderScope.

Расширенный FAQ

Provider 7 и Riverpod 3?

Следите за migration guides на pub.dev — API Notifier уже стандарт Riverpod 2.

Equatable нужен с record-like models?

Dart не имеет record до 3 — для классов используйте equatable package; Riverpod сравнивает по identity state.

Riverpod без Flutter?

Пакет riverpod (pure Dart) — логика в CLI и server Dart.

InheritedWidget вручную?

Provider построен поверх InheritedWidget; Riverpod — отдельный механизм.

State management для games?

Flutter Flame — отдельные паттерны; Provider/Riverpod для UI меню и meta-game.

Web и Riverpod?

Работает — flutter run -d chrome; CORS для API — HTTP.


Полный листинг Provider main (reference)

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'models/favorite_item.dart';
import 'provider_app/favorites_model.dart';

void main() {
runApp(
ChangeNotifierProvider(
create: (_) => FavoritesModel()..bootstrap(),
child: const FavoritesApp(),
),
);
}

class FavoritesApp extends StatelessWidget {
const FavoritesApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Favorites Provider',
theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
home: const FavoritesHomePage(),
);
}
}

class FavoritesHomePage extends StatelessWidget {
const FavoritesHomePage({super.key});

@override
Widget build(BuildContext context) {
final items = context.watch<FavoritesModel>().items;

return Scaffold(
appBar: AppBar(
title: Text('Избранное (${items.length})'),
actions: [
if (items.isNotEmpty)
IconButton(
icon: const Icon(Icons.delete_sweep),
onPressed: () => context.read<FavoritesModel>().clear(),
),
],
),
body: items.isEmpty
? const Center(child: Text('Нажмите +'))
: ListView.builder(
itemCount: items.length,
itemBuilder: (context, i) {
final item = items[i];
return Dismissible(
key: ValueKey(item.id),
onDismissed: (_) =>
context.read<FavoritesModel>().remove(item.id),
child: ListTile(title: Text(item.title)),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
final n = DateTime.now().millisecondsSinceEpoch;
context.read<FavoritesModel>().add(
FavoriteItem(id: '$n', title: 'Item $n'),
);
},
child: const Icon(Icons.add),
),
);
}
}

Добавьте в FavoritesModel пустой bootstrap() для будущей загрузки из prefs.


Полный листинг Riverpod notifier (reference)

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/favorite_item.dart';

class FavoritesNotifier extends Notifier<List<FavoriteItem>> {
@override
List<FavoriteItem> build() => [];

void bootstrap() {
// ref.read(storageProvider) — в расширенной версии
}

bool contains(String id) => state.any((e) => e.id == id);

void toggle(FavoriteItem item) {
if (contains(item.id)) {
remove(item.id);
} else {
add(item);
}
}

void add(FavoriteItem item) {
if (contains(item.id)) return;
state = [...state, item];
}

void remove(String id) {
state = state.where((e) => e.id != id).toList(growable: false);
}

void clear() => state = [];
}

final favoritesProvider =
NotifierProvider<FavoritesNotifier, List<FavoriteItem>>(
FavoritesNotifier.new,
);

final favoritesCountProvider = Provider<int>((ref) {
return ref.watch(favoritesProvider.select((list) => list.length));
});

favoritesCountProvider — derived state без дублирования логики в UI.


Чек-лист перед релизом

  • Нет read там, где нужен watch
  • notifyListeners после каждой мутации (Provider)
  • ProviderScope корень дерева (Riverpod)
  • Async UI показывает loading и error
  • Widget-тесты на ключевые сценарии
  • Unit-тесты notifier без сети
  • API keys не в репозитории

См. мобильные приложения — релиз.


Глоссарий

ТерминСмысл
BuildContextСвязь виджета с деревом; нужен Provider
ChangeNotifierКласс с notifyListeners
ProviderScopeКорень Riverpod
WidgetRefДоступ к провайдерам в Riverpod
AsyncValueloading / data / error обёртка
familyПровайдер с параметром
autoDisposeОсвобождение при отсутствии слушателей

Полный CatalogTab с http (reference)

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import '../models/favorite_item.dart';
import 'favorites_notifier.dart';

Future<List<FavoriteItem>> fetchCatalogFromNetwork() async {
// Замените URL на свой API или mock-сервер
final uri = Uri.parse('https://jsonplaceholder.typicode.com/users');
final response = await http.get(uri);
if (response.statusCode != 200) {
throw Exception('HTTP ${response.statusCode}');
}
final list = jsonDecode(response.body) as List<dynamic>;
return list
.map((e) => FavoriteItem(
id: '${e['id']}',
title: e['name'] as String,
))
.toList();
}

final remoteCatalogProvider = FutureProvider<List<FavoriteItem>>((ref) {
return fetchCatalogFromNetwork();
});

class CatalogTab extends ConsumerWidget {
const CatalogTab({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final catalog = ref.watch(remoteCatalogProvider);

return catalog.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Ошибка: $e'),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () => ref.invalidate(remoteCatalogProvider),
child: const Text('Повторить'),
),
],
),
),
data: (items) => RefreshIndicator(
onRefresh: () async => ref.invalidate(remoteCatalogProvider),
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
final inFav = ref.watch(
favoritesProvider.select((list) => list.any((f) => f.id == item.id)),
);
return ListTile(
title: Text(item.title),
trailing: IconButton(
icon: Icon(inFav ? Icons.star : Icons.star_border),
onPressed: () {
final notifier = ref.read(favoritesProvider.notifier);
if (inFav) {
notifier.remove(item.id);
} else {
notifier.add(item);
}
},
),
);
},
),
),
);
}
}

Зависимость httpконсоль и HTTP во Flutter/Dart. jsonplaceholder — публичный учебный API без auth.


Типы провайдеров Riverpod — шпаргалка

ProviderНазначение
ProviderRead-only вычисление, сервисы
StateProviderПростое mutable state (int, String)
NotifierProviderСложная логика state
AsyncNotifierProviderAsync load + mutations
FutureProviderOne-shot async
StreamProviderStream из Firebase/WebSocket
StateNotifierProviderLegacy (Riverpod 1 style)

Связанные материалы

ТемаМатериал
Flutter основыFlutter
HTTPКонсоль и HTTP во Flutter/Dart
Dart разделDart — о разделе
Виджеты LabFlutter — готовые виджеты
Декларативный UIKotlin Compose

В подборках

Статья дополняет маршрут Flutter и блок мобильной разработки — мобильные приложения.


Второй проход — InheritedWidget, BLoC и performance (черновик)

InheritedWidget (как работает Provider под капотом)

Provider использует InheritedWidget для распространения значения вниз по дереву. Учебный минимум:

class CounterScope extends InheritedWidget {
const CounterScope({
super.key,
required this.count,
required super.child,
});

final int count;

static CounterScope of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CounterScope>()!;
}

@override
bool updateShouldNotify(CounterScope old) => count != old.count;
}

dependOnInheritedWidgetOfExactType подписывает виджет на изменения — аналог watch.

Когда смотреть BLoC

BLoC — события → состояния → UI. Подходит для больших команд с явными state machines. Provider/Riverpod проще для старта; BLoC — если нужен audit trail событий (analytics, undo).

КритерийProvider/RiverpodBLoC
Порог входаНижеВыше
СобытияНеявные (методы)Явные Event class
ТестыProviderContainerblocTest

ListView performance

ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ListTile(title: Text(items[index].title)),
)

Не стройте ListView(children: items.map(...).toList()) на тысячах элементов — builder ленивый.

State restoration

Для сохранения состояния при kill процесса — RestorationMixin, restorationId на Scaffold. Riverpod: StateNotifier + SharedPreferences provider для персистентности избранного.

Integration test

// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('tap FAB adds item', (tester) async {
// pumpWidget MyApp, tap FAB, expect find.text
});
}

Запуск: flutter test integration_test/. Отличается от unit ProviderContainer — полный UI on device/emulator.


Дополнительные темы Flutter (кратко)

Golden tests

await expectLater(
find.byType(MyWidget),
matchesGoldenFile('goldens/my_widget.png'),
);

Фиксируют pixel-perfect UI; обновление: flutter test --update-goldens.

Flavors и --dart-define

flutter run --dart-define=API_URL=https://staging.example.com

Разделение staging/production без дублирования кода — см. конфигурации.

Performance overlay

import 'package:flutter/rendering.dart';
void main() {
debugPaintLayerBordersEnabled = true;
runApp(const MyApp());
}

Полезно при jank и лишних rebuild.


Чек-лист перед релизом Flutter

  • flutter analyze без ошибок
  • flutter test зелёный
  • Версия в pubspec.yaml и store listing совпадают
  • ProGuard/R8 rules для Android release проверены
  • iOS signing и capabilities настроены в Xcode

Связанные материалы: Dart — о разделе · мобильная разработка.


Дополнение: accessibility

Semantics(
label: 'Add item button',
child: FloatingActionButton(onPressed: _add, child: const Icon(Icons.add)),
)

VoiceOver и TalkBack читают Semantics — проверяйте на реальном устройстве перед релизом.


Дополнение: internationalization

import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// MaterialApp.localizationsDelegates + supportedLocales

Строки UI выносят в ARB-файлы; не хардкодьте текст в виджетах для store locales.


Итог статьи

Flutter-практикум завершён чек-листом релиза, golden tests и a11y — переходите к Dart intro для языковых основ.


Содержание