Рубрики
Flutter

Riverpod 2.0: Полное руководство для начинающих

Полное руководство по Riverpod 2.0: основные концепции, провайдеры, кодогенерация и примеры использования во Flutter.

Riverpod 2.0 — это major update популярной библиотеки управления состоянием для Flutter. В этом руководстве разберём основные концепции, провайдеры и кодогенерацию с примерами.

Что такое Riverpod?

Riverpod (Provider наоборот) — это современная система управления состоянием от Remi Rousselet, автора пакета provider. Это следующая эволюция, которая решает многие ограничения оригинального Provider.

Ключевые преимущества: — Compile-time безопасность — ошибки обнаруживаются при компиляции — Не требует BuildContext — доступ к состоянию из любого места — Auto-dispose — автоматическое освобождение ресурсов — Code generation — типобезопасные провайдеры — Тестирование — простое мокирование зависимостей

Установка

Добавьте зависимости в pubspec.yaml:

dependencies:
  flutter_riverpod: ^2.5.0

dev_dependencies:
  riverpod_generator: ^2.4.0
  riverpod_lint: ^2.3.0
  build_runner: ^2.4.0

Основные концепции

Provider

Provider — это источник данных. Он может быть: — Значением (число, строка, объект) — Функцией, которая вычисляет значение — Async-функцией для загрузки данных из API — Stream для real-time данных

// Простой провайдер значения
final counterProvider = Provider<int>((ref) => 42);

// Провайдер с вычислением
final doubleProvider = Provider<int>((ref) {
  final value = ref.watch(counterProvider);
  return value * 2;
});

StateProvider

StateProvider — для простого изменяемого состояния:

final countProvider = StateProvider<int>((ref) => 0);

// В виджете
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(countProvider);

    return ElevatedButton(
      onPressed: () => ref.read(countProvider.notifier).state++,
      child: Text('Count: $count'),
    );
  }
}

StateNotifierProvider

Для более сложной логики используйте StateNotifier:

class Counter extends StateNotifier<int> {
  Counter() : super(0);

  void increment() => state++;
  void decrement() => state--;
  void reset() => state = 0;
}

final counterNotifierProvider = StateNotifierProvider<Counter, int>((ref) {
  return Counter();
});

AsyncNotifierProvider

Для работы с asynchronous данными (API, базы данных):

@riverpod
class UserRepository extends _$UserRepository {
  @override
  Future<User> build(String userId) async {
    final response = await http.get('/api/users/$userId');
    return User.fromJson(jsonDecode(response.body));
  }
}

// Использование
class UserWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userRepositoryProvider('123'));

    return userAsync.when(
      data: (user) => Text(user.name),
      loading: () => CircularProgressIndicator(),
      error: (err, stack) => Text('Error: $err'),
    );
  }
}

Code Generation в Riverpod 2.0

Riverpod 2.0 поддерживает кодогенерацию для большей типобезопасности:

// Объявление провайдера с аннотацией
@riverpod
String greeting(GreetingRef ref) {
  return 'Hello, World!';
}

// Параметризованный провайдер
@riverpod
Future<User> fetchUser(FetchUserRef ref, int id) async {
  final response = await http.get('/api/users/$id');
  return User.fromJson(jsonDecode(response.body));
}

// Class-based провайдер с состоянием
@riverpod
class Counter extends _$Counter {
  @override
  int build() => 0;

  void increment() => state++;
  void decrement() => state--;
}

После добавления аннотации запустите генератор:

flutter pub run build_runner build

Чтение и обновление состояния

ref.watch — подписаться на изменения (в build методе):

final value = ref.watch(myProvider);

ref.read — получить значение без подписки (в callback):

onPressed: () {
  final value = ref.read(myProvider);
  print(value);
  // Или для StateProvider:
  ref.read(myProvider.notifier).state++;
}

ref.listen — слушать изменения и реагировать:

ref.listen<int>(countProvider, (previous, next) {
  print('Count changed from $previous to $next');
  if (next > 10) {
    showNotification('Limit reached!');
  }
});

ConsumerWidget и ConsumerStatefulWidget

Для использования провайдеров в виджетах:

// Stateless виджет с провайдерами
class MyScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(countProvider);
    return Text('Count: $count');
  }
}

// Stateful виджет
class MyScreen extends ConsumerStatefulWidget {
  @override
  ConsumerState<MyScreen> createState() => _MyScreenState();
}

class _MyScreenState extends ConsumerState<MyScreen> {
  @override
  Widget build(BuildContext context) {
    final count = ref.watch(countProvider);
    return ElevatedButton(
      onPressed: () => ref.read(countProvider.notifier).state++,
      child: Text('Count: $count'),
    );
  }
}

ProviderScope и ProviderContainer

Оберните приложение в ProviderScope:

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

Для тестов используйте ProviderContainer:

test('counter increments', () {
  final container = ProviderContainer();

  // Чтение значения
  expect(container.read(countProvider), 0);

  // Изменение значения
  container.read(countProvider.notifier).state++;

  expect(container.read(countProvider), 1);

  // Не забудьте удалить контейнер
  container.dispose();
});

Модификаторы провайдеров

.family

Параметризованные провайдеры:

final userProvider = FutureProvider.family<User, int>((ref, id) async {
  final response = await http.get('/api/users/$id');
  return User.fromJson(jsonDecode(response.body));
});

// Использование
final userAsync = ref.watch(userProvider(123));

.autoDispose

Автоматическое освобождение ресурсов когда никто не слушает:

final socketProvider = StreamProvider.autoDispose<WebSocket>((ref) {
  final socket = WebSocket.connect('ws://example.com');

  ref.onDispose(() => socket.close());

  return socket;
});

Полный пример приложения

Создадим простое ToDo приложение:

// Модель задачи
class Todo {
  final String id;
  final String title;
  final bool completed;

  Todo({required this.id, required this.title, this.completed = false});

  Todo copyWith({String? title, bool? completed}) {
    return Todo(
      id: id,
      title: title ?? this.title,
      completed: completed ?? this.completed,
    );
  }
}

// Провайдер списка задач
@riverpod
class TodoList extends _$TodoList {
  @override
  List<Todo> build() {
    return [];
  }

  void add(String title) {
    state = [...state, Todo(id: DateTime.now().toString(), title: title)];
  }

  void toggle(String id) {
    state = [
      for (final todo in state)
        if (todo.id == id) todo.copyWith(completed: !todo.completed) else todo
    ];
  }

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

// UI
class TodoApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todos = ref.watch(todoListProvider);

    return Scaffold(
      appBar: AppBar(title: Text('Todo Riverpod')),
      body: ListView.builder(
        itemCount: todos.length,
        itemBuilder: (context, index) {
          final todo = todos[index];
          return ListTile(
            leading: Checkbox(
              value: todo.completed,
              onChanged: (_) => ref.read(todoListProvider.notifier).toggle(todo.id),
            ),
            title: Text(todo.title),
            trailing: IconButton(
              icon: Icon(Icons.delete),
              onPressed: () => ref.read(todoListProvider.notifier).remove(todo.id),
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddDialog(context, ref),
        child: Icon(Icons.add),
      ),
    );
  }
}

Тестирование

Riverpod упрощает тестирование:

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('adds todo', () {
    final container = ProviderContainer();

    // Добавляем задачу
    container.read(todoListProvider.notifier).add('Test task');

    // Проверяем
    expect(container.read(todoListProvider).length, 1);
    expect(container.read(todoListProvider).first.title, 'Test task');

    container.dispose();
  });

  testWidgets('toggles todo', (tester) async {
    await tester.pumpWidget(
      ProviderScope(
        child: MaterialApp(home: TodoApp()),
      ),
    );

    // Добавляем задачу через UI
    await tester.tap(find.byType(FloatingActionButton));
    await tester.enterText(find.byType(TextField), 'New task');
    await tester.tap(find.text('Add'));
    await tester.pump();

    // Переключаем
    await tester.tap(find.byType(Checkbox));
    await tester.pump();

    // Проверяем
    expect(find.text('New task'), findsOneWidget);
  });
}

Лучшие практики

  1. Используйте code generation для большей типобезопасности
  2. Разделяйте провайдеры по файлам по domain
  3. Применяйте autoDispose для ресурсов которые нужно освобождать
  4. Тестируйте провайдеры изолированно от UI
  5. Используйте family для параметризованных данных
  6. Избегайте watch в методах кроме build (используйте read)

Заключение

Riverpod 2.0 — это мощная и типобезопасная система управления состоянием для Flutter. Кодогенерация делает код чище, а compile-time проверки помогают избегать runtime ошибок.

Начните с простого: — Для простых счетчиков используйте StateProvider — Для сложной логики — StateNotifier или @riverpod class — Для API запросов — FutureProvider или AsyncNotifierProvider — Для streams — StreamProvider

Документация: https://riverpod.dev