How to wait for a method that is already being executed? - flutter

I'm developing a Flutter app which has some tabs inside, each of them depend on the database that is loaded on the first run. State is stored in a ScopedModel.
On every tab I have this code:
#override
void initState() {
super.initState();
loadData();
}
void loadData() async {
await MyModel.of(context).scopedLoadData();
_onCall = MyModel.of(context).onCall;
setState(() {
});
}
And this is the code snippet that matters for the ScopedModel:
Future<Null> scopedLoadData() async {
if (_isLoading) return;
_isLoading = true;
(...)
_isLoading = false;
}
If the user waits on the first tab for a few seconds everything is fine, since Database is loaded. However, if the user switches tabs right after app launch, the method scopedLoadData is still being executed so that I get a runtime error ("Unhandled Exception: NoSuchMethodError: The method 'ancestorWidgetOfExactType' was called on null.").
This exception happens because the scopedLoadData has not yet been completed. So I'm looking for a way to wait for a method that is still being executed.
Thanks!

Not sure without seeing your build method but I would start your build method with a guard clause.
if (_oncall == null) return Container(); // or Text("loading") or similar

use should be using a FutureBuilder on each tab to make sure the data is loaded before you try to build the widget... more code would be helpful

I solved the exception by getting rid of every:
setState(() { });
and implementing ScopedModelDescendant on every relevant tab on top of using notifyListeners() at the end of the database loading method.
This pulls the responsibility from the tabs for updating views and gives it to the scopedLoadData().

Related

Flutter dispose provider notifyListners()

I have a simple controller like this
class MatchListController with ChangeNotifier {
List<MatchData> liveMatches = [];
MatchListController() {
getMatchList();
}
getMatchList() async {
debugPrint('match list api called');
var response = await ApiService().matchList();
if (response != null) {
final databody = json.decode(response);
notifyListeners();
}
}
}
When my page loads it runs the function getMatchList() which is fine but If I change the page between function and when it return data notifyListeners hit and app crash. I am using loading but I have tab menu so I can change the page from there What I want is if page is close then notifyListeners dispose.
The answer that khalil mentioned is correct, there is also another solution:
Since you are using loading, I assume you want to prevent the page from changing, so you can wrap the tab menu buttons with an AbsorbPointer widget, and use setState to change its absorbing state according to your loading state. Your loading state can simply be a boolean inside your stateful widget.

Flutter - re-run previous page code after execution returns to it

I'm trying to figure out the best way to implement the proper navigation flow of a flutter app I'm building that involves a 3rd party authentication page (Azure AD B2C). Currently I have a page that serves simply as a "navigate to 3rd party auth login" page which is set as the initialRoute for my app. The first time through, it runs exactly the way I want it to, but I'm not able to figure out how to get that 'navigate to auth' page to re-run when navigated back to (after logout) so that the user ends up back at the 3rd party auth login page.
Basically what I'd like to do is, on logout - have the app navigate back to that page specified as the initialRoute page, and upon that page being navigated back to, have it re-launch the 3rd party auth login page just like it did the first time it executed.
I tried just awaiting the call to Navigator.push() and then calling setState((){}) afterwards, and that does re-display the page, but it just leaves that initial page sitting there, and doesn't end up triggering the execution the way it did the first time. initState() does not fire again, so neither does any of my code that's in there.
I've tried various methods off the Navigator object trying to reload the page or navigate to itself again, or just calling goToLogin() again after the await Navigator.push() call, nothing works.
Here's what I'm currently doing :
User launches the app, the initialRoute is LoginRedirect
class LoginRedirect extends StatefulWidget {
#override
_LoginRedirectState createState() => _LoginRedirectState();
}
class _LoginRedirectState extends State<LoginRedirect> {
#override
void initState() {
Utility.getConfig().then((value) {
config = value;
oauth = AadOAuth(config);
goToLogin(context);
});
super.initState();
}
void goToLogin(BuildContext context) async {
setState(() {
loading = true;
});
try {
await oauth.login(); // this launches the 3rd party auth screen which returns here after user signs in
String accessToken = await oauth.getAccessToken();
navigateToDashboard();
setState(() {
loading = false;
});
} on Exception catch (error) {
setState(() {
loading = false;
});
}
}
void navigateToDashboard() {
await navigator.push(context, MaterialPageRoute(builder: (BuildContext context) => Dashboard()));
// right here is where I'd like to call goToLogin() again after I Navigator.popUntil() back to this
// page, but if I try that I get an error page about how 'The specified child already
// has a parent. You must call removeView() on the child's parent first., java.lang
// .IllegalStateException and something about the bottom overflowed by 1063 pixels
}
}
After getting some config values and calling oauth.login() then I call a navigateToDashboard() method that pushes the Dashboard page on to the navigation stack.
Elsewhere in the code I have a logout button that ends up calling this code:
oauth.logout();
Navigator.popUntil(context, ModalRoute.withName('/LoginRedirect'));
which returns execution to where I called await Navigator.push() previously. But I can't figure out what I need to do there to have that LoginRedirect page execute again. I can't call goToLogin() again or it errors/crashes. I can't call initState() again, calling setState() doesn't do anything. I'm kinda stumped here, I thought this would be easy.
When logging out try: Navigator.pushReplacementNamed(context, "/LoginRedirect"); instead of Navigator.popUntil(context, ModalRoute.withName('/LoginRedirect'));

Api data null whe nthe pages load even i use initState

im using post method to get api data/response.body in initState and i get all the response/data then i put it in my variables surveyData, but when i load the pages with widget looped by the response.body length it goes error and say response.body.length is null but when i save the text editor/ hot reload, all the data entered and it's not null anymore and the widget appear.
fyi: the http.post method is my own function not from import 'package:http/http.dart' as http; so don't mind it
Variables that contain the response
dynamic surveyData;
initState Code
#override
void initState() {
super.initState();
// GET PAGES
surveyPages = widget.surveyPages;
// GET FORM DETAIL
http.post(
'survey-pre-mitsu/form-detail',
body: {
"survey_form_header_id": 1,
},
).then(
(res) {
surveyData = res['data'];
},
);
}
Widget that looped by surveyData.length
for (var i = 0; i < surveyData.length; i++)
AdditionalForm(questionLabel: surveyData[i]['label'],
questionType: surveyData[i]['type'],),
This is what the error
And this is what it looks like when i do hot reload
First, I suggest you to use future builder to resolve this problem.
First, I suggest performing post(even though its your own code) as a asynchronous operation, hence not inside initState(). Preferably write the logic in a separate class/file with packages like a provider package. Try this first and then post the error after that.
You need to add Some delay , In this Delay you can show a loading icon . for delay you can use :
Future.delayed(const Duration(milliseconds: 500), () {
// Here you can write your code
setState(() {
// Here you can write your code for open new view
});
});
This will solve your problem

How to dispose of an unmounted Widget in Flutter?

I am writing a flutter app and if you already started it once, I dont want the user to see the Intro Screen again
In my MaterialApp:
home: firstStart ? DeviceSelection() : StartScreen()
firstStart is a boolean, if it is the first start, it should start the App with the StartScreen() and if its not, it should go straight to DeviceSelection(). That all works fine, but my problem is the following error:
Error: This widget has been unmounted, so the State no longer has a context (and should be considered defunct).
Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.
I think it starts StartScreen too, even if firstStart is false, because it has to get its value out of the shared preferences, because I see it pop up shortly sometimes.
I already tried some stuff I found, like writing a dispose method:
#override
void dispose() {
super.dispose();
}
or
if (!mounted) {
Navigator.pop(context);
}
in a method that I call after the screen starts, but it doesnt work either. Any ideas what I could do to get rid of this error? Thanks
mb set a flag, like:
bool lookedIntro = false; // if user look it first
and then use:
#override
void initState() {
// take data flag from database or somewhere else (file)
// and check it
}
and also read about initState() more

(Flutter/Dart) Two async methods in initState not working

I have a function in my initState:
#override
void initState() {
_fetchListItems();
super.initState();
}
This function is very simple. It has two async await operations of sqflite, one of which waits for the other to complete in it:
_fetchListItems() async {
wait() async {
number = await db.getNumber(userId); }
await wait();
List rawFavouriteList = await db.getList(number);
setState((){
rawFavouriteList.forEach((item){
_favouriteList.add(Model.map(item));
}});
}
I have to wait for number to be fetched, only then I can fetch serialized List, which then I deserialize and populate then add to the List which is displayed in ListView.
The problem is, this is not working in initState. It's working fine when it's called through onPressed() of a button, but not in initState.
Points to Note:
1) There is no error being thrown
2) I have already tried more conservative alternatives by using await for even rawFavouriteListthrough a separate function like wait() before using setState() top populate the _favouriteList, even though it's working fine manually through buttons.
3) Everything works fine if I manually enter number value in the second database query and just remove the first query, i.e.
_fetchListItems() async {
List rawFavouriteList = await db.getList(42);
setState((){
rawFavouriteList.forEach((item){
_favouriteList.add(Model.map(item));
}});
}