Другое

Руководство по шаблону мутаций 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 или других жизненных циклах. Это не поддерживается, так как провайдер уже освобождается.

Вот моя текущая реализация:

dart
@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() вызывается после асинхронной операции, но провайдер мог быть утилизирован за это время. Вот как это исправить:

Ключевые изменения, которые необходимы:

  1. Добавить свойство mounted для отслеживания активности нотификатора
  2. Использовать ref.onDispose для настройки очистки
  3. Проверять mounted перед использованием ref после асинхронных операций
  4. Рассмотреть возможность перемещения операций с ref перед асинхронными промежутками, когда это возможно

Содержание


Понимание ошибки утилизации

В Riverpod 3.0 все методы ref и нотификатора, кроме “mounted”, теперь вызывают исключение, если используются после утилизации. Это критическое изменение по сравнению с 2.x, где поведение псевдо-синглтона было более терпимым.

Ошибка возникает потому, что:

  • Провайдер может быть перестроен, пока ваша асинхронная операция все еще выполняется
  • Предыдущий экземпляр провайдера утилизируется
  • Ваша асинхронная операция завершается и пытается использовать ref для утилизированного экземпляра

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


Правильный шаблон AsyncNotifier

Вот правильная реализация вашего NoteController с обработкой утилизации:

dart
@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);
      }
    });
  }
}

Ключевые улучшения:

  1. Добавлено ручное отслеживание свойства _mounted
  2. Использован ref.onDispose() для обновления флага
  3. Проверка _mounted перед использованием ref после асинхронной операции
  4. Перемещение ref.read() перед асинхронным промежутком для лучшей безопасности

Пошаговое руководство по миграции

1. Добавление отслеживания монтирования во все AsyncNotifier

dart
@riverpod
class YourNotifier extends _$YourNotifier {
  bool _mounted = true;

  @override
  FutureOr<void> build() async {
    ref.onDispose(() => _mounted = false);
  }
  
  // ... ваши методы
}

2. Обновление методов мутации

Для любого метода, который:

  • Использует ref.read(), ref.watch(), ref.invalidate() и т.д.
  • Имеет асинхронные операции
  • Выполняется после перестроения провайдера

Шаблон:

dart
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. Обработка сложных асинхронных цепочек

Для нескольких асинхронных операций:

dart
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

dart
// Хорошо: перемещение операций с 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

dart
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 могут быть более агрессивными. Рассмотрите необходимость использования:

dart
@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:

dart
@riverpod
class YourNotifier extends _$YourNotifier {
  bool _mounted = true;

  @override
  FutureOr<void> build() async {
    ref.onDispose(() => _mounted = false);
  }
}

Проблема 2: Использование ref в onDispose

Ошибка: Использование ref в хуках жизненного цикла не поддерживается

Решение: Используйте обратный вызов ref.onDispose вместо этого:

dart
// Вместо попытки использования ref в onDispose
// ref.onDispose(() => ref.invalidate(...)); // ❌ Неверно

// Используйте флаг mounted в ваших методах
if (_mounted) ref.invalidate(...); // ✅ Правильно

Проблема 3: Множественные асинхронные операции

Проблема: Сложные мутации с несколькими асинхронными шагами

Решение: Проверяйте статус монтирования после каждого асинхронного промежутка:

dart
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, рассмотрите возможность добавления хелпера:

dart
@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, вы можете добавить отслеживание прогресса:

dart
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. Отменяемые операции

Для операций, которые можно отменить:

dart
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);
      }
    }
  }
}

Источники

  1. Миграция с 2.0 на 3.0 | Riverpod
  2. Что нового в Riverpod 3.0 | Riverpod
  3. список изменений riverpod | Dart пакет
  4. Как проверить, смонтирован ли AsyncNotifier с Riverpod
  5. Свойство mounted в Notifier/AsyncNotifier · Issue #1879
  6. Сокращение шаблонного кода для “mounted” в новом классе AsyncNotifier · Issue #1926
  7. Невозможно использовать ref в нотификаторе после асинхронного промежутка · Issue #4096
  8. FAQ | Riverpod

Заключение

Ключевые выводы для мутаций AsyncNotifier в Riverpod 3.0:

  1. Всегда добавляйте отслеживание монтирования с помощью свойства _mounted и настройки ref.onDispose()
  2. Проверяйте _mounted перед использованием ref после любой асинхронной операции
  3. Перемещайте операции с ref перед асинхронными промежутками по возможности для минимизации риска
  4. Используйте AsyncValue.guard() для правильной обработки ошибок в мутациях
  5. Учитывайте поведение autoDispose - в 3.0 оно более агрессивно в отношении утилизации

Миграция с 2.x на 3.0 требует сдвига мышления в сторону безопасности утилизации. Хотя это добавляет некоторый шаблонный код, новые шаблоны делают управление состоянием более надежным и предотвращают распространенные проблемы с утечками памяти. Начните с добавления отслеживания монтирования во все ваши AsyncNotifier, а затем систематически обновляйте каждый метод для проверки статуса монтирования перед использованием операций с ref после асинхронных промежутков.

Для сложных приложений рассмотрите возможность реализации вспомогательных методов, показанных выше, для сокращения шаблонного кода и поддержания единообразных шаблонов во всей кодовой базе.

Авторы
Проверено модерацией
Модерация