fix(riverpod): a listener that reads the value is not a render #3
3 changed files with 41 additions and 1 deletions
|
|
@ -1334,7 +1334,13 @@ class RiverpodModel {
|
|||
if (!provider.isAsync) continue;
|
||||
final providerSites = sitesFor(provider.name);
|
||||
if (providerSites.isEmpty) continue;
|
||||
if (!providerSites.any((site) => site.readsValue)) continue;
|
||||
// Only a `ref.watch` renders the value. A `ref.listen` that reads it
|
||||
// drives a side effect and draws nothing, so it cannot leave a blank or
|
||||
// stale screen; listeners still count below as places that handle
|
||||
// error or loading.
|
||||
if (!providerSites.any((site) => site.watches && site.readsValue)) {
|
||||
continue;
|
||||
}
|
||||
final missing = <String>[];
|
||||
if (!providerSites.any((site) => site.handlesError)) missing.add('error');
|
||||
if (!providerSites.any((site) => site.handlesLoading)) {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ class FutureProvider<T> {
|
|||
class Ref {
|
||||
AsyncValue<T> watch<T>(FutureProvider<T> provider) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
void listen<T>(
|
||||
FutureProvider<T> provider,
|
||||
void Function(AsyncValue<T>? previous, AsyncValue<T> next) listener,
|
||||
) =>
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
class SubscriptionTier {}
|
||||
|
|
@ -46,6 +52,9 @@ final unhandledProvider =
|
|||
/// Consumed with `when(data:, loading:, error:)`.
|
||||
final handledProvider = FutureProvider<int>((ref) async => 1);
|
||||
|
||||
/// Consumed only by `ref.listen`, to mirror the value into a side effect.
|
||||
final listenOnlyProvider = FutureProvider<bool>((ref) async => false);
|
||||
|
||||
class SubscriptionView {
|
||||
SubscriptionView(this.ref);
|
||||
|
||||
|
|
@ -60,4 +69,14 @@ class SubscriptionView {
|
|||
loading: () => -1,
|
||||
error: (error, stack) => -2,
|
||||
);
|
||||
|
||||
/// Not a render: a listener that mirrors the value into a side effect (for
|
||||
/// example an analytics user property) draws nothing, so ignoring loading
|
||||
/// and error here leaves no blank or stale screen.
|
||||
void mirrorFlag(void Function(bool value) sink) {
|
||||
ref.listen(listenOnlyProvider, (previous, next) {
|
||||
final value = next.asData?.value;
|
||||
if (value != null) sink(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,4 +68,19 @@ void main() {
|
|||
isNot(contains('handledProvider')),
|
||||
);
|
||||
});
|
||||
|
||||
test('a provider consumed only by ref.listen is not a render gap', () {
|
||||
// A listener mirrors the value into a side effect and draws nothing, so
|
||||
// it cannot leave a blank or stale screen. Seen when dogfooding on
|
||||
// word_alive: an analytics user-property listener was reported as a UI
|
||||
// read with unhandled loading and error.
|
||||
final sites = analysis.flow.riverpod.sitesFor('listenOnlyProvider');
|
||||
expect(sites, hasLength(1));
|
||||
expect(sites.single.watches, isFalse);
|
||||
expect(sites.single.readsValue, isTrue);
|
||||
expect(
|
||||
analysis.flow.riverpod.gaps.map((g) => g.provider.name),
|
||||
isNot(contains('listenOnlyProvider')),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue