ITADN

Fully support multiple nested ContextPlus.root?

#26Openpasssy 创建于 2025-06-20
P
passsycommented
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 条评论