Reputation: 2186
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?
Upvotes: 19
Views: 19381
Reputation: 61
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.
Upvotes: 0
Reputation: 40433
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.
Upvotes: 4
Reputation: 34769
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!
Upvotes: 32