I have new navigator inside app main navigator and i want to observe new navigator push and pop methods, but it seems observer callbacks calls just for main navigator instead of new navigator. how can i fix it?
initializing :
final GlobalKey<NavigatorState> newNavigatorKey = GlobalKey<NavigatorState>();
final RouteObserver<PageRoute> _routeObserver = RouteObserver<PageRoute>();
onInit: (store) => _routeObserver.subscribe(this, ModalRoute.of(context)),//I think problem is from this line
unsubcribe :
onDispose: (store) => _routeObserver.unsubscribe(this)
new navigator:
Navigator(
key: newNavigatorKey,
observers: [_routeObserver],
onGenerateRoute: (routeSettings) {
return MaterialPageRoute(
builder: (context) =>
routs[routeSettings.name],
);
})
container class:
class CategoryNavigator extends StatelessWidget with RouteAware
RouteAware mixin delegates:
#override
void didPop() {
_routeState--;
super.didPopNext();
}
#override
void didPush() {
_routeState++;
super.didPushNext();
}
The reason you're observing main navigator events instead of the one that you've created is, as you pointed out the line in which the error is.
onInit: (store) => _routeObserver.subscribe(this, ModalRoute.of(context))
The context your passing here is of the widget that is building Navigator in it's build().
That means it is above that Navigator you created in the widget tree. Therefore you're getting events for main Navigatot
Related
I am running into an issue with flutter when I try to read data from local storage when the app loads.
I have an inherited widget that holds authentication information for the current user. When the app loads I want to look into local storage for session tokens. If the session tokens exist I would like to update the inherited widget with this information.
My screens are dynamic. If it knows the user is authenticated it takes them to the requested screen, otherwise it takes them to the register screen.
The issue I am running into is that I cannot update the inherited widget's state from an initState() method from a widget that depends on the inherited widget (My router widget)
How can I read from local storage when the app loads and update the inherited widget?
Error when running app:
flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
flutter: The following assertion was thrown building _InheritedAuthContainer:
flutter: inheritFromWidgetOfExactType(_InheritedAuthContainer) or inheritFromElement() was called before
flutter: RootState.initState() completed.
flutter: When an inherited widget changes, for example if the value of Theme.of() changes, its dependent
flutter: widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor
flutter: or an initState() method, then the rebuilt dependent widget will not reflect the changes in the
flutter: inherited widget.
flutter: Typically references to inherited widgets should occur in widget build() methods. Alternatively,
flutter: initialization based on inherited widgets can be placed in the didChangeDependencies method, which
flutter: is called after initState and whenever the dependencies change thereafter.
Router Widget (Root)
class Root extends StatefulWidget {
#override
State createState() => RootState();
}
class RootState extends State<Root> {
static Map<String, Widget> routeTable = {Constants.HOME: Home()};
bool loaded = false;
bool authenticated = false;
#override
void initState() {
super.initState();
if (!loaded) {
AuthContainerState data = AuthContainer.of(context);
data.isAuthenticated().then((authenticated) {
setState(() {
authenticated = authenticated;
loaded = true;
});
});
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
onGenerateRoute: (routeSettings) {
WidgetBuilder screen;
if (loaded) {
if (authenticated) {
screen = (context) => SafeArea(
child: Material(
type: MaterialType.transparency,
child: routeTable[routeSettings.name]));
} else {
screen = (conext) => SafeArea(
child: Material(
type: MaterialType.transparency, child: Register()));
}
} else {
screen = (context) => new Container();
}
return new MaterialPageRoute(
builder: screen,
settings: routeSettings,
);
});
}
}
Inherited Widget method that checks for auth and updates itself which triggers a rerender of my router widget
Future<bool> isAuthenticated() async {
if (user == null) {
final storage = new FlutterSecureStorage();
List results = await Future.wait([
storage.read(key: 'idToken'),
storage.read(key: 'accessToken'),
storage.read(key: 'refreshToken'),
storage.read(key: 'firstName'),
storage.read(key: 'lastName'),
storage.read(key: 'email')
]);
if (results != null && results[0] != null && results[1] != null && results[2] != null) {
//triggers a set state on this widget
updateUserInfo(
identityToken: results[0],
accessToken: results[1],
refreshToken: results[2],
firstName: results[3],
lastName: results[4],
email: results[5]
);
}
}
return user != null && (JWT.isActive(user.identityToken) || JWT.isActive(user.refreshToken));
}
Main
void main() => runApp(
EnvironmentContainer(
baseUrl: DEV_API_BASE_URL,
child: AuthContainer(
child: Root()
)
)
);
What is a correct way of checking local storage on app load and updating the inherited widget that holds this information?
Actually you cannot access InheritedWidget from an initState method. Instead try accessing it from didChangeDependencies.
Example:
#override
void didChangeDependencies() {
super.didChangeDependencies();
if (!loaded) {
AuthContainerState data = AuthContainer.of(context);
data.isAuthenticated().then((authenticated) {
setState(() {
authenticated = authenticated;
loaded = true;
});
});
}
}
Another way would be to schedule the data fetch in initState with SchedulerBinding. You can find the docs here
SchedulerBinding.instance.addPostFrameCallback((_) {
// your login goes here
});
Note: remember the didChangeDependencies will be called whenever the state or dependencies of any parent InheritedWidget changes. Please look at the docs here.
Hope this helps!
While the answer by #hemanth-raj is correct, I would actually advocate a slightly different way of doing this. Instead of constructing the AuthContainer with no data, you could actually do the user session loading before you construct your widgets and pass the data in directly. This example uses the scoped_model plugin to abstract away the inherited widget boilerplate (which I highly recommend over writing inherited widgets manually!) but is otherwise pretty similar to what you've done.
Future startUp() async {
UserModel userModel = await loadUser();
runApp(
ScopedModel<UserModel>(
model: userModel,
child: ....
),
);
}
void main() {
startup();
}
This is more or less what I do in my app and I haven't had any problems with it (although you'd probably want to put in some error handling if there's any chance of loadUser failing)!
This should made your userState code much cleaner =).
And an FYI, what I've done in my UserModel is have a bool get loggedIn => ... that knows which information needs to be in there to tell whether the user is logged in or not. That way I don't need to track it separately but I still get a nice simple way to tell from outside the model.
Do like this as example given in this :
void addPostFrameCallback(FrameCallback callback) {
// Login logic code
}
Schedule a callback for the end of this frame.
Does not request a new frame.
This callback is run during a frame, just after the persistent frame callbacks (which is when the main rendering pipeline has been flushed). If a frame is in progress and post-frame callbacks haven't been executed yet, then the registered callback is still executed during the frame. Otherwise, the registered callback is executed during the next frame.
The callbacks are executed in the order in which they have been added.
Post-frame callbacks cannot be unregistered. They are called exactly once.
I have a problem/question regarding the bloc plattern with flutter.
Currently, i am starting my app like this
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return BlocProvider(
bloc: MyBloc(),
child: MaterialApp(
title: "MyApp",
home: MyHomePage(),
routes: {
'/homePage': (context) => MyHomePage(),
'/otherPage': (context) => OtherPage(),
'/otherPage2': (context) => OtherPage2(),
...
},
));
So that i can retrieve/access myBloc like
myBloc = BlocProvider.of(context) as MyBloc;
and the data represented by the state like
BlocBuilder<MyBlocEvent, MyObject>(
bloc: myBloc,
builder: (BuildContext context, MyObject myObject) {
....
var t = myObject.data;
....
myBloc.onFirstEvent();
...
};
wherever i need it.
MyBloc is implemented like this:
abstract clas MyBlocEvent {}
class FirstEvent extends MyBlocEvent {}
class SecondEvent extends MyBlocEvent {}
class MyBloc extends Bloc<MyBlocEvent , MyObject>
void onFirstEvent()
{
dispatch(FirstEvent());
}
void onSecondEvent()
{
dispatch(SecondEvent());
}
#override
Stream<MyObject> mapEventToState( MyObject state, MyBlocEvent event) async* {
if (event is FirstEvent) {
state.data = "test1";
}
else if (event is SecondEvent) {
state.otherData = 5;
}
yield state;
}
The problem i now have, is that as soon as i change on of the state values and call
Navigator.pop(context)
to go back in the current stack, i can't change anything is the state anymore because the underlying stream seems to be closed. It fails with the message:
Another exception was thrown: Bad state: Cannot add new events after calling close"
Now this only happens after i call pop. If i only push new screens i can happily change the state data without any problems.
Am i doing something wrong regarding the Navigation here or is there something else i didn't catch regarding flutter or the bloc pattern itself?
Bad state: Cannot add new events after calling close
This error means that you are calling add on a StreamController after having called close:
var controller = StreamController<int>();
controller.close();
controller.add(42); // Bad state: Cannot add new events after calling close
It is likely related to you calling close inside the dispose method the "wrong" widget.
A good rule of thumb is to never dispose/close an object outside of the widget that created it. This ensure that you cannot use an object already disposed of.
Hope this helps in your debugging.
The navigation of the app depends on your widget designs.
I use stateless widgets and render the view using bloc's data.
Whenever i navigate to another page, i would pop the current widget and navigate to the next widget.
The next stateless widget declare the bloc,
then in your subsequent stateless widgets should contain calls like MyBloc.dispatch(event(param1: value1, param2: value2));
In MyBloc, you need to set the factory of your state that contains final values;
#override
Stream<MyObject> mapEventToState( MyObject state, MyBlocEvent event) async* {
if (event is FirstEvent) {
// set it in the state, so this code is omitted
// state.data = "test1";
// add this
yield state.sampleState([], "test1");
}
else if (event is SecondEvent) {
// state.otherData = 5;
yield state.sampleState([], 5);
} else {
yield state.sampleState([], null);
}
The MyObjectState needs to be setup like this,
class MyObjectState {
final List<Bar> bars;
final String Foo;
const MyObjectState(
{this.bars,
this.foo,
});
factory MyObjectState.sampleState(List<Bar> barList, String value1) {
return MyObjectState(bars: barList, foo: message);
}
}
So that the stateless widget can use the bloc like this
MyBloc.currentState.sampleState.foo
You can try run Felix Angelov's flutter project.
Login Flow Example
I have tried this function in flutter version v0.5.1 and it is working fine, no issue. After I have updated to the latest version v0.8.4 I get the exception below.
ExcceptionDatainheritFromWidgetOfExactType(_LocalizationsScope) was
called before ProfileScreen.initState() completed.
When an inherited widget changes, for example if the value of Theme.of() changes, its dependent widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor or an initState() method, then the rebuilt dependent widget will not reflect the changes in the inherited widget.
Typically references to to inherited widgets should occur in widget build() methods. Alternatively, initialization based on inherited widgets can be placed in the didChangeDependencies method, which is called after initState and whenever the dependencies change thereafter.
#override
void initState() {
super.initState();
makeRequest();
}
Future<DataModel> makeRequest() async {
_onLoading();
http.Response response = await http.get(Uri.encodeFull(getProfile),
headers: {"Accept": "application/json"});
/// json data calling.....
}
void _onLoading() {
if (loadCheck) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return new Dialog(
// progress dialog calling );
}
);
} else {
Navigator.pop(context);
}
}
You need the context from
#override Widget build(BuildContext context) {
....
....
makeRequest()
method paste after Widget build... and pass parameter (context) into parenthesis like this
makeRequest(context)
and again
_onLoading(context)
and use
showDialog(
context: context, //The context from Widget Build(BuildContext context)...
It is a common problem if you use Provider and InitState.
I see that I can access InheritedWidgets inside the build() method like this: final inheritedWidget = ChronoApp.of(context); but what if I want to access it somewhere else, say in initState() which has no context. How would I do this?
What I found to work for me is getting the parent context and using it in the didChangeDependencies() function that is called after initState. Like this
#override
// TODO: implement context
BuildContext get context => super.context;
#override
void didChangeDependencies() {
bloc = LoginBlocProvider.of(context);
bloc.isAuthenticated.listen((bool value) {
setState(() {
isLoading = false;
});
if (value) {
Navigator.push(context, MaterialPageRoute(
builder: (BuildContext context) => HomeScreen()
));
}
});
super.didChangeDependencies();
}
From de didChangeDependencies() docs:
This method is also called immediately after initState. It is safe to
call BuildContext.inheritFromWidgetOfExactType from this method.
I'm still trying to fully understand this feature but this is what worked for me
According to this docs context should be available in initState using the context getter.
https://docs.flutter.io/flutter/widgets/State/context.html
The framework associates State objects with a BuildContext after creating them with StatefulWidget.createState and before calling initState.
Brief Note:
In all of my code examples you will see things like material.Widget instead of just Widget. This is because I like to name my imports like this for example:
import 'package:flutter/material.dart' as material;
My Question:
I am trying to make a ListView show newly added Widgets at the top of the list. I have a simple stateful widget that only contains a list of models (classes that contain necessary information to construct the widgets that are going into the ListView), and should construct ListView's children based off of that list.
class Page extends material.StatefulWidget {
final List<CardModel> cardModels;
Page(this.cardModels);
#override
_PageState createState() => new _PageState(cardModels);
}
class _PageState extends material.State<Page> {
List<CardModel> cardModels;
_PageState(this.cardModels);
#override
material.Widget build(material.BuildContext context) {
return new material.ListView(
children: cardModels.map((cardModel) => new Card(cardModel.cardID)).toList(),
);
}
}
The behavior that I expect from this is that whenever the build method is called (and setState is properly used), that the ListView should be reconstructed properly and contain child widgets in the order of the list. It successfully does this if I simply add new models to cardModels sequentially:
cardModels.add(new CardModel(nextID++));
You can see widgets get added sequentially with their id's incrementing properly:
However, I want newer widgets to be inserted at the top (which would be shown with higher ids at the top). In order to accomplish this, I try inserting new models at the beginning of the list:
cardModels.insert(0, new CardModel(nextID++));
Unfortunately, instead of seeing the correct widgets, I just get widget with id 0 over and over again:
I know that the list of models is being updated correctly because I can print it out and see the ids in descending order. I am assuming that there is something about how flutter detects changes to widgets that is causing this behavior, but after a lot of reading, I still have not been able to figure it out. Any help would be much appreciated. Also, I call set state in the widget that ends up building the page (the widget containing the ListView) as one of its children:
btn = new FloatingActionButton(
onPressed: () => setState(_pageController.addCard),
tooltip: 'Upload File',
child: new Icon(Icons.file_upload),
);
_pageController is the object that modifies the list of models. If this is not enough information, let me know and I am happy to provide more code or answer any questions.
There is a simple quick fix, using the didWidgetUpdate override,
by checking if the cardModels object in the oldWidget is the same in the cardModels being passed as a parameter.
like so
#override
void didUpdateWidget(covariant Page oldWidget) {
if (widget.cardModels != oldWidget.cardModels) {
setState((){
cardModels = widget.cardModels;
});
}
super.didUpdateWidget(oldWidget);
}
Full example below
class Page extends material.StatefulWidget {
final List<CardModel> cardModels;
Page(this.cardModels);
#override
_PageState createState() => new _PageState(cardModels);
}
class _PageState extends material.State<Page> {
List<CardModel> cardModels;
#override
void initState(){
cardModels = widget.cardModels;
super.initState();
}
#override
void didUpdateWidget(covariant Page oldWidget) {
if (widget.cardModels != oldWidget.cardModels) {
setState((){
cardModels = widget.cardModels;
});
}
super.didUpdateWidget(oldWidget);
}
_PageState(this.cardModels);
#override
material.Widget build(material.BuildContext context) {
return new material.ListView(
children: cardModels.map((cardModel) => new Card(cardModel.cardID)).toList(),
);
}
}
Turns out, the Card widget I made did not need to be stateful. I changed it to stateless and it solved the problem. No idea why having them as stateful would break it though.
On FloatingActionButton change the function to insert new item and then invoke setState like:
cardModels.insert(0, new CardModel(nextId++)) ;
setState(() {
cardModels = cardModels;
});
Hope that helped!