Flutter use context when handling dynamic link from app closed - flutter

I see this weird behaviour when handling dynamic links. What I want to do is that when coming from a link containing matchId parameter I want to clean up the navigation stack, put up the AvailableMatches page and after the MatchDetails page and finally show a modal. This is the code I use in the link handler
FirebaseDynamicLinks.instance.onLink(
onSuccess: (PendingDynamicLinkData dynamicLink) async {
final Uri deepLink = dynamicLink?.link;
if (deepLink != null) {
handleLink(deepLink);
});
Future<void> handleLink(Uri deepLink) async {
var context = navigatorKey.currentContext;
var matchId = deepLink.queryParameters["match_id"];
Navigator.of(context).pushNamedAndRemoveUntil(
AvailableMatches.routeName,
(Route<dynamic> route) => false
);
Navigator.of(context).pushNamed(MatchDetails.routeName,
arguments: ScreenArguments(
matchId, false)
);
await showModalBottomSheet(context: context, builder: (context) => Text("done"));
}
If the app is already open this works fine. If the app is starting from this link I have the following initState in the first StatefulWidget
void initState() {
super.initState();
initDynamicLinks();
loadData(context);
}
Future<void> loadData(BuildContext context) async {
final PendingDynamicLinkData data = await FirebaseDynamicLinks.instance.getInitialLink();
handleLink(data.link);
}
In this case the handeLink method works until the latest model. The pages are pushed correctly on the stack however the last model doesn't show up.
I am adding prints and things like that but it seems that this line never gets executed. There is no crash or exception. It just gets ignored

Related

How to refactor this code to avoid passing BuildContext between async scopes?

I have a PermissionsManager class, and I'm getting a "Do not use BuildContext across async gaps" for this particular method:
class PermissionsManager {
static Future<void> requestLocationPermission(BuildContext context) async {
final status = await Permission.location.request();
if (!status.isGranted) {
await showOpenSettingsDialog(context,
title: "Grant Location Access",
message:
"TODO");
}
}
}
I thought about splitting this into multiple functions, but then the caller needs to check the status, and based on the status call another method that will show this dialog box.
Is there a way to do this in the same method and handle this build context issue?
Good question! Assuming you are in a "Stateful Widget", add if (mounted) check before using BuildContext across an async gap.
For example:
onPressed: () async {
final status = await Future.delayed(const Duration(seconds: 1));
if (mounted) { // <-- add this check
if (!status.isGranted) {
showOpenSettingsDialog(context);
}
}
}
The reason we don't want to use BuildContext across an async gap is because the widget could've become unmounted during the wait. If we check if (mounted) we won't have this concern. Basically, if the widget is somehow destroyed during the wait, we just don't show the dialog any more.
If this is a stateless widget, you can convert it into a stateful one.
You can also read my detailed explanation on this topic here.
Store the NavigatorState before executing your requestLocationPermission function, and then use it to handle the navigation:
onPressed: () async {
final navigator = Navigator.of(context); // store the Navigator
await requestLocationPermission(navigator); // use the Navigator, not the BuildContext
},
class PermissionsManager {
static Future<void> requestLocationPermission(NavigatorState navigator) async {
final status = await Permission.location.request();
if (!status.isGranted) {
await showOpenSettingsDialog(
context,
title: "Grant Location Access",
message: "TODO",
);
}
navigator.pop(); // Do whatever with your navigator;
}
}
This answer is basically a shorthand of: https://stackoverflow.com/a/69512692
Which I highly suggest for you to look at it for a more detailed explanation.

How to get data from bloc stream from other page in flutter

I have a problem like this :
In Splash Page , i check in sharedpreference to get saved token when login successfully .If i have token , i request Api to get account information and move to next page like this:
Future check() async {
String _getToken = await splashBloc.getTokenFormSharedPref();
if (_getToken=='0') {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => LoginMain()));
} else {
splashBloc.getAccountInfo();
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => HomeScreenMain()));
}
}
and this is BLoC class:
class SplashBloc extends BlocBase{
String _getToken = '';
Future<String> getTokenFormSharedPref() async{
SharedPreferences prefs = await SharedPreferences.getInstance();
_getToken = (prefs.getString('token') ?? '0');
return _getToken;
}
final accountInfoController = new StreamController<Account>();
Sink<Account> get accountInfoSink => accountInfoController.sink;
Stream<Account> get accountInfoStream => accountInfoController.stream;
Future getAccountInfo() async{
Account account = await NetworkService().getAccountInfo2(_getToken);
accountInfoSink.add(account);
print('from splash: '+account.fullName);
}
#override
void dispose() {
accountInfoController.close();
}
}
When i check log , it totally request successfully and the problem is how can i acesss data in streambuilder in next page that is HomeScreenMain()?
Thanks for help!!
You can declare a variable in HomeScreenMain() and send the data you received before to the class constructor like this:
HomeScreenMain() {
final data;
HomeScreenMain(this.data)
//....
}
and when you want to call this widget you can pass that data from block to this widget
You appear to use a very basic approach with BLoC. Not sure if my answer helps there.
But if you use the library flutter_bloc, then you can use on the next page
ˋfinal bloc = context.read;
This looks for a provider of this bloc type upstream in the Widget tree and assigns it to ˋbloc

How to close a screen from PageView class flutter

Greeting,
I have a really specific question to ask. I have to explain it with steps and pictures so there they are.
I have an app with three screens:
Main Feed Screen,
Main Chat and Requests Screen,
Main Profile Screen,
And they are all a part of a PageView. This PageView class is controlled inside of a class called main_tab_controller.dart. In that class, in initState(), I have a Firebase Messaging method that is called every time I get a notification (onMessage). So, every time I get this notification, I show an overlay that looks like this.
And it works perfectly on these three main screen. If it's a chat notification, I will direct the PageView to the second screen i.e MainChatAndRequest Screen, and open the chat screen. If it's a request notification, I will direct the PageView to the second screen i.e MainChatAndRequest Screen, and open the requests screen.
But the issue that I am having is the following. In my MainFeedScreen and MainProfileScreen, I have some other screens that I open. For example in MainFeedScreen, I open UserDetailsScreen or FilterScreen. Or in the MainProfileScreen, I open SettingsScreen or EditUserProfileScreen.
So my question is: For example, if I navigate to MainProfileScreen and in that screen open SettingsScreen, and I get the overlay top message, how do I close the SettingsScreen that is currently open and navigate back to the second screen i.e MainChatsAndRequestsScreen from the Firebase Messaging Function that is in initState() of main_tab_controller.dart that is the parent to all of the other screens.
You have the Image Below:
I have tried everything, Navigator.popUntil(context), Navigator.pushReplacement(context), used Navigator.pushNamed(context) but nothing worked. If someone can help me, it would be much appreciated.
Just to give you the better undertanding of the screens:
The Parent Screen is the PageView with three screens:
Main Feed Screen
Main Chat and Requests Screen
Main Profile Screen
and then in Main Feed Screen you have:
Filters Screen
Profile Details Screen
in Main Chat and Requests Screen you have two TabBar Screens:
Chats Screen
Requests Screen
and in Main Profile Screen you have:
Settings Screen
Edit Profiles Screen
PageView Code Snippet:
#override
void initState() {
pageController = PageController(initialPage: _currentIndex);
chatAndRequestController = TabController(length: 2, vsync: this);
var chatAndRequestProvider =
Provider.of<ChatAndRequestProvider>(context, listen: false);
super.initState();
fbm.requestNotificationPermissions();
fbm.configure(
onMessage: (Map<String, dynamic> message) async {
print("onMessage: $message");
bool isRequest;
var mode = (Platform.isIOS) ? message['mode'] : message['data']['mode'];
var imageUrl = '';
switch (mode) {
case 'chat':
isRequest = false;
imageUrl =
chatAndRequestProvider.chatsList.first[kProfilePictureUrl];
break;
case 'sentRequest':
isRequest = true;
imageUrl = (Platform.isIOS)
? message['profilePictureUrl']
: message['data']['profilePictureUrl'];
break;
case 'acceptRequest':
isRequest = false;
imageUrl = (Platform.isIOS)
? message['profilePictureUrl']
: message['data']['profilePictureUrl'];
break;
default:
isRequest = false;
break;
}
AudioCache player = new AudioCache();
const alarmAudioPath = "sounds/notification_sound.mp3";
player.play(alarmAudioPath);
print('Show this ting');
if (_currentIndex != 1) {
if (!isDialogOpen) {
isDialogOpen = true;
_showAnimatedBox(
context,
(Platform.isIOS)
? message['aps']['alert']['title']
: message['notification']['title'],
(Platform.isIOS)
? message['aps']['alert']['body']
: message['notification']['body'],
imageUrl,
isRequest,
);
}
}
},
onLaunch: (Map<String, dynamic> message) async {
print("onLaunch: $message");
},
onResume: (Map<String, dynamic> message) async {
print("onResume: $message");
},
);
notificationPlugin
.setListenerForLowerVersions(onNotificationInLowerVersions);
notificationPlugin.setOnNotificationClick(onNotificationClick);
_children.addAll([
MainFeedScreen(
analytics: widget.analytics,
observer: widget.observer,
latitude: widget.latitude,
longitude: widget.longitude,
),
MainChatAndRequestScreen(
analytics: widget.analytics,
observer: widget.observer,
pageContoller: chatAndRequestController,
),
MainProfileScreen(analytics: widget.analytics, observer: widget.observer),
]);
}
Future _showAnimatedBox(context, topText, bottomText, imageUrl, isRequest) {
showDialog(
context: context,
builder: (BuildContext builderContext) {
_timer = Timer(Duration(seconds: 4), () {
Navigator.of(context).pop();
isDialogOpen = false;
});
return Dismissible(
key: Key('dismissible'),
direction: DismissDirection.up,
onDismissed: (_) {
Navigator.of(context).pop();
isDialogOpen = false;
},
child: FunkyNotification(
() {
var chatAndRequestProvider =
Provider.of<ChatAndRequestProvider>(context, listen: false);
// var contextProv =
// Provider.of<ContextProvider>(context, listen: false);
chatAndRequestProvider.setAreThereNewChatsAndRequestFalse();
if (isRequest) {
pageController.jumpToPage(1);
chatAndRequestController.animateTo(1);
Navigator.of(context).pop();
// Navigator.of(contextProv.context).pop();
// SystemChannels.platform.invokeMethod('SystemNavigator.pop');
// Navigator.popUntil(
// context,
// ModalRoute.withName('/mainProfileScreen'),
// );
// Navigator.of(context)
// .popUntil(ModalRoute.withName('/mainProfileScreen'));
// Navigator.pushAndRemoveUntil(
// context,
// MaterialPageRoute(
// builder: (BuildContext context) => MainTabBarController(
// analytics: null,
// observer: null,
// latitude: 100.23423234,
// longitude: 12.324234234,
// isProfileBlocked: false,
// isVersionGood: true,
// ),
// ),
// (route) => true,
// );
} else {
var chatAndRequestProvider =
Provider.of<ChatAndRequestProvider>(context,
listen: false);
pageController.jumpToPage(1);
chatAndRequestController.animateTo(0);
Navigator.of(context).pop();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(
appMode:
chatAndRequestProvider.chatsList.first[kAppMode],
peerId: chatAndRequestProvider.chatsList.first[kUserId],
peerAvatar: chatAndRequestProvider
.chatsList.first[kProfilePictureUrl],
peerName: chatAndRequestProvider
.chatsList.first[kNameAndSurname],
friendshipStatus: chatAndRequestProvider
.chatsList.first['friendsStatus'],
analytics: widget.analytics,
observer: widget.observer,
),
),
);
}
},
topText,
bottomText,
imageUrl,
),
);
}).then((val) {
if (_timer.isActive) {
_timer.cancel();
}
isDialogOpen = false;
});
}
I will try make my answer as general as possible in order to make it easier for others to follow along.
The problem in a nutshell is that you have a nested set of screens distributed between a set of pageviews, and you want to switch between the pageviews from an external event (The overlay in this case).
Below is an example:
TL;DR
I couldn't provide the full code since I don't have your full source code. But here is an example 😉
Note: This example uses Provider.
Sample Event Code
// Remove all the screens in the route
Navigator.of(context).popUntil((route) => route.isFirst); // If the screen is not the first replace the check
// Change the second pageview page
Provider.of<ChatSelectPageView>(context, listen: false).setPage(selectedSecondPageViewPage);
// In case it is required to add intermediate screens between the first and the second pageview it must be added here
// Change the main pageview page
_mainPageViewcontroller.animateToPage(1);
Second PageView
// Reads the page index present in the provider
int selectedPage = Provider.of<ChatSelectPageView>(context, listen: false).page;
// Changes to the cotroller page to the selected page
_pageController.jumpToPage(selectedPage);
ChatSelectPageView
class ChatSelectPageView extends ChangeNotifier {
int page = 0;
void setPage(int _page) {
page = _page;
// Depending on your implementation it might be better to remove this line to reduce the number of builds
notifyListeners();
}
}
TS;WM
In order to achieve the desired behavior, there is multiple ways to achieve it. If we want to stick to your implementation we will be a bit constrained. But in this case what I would suggest you do is to use some kind of global state management library such as provider, it can be done without any library but the state will get very messy very quickly.
As you mentioned above you tried Navigator.popUntil but it didn't work, I suspect the reason for this is that you are providing the wrong context. Since Navigator.**** relies on the context to work, i.e. to pop a screen you must provide its context. Or the route check is wrong.
This code is to be written in the external event in your case it will be written in the click listener of the overlay.
Use a state management solution such as Provider to pass the state to the descendants of the main Pageview down to the screens. This provider will be of type ChangeNotifierProvider. When the overlay is clicked, a flag will be set to be the desired pageview page index (I am speaking about the 2nd pageview). In your case this flag is used to select chats or requests.
After that is done you call Navigator.of(context).popUntil((route) => route.isFirst); assuming that the pageview is present on the first page of your app. In the case where it is not on that page, you will have to use Navigator.of(context).popUntil() with a custom logic.
After that we will have to navigate back to the 2nd pageview, or change the first pageview to be the 2nd page in your case. The second pageview will be already switched since we changed the flag in provider before.

I have a question about navigating to the next page conditionally in initstate

I want to implement Auto Login with Shared preferences.
What I want to implement is that as soon as 'LoginPage' starts, it goes to the next page without rendering LoginPage according to the Flag value stored in Shared preferences.
However, there is a problem in not becoming Navigate even though implementing these functions and calling them from initstate. What is the problem?
//Login Page
void autoLogIn() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final String userId = prefs.getString('username');
print("ddddddddddddddd");
SocketProvider provider = Provider.of<SocketProvider>(context);
Future.delayed(Duration(milliseconds: 100)).then((_) {**//I tried giving Delay but it still didn't work.**
Navigator.of(context).pushNamedAndRemoveUntil("/MainPage", (route) => false);
});
}
#override
void initState() {
// TODO: implement initState
loginBloc = BlocProvider.of<LoginBloc>(context);
if(!kReleaseMode){
_idController.text = "TESTTEST";
_passwordController.text = "1234123";
}
initBadgeList();
autoLogIn();**//This is the function in question.**
super.initState();
print("1111111111111111");
}
I don't think you should show LoginPage widget if user is already logged in and then navigate to main page.
I suggest you to use FutureBuilder and show either splash screen or loader while performing await SharedPreferences.getInstance(). In this case your App widget should look like this:
class App extends MaterialApp {
App()
: super(
title: 'MyApp',
...
home: FutureBuilder(
future: SharedPreferences.getInstance(),
builder: (context, snapshot) {
if (snapshot.data != null) {
final SharedPreferences prefs = snapshot.data;
final userId = prefs.getString('username');
...
return userId == null ?? LoginPage() : MainPage();
} else {
return SplashScreenOrLoader();
}
}));
}
But if you still want to show LoginPage first, just replace SplashScreenOrLoader() with LoginPage() in code above.

Navigation inside future method flutter

I am trying to navigate to a screen from a future method. However I get an error saying undefined name context. I tried navigating from Widget build but the parameter is created within this method and I need it for navigating. I've been stuck on this for a very long time. Any help will be really appreciated.
Future<void> addBookingConversation(Booking booking) async {
Conversation conversation = Conversation();
await conversation.addConversationToFirestore(booking.posting.host); //additional method working fine
String text = "Hi, my name is ${AppConstants.currentUser.firstName}";
await conversation.addMessageToFirestore(text); //additional method working fine
//this is where i should navigate to the conversation page and facing the error here
Navigator.push(
context, //error here context undefined
MaterialPageRoute(builder:
(context) => ConversationPage(conversation: conversation,),
),
);
}
class ConversationPage extends StatefulWidget {
final Conversation conversation;
static final String routeName = '/conversationPageRoute';
ConversationPage({this.conversation, Key key}) : super(key: key);
#override
_ConversationPageState createState() => _ConversationPageState();
}
class _ConversationPageState extends State<ConversationPage> {
Conversation _conversation;
// additional code of wiget build
}
I don't know where your function resides, so this is some general advice:
If you cannot access a variable in your method you have two options: pass it in as a parameter from the caller. Or return the result to the caller so they can do the part where the variable is needed themselves.
What does that mean for your scenario: either you need the context as an additional parameter in your method, or you need to return Future<Conversation> from your method and handle the navigation where it's called.
Personally, I'd favor the second option, since your business logic of starting a conversation and your in-app navigation are two different concerns that should not be mixed in one method.
If you want to call the navigator method anywhere in the app.
class NavigationService {
final GlobalKey<NavigatorState> globalKey = GlobalKey<NavigatorState>();
Future<dynamic> navigateTo(Route Route) {
return globalKey.currentState.push(Route);
}
}
and in main.dart.
navigatorKey: NavigationService().globalKey,
and then anywhere within the app.
Just use this
Future<void> addBookingConversation(Booking booking) async {
Conversation conversation = Conversation();
await conversation.addConversationToFirestore(booking.posting.host);
//additional method working fine
String text = "Hi, my name is ${AppConstants.currentUser.firstName}";
await conversation.addMessageToFirestore(text); //additional method working
fine
//this is where i should navigate to the conversation page and facing the
error here
NavigationService().navigateTo(
MaterialPageRoute(builder:
(context) => ConversationPage(conversation: conversation,),
),);
}
Wrap your Navigator inside :
WidgetsBinding.instance.addPostFrameCallback((_){
// 👈 Your Navigation here
});
Your Code:
Future<void> addBookingConversation(Booking booking) async {
...
WidgetsBinding.instance.addPostFrameCallback((_){
Navigator.push( //👈 add your navigation here
context, //error here context undefined
MaterialPageRoute(builder:
(context) => ConversationPage(conversation: conversation,),
),
);
...
}
This method help you to navigate the route without FutureBuilder. see the code
onPressed: () async {
// then await the future You want to complete and then use `.then()`
//method to implement the code that you want to implement when the future is completed
await //call your future widget //
.then((result) {
print('future completed');
// Navigate here
// For errors use onError to show or check the errors.
}).onError((error, stackTrace) {
print(error);
});
}