Riverpod Provider를 사용하다보면 명시적으로 Provider값을 최초 Build에 기입된 값으로 초기화시켜주고 싶거나 아예 메모리에서 소거해버리고 싶은 상황이 생긴다.
예를 들어, 사용자 로그아웃을 하면서 사용자 정보를 들고 있던 keepAlive Provider를 AsyncLoading()으로 초기화 하는 등..
이때를 위해 WidgetRef 오브젝트는 invalidate라는 메소드를 지원한다. invalidate에 대한 공식문서 설명은 다음 2줄로 요약되어있다.
If the provider is listened to, a new state will be created.
If the provider is not listened to, the provider will be fully destroyed.
https://riverpod.dev/docs/essentials/auto_dispose
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
// On click, destroy the provider.
ref.invalidate(someProvider);
},
child: const Text('dispose a provider'),
);
}
}
watch 하고 있는 코드가 없다면 완전히 소거되는 것이고, watch하는 코드가 있다면 새로운 state로 곧바로 재생성된다고 한다.
ref.invalidate()
우선 invalidate 메소드는 아래 Provider Container 클래스 메소드를 호출하게 된다.
void invalidate(ProviderOrFamily provider) {
if (provider is ProviderBase) {
final reader = _getOrNull(provider);
reader?._element?.invalidateSelf();
} else {
provider as Family;
final familyContainer =
_overrideForFamily[provider]?.container ?? _root ?? this;
for (final stateReader in familyContainer._stateReaders.values) {
if (stateReader.origin.from != provider) continue;
stateReader._element?.invalidateSelf();
}
}
}
해당 메소드는 지정된 provider가 family형태로 선언된 provider라면 관련된 모든 provider들을 invalidateSelf시킨다.
family 형태가 아니라면 지정된 provider만 invalidateSelf시킨다.
void invalidateSelf() {
if (_mustRecomputeState) return;
_mustRecomputeState = true;
runOnDispose();
mayNeedDispose();
_container.scheduler.scheduleProviderRefresh(this);
visitChildren(
elementVisitor: (element) => element._markDependencyMayHaveChanged(),
notifierVisitor: (notifier) => notifier.notifyDependencyMayHaveChanged(),
);
}
invalidateSelf에서는 mustRecomputeState를 통해 이미 dispose가 진행되고 있는 상태면 return시켜 재귀적인 현상을 방지한다.
runOnDispose 메소드를 실행하여 dispose를 실행시키고, 해당 provider의 의존성을 갖고 있는 상태들에게 해당 이벤트를 알린다.
void runOnDispose() {
if (!_mounted) return;
_mounted = false;
final subscriptions = _subscriptions;
if (subscriptions != null) {
while (subscriptions.isNotEmpty) {
late int debugPreviousLength;
if (kDebugMode) {
debugPreviousLength = subscriptions.length;
}
final sub = subscriptions.first;
sub.close();
if (kDebugMode) {
assert(
subscriptions.length < debugPreviousLength,
'ProviderSubscription.close did not remove the subscription',
);
}
}
}
_onDisposeListeners?.forEach(runGuarded);
for (final observer in _container.observers) {
runBinaryGuarded(
observer.didDisposeProvider,
_origin,
_container,
);
}
_onDisposeListeners = null;
_onCancelListeners = null;
_onResumeListeners = null;
_onAddListeners = null;
_onRemoveListeners = null;
_onChangeSelfListeners = null;
_onErrorSelfListeners = null;
_didCancelOnce = false;
}
runOnDispose()는 해당 state에 달려있는 모든 Listner들을 해제시켜주고 null로 초기화하는 작업을 진행한다.
void _scheduleTask() {
// Don't schedule a task if there is already one pending or if the scheduler
// is disposed.
// It is possible that during disposal of a ProviderContainer, if a provider
// uses ref.keepAlive(), the keepAlive closure will try to schedule a task.
// In this case, we don't want to schedule a task as the container is already
// disposed.
if (_pendingTaskCompleter != null || _disposed) return;
_pendingTaskCompleter = Completer<void>();
vsync(_task);
}
void _task() {
final pendingTaskCompleter = _pendingTaskCompleter;
if (pendingTaskCompleter == null) return;
pendingTaskCompleter.complete();
_performRefresh();
_performDispose();
_stateToRefresh.clear();
_stateToDispose.clear();
_pendingTaskCompleter = null;
}
앞전 InvalidateSelf 메소드에서의 스케쥴러 메소드는
invalidate가 이미 팬딩중인거나, null이라면 return을 시키고
아니라면
void _disposeProvider(ProviderBase<Object?> provider) {
final reader = _getOrNull(provider);
// The provider is already disposed, so we don't need to do anything
if (reader == null) return;
reader._element?.dispose();
if (reader.isDynamicallyCreated) {
// Since the StateReader is implicitly created, we don't keep it
// on provider dispose, to avoid memory leak
void removeStateReaderFrom(ProviderContainer container) {
/// Checking if the reader is the same instance is important,
/// as it is possible that the provider was overridden.
if (container._stateReaders[provider] == reader) {
container._stateReaders.remove(provider);
}
container._children.forEach(removeStateReaderFrom);
}
removeStateReaderFrom(this);
} else {
reader._element = null;
}
}
disposeProvider 메소드에서 remove(스카이 엔진 메소드)를 통해 RAM에서 소거시키게 된다.
중간에 재귀함수들이 많고, 여러번 예외처리를 통해 반복적으로 실행되는 메소드들이 많아서 모든 현상을 이해하기는 어려웠다.
다만 다음 3가지 업무를 invalidate가 수행한다는 것을 알게 됐다.
이런 결론을 통해 Family형태로 만들어진 Provider들을 invalidate할 때 한꺼번에 메모리에서 소거되는것을 고려한 상황이 맞는지 파악할 필요가 있음을 알게 되었다.
또 필요한 상황이 아니라면 keepAlive로 선언하지 말아서 메모리 관리가 자동적으로 되도록 구조를 만들어야겠다.