Add devtool bindings (#356)

This commit is contained in:
Remi Rousselet
2021-03-04 11:42:35 +00:00
committed by GitHub
parent 3ac52bbfa2
commit fc66736c54
14 changed files with 398 additions and 27 deletions

View File

@ -81,7 +81,7 @@ final paginatedQuestionsProvider = FutureProvider.autoDispose
final page = parsed.copyWith( final page = parsed.copyWith(
items: parsed.items.map((e) { items: parsed.items.map((e) {
final document = parse(e.body); final document = parse(e.body);
return e.copyWith(body: document.body.text.replaceAll('\n', ' ')); return e.copyWith(body: document.body!.text.replaceAll('\n', ' '));
}).toList(), }).toList(),
); );

View File

@ -15,7 +15,7 @@ dependencies:
freezed_annotation: ^0.13.0-nullsafety.0 freezed_annotation: ^0.13.0-nullsafety.0
hooks_riverpod: hooks_riverpod:
path: ../../packages/hooks_riverpod path: ../../packages/hooks_riverpod
html: ^0.14.0 html: ^0.15.0
dev_dependencies: dev_dependencies:
build_runner: ^1.11.0 build_runner: ^1.11.0

View File

@ -7,9 +7,9 @@ const _uuid = Uuid();
class Todo { class Todo {
Todo({ Todo({
required this.description, required this.description,
required this.id,
this.completed = false, this.completed = false,
String? id, });
}) : id = id ?? _uuid.v4();
final String id; final String id;
final String description; final String description;
@ -28,7 +28,10 @@ class TodoList extends StateNotifier<List<Todo>> {
void add(String description) { void add(String description) {
state = [ state = [
...state, ...state,
Todo(description: description), Todo(
id: _uuid.v4(),
description: description,
),
]; ];
} }

View File

@ -145,7 +145,7 @@ class _ProviderScopeElement extends StatefulElement {
// filling the state properties here instead of inside State // filling the state properties here instead of inside State
// so that it is more readable in the devtool (one less indentation) // so that it is more readable in the devtool (one less indentation)
for (final entry in container.debugProviderValues!.entries) { for (final entry in container.debugProviderValues.entries) {
final name = entry.key.name ?? describeIdentity(entry.key); final name = entry.key.name ?? describeIdentity(entry.key);
properties.add(DiagnosticsProperty(name, entry.value)); properties.add(DiagnosticsProperty(name, entry.value));
} }
@ -255,7 +255,7 @@ class UncontrolledProviderScope extends InheritedWidget {
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
for (final entry in container.debugProviderValues!.entries) { for (final entry in container.debugProviderValues.entries) {
final name = entry.key.name ?? describeIdentity(entry.key); final name = entry.key.name ?? describeIdentity(entry.key);
properties.add(DiagnosticsProperty(name, entry.value)); properties.add(DiagnosticsProperty(name, entry.value));
} }

View File

@ -86,10 +86,10 @@ void main() {
container.read(provider0.state); container.read(provider0.state);
container.read(provider1.state); container.read(provider1.state);
final familyState0 = container.debugProviderElements!.firstWhere((p) { final familyState0 = container.debugProviderElements.firstWhere((p) {
return p.provider == provider0.state; return p.provider == provider0.state;
}); });
final familyState1 = container.debugProviderElements!.firstWhere((p) { final familyState1 = container.debugProviderElements.firstWhere((p) {
return p.provider == provider1.state; return p.provider == provider1.state;
}); });
@ -159,10 +159,10 @@ void main() {
container.read(provider0.state); container.read(provider0.state);
container.read(provider1.state); container.read(provider1.state);
final familyState0 = container.debugProviderElements!.firstWhere((p) { final familyState0 = container.debugProviderElements.firstWhere((p) {
return p.provider == provider0.state; return p.provider == provider0.state;
}); });
final familyState1 = container.debugProviderElements!.firstWhere((p) { final familyState1 = container.debugProviderElements.firstWhere((p) {
return p.provider == provider1.state; return p.provider == provider1.state;
}); });
@ -356,7 +356,7 @@ void main() {
.firstState<ProviderScopeState>(find.byKey(firstOwnerKey)) .firstState<ProviderScopeState>(find.byKey(firstOwnerKey))
.container; .container;
final state1 = owner1.debugProviderElements! final state1 = owner1.debugProviderElements
.firstWhere((s) => s.provider == provider.state); .firstWhere((s) => s.provider == provider.state);
expect(state1.hasListeners, true); expect(state1.hasListeners, true);
@ -384,7 +384,7 @@ void main() {
.firstState<ProviderScopeState>(find.byKey(secondOwnerKey)) .firstState<ProviderScopeState>(find.byKey(secondOwnerKey))
.container; .container;
final state2 = container2.debugProviderElements! final state2 = container2.debugProviderElements
.firstWhere((s) => s.provider is StateNotifierStateProvider); .firstWhere((s) => s.provider is StateNotifierStateProvider);
expect(find.text('0'), findsNothing); expect(find.text('0'), findsNothing);
@ -418,7 +418,7 @@ void main() {
.firstState<ProviderScopeState>(find.byType(ProviderScope)) .firstState<ProviderScopeState>(find.byType(ProviderScope))
.container; .container;
final state = container.debugProviderElements! final state = container.debugProviderElements
.firstWhere((s) => s.provider == provider.state); .firstWhere((s) => s.provider == provider.state);
expect(state.hasListeners, true); expect(state.hasListeners, true);

View File

@ -0,0 +1,99 @@
// ignore_for_file: public_member_api_docs
part of 'provider.dart';
void Function(
String eventKind,
Map<Object?, Object?> event,
)? _debugPostEventOverride;
void debugPostEvent(
String eventKind, [
Map<Object?, Object?> event = const {},
]) {
if (_debugPostEventOverride != null) {
_debugPostEventOverride!(eventKind, event);
} else {
developer.postEvent(eventKind, event);
}
}
PostEventSpy spyPostEvent() {
assert(_debugPostEventOverride == null, 'postEvent is already spied');
final spy = PostEventSpy._();
_debugPostEventOverride = spy._postEvent;
return spy;
}
@protected
class PostEventCall {
PostEventCall._(this.eventKind, this.event);
final String eventKind;
final Map<Object?, Object?> event;
}
@protected
class PostEventSpy {
PostEventSpy._();
final logs = <PostEventCall>[];
void dispose() {
assert(
_debugPostEventOverride == _postEvent,
'disposed a spy different from the current spy',
);
_debugPostEventOverride = null;
}
void _postEvent(
String eventKind,
Map<Object?, Object?> event,
) {
logs.add(PostEventCall._(eventKind, event));
}
}
@protected
class RiverpodBinding {
RiverpodBinding._();
static final _instance = RiverpodBinding._();
static RiverpodBinding get debugInstance {
RiverpodBinding? binding;
assert(() {
binding = _instance;
return true;
}(), '');
return binding!;
}
Map<String, ProviderContainer> _containers = {};
Map<String, ProviderContainer> get containers => _containers;
set containers(Map<String, ProviderContainer> value) {
debugPostEvent('riverpod:container_list_changed');
_containers = value;
}
void providerListChangedFor({required String containerId}) {
debugPostEvent(
'riverpod:provider_list_changed',
{'container_id': containerId},
);
}
void providerChanged({
required String containerId,
required String providerId,
}) {
debugPostEvent(
'riverpod:provider_changed',
{
'container_id': containerId,
'provider_id': providerId,
},
);
}
}

View File

@ -103,7 +103,12 @@ abstract class AlwaysAliveProviderBase<Created, Listened>
abstract class ProviderBase<Created, Listened> abstract class ProviderBase<Created, Listened>
implements ProviderListenable<Listened> { implements ProviderListenable<Listened> {
/// A base class for _all_ providers. /// A base class for _all_ providers.
ProviderBase(this._create, this.name); ProviderBase(this._create, this.name) {
assert(() {
debugId = '${_debugNextId++}';
return true;
}(), '');
}
final Created Function(ProviderReference ref) _create; final Created Function(ProviderReference ref) _create;
@ -131,6 +136,11 @@ abstract class ProviderBase<Created, Listened>
/// An internal method that defines how a provider behaves. /// An internal method that defines how a provider behaves.
ProviderElement<Created, Listened> createElement(); ProviderElement<Created, Listened> createElement();
/// A unique identifier for this provider, used by devtools to differentiate providers
///
/// Available only during development.
late final String debugId;
@override @override
String toString() { String toString() {
final content = { final content = {
@ -705,6 +715,13 @@ class ProviderElement<Created, Listened> implements ProviderReference {
if (!_didMount) { if (!_didMount) {
return; return;
} }
assert(() {
RiverpodBinding.debugInstance.providerChanged(
containerId: container.debugId,
providerId: provider.debugId,
);
return true;
}(), '');
_notificationCount++; _notificationCount++;
notifyMayHaveChanged(); notifyMayHaveChanged();
} }
@ -792,6 +809,9 @@ but $provider does not depend on ${_debugCurrentlyBuildingElement!.provider}.
_mounted = true; _mounted = true;
state._element = this; state._element = this;
assert(() { assert(() {
RiverpodBinding.debugInstance
.providerListChangedFor(containerId: container._debugId);
_debugIsFlushing = true; _debugIsFlushing = true;
return true; return true;
}(), ''); }(), '');
@ -824,6 +844,12 @@ but $provider does not depend on ${_debugCurrentlyBuildingElement!.provider}.
@protected @protected
@mustCallSuper @mustCallSuper
void dispose() { void dispose() {
assert(() {
RiverpodBinding.debugInstance
.providerListChangedFor(containerId: container._debugId);
return true;
}(), '');
_mounted = false; _mounted = false;
_runOnDispose(); _runOnDispose();

View File

@ -26,6 +26,8 @@ void _runBinaryGuarded<A, B>(void Function(A, B) cb, A value, B value2) {
ProviderBase? _circularDependencyLock; ProviderBase? _circularDependencyLock;
int _debugNextId = 0;
/// {@template riverpod.providercontainer} /// {@template riverpod.providercontainer}
/// An object that stores the state of the providers and allows overriding the /// An object that stores the state of the providers and allows overriding the
/// behavior of a specific provider. /// behavior of a specific provider.
@ -43,6 +45,15 @@ class ProviderContainer {
}) : _parent = parent, }) : _parent = parent,
_localObservers = observers, _localObservers = observers,
_root = parent?._root ?? parent { _root = parent?._root ?? parent {
assert(() {
_debugId = '${_debugNextId++}';
RiverpodBinding.debugInstance.containers = {
...RiverpodBinding.debugInstance.containers,
_debugId: this,
};
return true;
}(), '');
if (parent != null) { if (parent != null) {
if (observers != null) { if (observers != null) {
throw UnsupportedError( throw UnsupportedError(
@ -74,6 +85,22 @@ class ProviderContainer {
} }
} }
late final String _debugId;
/// A unique ID for this object, used by the devtool to differentiate two [ProviderContainer].
///
/// Should not be used.
@visibleForTesting
String get debugId {
String? id;
assert(() {
id = _debugId;
return true;
}(), '');
return id!;
}
final ProviderContainer? _root; final ProviderContainer? _root;
final ProviderContainer? _parent; final ProviderContainer? _parent;
@ -342,6 +369,12 @@ class ProviderContainer {
); );
} }
assert(() {
RiverpodBinding.debugInstance.containers =
Map.from(RiverpodBinding.debugInstance.containers)..remove(_debugId);
return true;
}(), '');
debugVsyncs.clear(); debugVsyncs.clear();
_parent?._children.remove(this); _parent?._children.remove(this);
@ -395,8 +428,8 @@ class ProviderContainer {
/// The states of the providers associated to this [ProviderContainer], sorted /// The states of the providers associated to this [ProviderContainer], sorted
/// in order of dependency. /// in order of dependency.
List<ProviderElement>? get debugProviderElements { List<ProviderElement> get debugProviderElements {
List<ProviderElement>? result; late List<ProviderElement> result;
assert(() { assert(() {
result = _visitStatesInOrder().toList(); result = _visitStatesInOrder().toList();
return true; return true;
@ -405,8 +438,8 @@ class ProviderContainer {
} }
/// The value exposed by all providers currently alive. /// The value exposed by all providers currently alive.
Map<ProviderBase, Object?>? get debugProviderValues { Map<ProviderBase, Object?> get debugProviderValues {
Map<ProviderBase, Object?>? res; late Map<ProviderBase, Object?> res;
assert(() { assert(() {
res = { res = {
for (final entry in _stateReaders.entries) for (final entry in _stateReaders.entries)
@ -423,6 +456,11 @@ class ProviderContainer {
/// ///
/// This can be used for logging or making devtools. /// This can be used for logging or making devtools.
abstract class ProviderObserver { abstract class ProviderObserver {
/// An object that listens to the changes of a [ProviderContainer].
///
/// This can be used for logging or making devtools.
const ProviderObserver();
/// A provider was initialized, and the value exposed is [value]. /// A provider was initialized, and the value exposed is [value].
void didAddProvider(ProviderBase provider, Object? value) {} void didAddProvider(ProviderBase provider, Object? value) {}

View File

@ -1,3 +1,5 @@
import 'dart:developer' as developer;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'builders.dart'; import 'builders.dart';
@ -5,8 +7,9 @@ import 'framework.dart';
import 'state_notifier_provider.dart'; import 'state_notifier_provider.dart';
import 'stream_provider.dart'; import 'stream_provider.dart';
part 'provider/base.dart'; part 'devtool.dart';
part 'provider/auto_dispose.dart'; part 'provider/auto_dispose.dart';
part 'provider/base.dart';
/// {@template riverpod.provider} /// {@template riverpod.provider}
/// A provider that exposes a read-only value. /// A provider that exposes a read-only value.

View File

@ -0,0 +1,175 @@
import 'package:riverpod/riverpod.dart';
import 'package:riverpod/src/provider.dart';
import 'package:test/test.dart';
import 'matchers.dart';
void main() {
late PostEventSpy spy;
setUp(() {
spy = spyPostEvent();
});
tearDown(() => spy.dispose());
test('calls postEvent whenever a provider is updated', () {
expect(RiverpodBinding.debugInstance.containers, isEmpty);
expect(spy.logs, isEmpty);
final container = ProviderContainer();
addTearDown(container.dispose);
final provider = StateProvider((ref) => 42);
final state = container.read(provider);
spy.logs.clear();
expect(spy.logs, isEmpty);
state.state++;
expect(
spy.logs,
[
isPostEventCall('riverpod:provider_changed', {
'container_id': container.debugId,
'provider_id': provider.debugId,
}),
],
);
spy.logs.clear();
});
test('RiverpodBinding contains the list of ProviderContainers', () {
expect(RiverpodBinding.debugInstance.containers, isEmpty);
expect(spy.logs, isEmpty);
final first = ProviderContainer();
expect(
spy.logs,
[isPostEventCall('riverpod:container_list_changed', isEmpty)],
);
spy.logs.clear();
expect(
RiverpodBinding.debugInstance.containers,
{first.debugId: first},
);
final second = ProviderContainer();
expect(
spy.logs,
[isPostEventCall('riverpod:container_list_changed', isEmpty)],
);
spy.logs.clear();
expect(
RiverpodBinding.debugInstance.containers,
{first.debugId: first, second.debugId: second},
);
first.dispose();
expect(
spy.logs,
[isPostEventCall('riverpod:container_list_changed', isEmpty)],
);
spy.logs.clear();
expect(
RiverpodBinding.debugInstance.containers,
{second.debugId: second},
);
second.dispose();
expect(
spy.logs,
[isPostEventCall('riverpod:container_list_changed', isEmpty)],
);
spy.logs.clear();
expect(RiverpodBinding.debugInstance.containers, isEmpty);
});
test(
'ProviderContainer calls postEvent whenever it mounts/unmount a provider',
() async {
final container = ProviderContainer();
addTearDown(container.dispose);
spy.logs.clear();
final provider = Provider.autoDispose((ref) => 0);
final provider2 = Provider((ref) => 0);
expect(spy.logs, isEmpty);
var sub = container.listen(provider);
expect(
spy.logs,
[
isPostEventCall(
'riverpod:provider_list_changed',
<Object?, Object?>{'container_id': container.debugId},
)
],
);
spy.logs.clear();
var sub2 = container.listen(provider2);
expect(
spy.logs,
[
isPostEventCall(
'riverpod:provider_list_changed',
<Object?, Object?>{'container_id': container.debugId},
)
],
);
spy.logs.clear();
sub.close();
expect(spy.logs, isEmpty);
await Future.value(null);
expect(
spy.logs,
[
isPostEventCall(
'riverpod:provider_list_changed',
<Object?, Object?>{'container_id': container.debugId},
)
],
);
spy.logs.clear();
sub2.close();
await Future.value(null);
expect(spy.logs, isEmpty);
await Future.value(null);
expect(spy.logs, isEmpty, reason: 'provider2 is not autoDispose');
// re-subscribe to the provider that was unmounted
sub = container.listen(provider);
expect(
spy.logs,
[
isPostEventCall(
'riverpod:provider_list_changed',
<Object?, Object?>{'container_id': container.debugId},
)
],
);
spy.logs.clear();
// re-subscribe to the provider that was no-longer listened but still mounted
sub2 = container.listen(provider2);
expect(spy.logs, isEmpty);
});
}

View File

@ -18,6 +18,16 @@ Matcher isProvider(RootProvider provider) {
void main() { void main() {
// TODO flushing inside mayHaveChanged calls onChanged only after all mayHaveChanged were executed // TODO flushing inside mayHaveChanged calls onChanged only after all mayHaveChanged were executed
test('ProviderObservers can have const constructors', () {
final root = ProviderContainer(
observers: [
const ConstObserver(),
],
);
root.dispose();
});
test('disposing parent container when child container is not dispose throws', test('disposing parent container when child container is not dispose throws',
() { () {
final root = ProviderContainer(); final root = ProviderContainer();
@ -66,7 +76,7 @@ void main() {
expect(container.read(provider), 42); expect(container.read(provider), 42);
final state = container.debugProviderElements!.single; final state = container.debugProviderElements.single;
expect(state.hasListeners, false); expect(state.hasListeners, false);
@ -780,3 +790,7 @@ class MockMarkMayHaveChanged extends Mock {
class MockDidUpdateProvider extends Mock { class MockDidUpdateProvider extends Mock {
void call(); void call();
} }
class ConstObserver extends ProviderObserver {
const ConstObserver();
}

View File

@ -0,0 +1,13 @@
import 'package:riverpod/src/provider.dart';
import 'package:test/test.dart';
Matcher isPostEventCall(Object kind, Object? event) {
var matcher =
isA<PostEventCall>().having((e) => e.eventKind, 'eventKind', kind);
if (event != null) {
matcher = matcher.having((e) => e.event, 'event', event);
}
return matcher;
}

View File

@ -40,10 +40,10 @@ void main() {
container.read(provider0.state); container.read(provider0.state);
container.read(provider1.state); container.read(provider1.state);
final familyState0 = container.debugProviderElements!.firstWhere((p) { final familyState0 = container.debugProviderElements.firstWhere((p) {
return p.provider == provider0.state; return p.provider == provider0.state;
}); });
final familyState1 = container.debugProviderElements!.firstWhere((p) { final familyState1 = container.debugProviderElements.firstWhere((p) {
return p.provider == provider1.state; return p.provider == provider1.state;
}); });
@ -108,10 +108,10 @@ void main() {
container.read(provider0.state); container.read(provider0.state);
container.read(provider1.state); container.read(provider1.state);
final familyState0 = container.debugProviderElements!.firstWhere((p) { final familyState0 = container.debugProviderElements.firstWhere((p) {
return p.provider == provider0.state; return p.provider == provider0.state;
}); });
final familyState1 = container.debugProviderElements!.firstWhere((p) { final familyState1 = container.debugProviderElements.firstWhere((p) {
return p.provider == provider1.state; return p.provider == provider1.state;
}); });
@ -258,7 +258,7 @@ void main() {
verifyNoMoreInteractions(listener); verifyNoMoreInteractions(listener);
expect( expect(
container.debugProviderElements!.map((e) => e.provider), container.debugProviderElements.map((e) => e.provider),
[provider, computed, provider2], [provider, computed, provider2],
); );
}); });

View File

@ -295,7 +295,7 @@ void main() {
} }
List<ProviderBase> compute(ProviderContainer container) { List<ProviderBase> compute(ProviderContainer container) {
return container.debugProviderElements!.map((e) => e.provider).toList(); return container.debugProviderElements.map((e) => e.provider).toList();
} }
class A {} class A {}