How to scope a provider in riverpod? - flutter

I'm starting to use riverpod and I'm trying to migrate my existing code which was using provider.
With provider, the Providers were scoped in the widget tree. Only the children of the Provider widget could access its model.
It says in the riverpod's doc:
Allows easily accessing that state in multiple locations. Providers are a complete replacement for patterns like Singletons, Service Locators, Dependency Injection or InheritedWidgets.
* "Providers" here refers to the Provider class of the package riverpod.
And the provider package was a simplification/wrapper/API around InheritedWidgets so I guess what was possible with provider is also possible with riverpod.
But I cannot manage to find out how to do it.
Here is a small example of what I am trying to migrate.
class Counter extends ValueNotifier<int> {
Counter(): super(0);
}
class MyWidget extends StatelessWidget {
const MyWidget();
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<Counter>(
create: (_) => Counter();
child: Builder(
builder: (context) {
return Row(
children: [
IconButton(
onPressed: () {
context.read<Counter>().value++;
},
icon: Icon(Icons.add),
),
Text('Count: ${context.watch<Counter>().value}'),
],
);
},
),
);
}
}
Wherever there is a MyWidget widget, the sub-widget has access to a unique/scoped model Counter.

Scoping providers is done through ProviderScope
You can do:
final provider = ChangeNotifierProvider<Counter>((ref) => throw UnimplementedError());
// in some widget:
return ProviderScope(
overrides: [
provider.overrideWithProvider(
ChangeNotifierProvider((ref) => Counter());
),
],
)
Although if you can avoid scoping, do so. Scoping providers is generally not recommended and should be avoided as that's fairly advanced.
If you're using a scoped provider so that the state is destroyed when the user leaves the page, a much simpler solution is to use autoDispose:
final provider = ChangeNotifierProvider.autoDispose<Counter>((ref) => Counter());
// No longer needed to scope the provider

Inside ProviderScope is an InheritedWidget, so you can you multiple ProviderScope just like Provider. Please checkout the code of Remi in this ticket link.
Or you can use .family and .autoDispose for your purpose.

Related

When do we initialise a provider in flutter?

I just arrived on a flutter project for a web app, and all developers have a problem using flutter provider for state management.
What is the problem
When you arrive on a screen, the variables of the corresponding provider are initialised by calling a function of the provider. This function calls an api, and sets the variables in the provider.
Problem : This function is called in the build section of the widget. Each time the window is resized, the widget is rebuilt, and the function is called again.
What we want
We want to call an api when the page is first displayed, set variables with the result, and not call the api again when the widget is rebuilt.
What solution ?
We use a push from the first screen to go to the second one. We can call the function of the provider at this moment, to initialise the provider just before the second screen.
→ But a refresh on the second page will clear the provider variables, and the function to initialise them will not be called again.
We call the function to initialise the provider in the constructor of the second screen. Is it a good pattern ?
Thank you for your help in my new experience with flutter :)
I think you're mixing a couple different issues here:
How do you correctly initialize a provider
How do you call a method on initialization (only once)
For the first question:
In your main.dart file you want to do something like this:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => SomeProvider()),
ChangeNotifierProvider(create: (context) => AnotherProvider()),
],
child: YourRootWidget();
);
}
Then in a widget (that probably represents a "screen" in your app), you need to do something like this to consume state changes from that provider:
#override
Widget build(BuildContext context) {
return Container(
child: Consumer<SomeProvider>(
builder: (context, provider, child) {
return Text(provider.someState);
}
),
)
}
And you need to do something like this to get access to the provider to mutate state:
#override
Widget build(BuildContext context) {
SomeProvider someProvider = Provider.of<SomeProvider>(context, listen: false);
return Container(
child: TextButton(
child: Text('Tap me'),
onPressed: () async {
await someProvider.mutateSomeState();
}
),
)
}
Regarding the second question... You can (I think) just use the initState() method on a widget to make the call only 1 time. So...
#override
void initState() {
super.initState();
AnotherProvider anotherProvider = Provider.of<AnotherProvider>(context, listen: false);
Future.microtask(() {
anotherProvider.doSomethingElse();
});
}
If I'm off on any of that, I'm sorry. That mirrors my implementation and works fine/well.
A caveat here is that I think RiverPod is likely the place you really want to go (it's maybe easier to work with and has additional features that are helpful, etc.) but I've not migrated to RiverPod yet and do not have that figured out all the way.
Anyway... Good luck!
As far as I understood, you can wrap your application with MultiProvider and call the API before going to the second screen.

Do I have to specifically call dispose() when I use Provider<T> class?

I was reading the documentation and it states that ChangeNotifierProviders will automatically be disposed.
https://pub.dev/packages/provider#existing-providers
Provider
The most basic form of provider. It takes a value and exposes it, whatever the value is.
ChangeNotifierProvider
A specification of ListenableProvider for ChangeNotifier. It will automatically call ChangeNotifier.dispose when needed.
So, when I'm using a basic Provider class as the top of my widget tree, do I need to dispose() it in my app's lifecycle? Given it will not be automatically disposed, and the reason behind?
void main() {
runApp(
Provider(
create: (context) => MemberRepository(),
child: const MyApp(),
),
);
}

Flutter: accessing providers from other providers

For my flutter project, I am using the following multiple providers below:
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<FirstProvider>(
create: (context) => FirstProvider(),
),
ChangeNotifierProvider<SecondProvider>(
create: (context) => SecondProvider(),
),
ChangeNotifierProvider<ThirdProvider>(
create: (context) => ThirdProvider(),
),
ChangeNotifierProvider<FourthProvider>(
create: (context) => FourthProvider(),
),
],
child: const MainApp(),
);
}
Because sometimes I need to either get data or call functions from different providers from another provider, I am using it like this:
//First Provider
class FirstProvider with ChangeNotifier {
void callFunctionFromSecondProvider({
required BuildContext context,
}) {
//Access the SecondProvider
final secondProvider= Provider.of<SecondProvider>(
context,
listen: false,
);
secondProvider.myFunction();
}
}
//Second Provider
class SecondProvider with ChangeNotifier {
bool _currentValue = true;
void myFunction(){
//Do something
}
}
The callFunctionFromSecondProvider()of the FirstProvider is called from a widget and it will call myFunction() successfully, most of times.
Depending on the complexity of the function, I am sometimes experiencing that I can't access the SecondProvider, presumably due to context being null, when the widget state changes.
I am reading some documents online regarding provider, and they are suggesting changenotifierproxyprovider for what I understood as 1 to 1 provider relationship.
However, in my case, one provider needs to be accessed by multiple providers and vice versa.
Question:
Is there a more appropriate way that I can approach my case where one provider can be accessed by multiple providers?
EDIT:
Accessing provider should also be able to access different variable values without creating a new instance.
Instead of passing context to the callFunctionFromSecondProvider function add the second provider as the parameter. So the function looks like the below.
Not sure this is the correct way of doing that but my context null issue was fixed this way.
void callFunctionFromSecondProvider({
required SecondProvider secondProvider,
}) {
secondProvider.myFunction();
}
}
Alright.
So it looks like Riverpod by the same author is the way to go as it addresses alot of flaws such as Provider being dependent on the widget tree, in my case, where the underlying issue came from.
—--------
For the time being, I still need to use the provider and for a quick and dirty solution, I am providing the context of not only the current widget that I am trying to access the provider, but also passing the parent context of the widget directly, so that in case a modal (for example) is closed, then any subsequent provider call can still be executed using the parent context.
Hope this helps.

Triggering Widget Rebuilds with Provider's context.read<T>() Method

According to Flutter's documentation and this example, as I'm understanding it, a key difference between the Provider package's context.read<T> and context.watch<T> methods relate to triggering widget rebuilds. You can call context.watch<T>() in a build method of any widget to access current state, and to ask Flutter to rebuild your widget anytime the state changes. You can't use context.watch<T>() outside build methods, because that often leads to subtle bugs. Instead, they say, use context.read<T>(), which gets the current state but doesn't ask Flutter for future rebuilds.
I tried making this simple app:
class MyDataNotifier extends ChangeNotifier {
String _testString = 'test';
// getter
String get testString => _testString;
// update
void updateString(String aString) {
_testString = aString;
notifyListeners();
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => MyDataNotifier(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text(context.read<MyDataNotifier>().testString),
),
body: Container(
child: Level1(),
),
),
);
}
}
class Level1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: (val) {
context.read<MyDataNotifier>().updateString(val);
},
),
Text(context.read<MyDataNotifier>().testString),
],
);
}
}
All the calls are to counter.read<T>(). The app's state changes, but the UI is not rebuilt with the new value. I have to change one of the calls to counter.watch<T>() to get the state to rebuild.
On the other hand, in DZone's simple example, the UI rebuilds, and all the calls are to context.read().
What's different between their code and mine? Why can't I rebuild with counter.read() calls?
TLDR: after a quick glance, the DZone article looks like it has a bug.
Longer answer
context.watch<Foo>() does 2 things:
return the instance of the state from the tree
mark context as dependent on Foo
context.read<Foo>() only does 1).
Whenever your UI depends on Foo, you should use context.watch, since this appropriately informs Flutter about that dependency, and it will be rebuilt properly.
In general, it boils down to this rule of thumb:
Use context.watch in build() methods, or any other method that returns a Widget
Use context.read in onPressed handlers (and other related functions)
The main reason people seem to use context.read inappropriately is for performance reasons. In general, preferring context.read over context.watch for performance is an anti-pattern. Instead, you should use context.select if you want to limit how often a widget rebuilds. This is most useful whenever you have a value that changes often.
Imagine you have the following state:
class FooState extends ChangeNotifier {
// imagine this us updated very often
int millisecondsSinceLastTap;
// updated less often
bool someOtherProperty = false;
}
If you had a widget that displays someOtherProperty, context.watch could cause many unnecessary rebuilds. Instead, you can use context.select only depend on a processed part of the state:
// read the property, rebuild only when someOtherProperty changes
final property = context.select((FooState foo) => foo.someOtherProperty);
return Text('someOtherProperty: $property');
Even with a frequently updating value, if the output of the function provided to select doesn't change, the widget won't rebuild:
// even though millisecondsSinceLastTap may be updating often,
// this will only rebuild when millisecondsSinceLastTap > 1000 changes
final value = context.select((FooState state) => state.millisecondsSinceLastTap > 1000);
return Text('${value ? "more" : "less"} than 1 second...');

BlocProvider.of() called with a context that does not contain a Bloc of type MainBloc

I have a MainBloc that resides inside a main route, this route has a bottom app bar with multiple sub-routes, I want the same BLoC to run on all five sub-routes so that when one of them changes the state of the block the others will see the effect.
I tried this SO question but its really far from what I'm looking for, also I tried following what the error advised me to, but didn't work, here is the message that I got:
This can happen if:
1. The context you used comes from a widget above the BlocProvider.
2. You used MultiBlocProvider and didn't explicity provide the BlocProvider types.
Good: BlocProvider<MainBloc>(builder: (context) => MainBloc())
Bad: BlocProvider(builder: (context) => MainBloc()).
Main route:
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<MainBloc>(
builder: (BuildContext context) => MainBloc(),
),
BlocProvider<OtherBloc>(
builder: (BuildContext context) => OtherBloc(),
),
],
child: /..., //here I have the bottom app bar with 5 buttons to navigate between sub-routes
);
one of the sub-routes:
#override
Widget build(BuildContext context) {
final MainBloc bloc = BlocProvider.of<MainBloc>(context);
return /...; //here I have the context of this sub-route.
}
from what I've seen from tutorials and articles this code should work, but I can't seem to find why not.
The problem is you cannot access InheritedWidgets across routes unless you provide the InheritedWidget above MaterialApp. I would recommend wrapping your new route in BlocProvider.value to provide the existing bloc to the new route like:
Navigator.of(context).push(
MaterialPageRoute<MyPage>(
builder: (_) {
return BlocProvider.value(
value: BlocProvider.of<MyBloc>(context),
child: MyPage(),
);
},
),
);
You can find more detailed information about this in the bloc documentation
As this child has the bottom app bar:
child: /..., //here I have the bottom app bar
then I assume that the MultiBlocProvider(..) is not wrapping the whole part of app which is using this Bloc, my suggestion here is to wrap the "MaterialApp" with "MultiBlocProvider".
return MultiBlocProvider(
providers: [..],
child: MaterialApp(..) // Set MaterialApp as the child of the MultiBlocProvider
//..
)