Automatic disposal
В Riverpod можно настроить автоматическое освобождение ресурсов, связанных с provider, когда он больше не используется.
Включение и отключение автоматического освобождения ресурсов
Если вы используете генерацию кода, эта возможность включена по умолчанию. Её можно отключить через аннотацию:
// Отключить автоматическое освобождение ресурсов
(keepAlive: true)
String helloWorld(Ref ref) => 'Hello world!';
Если генерация кода не используется, автоматическое освобождение ресурсов можно включить с помощью параметра isAutoDispose: true
при создании provider:
final helloWorldProvider = Provider<String>(
// Включить автоматическое освобождение ресурсов
isAutoDispose: true,
(ref) => 'Hello world!',
);
Включение или отключение автоматического освобождения ресурсов не влияет на то,
будет ли состояние уничтожено при пересчёте provider.
При пересчёте provider его состояние всегда уничтожается.
Если provider принимает параметры, рекомендуется включать автоматическое освобождение ресурсов. В противном случае для каждой комбинации параметров будет создаваться отдельное состояние, что может привести к утечкам памяти.
Когда срабатывает автоматическое освобождение ресурсов?
Когда автоматическое освобождение ресурсов включено, Riverpod отслеживает, есть ли у provider слушатели. Для этого учитываются вызовы Ref.watch, Ref.listen и некоторых других методов.
Когда количество слушателей становится равным нулю, provider считается "неиспользуемым", и вызывается Ref.onCancel. После этого Riverpod ожидает один кадр (ср. await null). Если по прошествии этого кадра provider всё ещё не используется, он уничтожается, после чего вызывается Ref.onDispose.
Реакция на уничтожение состояния
В Riverpod существует несколько встроенных сценариев, при которых состояние может быть уничтожено:
- provider больше не используется и работает в режиме автоматического освобождения ресурсов (подробнее об этом позже). В этом случае всё состояние, связанное с provider, уничтожается.
- provider пересчитывается, например из-за
ref.watch. В этом случае предыдущее состояние уничтожается, а вместо него создаётся новое.
В обоих случаях может потребоваться выполнить какую-либо логику при уничтожении состояния.
Для этого существует ref.onDispose. Этот метод позволяет
зарегистрировать обработчик, который будет вызываться каждый раз при уничтожении состояния.
Например, его можно использовать для закрытия активного StreamController:
- riverpod
- riverpod_generator
final provider = StreamProvider<int>((ref) {
final controller = StreamController<int>();
// При уничтожении состояния закрываем StreamController.
ref.onDispose(controller.close);
// TO-DO: Добавьте значения в StreamController
return controller.stream;
});
Stream<int> example(Ref ref) {
final controller = StreamController<int>();
// При уничтожении состояния закрываем StreamController.
ref.onDispose(controller.close);
// TO-DO: Добавьте значения в StreamController
return controller.stream;
}
Колбэк, передаваемый в ref.onDispose не должен вызывать побочные эффекты.
Изменение состояния providers внутри onDispose может привести к непредсказуемому поведению.
Существуют и другие полезные события жизненного цикла:
ref.onCancel— вызывается, когда у provider удаляется последний слушатель.ref.onResume— вызывается, когда после вызоваonCancelк provider добавляется новый слушатель.
Вы можете вызывать ref.onDispose столько раз, сколько потребуется.
Не стесняйтесь вызывать его отдельно для каждого объекта, требующего освобождения ресурсов, внутри provider. Такой подход
упрощает поиск ситуаций, когда какой-либо объект был забыт и не был корректно освобождён.
Принудительное уничтожение provider с помощью ref.invalidate
Иногда может потребоваться принудительно уничтожить provider.
Это можно сделать с помощью ref.invalidate, который можно вызывать как из другого provider, так и из виджета.
При вызове ref.invalidate текущее состояние provider уничтожается.
После этого возможны два варианта:
- если provider отслеживается, для него будет создано новое состояние.
- если provider не отслеживается, он будет полностью уничтожен.
class MyWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
// При нажатии уничтожить provider.
ref.invalidate(someProvider);
},
child: const Text('dispose a provider'),
);
}
}
Провайдеры также могут инвалидировать сами себя с помощью ref.invalidateSelf.
Однако в этом случае всегда будет создаваться новое состояние.
Если вы хотите инвалидировать provider, принимающий параметры, можно инвалидировать только одну конкретную комбинацию параметров или сразу все комбинации параметров:
- riverpod
- riverpod_generator
final provider = Provider.autoDispose.family<String, String>((ref, name) {
return 'Hello $name';
});
// ...
void onTap() {
// Инвалидировать все возможные комбинации параметров этого provider.
ref.invalidate(provider);
// Инвалидировать только конкретную комбинацию параметров.
ref.invalidate(provider('John'));
}
String label(Ref ref, String userName) {
return 'Hello $userName';
}
// ...
void onTap() {
// Инвалидировать все возможные комбинации параметров этого provider.
ref.invalidate(labelProvider);
// Инвалидировать только конкретную комбинацию параметров.
ref.invalidate(labelProvider('John'));
}
Тонкая настройка освобождения ресурсов с ref.keepAlive
Как уже упоминалось выше, при включённом автоматическом освобождении ресурсов состояние уничтожается, если у provider нет слушателей в течение одного полного кадра.
Однако иногда требуется более точный контроль над этим поведением. Например, вы можете захотеть сохранять результат успешных сетевых запросов, но не кэшировать неудачные.
Это можно реализовать с помощью ref.keepAlive, после включения автоматического освобождения.
С его помощью вы сами решаете когда состояние перестаёт автоматически уничтожаться.
- riverpod
- riverpod_generator
final provider = FutureProvider.autoDispose<String>((ref) async {
final response = await http.get(Uri.parse('https://example.com'));
// Делаем provider "живым" только после успешного завершения запроса.
// Если запрос завершится с ошибкой (и будет выброшено исключение),
// то когда provider перестанет использоваться, его состояние будет уничтожено.
final link = ref.keepAlive();
// С помощью `link` можно вернуть автоматическое освобождение ресурсов:
// link.close();
return response.body;
});
Future<String> example(Ref ref) async {
final response = await http.get(Uri.parse('https://example.com'));
// Делаем provider "живым" только после успешного завершения запроса.
// Если запрос завершится с ошибкой (и будет выброшено исключение),
// то когда provider перестанет использоваться, его состояние будет уничтожено.
ref.keepAlive();
// С помощью `link` можно вернуть автоматическое освобождение ресурсов:
// link.close();
return response.body;
}
Если provider пересчитывается, автоматическое освобождение ресурсов снова включается.
Также можно использовать возвращаемое значение ref.keepAlive для возврата к автоматическому освобождению ресурсов.
Пример: сохранение состояния на заданное время
В настоящее время Riverpod не предоставляет встроенного механизма для сохранения состояния
на строго заданный промежуток времени.
Однако реализовать такую логику легко и удобно с использованием уже рассмотренных инструментов.
С помощью Timer + ref.keepAlive можно удерживать состояние provider активным в течение определённого времени.
Чтобы сделать этот подход переиспользуемым, можно оформить его в виде метода расширения:
extension CacheForExtension on Ref {
/// Сохраняет provider активным в течение [duration].
void cacheFor(Duration duration) {
// Сразу предотвращаем уничтожение состояния.
final link = keepAlive();
// После истечения времени снова включаем автоматическое освобождение ресурсов.
final timer = Timer(duration, link.close);
// Опционально: при пересоздании provider (например, через ref.watch)
// отменяем запланированный таймер.
onDispose(timer.cancel);
}
}
Затем мы можем использовать его следующим образом:
- riverpod
- riverpod_generator
final provider = FutureProvider.autoDispose<Object>((ref) async {
/// Сохраняет состояние активным в течение 5 минут
ref.cacheFor(const Duration(minutes: 5));
return http.get(Uri.https('example.com'));
});
Future<Object> example(Ref ref) async {
/// Сохраняет состояние активным в течение 5 минут
ref.cacheFor(const Duration(minutes: 5));
return http.get(Uri.https('example.com'));
}
Эту логику можно адаптировать под ваши нужды.
Например, можно использовать ref.onCancel/ref.onResume, чтобы уничтожать состояние только в том случае,
если provider не использовался (не имел слушателей) в течение определённого промежутка времени.