Руководство по шаблону мутаций AsyncNotifier в Riverpod 3.0
Изучите правильный шаблон реализации методов AsyncNotifier в Riverpod 3.0 для предотвращения ошибок утилизации. Полное руководство с примерами кода и лучшими практиками для безопасных мутаций.
Какой правильный паттерн для реализации методов AsyncNotifier, выполняющих мутации в Riverpod 3.0?
Недавно я обновился с Riverpod 2.x до 3.0 и столкнулся с ошибкой освобождения ресурсов:
Необработанное исключение: Нельзя использовать Ref noteControllerProvider после его освобождения. Это обычно происходит, если:
- Провайдер был перестроен, но предыдущая “сборка” все еще ожидала выполнения и продолжает выполнять операции. Поэтому вы должны либо использовать ref.onDispose для отмены ожидающих работ, либо проверять ref.mounted после асинхронных интервалов или всего, что может сделать провайдер недействительным.
- Вы пытались использовать Ref внутри onDispose или других жизненных циклах. Это не поддерживается, так как провайдер уже освобождается.
Вот моя текущая реализация:
@riverpod
class NoteController extends _$NoteController {
@override
FutureOr<void> build() async {}
Future<void> deleteNote(String id) async {
final repository = ref.read(noteRepositoryProvider);
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await repository.deleteNote(id);
ref.invalidate(getNotesProvider);
});
}
}
Какие изменения мне нужно внести в мои методы AsyncNotifier, чтобы правильно обрабатывать мутации в Riverpod 3.0 и избегать ошибок освобождения ресурсов?
Правильная реализация AsyncNotifier в Riverpod 3.0: обработка состояний утилизации и асинхронных промежутков
Правильный шаблон для реализации методов AsyncNotifier в Riverpod 3.0 требует корректной обработки состояний утилизации и асинхронных промежутков. Ваш текущий код не работает, потому что ref.invalidate() вызывается после асинхронной операции, но провайдер мог быть утилизирован за это время. Вот как это исправить:
Ключевые изменения, которые необходимы:
- Добавить свойство
mountedдля отслеживания активности нотификатора - Использовать
ref.onDisposeдля настройки очистки - Проверять
mountedперед использованиемrefпосле асинхронных операций - Рассмотреть возможность перемещения операций с ref перед асинхронными промежутками, когда это возможно
Содержание
- Понимание ошибки утилизации
- Правильный шаблон AsyncNotifier
- Пошаговое руководство по миграции
- Лучшие практики для мутаций
- Распространенные проблемы и решения
- Продвинутые шаблоны
Понимание ошибки утилизации
В Riverpod 3.0 все методы ref и нотификатора, кроме “mounted”, теперь вызывают исключение, если используются после утилизации. Это критическое изменение по сравнению с 2.x, где поведение псевдо-синглтона было более терпимым.
Ошибка возникает потому, что:
- Провайдер может быть перестроен, пока ваша асинхронная операция все еще выполняется
- Предыдущий экземпляр провайдера утилизируется
- Ваша асинхронная операция завершается и пытается использовать
refдля утилизированного экземпляра
Согласно списку изменений Riverpod, это изменение поведения было сделано намеренно для более предсказуемого и безопасного управления состоянием.
Правильный шаблон AsyncNotifier
Вот правильная реализация вашего NoteController с обработкой утилизации:
@riverpod
class NoteController extends _$NoteController {
bool _mounted = true;
@override
FutureOr<void> build() async {
// Настройка отслеживания утилизации
ref.onDispose(() => _mounted = false);
}
Future<void> deleteNote(String id) async {
// Получение репозитория до асинхронного промежутка
final repository = ref.read(noteRepositoryProvider);
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await repository.deleteNote(id);
// Проверка, все ли еще смонтировано перед использованием ref
if (_mounted) {
ref.invalidate(getNotesProvider);
}
});
}
}
Ключевые улучшения:
- Добавлено ручное отслеживание свойства
_mounted - Использован
ref.onDispose()для обновления флага - Проверка
_mountedперед использованиемrefпосле асинхронной операции - Перемещение
ref.read()перед асинхронным промежутком для лучшей безопасности
Пошаговое руководство по миграции
1. Добавление отслеживания монтирования во все AsyncNotifier
@riverpod
class YourNotifier extends _$YourNotifier {
bool _mounted = true;
@override
FutureOr<void> build() async {
ref.onDispose(() => _mounted = false);
}
// ... ваши методы
}
2. Обновление методов мутации
Для любого метода, который:
- Использует
ref.read(),ref.watch(),ref.invalidate()и т.д. - Имеет асинхронные операции
- Выполняется после перестроения провайдера
Шаблон:
Future<void> yourMethod() async {
// Перемещение операций с ref перед асинхронными промежутками, когда возможно
final dependency = ref.read(someProvider);
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await someAsyncOperation();
// ВАЖНО: Проверить смонтированность перед использованием ref
if (_mounted) {
ref.invalidate(otherProvider);
}
});
}
3. Обработка сложных асинхронных цепочек
Для нескольких асинхронных операций:
Future<void> complexOperation() async {
state = const AsyncLoading();
try {
// Этап 1 - до любой возможной утилизации
final data = await fetchPhase1Data();
if (!_mounted) return; // Ранний выход, если утилизирован
// Этап 2 - после первого асинхронного промежутка
final result = await fetchPhase2Data(data);
if (!_mounted) return;
// Безопасное использование ref
if (_mounted) {
state = AsyncValue.data(result);
}
} catch (e) {
if (_mounted) {
state = AsyncValue.error(e);
}
}
}
Лучшие практики для мутаций
1. Минимизация асинхронных промежутков с использованием ref
// Хорошо: перемещение операций с ref перед await
Future<void> goodExample() async {
final repository = ref.read(repoProvider); // До асинхронного промежутка
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await repository.delete(id);
if (_mounted) ref.invalidate(notesProvider);
});
}
// Плохо: использование ref после await
Future<void> badExample() async {
state = const AsyncLoading();
await repository.delete(id); // Асинхронный промежуток
ref.invalidate(notesProvider); // Риск утилизации!
}
2. Правильное использование AsyncValue.guard
Future<void> safeMutation() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
try {
await performMutation();
if (_mounted) {
ref.invalidate(dependentProvider);
}
} catch (e) {
rethrow; // AsyncValue.guard обрабатывает состояние ошибки
}
});
}
3. Осторожное использование AutoDispose
Официальное руководство по миграции предупреждает, что autoDispose провайдеры в 3.0 могут быть более агрессивными. Рассмотрите необходимость использования:
@riverpod
class NoteController extends _$NoteController {
// Используйте keepAlive для предотвращения преждевременной утилизации
@override
FutureOr<void> build() async {
// ...
ref.onDispose(() => _mounted = false);
}
}
Распространенные проблемы и решения
Проблема 1: Забыто отслеживание монтирования
Ошибка: Cannot use "ref" after the widget was disposed (Невозможно использовать “ref” после того, как виджет был утилизирован)
Решение: Всегда добавляйте отслеживание монтирования в ваш AsyncNotifier:
@riverpod
class YourNotifier extends _$YourNotifier {
bool _mounted = true;
@override
FutureOr<void> build() async {
ref.onDispose(() => _mounted = false);
}
}
Проблема 2: Использование ref в onDispose
Ошибка: Использование ref в хуках жизненного цикла не поддерживается
Решение: Используйте обратный вызов ref.onDispose вместо этого:
// Вместо попытки использования ref в onDispose
// ref.onDispose(() => ref.invalidate(...)); // ❌ Неверно
// Используйте флаг mounted в ваших методах
if (_mounted) ref.invalidate(...); // ✅ Правильно
Проблема 3: Множественные асинхронные операции
Проблема: Сложные мутации с несколькими асинхронными шагами
Решение: Проверяйте статус монтирования после каждого асинхронного промежутка:
Future<void> complexOperation() async {
final step1 = await asyncStep1();
if (!_mounted) return;
final step2 = await asyncStep2(step1);
if (!_mounted) return;
if (_mounted) {
ref.invalidate(dependentProvider);
}
}
Продвинутые шаблоны
1. Использование аннотации Riverpod для проверки монтирования
Из GitHub issue #1926, рассмотрите возможность добавления хелпера:
@riverpod
class NoteController extends _$NoteController {
bool _mounted = true;
@override
FutureOr<void> build() async {
ref.onDispose(() => _mounted = false);
}
// Вспомогательный метод для безопасного использования ref
void safeRef(void Function() action) {
if (_mounted) action();
}
Future<void> deleteNote(String id) async {
final repository = ref.read(noteRepositoryProvider);
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
await repository.deleteNote(id);
safeRef(() => ref.invalidate(getNotesProvider));
});
}
}
2. Отслеживание прогресса для длительных операций
Как упоминается в Что нового в Riverpod 3.0, вы можете добавить отслеживание прогресса:
Future<void> slowDelete(String id) async {
state = AsyncLoading(progress: 0.0);
state = await AsyncValue.guard(() async {
final repository = ref.read(noteRepositoryProvider);
state = AsyncLoading(progress: 0.3);
await repository.prepareDelete(id);
state = AsyncLoading(progress: 0.7);
await repository.deleteNote(id);
if (_mounted) {
state = AsyncLoading(progress: 1.0);
await Future.delayed(const Duration(milliseconds: 100)); // Обратная связь с UI
ref.invalidate(getNotesProvider);
}
});
}
3. Отменяемые операции
Для операций, которые можно отменить:
CancelableOperation? _currentOperation;
@riverpod
class CancellableController extends _$CancellableController {
bool _mounted = true;
@override
FutureOr<void> build() async {
ref.onDispose(() {
_mounted = false;
_currentOperation?.cancel();
});
}
Future<void> startOperation() async {
if (_currentOperation != null) {
_currentOperation!.cancel();
}
_currentOperation = CancelableOperation.fromFuture(
longRunningOperation(),
onCancel: () => state = const AsyncValue.data(null)
);
try {
final result = await _currentOperation!.value;
if (_mounted) {
state = AsyncValue.data(result);
}
} catch (e) {
if (_mounted) {
state = AsyncValue.error(e);
}
}
}
}
Источники
- Миграция с 2.0 на 3.0 | Riverpod
- Что нового в Riverpod 3.0 | Riverpod
- список изменений riverpod | Dart пакет
- Как проверить, смонтирован ли AsyncNotifier с Riverpod
- Свойство
mountedв Notifier/AsyncNotifier · Issue #1879 - Сокращение шаблонного кода для “mounted” в новом классе AsyncNotifier · Issue #1926
- Невозможно использовать ref в нотификаторе после асинхронного промежутка · Issue #4096
- FAQ | Riverpod
Заключение
Ключевые выводы для мутаций AsyncNotifier в Riverpod 3.0:
- Всегда добавляйте отслеживание монтирования с помощью свойства
_mountedи настройкиref.onDispose() - Проверяйте
_mountedперед использованиемrefпосле любой асинхронной операции - Перемещайте операции с ref перед асинхронными промежутками по возможности для минимизации риска
- Используйте
AsyncValue.guard()для правильной обработки ошибок в мутациях - Учитывайте поведение autoDispose - в 3.0 оно более агрессивно в отношении утилизации
Миграция с 2.x на 3.0 требует сдвига мышления в сторону безопасности утилизации. Хотя это добавляет некоторый шаблонный код, новые шаблоны делают управление состоянием более надежным и предотвращают распространенные проблемы с утечками памяти. Начните с добавления отслеживания монтирования во все ваши AsyncNotifier, а затем систематически обновляйте каждый метод для проверки статуса монтирования перед использованием операций с ref после асинхронных промежутков.
Для сложных приложений рассмотрите возможность реализации вспомогательных методов, показанных выше, для сокращения шаблонного кода и поддержания единообразных шаблонов во всей кодовой базе.