How to navigate in flutter during widget build? - flutter

I'm trying to detect that user is no longer authenticated and redirect user to login. This is how I'm doing it
Widget build(BuildContext context) {
return FutureBuilder(
future: _getData(context),
builder: (context, snapshot) {
try {
if (snapshot.hasError && _isAuthenticationError(snapshot.error)) {
Navigator.push(context, MaterialPageRoute(builder: (context) => LoginView()));
}
Unfortunately doing navigation on build is not working. It throws this error
flutter: setState() or markNeedsBuild() called during build.
flutter: This Overlay widget cannot be marked as needing to build because the framework is already in the
flutter: process of building widgets. A widget can be marked as needing to be built during the build
I cannot just return LoginView widget since parent widget containts app bar and floating button and login view needs to be displayed without these controlls.. I need to navigate.
Is it possible to do it?

Wrap it in Future.microtask. This will schedule it to happen on the next async task cycle (i.e. after build is complete).
Future.microtask(() => Navigator.push(
context,
MaterialPageRoute(builder: (context) => LoginView())
));

Streams in flutter
The usual thing is to use a flow where user changes occur.
When the user logs off, he detects that change and can direct it to another window.

problem here :
snapshot.hasError && _isAuthenticationError(snapshot.error)
Instead of this use OR
snapshot.hasError || _isAuthenticationError(snapshot.error)

Related

Trying to use showDialog()/show Pop up on app startup

What I want to achieve: I want to open a pop up explaining my app when it starts.
My approach: As far as I understand it from googling the issue, I should use the showDialog() method. In its most basic form:
showDialog(
context: context,
builder: (context) {
return Text('data');
});
I tried returning actual dialogs (e.g. AlertDialog) but it doesn't change the behavior so I'm just using Text() with a string as a placeholder for now.
The problem:
No matter where I place the showDialog function, it doesn't work as intended (also see scrennshots below):
Placing it in initState: I get an error message about inherited Widgets being called before the initState is done + an explanation about dependiencies I can barely follow.
Placing it in the build method: I get an error message that setState() or markNeedsBuild() gets called while the app is already buildung widgets.
Placing it in DidChangeAppLifeCycleState(): This is actually working and opening the pop when I pause the app and then resume it. It is not opening on app startup though.
Wrapping it in WidgetsBinding.instance!.addPostFrameCallback(): An idea I picked up here: How to show a popup on app start in Flutter. Doesn't change the outcome of the error messages, neither in initState nor in build.
Any ideas?
Screenshots:
From initState:
From build method:
From DidChangeAppLifecycleState (the "succesful" variant:
Will you please try below code in your init method? I hope this may work.
Future.delayed(Duration.zero, () async {
myFunction();
});
Using WidgetsBinding.instance.addPostFrameCallback inside initState perform its inner task after the 1st frame is complete.
addPostFrameCallback Schedule a callback for the end of this frame.
Next issue arise for not having material. You can directly return AlertDialog on builder or wrap with any material widget like Material, Scaffold..
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
showDialog(
context: context,
builder: (context) {
return const AlertDialog(
content: Text('data'),
);
},
);
});
}
If you are running version<3 null safety, use WidgetsBinding.instance?.addPostFrameCallback
One of the methods with WidgetsBinding.instance!.addPostFrameCallback() works fine .
If you show a normal show dialog with the press of a button too it will produce the same result.
Here, you need to wrap the text("data") in a dialog widget such as alertDialog or simpleDialog widget as needed and it will display the dialog within the current scaffold as -
WidgetsBinding.instance!.addPostFrameCallback((_) async {
return await showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: Text("data"),
);
});
});
I tried adding this in the init state and the dialog pops up fine when I restart the app
Thanks a lot for your answers. I ficed the issue by rewriting with your suggestions; and it works. I tihnk the issue was that I did not have _ or anything else in my WidgetsBinding code. So I did:
WidgetsBinding.instance?.addPostFrameCallback(() {})
instead of
WidgetsBinding.instance?.addPostFrameCallback((_) {})

Flutter/Dart - Edit Page in PageView, then Refresh and Scroll back to Same Place?

Currently, I can submit edits to a single page in a PageView and then either Navigator.push to a newly created single edited page or Navigator.pop back to the original Pageview containing the unedited page.
But I'd prefer to pop back to the the same place in an updated/refreshed Pageview. I was thinking I could do this on the original PageView page:
Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
But after editing, how can I pop back to a refreshed PageView which is scrolled to the now updated original page? Or is there a better way? Someone mentioned keys, but I've not yet learned to use them.
The question deals with the concept of Reactive App-State. The correct way to handle this is through having an app state management solution like Bloc or Redux.
Explanation: The app state takes care of the data which you are editing. the EditPage just tells the store(App-State container) to edit that data and the framework takes care of the data that should be updated in the PageView.
as a temporary solution you can use an async call to Navigation.push() and refresh the PageView State once the EditPage comes back. you can also use an overloaded version of pop() to return a success condition which aids for a conditional setState().
Do you know that Navigator.pushReplacement(...) returns a Future<T> which completes when you finally return to original context ?
So how are you going to utilize this fact ?
Lets say you want to update a String of the original page :
String itWillBeUpdated="old value";
#override
Widget build(BuildContext ctx)
{
.
.
.
onPressesed:() async {
itWillBeUpdated= await Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
setState((){});
},
}
On your editing page , you can define Navigator.pop(...) like this :
Navigator.pop<String>(context, "new string");
by doing this , you can provide any data back to the original page and by calling setState((){}) , your page will reflect the changes
This isn't ideal, but works somewhat. First I created a provider class and added the following;
class AudioWidgetProvider with ChangeNotifier {
int refreshIndex;
setRefreshIndex (ri) {
refreshIndex = ri;
return refreshIndex;
}
}
Then in my PageView Builder on the first page, I did this;
Widget build(context) {
var audioWidgetProvider = Provider.of<AudioWidgetProvider>(context);
return
PreloadPageView.builder(
controller: PreloadPageController(initialPage: audioWidgetProvider.refreshIndex?? 0),
Then to get to the EditPage (2nd screen) I did this;
onPressed: () async {
audioWidgetProvider.setRefreshIndex(currentIndex);
Navigator.pushReplacement(context,new MaterialPageRoute(builder: (BuildContext context) => EditPage()),); }
And finally I did this to return to a reloaded PageView scrolled to the edited page;
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) =>HomePage()));
The only problem now is that the PageView list comes from a PHP/Mysql query and I'm not sure what to do if new items are added to the list from the Mysql database. This means the currentIndex will be wrong. But I guess that's the topic of another question.

How to return from a widget to StreamBuilder after navigating to another page in flutter app?

So the first picture has codes from the first page where I have a StreamBuilder in my flutter app, there I tried navigating to the stateful widget named PickupScreen containing the code in second picture and it gave me an error which says SetState( or markNeedsbuild() called during build.So I called the widget directly and it worked but the problem is I cannot go the previous page I know I can't use pop but I need a solution for returning to previous page and show the else code.Currently I am Navigating to another page named HomePage.
You can't do it like that. Either you manage your screen state based on your widget.channel.stream or you must design your UI in a different way so you can actually push a new screen on your screen 1 and then you will be able to pop.
If you still want to keep it like that, you can do something like the following in your first screen:
return StreamBuilder(
stream: widget.channel.stream,
builder: (context, snapshot){
if(snapshot.hasData && jsonDecode(snapshot.data['ntype'] == 10){
WidgetsBinding.instance.addPostFrameCallback((_) => Navigator.push(context, MaterialPageRoute(builder: (_) => PickupScreen(data: jsonDecode(snapshot.data))));
return const SizedBox();
} else {
return _isLoading ? LoadingScreen() : Scaffold();
}
});
Also, next time, make sure you copy/paste code instead of screenshots to make it easy to edit with modifications. :)

perform a navigation inside the build method of a widget? - flutter

Fast question: how can I perform a navigation inside the build method of a widget?
I'm developing a Flutter App.
I use Bloc architecture.
I have screen with a create form. When the user presses a button, it calls a REST api. While the call is being executed I display a circular progress. When the progress ends I want the screen to be popped (using navigation).
To display the job status I use a Stream in the bloc and a StreamBuilder in the widget. So I want to do something like this:
return StreamBuilder<Job<T>>(
stream: jobstream,
builder: (context, snapshot) {
if (snapshot.hasData) {
if (snapshot.data.jobStatus == JobStatus.progress)
// job being executed, display progress
return Center(child: CircularProgressIndicator());
else if (snapshot.data.jobStatus == JobStatus.success)
Navigator.pop(context); // Problem here!
return Center(child: CircularProgressIndicator());
} else {
return DisplayForm();
}
});
I have problems with the line: Navigator.pop(context);
Because navigation is not allowed during a build.
How can I do this navigation?
My currect dirty solution is using the following function, but its ugly:
static void deferredPop(BuildContext context) async{
Future.delayed(
Duration(milliseconds: 1),
() => Navigator.pop(context)
);
}
You can add a callback to be executed after the build method is complete with this line of code:
WidgetsBinding.instance.addPostFrameCallback((_) => Navigator.pop(context));
'Because navigation is not allowed during a build.' that being said you should consider creating a separate function that will receive as an input something that you will take into consideration whether to pop that screen.

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