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

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...');

Related

Is it a good practise to use many valuelistenable builders instead of setState in flutter?

I am making a flutter app and on one of the pages, I have many changing variables.
I am confused if I should use ValueListenableBuilder for all the variables or I should just use setState for all the variables.
It really depends. If it is a really simple widget with not that many variables changing, just use setState.
However, if you have MANY variables that has to change constantly. You'd better use ValueListenableBuilder, and actually that's one of the reasons to use the state management.
for example, take a look at the code below.
class SampleStatefulWidget extends StatefulWidget {
const SampleStatefulWidget({super.key});
#override
State<SampleStatefulWidget> createState() => _SampleStatefulWidgetState();
}
class _SampleStatefulWidgetState extends State<SampleStatefulWidget> {
final _variable1 = 'hello';
final _variable2 = 'world';
final _variable3 = 'foo';
final _variable4 = 'bar';
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
Text(_variable1),
Text(_variable2),
Text(_variable3),
Text(_variable4),
],
),
),
);
}
}
for the above code, if you use setState, the whole widget will be drawn again even though you just updated _variable1.
however, if you use ValueListenableBuilder, it will only update that part where _variable1 is in.
Therefore, if it is a complex widget with bunch of variables that constantly change, use some libraries that will help you manage states such as Provider or Riverpod. (I personally really don't recommend GetX)
or you can create your own BLoC for BLoC Pattern, or you can also use library for BLoC as well.

How can you access GoRoute state.params outside of the routing process?

I hope some simple pseudocode is enough so that both me and you can understand the question and answer.
The problem I'm facing is especially hard when using flutter-web, where the refresh restarts the whole program.
I want to use the path parameters to build objects in a child widgets build method.
GoRouter(routes:...,GoRoute(path="/categories/:category/",
builder:(context,state){
category = state.params['category];
return ParentWidget(category);}
ParentWidget extends StatelessWidget {
build(context){
return ChildWidget();}}
ChildWidget extends StatlessWidget {
build(context){
return "do something with category";}}
Now one way which I can think of and should technically work without any errors would be to pass the params first into the ParentWidget and then pass it along to the next child and so on. But if there's a long chain of child widgets it gets quite tedious and I'm guessing error prone as well. The other thing I was thinking was to use providers: pass the param once again to the parent widget and then make the parent widget send it to a provider. But then the question becomes, where do I do it? Apparently I shouldn't update a provider on build(), but if I do it on initState() it only does it bugs out if I change into a route that include the same widget tree but different path e.g. /categories/apples -> /categories/bananas.
Ps. For some reason I don't remember what the problem with refreshing was. (It has something to do with resetting the providers). But I'll update it when I remember.
go_router has it's params in its state.
Hence pass the state to the page
Router
GoRoute(
name: "test",
path: "/test/:id",
builder: (context, state) {
return SampleWidget(
goRouterState: state, 👈 Pass state here
);
},
),
Usage
context.goNamed("test", params: {"id": "123"}),
Accesing in the page
class SampleWidget extends StatelessWidget {
GoRouterState? goRouterState;
SampleWidget({super.key, this.goRouterState});
#override
Widget build(BuildContext context) {
print(goRouterState?.params.toString()); 👈 access anywhere like so
return const Scaffold(
body: ...
);
}
}

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.

can i return the GetBuilder in StatelessWidget build function directly?

some days ago, i raise an issue which link https://github.com/jonataslaw/getx/issues/2038#issue-1075353819
now i think there are something wrong in use getx ?
in my project i always return GetBuilder in build function, because my Scaffold has too many logic and state to use.
like this
class AppLogsPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetBuilder<AppLogsLogic>(builder: (l) => Scaffold(
appBar: AppBar(
title: Text("app logs"),
),
// ... many logic and state use
),
);
}
}
but i saw the official document many times, it show me place the GetBuilder in the area where has some logic or state should be inject.
so i want to know it will make me face some problem? or not the best practice in Getx

Why can't I use context.read in build(), but I can use Provider.of with listen: false?

It's stated in the docs that these are the same, and context.read is just a shortcut for Provider.of<x>(context, listen: false).
There's also an error in the console if I try to use context.read in a build method, but it doesn't explain the reason.
I also found this topic: Is Provider.of(context, listen: false) equivalent to context.read()?
But it doesn't answer "why".
context.read is not allowed inside build because it is very dangerous to use there, and there are much better solutions available.
Provider.of is allowed in build for backward-compatibility.
Overall, the reasoning behind why context.read is not allowed inside build is explained in its documentation:
DON'T call [read] inside build if the value is used only for events:
Widget build(BuildContext context) {
// counter is used only for the onPressed of RaisedButton
final counter = context.read<Counter>();
return RaisedButton(
onPressed: () => counter.increment(),
);
}
While this code is not bugged in itself, this is an anti-pattern.
It could easily lead to bugs in the future after refactoring the widget
to use counter for other things, but forget to change [read] into [watch].
CONSIDER calling [read] inside event handlers:
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
// as performant as the previous previous solution, but resilient to refactoring
context.read<Counter>().increment(),
},
);
}
This has the same efficiency as the previous anti-pattern, but does not
suffer from the drawback of being brittle.
DON'T use [read] for creating widgets with a value that never changes
Widget build(BuildContext context) {
// using read because we only use a value that never changes.
final model = context.read<Model>();
return Text('${model.valueThatNeverChanges}');
}
While the idea of not rebuilding the widget if something else changes is
good, this should not be done with [read].
Relying on [read] for optimisations is very brittle and dependent
on an implementation detail.
CONSIDER using [select] for filtering unwanted rebuilds
Widget build(BuildContext context) {
// Using select to listen only to the value that used
final valueThatNeverChanges = context.select((Model model) => model.valueThatNeverChanges);
return Text('$valueThatNeverChanges');
}
While more verbose than [read], using [select] is a lot safer.
It does not rely on implementation details on Model, and it makes
impossible to have a bug where our UI does not refresh.
The problem is that you try to call context before the widget has finished building, to run your code after the widget has finished building provide your code to the post frame callback function.
For Example:
WidgetsBinding.instance.addPostFrameCallback((_) {
// your code in here
});