Show Dialog In InitState Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe - flutter

At first time screen appears, i want check if user GPS is enable and Location Permission is granted. Then if one of them is not fulfilled , i show dialog to open app setting.
Source Code
_initPermission(BuildContext context) async {
final geolocationStatus = await commonF.getGeolocationPermission();
final gpsStatus = await commonF.getGPSService();
if (geolocationStatus != GeolocationStatus.granted) {
showDialog(
context: context,
builder: (ctx) {
return commonF.showPermissionLocation(ctx);
},
);
} else if (!gpsStatus) {
showDialog(
context: context,
builder: (ctx) {
return commonF.showPermissionGPS(ctx);
},
);
}
}
Then i called this function in initState like this :
InitState
void initState() {
super.initState();
Future.delayed(Duration(milliseconds: 50)).then((_) => _initPermission(context));
}
The problem is , every first time the screen appears it will give me error like this :
Error
[ERROR:flutter/lib/ui/ui_dart_state.cc(166)] Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe.
At this point the state of the widget's element tree is no longer stable.
To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.
What i have done :
Change InitState like this
//1
WidgetsBinding.instance.addPostFrameCallback((_) {
_initPermission(context);
});
//2
SchedulerBinding.instance.addPostFrameCallback((_) => _initPermission(context));
//3
Timer.run(() {
_initPermission(context);
})
Adding Global Key For Scaffold
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
Scaffold(
key: _scaffoldKey,
Searching similiar problem with me
Show Dialog In Initstate
Looking up a deactivated widget's ancestor is unsafe
Load Data In Initstate
The results that I have done is nothing , the error still appear in first time screen appear.
But strangely, this only happens when first time screen appear . When i do Hot Restart the error message is gone.
[Failed if screen first time appear]
[Hot restart , error gone]

I have no way to test it (I dont have the GPS package) but try changing
Future.delayed(Duration(milliseconds: 50)).then((_) => _initPermission(context));
to
Future.delayed(Duration(milliseconds: 50)).then((_) => _initPermission()); //this inside the initstate
_initPermission() async{
final geolocationStatus = await commonF.getGeolocationPermission();
final gpsStatus = await commonF.getGPSService();
if (geolocationStatus != GeolocationStatus.granted) {
await showDialog(
context: context,
builder: (ctx) => commonF.showPermissionLocation
},
);
} else if (!gpsStatus) {
await showDialog(
context: context,
builder: (ctx) => commonF.showPermissionGPS
},
);
}
} // the stateful widget can use the context in all its methods without passing it as a parameter
The error dissapears in hot restart because it just refresh the state of the widget, but it's already created (If you do hot restart and print something inside the initState, for example print('This is init');, you wont see it either because the refresh doesn't dispose and init the widget, so it won't run that piece of code again)
EDIT
Based on your gist I just made a minimal reproducible example and run with no problem in DartPad, later I will try it on VS but for now can you check if there is anything different
enum GeolocationStatus{granted, denied} //this is just for example
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
showPermissionGPS() {
return AlertDialog(
title: Text('GPSStatus'),
);
}
_showPermissionLocation() {
return AlertDialog(
title: Text('Permission Localization'),
);
}
#override
initState(){
super.initState();
Future.delayed(Duration(milliseconds: 50)).then((_) => _initPermission());
}
_initPermission() async{
final geolocationStatus = await Future<GeolocationStatus>.value(GeolocationStatus.granted); //mocked future with a response to test
final gpsStatus = await Future<bool>.value(false); //mocked future with a response to test
if (geolocationStatus != GeolocationStatus.granted) {
await showDialog(
context: context,
builder: (ctx) => _showPermissionLocation() //this mock commonF.showPermissionLocation and returns an AlertDialog
);
} else if (!gpsStatus) {
await showDialog(
context: context,
builder: (ctx) => _showPermissionGPS() //this mock commonF.showPermissionGPS and returns an AlertDialog
);
}
}
#override
Widget build(BuildContext context) {
return Text('My text');
}
}

Related

Flutter awesome notifications how to fix StateError (Bad state: Stream has already been listened to.)

I am getting this error when I have signed out from my flutter app and trying to log in again:
StateError (Bad state: Stream has already been listened to.)
The code that gives me this error is on my first page:
#override
void initState() {
AwesomeNotifications().actionStream.listen((notification) async {
if (notification.channelKey == 'scheduled_channel') {
var payload = notification.payload['payload'];
var value = await FirebaseFirestore.instance
.collection(widget.user.uid)
.doc(payload)
.get();
navigatorKey.currentState.push(PageRouteBuilder(
pageBuilder: (_, __, ___) => DetailPage(
user: widget.user,
i: 0,
docname: payload,
color: value.data()['color'].toString(),
createdDate: int.parse((value.data()['date'].toString())),
documentId: value.data()['documentId'].toString(),)));
}
});
super.initState();
}
And on another page that contains the sign out code.
await FirebaseAuth.instance.signOut();
if (!mounted) return;
Navigator.pushNamedAndRemoveUntil(context,
"/login", (Route<dynamic> route) => false);
What can I do to solve this? Is it possible to stop listen to actionstream when I log out? Or should I do it in another way?
Streams over all are single use, they replace the callback hell that that ui is, at first a single use streams can seem useless but that may be for a lack of foresight. Over all (at lest for me) flutter provides all the necessary widgets to not get messy with streams, you can find them in the Implementers section of ChangeNotifier and all of those implement others like TextEditingController.
With that, an ideal (again, at least for me) is to treat widgets as clusters where streams just tie them in a use case, for example, the widget StreamBuilder is designed to build on demand so it only needs something that pumps changes to make a "live object" like in a clock, a periodic function adds a new value to the stream and the widget just needs to listen and update.
To fix your problem you can make .actionStream fit the case you are using it or change a bit how are you using it (having a monkey patch is not good but you decide if it is worth it).
This example is not exactly a "this is what is wrong, fix it", it is more to showcase a use of how pushNamedAndRemoveUntil and StreamSubscription can get implemented. I also used a InheritedWidget just because is so useful in this cases. One thing you should check a bit more is that the variable count does not stop incrementing when route_a is not in focus, the stream is independent and it will be alive as long as the widget is, which in your case, rebuilding the listening widget is the error.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(App());
const String route_a = '/route_a';
const String route_b = '/route_b';
const String route_c = '/route_c';
class App extends StatelessWidget {
Stream<int> gen_nums() async* {
while (true) {
await Future.delayed(Duration(seconds: 1));
yield 1;
}
}
#override
Widget build(BuildContext ctx) {
return ReachableData(
child: MaterialApp(
initialRoute: route_a,
routes: <String, WidgetBuilder>{
route_a: (_) => Something(stream: gen_nums()),
route_b: (_) => FillerRoute(),
route_c: (_) => SetMount(),
},
),
);
}
}
class ReachableData extends InheritedWidget {
final data = ReachableDataState();
ReachableData({super.key, required super.child});
static ReachableData of(BuildContext ctx) {
final result = ctx.dependOnInheritedWidgetOfExactType<ReachableData>();
assert(result != null, 'Context error');
return result!;
}
#override
bool updateShouldNotify(ReachableData old) => false;
}
class ReachableDataState {
String? mount;
}
// route a
class Something extends StatefulWidget {
// If this widget needs to be disposed then use the other
// constructor and this call in the routes:
// Something(subscription: gen_nums().listen(null)),
// final StreamSubscription<int> subscription;
// Something({required this.subscription, super.key});
final Stream<int> stream;
Something({required this.stream, super.key});
#override
State<Something> createState() => _Something();
}
class _Something extends State<Something> {
int count = 0;
void increment_by(int i) => setState(
() => count += i,
);
#override
void initState() {
super.initState();
widget.stream.listen(increment_by);
// To avoid any funny errors you should set the subscription
// on pause or the callback to null on dispose
// widget.subscription.onData(increment_by);
}
#override
Widget build(BuildContext ctx) {
var mount = ReachableData.of(ctx).data.mount ?? 'No mount';
return Scaffold(
body: InkWell(
child: Text('[$count] Push Other / $mount'),
onTap: () {
ReachableData.of(ctx).data.mount = null;
Navigator.of(ctx).pushNamed(route_b);
},
),
);
}
}
// route b
class FillerRoute extends StatelessWidget {
const FillerRoute({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Go next'),
// Option 1: go to the next route
// onTap: () => Navigator.of(ctx).pushNamed(route_c),
// Option 2: go to the next route and extend the pop
onTap: () => Navigator.of(ctx)
.pushNamedAndRemoveUntil(route_c, ModalRoute.withName(route_a)),
),
);
}
}
// route c
class SetMount extends StatelessWidget {
const SetMount({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Set Mount'),
onTap: () {
ReachableData.of(ctx).data.mount = 'Mounted';
// Option 1: pop untill reaches the correct route
// Navigator.of(ctx).popUntil(ModalRoute.withName(route_a));
// Option 2: a regular pop
Navigator.of(ctx).pop();
},
),
);
}
}

ChangeNotifier inaccessible in grandchildren widget of where it was provided

I am trying to use flutter provider in order to carry my state down a widget sub-tree/route, and while it works for the direct child of the widget that provided the change notifier class, it does not for the next one in line.
As far as I understand, the change notifier class should be passed down. To be more specific, I am trying to access it through context.read() in a function being called in its initState function.
Am I doing something wrong?
The code below illustrates my code.
Where it class notifier is provided:
onTap: () {
// Select body area
context.read<Patient>().selectBodyArea(areas[index]);
// Open complaint list
FlowRepresentation flow = context.read<Patient>().getFlow();
Navigator.push(
context,
MaterialPageRoute(builder: (context) =>
ChangeNotifierProvider.value(
value: flow,
child: const ChiefComplaintList()
)
)
);
}
Navigation to the problem widget in ChiefComplaintList:
onTap: () {
// Select complaint
context.read<FlowRepresentation>().selectComplaint(ccs[index]);
// Show factors
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const AttenuationFactors())
);
}
Where I'm having trouble accessing the change notifier class:
void getData() async {
_nrFactors = await context.read<FlowRepresentation>().getAttenuationFactors();
setState(() {}); // rebuild widget with data
}
#override
void initState() {
super.initState();
print("Initiated Attenuation Factors Lists State");
getData();
}

How can i reload my page every time i am on it on my flutter app?

Assume that I'm on page-A now. I navigate to page-B. When I pop the page-B and come back to page-A, currently nothing happens. How can I reload page-A and load the new API data from the init state of page-A? Any Ideas?
first main page
void refreshData() {
id++;
}
FutureOr onGoBack(dynamic value) {
refreshData();
setState(() {});
}
void navigateSecondPage() {
Route route = MaterialPageRoute(builder: (context) => SecondPage());
Navigator.push(context, route).then(onGoBack);
}
second page
RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go Back'),
),
more details check here
From the explanation that you have described, so when you are popping the page.
This below Code will be on the second page.
Navigator.of(context).pop(true);
so the true parameter can be any thing which ever data that you want to send.
And then when you are pushing from one page to another this will be the code.
this is on the first page.
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const PageOne()),
);
so if you print the result you will get the bool value that you send from the second page.
And based on bool you can hit the api. if the bool true make an api call.
Let me know if this works.
There are one more solutions for this situtation.
İf you want to trigger initState again
You can use pushAndRemoveUntil method for navigation. ( if you use only push method this is not remove previous page on the stack)
You can use key
You can set any state manegement pattern.( not for only trigger initState again)
There are 2 ways:
Using await
await Navigator.push(context, MaterialPageRoute(builder: (context){
return PageB();
}));
///
/// REFRESH DATA (or) MAKE API CALL HERE
Passing fetchData constructor to pageB and call it on dispose of pageB
class PageA {
void _fetchData() {}
Future<void> goToPageB(BuildContext context) async {
await Navigator.push(context, MaterialPageRoute(builder: (context) {
return PageB(onFetchData: _fetchData);
}));
}
}
class PageB extends StatefulWidget {
const PageB({Key? key, this.onFetchData}) : super(key: key);
final VoidCallback? onFetchData;
#override
State<PageB> createState() => _PageBState();
}
class _PageBState extends State<PageB> {
#override
void dispose() {
widget.onFetchData?.call();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}

StreamBuilder / ChangeNotifierProvider- setState() or markNeedsBuild() called during build

Streambuilder, ChangeNotifier and Consumer cannot figure out how to use correctly. Flutter
I've tried and tried and tried, I've searched a lot but I cannot figure this out:
I'm using a Streambuilder this should update a ChangeNotifier that should trigger rebuild in my Consumer widget. Supposedly...
but even if I call the provider with the (listen: false) option I've got this error
The following assertion was thrown while dispatching notifications for
HealthCheckDataNotifier: setState() or markNeedsBuild() called during
build. the widget which was currently being built when the offending call was made was:
StreamBuilder<List>
Important: I cannot create the stream sooner because I need to collect other informations before reading firebase, see (userMember: userMember)
Widget build(BuildContext context) {
return MultiProvider(
providers: [
/// I have other provider...
ChangeNotifierProvider<HealthCheckDataNotifier>(create: (context) => HealthCheckDataNotifier())
],
child: MaterialApp(...
then my Change notifier look like this
class HealthCheckDataNotifier extends ChangeNotifier {
HealthCheckData healthCheckData = HealthCheckData(
nonCrewMember: false,
dateTime: DateTime.now(),
cleared: false,
);
void upDate(HealthCheckData _healthCheckData) {
healthCheckData = _healthCheckData;
notifyListeners();
}
}
then the Streambuilder
return StreamBuilder<List<HealthCheckData>>(
stream: HeathCheckService(userMember: userMember).healthCheckData,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
if (snapshot.hasData) {
if (snapshot.data!.isNotEmpty) {
healthCheckData = snapshot.data?.first;
}
if (healthCheckData != null) {
timeDifference = healthCheckData!.dateTime.difference(DateTime.now()).inHours;
_cleared = healthCheckData!.cleared;
if (timeDifference < -12) {
healthCheckData!.cleared = false;
_cleared = false;
}
///The problem is here but don't know where to put this or how should be done
Provider.of<HealthCheckDataNotifier>(context, listen: false).upDate(healthCheckData!);
}
}
return Builder(builder: (context) {
return Provider<HealthCheckData?>.value(
value: healthCheckData,
builder: (BuildContext context, _) {
return const HealthButton();
},
);
});
} else {
return const Text('checking health'); //Scaffold(body: Loading(message: 'checking...'));
}
});
and finally the Consumer (note: the consumer is on another Route)
return Consumer<HealthCheckDataNotifier>(
builder: (context, hN, _) {
if (hN.healthCheckData.cleared) {
_cleared = true;
return Container(
color: _cleared ? Colors.green : Colors.amber[900],
Hope is enough clear,
Thank you so very much for your time!
it is not possible to setState(or anything that trigger rerender) in the builder callback
just like you don't setState in React render
const A =()=>{
const [state, setState] = useState([])
return (
<div>
{setState([])}
<p>will not work</p>
</div>
)
}
it will not work for obvious reason, render --> setState --> render --> setState --> (infinite loop)
so the solution is similar to how we do it in React, move them to useEffect
(example using firebase onAuthChange)
class _MyAppState extends Stateful<MyApp> {
StreamSubscription<User?>? _userStream;
var _waiting = true;
User? _user;
#override
void initState() {
super.initState();
_userStream = FirebaseAuth.instance.authStateChanges().listen((user) async {
setState(() {
_waiting = false;
_user = user;
});
}, onError: (error) {
setState(() {
_waiting = false;
});
});
}
#override
void dispose() {
super.dispose();
_userStream?.cancel();
}
#override
Widget build(context) {
return Container()
}
}

Flutter - FutureBuilder fires twice on hot reload

In my flutter project when I start the project in the simulator everything works fine and the future builder only fires once, but when I do hot reload the FutureBuilder fires twice which causes an error any idea how to fix this?
Future frameFuture() async {
var future1 = await AuthService.getUserDataFromFirestore();
var future2 = await GeoService.getPosition();
return [future1, future2];
}
#override
void initState() {
user = FirebaseAuth.instance.currentUser!;
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: frameFuture(),
builder: (context, snap) {
if (snap.connectionState == ConnectionState.done && snap.hasData) return HomePage();
else return Container(
color: Colors.black,
child: Center(
child: spinKit,
),
);
}
);
}
I solved the issue. I put the Future function in the initState and then used the variable in the FutureBuilder. I'm not sure why it works this way, but here's the code:
var futures;
Future frameFuture() async {
var future1 = await AuthService.getUserDataFromFirestore();
var future2 = await GeoService.getPosition();
return [future1, future2];
}
#override
void initState() {
user = FirebaseAuth.instance.currentUser!;
super.initState();
futures = frameFuture();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: futures,
builder: (context, snap) {
if (snap.connectionState == ConnectionState.done && snap.hasData) return HomePage();
else return Container(
color: Colors.black,
child: Center(
child: spinKit,
),
);
}
);
}
The solution as you already figured out is to move the future loading process to the initState of a StatefulWidget, but I'll explain the why it happens:
You were calling your future inside your build method like this:
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: frameFuture(),
The issue is that Flutter calls the build method each time it renders the Widget, whenever a dependency changes(InheritedWidget, setState) or Flutter decides to rebuild it. So each time you redraw your UI frameFuture() gets called, this makes your build method to have side effects (this async call) which it should not, and is encouraged for widgets not to have side effects.
By moving the async computation to the initState you're only calling it once and then accessing the cached variable futures from your state.
As a plus here is an excerpt of the docs of the FutureBuilder class
"The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted."
Hope this makes clear the Why of the solution.
This can happen even when the Future is called from initState. The prior solution I was using felt ugly.
The cleanest solution is to use AsyncMemoizer which effectively just checks if a function is run before
import 'package:async/async.dart';
class SampleWid extends StatefulWidget {
const SampleWid({Key? key}) : super(key: key);
final AsyncMemoizer asyncResults = AsyncMemoizer();
#override
_SampleWidState createState() => _SampleWidState();
}
class _SampleWidState extends State<SampleWid> {
#override
void initState() {
super.initState();
_getData();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: widget.asyncResults.future,
builder: (context, snapshot) {
if (!snapshot.hasData) return yourLoadingAnimation();
// ... Do things with the data!
});
}
// The async and await here aren't necessary.
_getData() async () {
await widget.asyncResults.runOnce(() => yourApiCall());
}
}
Surprisingly, there's no .reset() method. It seems like the best way to forcibly rerun it is to override it with a new AsyncMemoizer(). You could do that easily like this
_getData() async ({bool reload = false}) {
if (reload) widget.asyncResults = AsyncMemoizer();
await widget.asyncResults.runOnce(() => yourApiCall());
}