How to get a context for Navigator in Widget initState()? - flutter

I want my app to work offline without a user set, and asking for a login when connectivity is back
en excerpt of the code I'm trying:
class _MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
Connectivity().onConnectivityChanged.listen((ConnectivityResult result) =>
checkConnectivity().then((isOnline) {
if (isOnline && MyApp.store.state.user == null)
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => LoginPage()),
);
}));
}
#override
Widget build(BuildContext context) {
return MaterialApp(
routes: {
...
but all I can get is this error:
Unhandled Exception: Navigator operation requested with a context that does not include a Navigator.
The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.
I tried to wrap my Navigator call inside a Future.delayed as described here but I got the same error

Related

Provider to be initialized asynchronously from `initState()` but get `could not find the correct Provider`

I develop an ad app, with a message button on the detailed view.
When the user tap on it, the chats view (stateful widget) is pushed to the screen.
The initState() is there to call the asyncInitMessages() which asynchronously fetches the chats and related message from the distant database. The asyncInitMessages() belongs to the Chats class which extends ChangeNotifier.
/// A chat conversation
class Chats extends ChangeNotifier {
/// Internal, private state of the chat.
void asyncInitMessages(
{required ClassifiedAd ad,
required String watchingUserId,
required bool isOwner}) async {
// blah blah
}
}
The ClassifiedAdMessagesViewstateful widget class implementation is as follows (snipet):
#override
void initState() {
// == Fetch conversation and messages
asyncInitMessages();
}
void asyncInitMessages() async {
// === Update all messages
try {
Provider.of<Chats>(context, listen: false).asyncInitMessages(
ad: widget.ad,
watchingUserId: widget.watchingUser!.uid,
isOwner: _isOwner);
} catch (e) {
if (mounted) {
setState(() {
_error = "$e";
_ready = true;
});
}
}
}
#override
Widget build(BuildContext context) {
// <<<<<<<<<<< The exception fires at the Consumer line right below
return Consumer<Chats>(builder: (context, chats, child) {
return Scaffold(
// ... blah blah
Finally, when running ll that, I got the exception in the build at the Consumer line:
could not find the correct Provider<chats>
Help greatly appreciated.
[UPDATED]
Here is the main (very far up from the messages screen)
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
//if (Firebase.apps.isEmpty) {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// } else {
// Firebase.app(); // if already initialized, use that one
// }
if (USE_DATABASE_EMULATOR) {
FirebaseDatabase.instance.useDatabaseEmulator(emulatorHost, emulatorPort);
}
runApp(RootRestorationScope(
restorationId: 'root',
child: ChangeNotifierProvider(
create: (context) => StateModel(),
child: const App())));
}
class App extends StatefulWidget {
const App({super.key});
#override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
#override
Widget build(BuildContext context) {
return PersistedAppState(
storage: const JsonFileStorage(),
child: MultiProvider(
providers: [
ChangeNotifierProvider<ThemeModel>.value(value: _themeModel),
//ChangeNotifierProvider<AuthModel>.value(value: _auth),
],
child: Consumer<ThemeModel>(
builder: (context, themeModel, child) => MaterialApp(
// blah blah
}
}
}
And the component just on top of the
/// Classified ad detail view
class ClassifiedAdDetailView extends StatefulWidget {
final User? watchingUser;
final ClassifiedAd ad;
const ClassifiedAdDetailView(
{Key? key, required this.watchingUser, required this.ad})
: super(key: key);
#override
State<ClassifiedAdDetailView> createState() => _ClassifiedAdDetailViewState();
}
class _ClassifiedAdDetailViewState extends State<ClassifiedAdDetailView>
with TickerProviderStateMixin {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Chats(),
builder: ((context, child) => Scaffold(
// blah blah
ElevatedButton(
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ClassifiedAdMessagesView(
ad: ad,
watchingUser: widget.watchingUser)));
}),
Providers must be located in the widget tree above the widget where you want to use them with Consumer or Provider.of. When you push a new route with Navigator, it won't be add the pushed route below the widget from where you push, it will add it at the same level where home of MaterialApp is located.
(I think the error message you get also states that you can't access the providers between routes.)
In general the tree will look like this if you push some routes (check it with the Flutter Widget Inspector):
MaterialApp
home
widget1
widget2
widget21
widget22
page1
widget1
widget2
page2
page3
In your code you create the provider in ClassifiedAdDetailView and then push
ClassifiedAdMessagesView from this in the onPressed method. You won't be access this provider from ClassifiedAdMessagesView because the tree will be like (simplified):
MaterialApp
home
ClassifiedAdDetailView
ClassifiedAdMessagesView
The solution is to "lift the state up" and place the provider above every widget from where you need to access it. It can be a part of your existing Multiprovider above MaterialApp but if it is too far, you need to find a proper place that is above both ClassifiedAdDetailView and ClassifiedAdMessagesView.

Subscribe to application transition changes

I need my app to react whenever the user:
switches to another app
returns back to the app
tries to close the app
How can I subscribe to these changes?
You can use the onGenerateRoute property on the MaterialApp to listen and execute your methods each time a named route is made ( when you navigate over pages in your pages) :
MaterialApp(
onGenerateRoute: (settings) {
print("navigated to new route")
},
//...
You can use WillPopScope widget to listen for the navigator's pop() method which goes back to the previous screen if it exists in the stack route, and close the app when it's the only one left in the stack route.
WillPopScope(
onWillPop: () async {
print("route popped");
return Future.value(true);
}, child: WillPopScope(
onWillPop: () async {
print("route popped");
return Future.value(true); // will allow popping, if the value is false, it will not pop.
},
child: Scaffold(//...)
if you're willing to use a custom personalized Navigator in your app, then you can set from it the onGeneratedRoute and also the onPopPage which let you listen to pop() over the sub-tree it takes:
Navigator(
onPopPage: /*...*/,
onGenerateRoute: /*...*/,
),
Another option you can do, is to use life cycle methods to execute a piece of code whenever a widget screen is rendered ( initState() will run), and when you pop() it, it will be disposed ( dispose() will execute ), but it requires using StatefulWidget:
class Example extends StatefulWidget {
const Example({super.key});
#override
State<Example> createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
1- when switch to another app as well as return to the app You can use
WidgetsBindingObserver in your widgets and listen to AppLifecycleState.
2- in order to handle resume callback
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
switch (state) {
case AppLifecycleState.resumed:
if (resumeCallBack != null) {
await resumeCallBack();
}
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
if (suspendingCallBack != null) {
await suspendingCallBack();
}
break;
}
}
3- to perform some function on app close try to use willpopscope

Flutter Receive sharing intent and page change

I'm working on a mailing application and i'm stuck when i try to share a file to send it by email with the receive_sharing_intent package
I'm able to get the file in the application with it's path and all, but then i need to redirect the user to the mail editor page. When i try to use this to get to the mailEditor page :
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MailEditor(
message: message,
editorType: 'new',
),
),
);
I get this error :
[ERROR:flutter/shell/common/shell.cc(213)] Dart Error: Unhandled exception:
Navigator operation requested with a context that does not include a Navigator.
The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.
This package being developed by the community i couldn't find much help anywhere on the internet.
I failed to use the ReceiveSharingIntent method anywhere else than the main.dart file, so maybe there's a way to use it directly on my mailEditor page that i didn't find?
If more code is needed here is my ReceiveSharingIntent method :
class MyApp extends StatefulWidget {
// This widget is the root of your application.
#override
_MssProState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
void dispose() {
_intentDataStreamSubscription.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
// For sharing images coming from outside the app while the app is in the memory
_intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) {
_sharedFiles = value;
Message message = Message();
for (var i = 0; i < _sharedFiles.length; i++) {
File file = File(_sharedFiles[i].path);
Attachment attachment = Attachment(
fileName: basename(file.path),
attachmentPart: i + 1,
contentType: lookupMimeType(file.path),
name: basename(file.path),
);
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => MailEditor(
message: message,
editorType: 'new',
),
),
);
});
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Login(),
routes: {
'/login': (BuildContext context) => Login(),
},
);
}
Thanks a lot for any help and suggestions, i've been stuck on this for a while.
Edit:
Added more code for context
Navigator should be used inside the build() method. If you want to use context outside of it, pass it as an argument like BuildContext context.

How to reload the page whenever the page is on screen - flutter

Is there any callbacks available in flutter for every time the page is visible on screen? in ios there are some delegate methods like viewWillAppear, viewDidAppear, viewDidload.
I would like to call a API call whenever the particular page is on-screen.
Note: I am not asking the app states like foreground, backround, pause, resume.
Thank You!
Specifically to your question:
Use initState but note that you cannot use async call in initState because it calls before initializing the widget as the name means. If you want to do something after UI is created didChangeDependencies is great. But never use build() without using FutureBuilder or StreamBuilder
Simple example to demostrate:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(MaterialApp(home: ExampleScreen()));
}
class ExampleScreen extends StatefulWidget {
ExampleScreen({Key key}) : super(key: key);
#override
_ExampleScreenState createState() => _ExampleScreenState();
}
class _ExampleScreenState extends State<ExampleScreen> {
List data = [];
bool isLoading = true;
void fetchData() async {
final res = await http.get("https://jsonplaceholder.typicode.com/users");
data = json.decode(res.body);
setState(() => isLoading = false);
}
// this method invokes only when new route push to navigator
#override
void initState() {
super.initState();
fetchData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: isLoading
? CircularProgressIndicator()
: Text(data?.toString() ?? ""),
),
);
}
}
Some lifecycle method of StatefulWidget's State class:
initState():
Describes the part of the user interface represented by this widget.
The framework calls this method in a number of different situations:
After calling initState.
After calling didUpdateWidget.
After receiving a call to setState.
After a dependency of this State object changes (e.g., an InheritedWidget referenced by the previous build changes).
After calling deactivate and then reinserting the State object into the tree at another location.
The framework replaces the subtree below this widget with the widget
returned by this method, either by updating the existing subtree or by
removing the subtree and inflating a new subtree, depending on whether
the widget returned by this method can update the root of the existing
subtree, as determined by calling Widget.canUpdate.
Read more
didChangeDependencies():
Called when a dependency of this State object changes.
For example, if the previous call to build referenced an
InheritedWidget that later changed, the framework would call this
method to notify this object about the change.
This method is also called immediately after initState. It is safe to
call BuildContext.dependOnInheritedWidgetOfExactType from this method.
Read more
build() (Stateless Widget)
Describes the part of the user interface represented by this widget.
The framework calls this method when this widget is inserted into the
tree in a given BuildContext and when the dependencies of this widget
change (e.g., an InheritedWidget referenced by this widget changes).
Read more
didUpdateWidget(Widget oldWidget):
Called whenever the widget configuration changes.
If the parent widget rebuilds and request that this location in the
tree update to display a new widget with the same runtimeType and
Widget.key, the framework will update the widget property of this
State object to refer to the new widget and then call this method with
the previous widget as an argument.
Read more
Some widgets are stateless and some are stateful. If it's a stateless widget, then only values can change but UI changes won't render.
Same way for the stateful widget, it will change for both as value as well as UI.
Now, will look into methods.
initState(): This is the first method called when the widget is created but after constructor call.
#override
void initState() {
// TODO: implement initState
super.initState();
}
didChangeDependecies() - Called when a dependency of this State object changes.Gets called immediately after initState method.
#override
void didChangeDependencies() {
super.didChangeDependencies();
}
didUpdateWidget() - It gets called whenever widget configurations gets changed. Framework always calls build after didUpdateWidget
#override
void didUpdateWidget (
covariant Scaffold oldWidget
)
setState() - Whenever internal state of State object wants to change, need to call it inside setState method.
setState(() {});
dispose() - Called when this object is removed from the tree permanently.
#override
void dispose() {
// TODO: implement dispose
super.dispose();
}
You don't need StatefulWidget for calling the api everytime the screen is shown.
In the following example code, press the floating action button to navigate to api calling screen, go back using back arrow, press the floating action button again to navigate to api page.
Everytime you visit this page api will be called automatically.
import 'dart:async';
import 'package:flutter/material.dart';
main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => ApiCaller())),
),
);
}
}
class ApiCaller extends StatelessWidget {
static int counter = 0;
Future<String> apiCallLogic() async {
print("Api Called ${++counter} time(s)");
await Future.delayed(Duration(seconds: 2));
return Future.value("Hello World");
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Api Call Count: $counter'),
),
body: FutureBuilder(
future: apiCallLogic(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) return const CircularProgressIndicator();
if (snapshot.hasData)
return Text('${snapshot.data}');
else
return const Text('Some error happened');
},
),
);
}
}
This is the simple code with zero boiler-plate.
The simplest way is to use need_resume
1.Add this to your package's pubspec.yaml file:
dependencies:
need_resume: ^1.0.4
2.create your state class for the stateful widget using type ResumableState instead of State
class HomeScreen extends StatefulWidget {
#override
HomeScreenState createState() => HomeScreenState();
}
class HomeScreenState extends ResumableState<HomeScreen> {
#override
void onReady() {
// Implement your code inside here
print('HomeScreen is ready!');
}
#override
void onResume() {
// Implement your code inside here
print('HomeScreen is resumed!');
}
#override
void onPause() {
// Implement your code inside here
print('HomeScreen is paused!');
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
child: Text('Go to Another Screen'),
onPressed: () {
print("hi");
},
),
),
);
}
}
If you want to make an API call, then you must be (or really should be) using a StatefulWidget.
Walk through it, let's say your stateful widget receives some id that it needs to make an API call.
Every time your widget receives a new id (including the first time) then you need to make a new API call with that id.
So use didUpdateWidget to check to see if the id changed and, if it did (like it does when the widget appears because the old id will be null) then make a new API call (set the appropriate loading and error states, too!)
class MyWidget extends StatefulWidget {
Suggestions({Key key, this.someId}) : super(key: key);
String someId
#override
State<StatefulWidget> createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> {
dynamic data;
Error err;
bool loading;
#override
Widget build(BuildContext context) {
if(loading) return Loader();
if(err) return SomeErrorMessage(err);
return SomeOtherStateLessWidget(data);
}
#override
void didUpdateWidget(covariant MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// id changed in the widget, I need to make a new API call
if(oldWidget.id != widget.id) update();
}
update() async {
// set loading and reset error
setState(() => {
loading = true,
err = null
});
try {
// make the call
someData = await apiCall(widget.id);
// set the state
setState(() => data = someData)
} catch(e) {
// oops an error happened
setState(() => err = e)
}
// now we're not loading anymore
setState(() => loading = false);
}
}
I'm brand new to Flutter (literally, just started playing with it this weekend), but it essentially duplicates React paradigms, if that helps you at all.
Personal preference, I vastly prefer this method rather than use FutureBuilder (right now, like I said, I'm brand new). The logic is just easier to reason about (for me).

Flutter widget state not updating - throws 'Looking up a deactivated widget's ancestor is unsafe'

I am trying to implement login/logout in Flutter using this example. Log in works fine and the console output is:
flutter: LOGIN WIDGET BUILD CONTEXT:
flutter: LoginScreen(dirty, state: LoginScreenState#552db)
flutter: _ctx:
flutter: LoginScreen(state: LoginScreenState#552db)
But after logging out, I can't log back in (the context is lost). After logging out and then trying to log back in, the context within onAuthStateChanged() is lost after logout:
flutter: LOGIN WIDGET BUILD CONTEXT:
flutter: LoginScreen(dirty, state: LoginScreenState#d112e)
flutter: _ctx
flutter: LoginScreen
login.dart
class LoginScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new LoginScreenState();
}
}
class LoginScreenState extends State<LoginScreen>
BuildContext _ctx;
#override
onAuthStateChanged(AuthState state) {
print("_ctx");
print(_ctx.toString());
if(state == AuthState.LOGGED_IN) {
print("ready to login");
Navigator.of(_ctx).pushReplacementNamed("/home");
}
}
#override
Widget build(BuildContext context) {
_ctx = context;
print("LOGIN WIDGET BUILD CONTEXT:");
print(_ctx.toString());
}
}
home.dart
class Settings extends StatelessWidget {
#override
Widget build(BuildContext context) => new Container(
child: new ListView(
children: <Widget>[
new ListTile(
//leading: Icon(Icons.map),
title: new Text('About')
),
new ListTile(
//leading: Icon(Icons.photo_album),
title: new Text('Logout'),
onTap: () {
Navigator.of(context).pushReplacementNamed("/login");
}
)
],
),
);
The routes:
final routes = {
'/login': (BuildContext context) => new LoginScreen(),
'/home': (BuildContext context) => new Tabs(),
'/' : (BuildContext context) => new LoginScreen(),
};
Wy isn't _ctx updated in onAuthStateChanged() and is there a better way to handle login state?
You are reusing an old BuildContext instance. Don't save the instance in the build method. You should never ever do this.
Your LoginScreen is a StatefulWidget and it's state (LoginScreenState) already has context property. Try using this instead.
You also need to dispose the listener, the example already has a dispose(AuthListener) method that you can use.
#override
void dispose() {
super.dispose(); // always call super for dispose/initState
AuthStateProvider().dispose(this);
}
In general the example is pretty old and I would suggest finding a more recent tutorial. Flutter and Dart is evolving very fast and a 2 year old example is not the way to go.
I was referring the same sample for my app and the problem is no "Unsubscribe" at AuthStateProvider during dispose(). Try to implement something like followings at State:
#override
void dispose() {
var authStateProvider = new AuthStateProvider();
authStateProvider.unSubscribe(this);
}
As answered by Aman Chu add the following method in both login_screen.dart and home_screen.dart
It unsubscribes the listener whenever we switch screens.
#override
void dispose() {
var authStateProvider = new AuthStateProvider();
authStateProvider.unSubscribe(this);
}
Also you need to have unSubscribe method on auth.dart which is given below:
void unSubscribe(AuthStateListener listener) {
for(var l in _subscribers) {
if(l == listener)
_subscribers.remove(l);
}
}