Post Frame Callback - Looping - flutter

I have been checking performance on my app and noticed that one of the widget constantly loops. in that Widget I am using the following code to retrieve data from Firestore DB, however for this example I have simplified it with the same looping result.
Question: Is there a reason why Widget Binding is called so many times and in a loop? I was under the impression it was called once on widget build complete. Should I be using something else for a one off post build function?
I have 7 of these widgets in a listView, so I should she maximum 7 I believe.
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPersistentFrameCallback((timeStamp) async {
print(' Is callback done?');
if (mounted) {
setState(() {
isLoaded = true;
});
}
});
}
The widget itself is very simple with just a Text Widget
#override
Widget build(BuildContext context) {
return Text('Hello');
);
}
In my logs I see the following which just keeps going up and up and up.

Maybe you want addPostFrameCallback instead of addPersistentFrameCallback ?
As per documentation addPersistentFrameCallback:
Once registered, they are called for every frame for the lifetime of the application.
As per documentation addPostFrameCallback:
Post-frame callbacks cannot be unregistered. They are called exactly once.

Related

Flutter Difference between InitState and just putting inside Widget Build function

I had an error every time I restarted my App: This widget has been unmounted, so the State no longer has a context (and should be considered defunct). and saw that something was not correct with my initstate. The initState was:
#override
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) {
BlocProvider.of<TutSkippedCubit>(context).load();
});
super.initState();
}
the methods loads the data from sharedprefs if I have already skipped the tut or not. Now I solved this issue with removing the initState method and putting the function call inside the widget build:
#override
Widget build(BuildContext context) {
BlocProvider.of<TutSkippedCubit>(context).load();
....
The widget build gets called when the pages loads, so, isn't it the same as the initial state? For what exactly is the methode initState() and I have the feeling that my way of handling this problem is a bad practise, but what would be a better way, how do I solve it?
The initState() method is to control what happens after the app is built. The problem is that you call BlocProvider before the app begins. The correct way is to put all the actions after super.initState() call and add the context to the BlocProvider inside build method. Like this:
TutSkippedCubit? tutSkippedCubitProvider;
#override
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) {
tutSkippedCubitProvider!.load();
});
}
#override
Widget build(BuildContext context) {
tutSkippedCubitProvider = BlocProvider.of<TutSkippedCubit>(context);
...
}
The initState and build method is called when the widget is inserted into the widget tree, but the build method also is called every time the state is changed.
You do need to have in mind that every time the state is changed your method BlocProvider.of<TutSkippedCubit>(context).load(); also is called.
Maybe, the code below can help you:
WidgetsBinding.instance.endOfFrame.then(
(_) async {
if (mounted) {
BlocProvider.of<TutSkippedCubit>(context).load();
}
},
);
You wouldn't be surprise of getting that error since you are using BlocProvider.<T>(context) out of a BuildContext. This context in bracket is the just the same as the one given in the build function.
The initState() is a method that is called when an object for your
stateful widget is created and inserted inside the widget tree.

decide the route based on the initialization phase of flutter_native_splash

I'm writing a flutter application using flutter 2.10 and I'm debugging it using an Android Emulator
I included the flutter_native_splash plugin from https://pub.dev/packages/flutter_native_splash and I use version 2.0.1+1
the problem that I'm having is that I decide what's the first screen that the user will see based on the initialization phase. I check the stored user token, see his premissions, verify them with the server, and forward him to him relevant route.
since the runApp() function executes in the background while the initialization phase is running I cannot choose the page that will be shown. and if I try to nativgate to a route in the initialization function I get an exception.
as a workaround for now I created an init_home route with FutureBuilder that awaits for a global variable called GeneralService.defaultRoute to be set and then changes the route.
class _InitHomeState extends State<InitHome> {
#override
Widget build(BuildContext context) {
return FutureBuilder<dynamic>(
future: () async {
var waitCount=0;
while (GeneralService.defaultRoute == "") {
waitCount++;
await Future.delayed(const Duration(milliseconds: 100));
if (waitCount>20) {
break;
}
}
if (GeneralService.defaultRoute == "") {
return Future.error("initialization failed");
}
Navigator.of(context).pushReplacementNamed(GeneralService.defaultRoute);
...
any ideas how to resolve this issue properly ?
I use a Stateful widget as Splash Screen.
In the build method, you just return the 'loading' view such as Container with a background color etc. (with texts or whatever you like but just consider it as the loading screen).
In the initState(), you call a function that we can name redirect(). This should be an async function that performs the queries/checks and at the end, calls the Navigator.of(context).pushReplacementNamed etc.
class _SplashState extends State<Splash> {
#override
void initState() {
super.initState();
redirect();
}
#override
Widget build(BuildContext context) {
return Container(color: Colors.blue);
}
Future<void> redirect() async {
var name = 'LOGIN';
... // make db calls, checks etc
Navigator.of(context).pushReplacementNamed(name);
}
}
Your build function just creates the loading UI, and the redirect function in the initState is the one running in the background and when it has finished computing, calls the Navigator.push to your desired page.

How to force initState every time the page is rendered in flutter?

I am adding some data into the SharedPreferenceson page2 of my app and I am trying to retrieve the data on the homepage. I have used an init function on page 1 as follows:
#override
void initState() {
super.initState();
_getrecent();
}
void _getrecent() async {
final prefs = await SharedPreferences.getInstance();
// prefs.clear();
String b = prefs.getString("recent").toString();
Map<String, dynamic> p = json.decode(b);
if (b.isNotEmpty) {
print("Shared pref:" + b);
setState(() {
c = Drug.fromJson(p);
});
cond = true;
} else {
print("none in shared prefs");
cond = false;
}
}
Since the initState() loads only once, I was wondering if there was a way to load it every time page1 is rendered. Or perhaps there is a better way to do this. I am new to flutter so I don't have a lot of idea in State Management.
you can override didChangeDependencies method. Called when a dependency of the [State] object changes as you use the setState,
#override
void didChangeDependencies() {
// your codes
}
Also, you should know that using setState updates the whole widget, which has an overhead. To avoid that you should const, declaring a widget const will only render once so it's efficient.
First thing is you can't force initState to rebuild your widget.
For that you have setState to rebuild your widget. As far as I can
understand you want to recall initState just to call _getrecent()
again.
Here's what you should ideally do :
A simple solution would be to use FutureBuilder. You just need to use _getrecent() in FutureBuilder as parent where you want to use the data you get from _getrecent(). This way everytime your Widget rebuilds it will call _getrecent().
You simply use setState() methode if you want to update widget. Here's documentation for this https://api.flutter.dev/flutter/widgets/State/setState.html
init function will render it only once initially, to avoid that -
You can use setState( ) method to re-render your whole widget.

Flutter reset StatefulWidget state when Widget is recreated

In my Flutter Widget I have a StreamBuilder that checks for snapshot.hasError and in this specific case it will return my ErrorRetryWidget().
builder: (context, AsyncSnapshot<MyObject> snapshot) {
...
if (snapshot.hasError) {
return ErrorRetryWidget();
}
}
The ErrorRetryWidget() just shows an error message and a retry button. When you press this button, I replace the button text by a progress indicator. Therefore I needed to make this widget stateful, as its state holds a isRetrying variable that I set to false in the initState, and then to true once pressed.
When pressing the button, the ErrorRetryWidget tells the parent through a VoidCallback to re-trigger the stream logic. It works well but the issue is that if the error comes back, my StreamBuilder will "return" the ErrorRetryWidget once again.
The constructor is called a new time, but not initState. How can I make it so the state resets every time the widget is re-created? Therefore, isRetrying is already (or still) set to true.
The only quick solution I found was to implement this in my error widget:
#override
void didUpdateWidget(covariant oldWidget) {
super.didUpdateWidget(oldWidget);
setState(() {
retrying = false;
});
}
Not sure it's a good practice.
Pass a unique key to let it create a new widget.
if (snapshot.hasError) {
return ErrorRetryWidget(key: UniqueKey());
}
I use didUpdateWidget just as you said, to reset the state of a stateful widget. It is also useful in animations.
The only comment I would add is to NOT use setState as you did because when the framework calls didUpdateWidget, it immediately calls build. Therefore, you don't have to trigger a call to build within didUpdateWidget. It ends up calling build two times.
#override
void didUpdateWidget(covariant oldWidget) {
super.didUpdateWidget(oldWidget);
retrying = false;
}

How to add an animation each time Widget build(BuildContext context) is called in Flutter

Here is my build method:
#override
Widget build(BuildContext context) {
if (isAuth) {
return buildAuthScreen();
} else if (isRegister) {
return buildRegisterUnAuthScreen();
} else {
return buildUnAuthScreen();
}
}
I wanted to know if there is way to call an animation each time my build function is called and a new page is returned, so that the new page is returned with an animation.
I already know, that there is a way to do it with Animation Page route, but I only want to display an animation when displaying a new buildScreen.
Thank you.
You should not start animation when build method is called. The build method may be called at every frame.
To start some side effect when widget property is changed, you should use StatefulWidget and initState and didUpdateWidget methods.