using ref.read inside build method for StateControllers - flutter

As per the Riverpod documentation, asynchronously we use ref.read such as inside a function and for synchronous code, we use ref.watch such as inside the build method.
Once I press a button, the function with ref.read will fire up and it will be for only one time. Here, I should use ref.watch as it is now inside build method and with onPressed it will be ref.read
Case 1:
// Bad practice
// Documentation says, "DON'T use ref.read inside the build method".
final counterProvider = StateProvider((ref) => 0);
Widget build(BuildContext context, WidgetRef ref) {
StateController counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
Case 2:
// Good Practice
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Text('button'),
),
Case 3:
// Good Practice
Widget build(BuildContext context, WidgetRef ref) {
StateController counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}
In all 3 cases, code remains asynchronous since it is called only when the button is pressed then why is case1 bad and case 3 good practice?

the docs says you shouldn't use ref.read inside your build method because it will not cause a rebuild when the state changes, and in most cases you'll need to update your ui.
You can definitely use it inside the build method, but it will just "read" the data, if your state updates, the ui won't, this why you should always use watch inside your build method, to always update your ui when the state changes.
check this dartpad example from riverpod-v2 docs, try to use read instead of watch, it will work, but it's not actually working right? the ui doesn't update when you press the increment button.

As per Remi Rousselet,
"It's about when read/watch are invoked, not when the state change is triggered."

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.

How to change displayed data the best way in Flutter

i want to change displayed data in Flutter? I wrote a function changeDataForTest (only a function for testing the event), which should change the data displayed in Text.
But if I click on this, it isn't changed. The value of the displayed string only changes, if i add (context as Element).reassemble(); after calling the method. Is this the normal way to go, or is there a smoother way to solve my problem?
dynamic changeDataForTest(neuerWert) {
this.data = neuerWert;
}
Column(
children: [
Center(
child: Text(
this.data + this.wiegehts,
),
),
FlatButton(
textColor: Color(0xFF6200EE),
onPressed: () {
changeDataForTest('neuerWert');
(context as Element).reassemble();
},
)
],
)
Thanks
Lukas
If you're using only a small widget, you could use a StatefulWidget using the method:
setState(() {
// change your variable
})
If your widget is complex and has lots of different possible variables, I'll not recommend using setState as this method calls the build method every time is being used.
One simple and fast option, is to use ValueNotifier:
final myVariable = ValueNotifier(false); // where you can replace 'false' with any Object
and then, using it this way:
ValueListenableBuilder(
valueListenable: myVariable,
builder: (context, value, child) {
return Text(value); // or any other use of Widgets
},
);
myVariable.value = true; // if you're looking for to change the current value
finally, if you logic is truly complex and you need to scale, I'll recommend to use a StateManagement library like:
Provider
Riverpod
BloC
Others
You can find those libraries and examples over: https://pub.dev

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
});

How to rebuild the state after pushing a new screen

When i push a new screen onTap with Navigator and pass a new class constructor, how can I have that new screen updates every time _playerTimer updates without having to click again
Since the state of my new class only updates onTap, please help!
The build method of FullScreenDialog is called once since its only being built when onTap is pressed
InkWell(
onTap: () {
return Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => FullScreenDialog(
_playerTimer,
));
},
child: VideoPlayer(
_controller,)
);
you have to use setState to rebuild the UI.
ex:
setState((){
_playerTimer = _playerTimer + 1;
});
that's about all the help I can give without seeing the rest of your code
When you instantiate a new class (in your code would be the FullScreenDialog) by passing an attribute, you're only saying to your code that a new class will be initialized by using the argument you provided.
If you want your FullScreenDialog class to always be updated when _playerTimer changes, you must observe this attribute inside this class by using setState(), which is a build-in function for StatefulWidgets that makes apps' UI update every time the observable attribute changes.
Example:
setState( () {
_playerTimer = getUpdatedTimer();
});
Supposing that inside getUpdatedTimer() method you will manage the logic for updating this variable, calling a service or so.
If you want this variable to be updated without interacting with interface, you probably will need a timer, too. Check this question to more details about it.
If you're starting with Flutter development, I suggest you to read this article (Adding interactivity to your Flutter app) about state management
and
setState method documentation.
Hope that helps.

Use showDialog with conditional in build method

I have a screen, which shows a button. If I press it, an async job is started. During this time, I want to show an AlertDialog with a spinning wheel. If that job is finished, i will dismiss the dialog or show some errors. Here is my (simplified) code:
Widget build(BuildContext context) {
if (otherClass.isTaskRunning()) {
showDialog( ... ); // Show spinning wheel
}
if (otherClass.hasErrors()) {
showDialog( ...); // Show errors
}
return Scaffold(
...
FlatButton(
onPress: otherClass.startJob
)
);
}
The build will be triggered when the job status is changed or if there are errors. So far, so good, but if I run this code, I got this error message:
Exception has occurred. FlutterError (setState() or markNeedsBuild()
called during build. This Overlay widget cannot be marked as needing
to build because the framework is already in the process of building
widgets. A widget can be marked as needing to be built during the
build phase only if one of its ancestors is currently building. This
exception is allowed because the framework builds parent widgets
before children, which means a dirty descendant will always be built.
Otherwise, the framework might not visit this widget during this build
phase. The widget on which setState() or markNeedsBuild() was called
was: Overlay-[LabeledGlobalKey#357d8] The widget which
was currently being built when the offending call was made was:
SettingsScreen)
So, the repaint of the screen will be overlap somehow. I am not sure how to fix this. It feels like I am using this completely wrong. What is the prefered way to handle this kind of interaction (trigger "long" running task, show progress indicator and possible errors)?
The problem is the dialog is going to show while the build method hasn't already finish. So if you want to show a Dialog, you should do it after the build method has finished. To do that, you can use this: WidgetsBinding.instance.addPostFrameCallback(), that will call a function after the last frame was built (just after build method ends).
Other thing you can do is using the ternary operator to show a loading widget like so:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: otherClass.isTaskRunning()
? CircularProgressIndicator()
: FlatButton(
onPressed: () {},
),
);
}
As being said in the comment, calling showDialog and similar inside build is anti-pattern. Here is the detailed explanation.
Although is a bit late, it is important to note showDialog is indeed a async method, returning at the time that the dialog dismisses, and build is a sync in nature. Under the hood, it use Navigator.of(context).push.
Referring to this question,
The build method is designed in such a way that it should be pure/without side effects. This is because many external factors can trigger a new widget build, such as: Route pop/push
So this directly caused flutter to complain setState is called during build. You could just use a FutureBuilder inside the dialog.
One thing is clear is that your SettingsScreen is not required to fetch anything and so you should not brother with any of them. As you are deferring to do that as the button fires, then you should do it within dialog.
Widget build(BuildContext context) {
return Scaffold(
body: FlatButton(
onPress: () async {
await showDialog(
context: context,
builder: (BuildContext context) {
return FutureBuilder(
future: otherClass.startJob(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) // Show error
if (snapshot.hasData) // Show data, or you can just close it by Navigator.pop
else // show spinning wheel
}
}
);
}
);
}
)
);
}