-
-
Notifications
You must be signed in to change notification settings - Fork 956
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NotifierProvider override can't be replaced #2458
Comments
That's how overrideWith / overrideWithProvider works. Once the value is instantiated, replacing the provider is no-op until the provider is fully disposed (autoDispose). Then and only then is the new override considered A different behavior would be problematic, as overrides are often created inside the build method of widgets. |
Does this mean I can't use I'm struggling to implement a feature flag, that switches an implementation. import 'package:flutter/material.dart' hide Listener;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Switch override with feature flag', (tester) async {
final count = NotifierProvider<Notifier<int>, int>(
() => throw 'Not provided, requires override',
);
final featureFlag = StateProvider<bool>((ref) => false);
await tester.pumpWidget(
ProviderScope(
overrides: [
count.overrideWith(() {
// How to switch from one instance to another? ref doesn't exist
// final bool flag = ref.watch(featureFlag.notifier);
// if (flag) {
// return AlwaysFive();
// }
return Counter();
}),
],
child: MaterialApp(
home: Consumer(
builder: (context, ref, child) {
return Scaffold(
body: Text(ref.watch(count).toString()),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(featureFlag.notifier).state = true;
},
),
);
},
),
),
),
);
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byType(FloatingActionButton));
await tester.pump();
expect(find.text('5'), findsOneWidget);
});
}
class Counter extends Notifier<int> {
@override
int build() {
return 0;
}
void increment() {
state++;
}
}
class AlwaysFive extends Notifier<int> {
@override
int build() {
return 5;
}
} I expect that all providers that depend on my |
Do: class Counter extends Notifier<int> {
@override
int build() {
if (ref.watch(flagProvider)) return 5;
return 0;
} |
What if Counter lives in a different package and has no access to This is the solution with Note: The import 'package:flutter/material.dart' hide Listener;
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
void main() {
testWidgets('Switch override with feature flag', (tester) async {
final featureFlag = ValueNotifier(false);
await tester.pumpWidget(
ListenableProvider<ValueNotifier<bool>>.value(
value: featureFlag,
builder: (context, child) {
final counter = context.watch<ValueNotifier<bool>>().value
? AlwaysFive()
: CounterImpl();
return ChangeNotifierProvider<Counter>.value(
value: counter,
builder: (context, _) {
return Builder(
builder: (context) {
return MaterialApp(
home: Scaffold(
body: Text(context.watch<Counter>().count.toString()),
floatingActionButton: FloatingActionButton(
onPressed: () {
context.read<ValueNotifier<bool>>().value = true;
},
),
),
);
},
);
},
);
},
),
);
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byType(FloatingActionButton));
await tester.pump();
expect(find.text('5'), findsOneWidget);
});
}
abstract class Counter extends ChangeNotifier {
int get count;
void increment();
}
class CounterImpl extends ChangeNotifier implements Counter {
int _count = 0;
@override
int get count => _count;
@override
void increment() {
_count++;
notifyListeners();
}
}
class AlwaysFive extends ChangeNotifier implements Counter {
@override
int get count => 5;
@override
void increment() {}
}
|
Your provider sample is flawed. You shouldn't create notifiers inside the build method like this. Any rebuild would destroy the state. But anyway, you cannot change the Notifier instance live. Not much else to say. But I don't see why you would have to use such an architecture. What are you trying to do exactly? You've only talked about the solution you're trying to use, not the problem you're trying to solve. |
This is just a sample. The real code saves the instance in a The real-world use case: Switch between two The user presses a Button, I want to change the instance of |
You haven't described a problem but a solution. I'm asking for the actual problem, the business one. Because chances are there's a different path possible. But anyway, if you absolutely want to use the OOP path, you can use the delegate pattern @riverpod
class YourNotifier extends _$YourNotifier {
YourNotifierDelegate delegate;
@override
Widgwt build() => ref.watch(delegateProvider).build();
void increment() => ref.watch(delegateProvider).build();
}
abstract class YourDelegate { /* Not a Notifier */
int build();
void increment();
}
@riverpod
YourDelegate delegateProvider(DelegateProviderRef ref) {
if (ref.watch(flagProvider)) return Always5();
return DefaultImpl();
} |
Sure I can create my own delegate. But isn't riverpod like the ultimate delegate? I expected it to offer such a feature. And it does work for normal What bugs me is that the pattern is working fine with normal import 'package:flutter/material.dart' hide Listener;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Switch override with feature flag', (tester) async {
final featureFlag = StateProvider<bool>((ref) => false);
// Works but doesn't notify when Counter changes
final count = Provider<Counter>((ref) {
final flag = ref.watch(featureFlag);
if (flag) {
return AlwaysFive();
}
return CounterImpl();
});
// Can't subscribe to featureFlag
final notifierCount = NotifierProvider<Counter, int>(() {
// ERROR: no ref,
final flag = ref.watch(featureFlag);
if (flag) {
return AlwaysFive();
}
return CounterImpl();
});
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
home: Consumer(
builder: (context, ref, child) {
return Scaffold(
body: Text(ref.watch(count).count.toString()),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(featureFlag.notifier).state = true;
},
),
);
},
),
),
),
);
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byType(FloatingActionButton));
await tester.pump();
expect(find.text('5'), findsOneWidget);
});
}
abstract class Counter implements Notifier<int> {
void increment();
int get count;
}
class CounterImpl extends Notifier<int> implements Counter {
@override
int build() {
return count;
}
@override
void increment() {
state++;
count++;
}
@override
int count = 0;
}
class AlwaysFive extends Notifier<int> implements Counter {
@override
int build() {
return 5;
}
@override
void increment() {}
@override
int get count => 5;
}
|
Unfortunately, the delegate pattern also doesn't work because @riverpod
class YourNotifier extends _$YourNotifier {
YourNotifierDelegate delegate;
@override
Widgwt build() => ref.watch(delegateProvider).build();
void increment() => ref.watch(delegateProvider).build();
}
abstract class YourDelegate { /* Not a Notifier */
int build();
void increment();
}
@riverpod
YourDelegate delegateProvider(DelegateProviderRef ref) {
if (ref.watch(flagProvider)) return Always5();
return DefaultImpl();
}
|
Got it working! The trick is to create a This example now switches between import 'package:flutter/material.dart' hide Listener;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Switch override with feature flag', (tester) async {
final featureFlag = StateProvider<bool>((ref) => false);
final counter = NotifierProvider<Counter, int>(
() => throw 'Not provided, requires override');
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
home: Consumer(
builder: (context, ref, child) {
final flag = ref.watch(featureFlag);
return ProviderScope(
key: flag ? const Key('fake') : const Key('real'),
overrides: [
if (flag)
counter.overrideWith(AlwaysFive.new)
else
counter.overrideWith(CounterImpl.new)
],
child: Consumer(
builder: (context, ref, child) {
return Scaffold(
body: Text(ref.watch(counter).toString()),
floatingActionButton: FloatingActionButton(
onPressed: () {
ref.read(featureFlag.notifier).state = true;
},
),
);
},
),
);
},
),
),
),
);
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byType(FloatingActionButton));
await tester.pump();
expect(find.text('5'), findsOneWidget);
});
}
abstract class Counter implements Notifier<int> {
void increment();
int get count;
}
class CounterImpl extends Notifier<int> implements Counter {
@override
int build() {
return count;
}
@override
void increment() {
state++;
count++;
}
@override
int count = 0;
}
class AlwaysFive extends Notifier<int> implements Counter {
@override
int build() {
return 5;
}
@override
void increment() {}
@override
int get count => 5;
}
|
I told you that the delegate should not be a notifier though. But anyway you didn't answer my question still. The delegate pattern I gave is only a workaround to forcibly what you think you should do. You clearly don't want to make a counter with a custom implementation which always returns 5. |
Not sure what I can add to the real-world problem other than what I stated. We have a user service that manages the whole login flow, provided by a third party. Our app is still in the prototype phase and works without a server. Auth is fully integrated, but not working due to some backend configuration. It's important that the real It's not that different from the counter. Instead of a counter that returns always |
What about mocking the network requests instead? Such as using an interceptor and returning a pre-set response? Then you'd stick to using the official AuthService, but with predefined responses. |
I really appreciate that you want to find alternative solutions and think outside the box, seriously! The auth package I hide behind the interface is from a 3rd party. It's very complex and I don't know the internals. Mocking responses might be hard at best or impossible due to signing and encryption of the responses. Either way a nightmare to maintain when it changes and way to much code for what I try to achieve. Hiding complex code behind an interface, and being able to swap the actual implementation is a common practice. I think we agree on that. The It all works as I want it to, except for when I use When I switch it to I opened a new issue where I show the API differences between Thanks for your time as I was discovering the internals of riverpod! |
I'm not sure why it would be a nightmare to mock the requests. That's how most people do it. They don't replace the Notifier, they replace a service dependency. |
Again, the Also, responses like JWTs can't be mocked easily. They expire and can't be created without private key. The 3rd party The only mistake that was made is that the |
Do you mean that Auth being a 3rd party or not doesn't change much. You can make a wrapper around it if needed, to recover the ownership of the class and update whatever you need to.
StateNotifier isn't supported by the new code-gen syntax though. You're not using it yet, but it'll replace the old syntax once metaprogramming is out. |
Correct.
That's a task for Future Pascal, earliest next year 🚀 |
Honestly, I'd use the delegate variant (but don't make the delegate implement Notifier as I said before) It's a pretty common pattern. Sure that's more tedious than you'd like, I get that. But it's also a pretty rare use case. To begin with, it's apparently not something you want to ship in release mode. |
I try to exchange an overridden NotifierProvider. But I'm not able to access the new object after
updateOverrides
.The text was updated successfully, but these errors were encountered: