Fully support multiple nested ContextPlus.root?
We want to use context plus for dependency injection in tests.
We have one `MyApp` widget and want to inject a either use a `ProdApi` or a `FakeApi ` class.
```dart
// Sample without dependency injection
import 'package:context_plus/context_plus.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
final Ref<Api> apiRef = Ref<Api>();
abstract class Api {
Future<void> sendHeartbeat();
}
class ProdApi implements Api {
@override
Future<void> sendHeartbeat() async {
// Simulate sending a heartbeat to the production server
print('Heartbeat sent to production server');
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ContextPlus.root(
child: Builder(builder: (context) {
apiRef.bind(context, () => ProdApi());
return MaterialApp(
home: HomeScreen(),
);
}),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Sample App'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
apiRef.of(context).sendHeartbeat();
},
child: const Text('Send to Server'),
),
],
),
),
);
}
}
```
Currently, `ContextPlus.root` lives within `MyApp`. This is fine for production, but we can't inject from the outside in our test environment
```dart
class FakeApi implements Api {
@override
Future<void> sendHeartbeat() async {
throw Exception('cannot reach server');
}
}
void main() {
testWidgets('send heartbeat to server', (tester) async {
final app = Builder(builder: (context) {
// Fails with "No ContextRef.root() found. Did you forget to add a ContextRef.root() widget?"
apiRef.bind(context, () => FakeApi());
return const MyApp();
});
await tester.pumpWidget(app);
});
}
```
There are two ways to solve this
### Move ContextRef.root
Remove `ContextPlus.root` and dependencies from MyApp and move it up to the `main` function
```dart
void main() {
runApp(ContextPlus.root(
child: Builder(
builder: (context) {
apiRef.bind(context, () => ProdApi());
return MyApp();
},
),
));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}
```
Add `ContextPlus.root` in the test
```dart
void main() {
testWidgets('send heartbeat to server', (tester) async {
final app = ContextPlus.root(
child: Builder(builder: (context) {
apiRef.bind(context, () => FakeApi());
return const MyApp();
}),
);
await tester.pumpWidget(app);
});
}
```
### Wrap with another ContextRef.root
Keep `ContextPlus.root` in `MyApp` but use `bindWhenUnbound` instead of `bind`.
In tests, add another `ContextPlus.root` to inject fakes.
```dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ContextPlus.root(
child: Builder(builder: (context) {
apiRef.bindWhenUnbound(context, () => ProdApi());
return MaterialApp(
home: HomeScreen(),
);
}),
);
}
}
```
```dart
void main() {
testWidgets('send heartbeat to server', (tester) async {
final app = ContextPlus.root(
child: Builder(builder: (context) {
apiRef.bind(context, () => FakeApi());
return const MyApp();
}),
);
await tester.pumpWidget(app);
});
}
```
```dart
extension BindUnboundRef<T> on Ref<T> {
/// Binds the Ref if it is not already bound to [context], otherwise returns the existing value from [context].
T bindWhenUnbound(
BuildContext context,
T Function() create, {
void Function(T value)? dispose,
Object? key,
}) {
try {
T found = of(context);
return found;
} catch (e) {
// If the Ref is not bound, bind it now
return bind(context, create, dispose: dispose, key: key);
}
}
}
```
---
I do prefer wrapping again with `ContextPlus.root` and it seems to work fine. But I'm not 100% sure about the internals, if this case is accounted for. I couldn't find tests for this case. That's why I'm coming here to ask if anything could go wrong with nested `ContextPlus.root`.
In case this is completely fine, I suggest:
- Add test cases for nested `ContextPlus.root`
- Add an actual Exception like [`ProviderNotFoundException`](https://pub.dev/documentation/provider/latest/provider/ProviderNotFoundException-class.html) which I can catch in `bindWhenUnbound`.
2 条评论