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();
}
Related
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();
},
),
);
}
}
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();
}
}
I have a app with a List<Match> as state, which is kept in
class MatchesChangeNotifier extends ChangeNotifier {
List<Match> matches;
refresh() async {
matches = await MatchesFirestore.fetchMatches(); // goes to db
notifyListeners();
}
}
then I have the following widgets (simplified)
class AvailableMatches extends StatelessWidget {
#override
Widget build(BuildContext context) {
var matches = context.watch<MatchesChangeNotifier>().matches;
return Column(children: matches.map(e => MatchInfo(e)).toList());
}
}
class MatchInfo extends StatelessWidget {
Match match;
MatchInfo(this.match);
Widget build(BuildContext context) {
// use match for a bunch of stuff
return InkWell(
child: ...,
onTap: () async {
await Navigator.push(context,
MaterialPageRoute(builder: (context) => MatchDetails(match)));
await context.read<MatchesChangeNotifier>().refresh();
}
}
In MatchDetails I have a bunch of buttons. One of them has this callback
onTap() async => await context.read<MatchesChangeNotifier>().refresh()
When I tap this button I see (debugging) that
AvailableMatches gets rebuilt (since it watches matches)
MatchInfo gets rebuilt (since it's down the tree w.r.t. AvailableMatches)
MatchDetails doesn't get re-built
Why is that? Is it because it is somehow called in the onTap function?
After I press the button I would like to see MatchDetails change when it's on top of the screen (i.e. the user is seeing it). Instead this doesn't happen
If I modify the way I push MatchDetails from MatchInfo with this ugly trick the thing works but I would like to avoid it
await Navigator.push(context,
MaterialPageRoute(builder: (context) =>
MatchDetails(context.watch<MatchesChangeNotifier>().matches.firstWhere((m) => m.id == match.id))));
I have the main/homepage widget of my app, let's call it home.dart.
Inside this widget, I have defined the drawer key in my Scaffold widget. The code for the Drawer object is in a separate file, navdrawer.dart.
home.dart
import 'navdrawer.dart';
. . .
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: NavDrawer(),
...
Now inside NavDrawer, I construct my Drawer widget which has a settings button, which links to the settings.dart screen.
Which I do like this:
navdrawer.dart
. . .
InkWell(
onTap: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => Settings()),
);
print(result);
},
child: ListTile(
leading: Icon(
Icons.settings,
color: AppTextColor,
),
title: Text('Settings'))),
So now, when the user presses the back button on the settings page, the Navigator.pop() call will return the data I need to the result variable in navdrawer.dart.
But my problem is ... how do I get this data to my home.dart screen/state?
I'll suggest you to use provider, scoped_model or other state management techniques for this. The easiest (but also the worthless) solution would be to use a global variable.
However, there's a middle ground. For simplicity I'm using dynamic for the type of result, you'd better know what Settings return, so use that type instead.
Your home.dart file
class _HomePageState extends State<HomePage> {
dynamic _result; // Create a variable.
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: NavDrawer(
onResult: (result) {
_result = result; // <-- This is your result.
}
),
);
}
}
Add following in your navdrawer.dart:
class NavDrawer extends StatelessWidget {
// Add these two lines.
final ValueChanged onResult;
NavDrawer({this.onResult});
// Other code ...
}
Modify your onTap method inside navdrawer.dart file:
onTap: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => Settings()),
);
onResult(result); // <-- Add this line.
}
Please set parameters into the pop method.
Like
Navigator.pop(context,true)
Define 'Static' global variable in homepage screen/widget
Then call the variable from anywhere :
1- homepage :
Static String getData;
2- when returned to Drawer :
homepage.getData=value;
Navigator.pop();
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');
}
}