I'm trying to show screens using auto_route, based on state kept using bloc. I have two objectives:
navigate when button is clicked (e.g. login)
if at any point, the state changes, again navigate to the correct screen (e.g. session expiry)
for usecase 1 I have a button:
child: ElevatedButton(
onPressed: () {
final sessionBloc = BlocProvider.of<SessionBloc>(context);
sessionBloc.add(SignInEvent());
},
child: const Text('Login'),
),
this delegates to my session bloc that has a handler that is defined as:
emit(
SignedIn(
const User(
displayName: 'displayName',
phoneNumber: 'phoneNumber',
idNumber: 'idNumber',
photoUrl: 'photoUrl'),
),
);
and I'm trying to use an authguard defined as:
class AuthGuard extends AutoRouteGuard {
AuthGuard({required Stream<SessionState> sessionBloc}) {
sessionBloc.listen((state) {
print('['+state.toString()+']');
_authenticated = state is SignedIn;
});
}
bool _authenticated = true;
#override
void onNavigation(NavigationResolver resolver, StackRouter router) {
if (!_authenticated) {
router.push(
LoginRouter(),
);
}
resolver.next(true);
}
}
I can see the print statement everytime I press the button, so that works fine (no surprise).
The problem is that I don't get any navigation to another screeen when pressing the button. I can only assume this is because although I'm updating the state and the authguard is aware of the new state, it does nothing till we actually try navigating i.e. onNavigate is called.
So how do I do that, how/where do I trigger navigation?
I would use either BlocConsumer or BlocListner and inside the listen method do the navigation to the desired screen. And make sure there is a route guard on the route you want to navigate to after authentication.
BlocListener<SessionBloc, SessionState>(
listener: (context, state) {
if(state is SignedIn){
//TODO: route to the page you want to go to
}
},
child: ElevatedButton(
onPressed: () {
final sessionBloc = BlocProvider.of<SessionBloc>(context);
sessionBloc.add(SignInEvent());
},
child: const Text('Login'),
),
);
Related
I'm currently trying to figure out how to prevent a dialog from closing directly by the back button.
For Dialogs, I have a base class, which I give a Widget with content.
Now I have a Screen, where the user gets to enter something in a dialog, and when the user presses the cancel button, a second Dialog pops up on top of the existing dialog.
With this second dialog, which also uses the base class, I can confirm or abort closing the previous dialog.
But when I use the back button of my phone, the dialog instantly closes, without asking to confirm.
Already tried wrapping the Dialog in the base class with WillPopScope, but that only resulted in the Dialog not closing anymore (maybe I did something wrong there, but I don't have the code of that try anymore)
Here is the code of the base class (I shortened it a bit by removing styling etc.):
class DialogBase {
final bool isDismissable;
final Function()? doAfterwards;
final Function(bool)? onConfirmClose;
DialogBase(
{required this.isDismissable, this.doAfterwards, this.onConfirmClose});
show(BuildContext context, Widget content) async {
await showDialog(
barrierDismissible: isDismissable,
context: context,
builder: (context) {
return Dialog(
child: Padding(
padding: const EdgeInsets.all(20),
child: content,
),
);
}).then((value) {
if (onConfirmClose != null && value != null) {
onConfirmClose!(value);
}
}).whenComplete(() async {
if (doAfterwards != null) {
doAfterwards!();
}
});
}
}
In a Dialog Content widget, the abort button does the following onPressed:
onPressed: () async {
await DialogBase(
isDismissable: false,
onConfirmClose: (bool result) {
if (result) {
Navigator.of(context).pop();
}
}).show(context, const ConfirmAbortDialogContent());
},
And the dialog itself gets called this way (which is also called on a button pressed:
void openAddNewPostDialog(BuildContext context) {
DialogBase(
isDismissable: false).show(context, const AddThreadPostDialogContent());
}
Any way on how to prevent a dialog from closing when the back button is used (and instead ask user to confirm)?
Try WillPopScope
Using onWillPop value I think you can achieve what you want
I would like to use Flutter’s (2.0) navigation for routing on my mobile app. I cannot find cookbook examples and followed the recommended guide, implementing the nested router example.
NOTE ===
If a rendered view has a unique route (uri) within the app I call it a Page.
If it does not have a unique route, I call it a Screen.
========
The base router selects between pages in the app. The nested router in the “Resource Dashboard” uses the resourceViewState object to select the screen to render within the Resource Dashboard” Page. Just by using the selectedIndex as below, I can change the screen depending on which index the user has selected in a Material Design Drawer.
As a result, when the user is on any non-default screen (i.e. Details A, Details B) in the above diagram, and there is a pop event, the user is returned to the default screen. If the user pops from the default screen, they are returned to the “Select Resource” Page outside of the Nested Router. But I have one more sticky case to handle (or maybe I am not handling these cases well :))
#override
Widget build(BuildContext context) {
int selectedIndex = resourceViewState.selectedIndex;
return Navigator(
key: navigatorKey,
pages: [
MaterialPage(
key: ValueKey(DEFAULT_PAGE),
child: Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(‘DEFAULT’),
),
drawer: ResourceDrawer(
resourceViewState: resourceViewState,
navKey: navigatorKey,
),
body: DefaultPage(),
),
),
if (selectedIndex != DEFAULT_PAGE) ...[
MaterialPage(
key: ValueKey(selectedIndex),
child: Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(_getTitle(selectedIndex)),
),
drawer: ResourceDrawer(
deviceState: deviceState,
navKey: navigatorKey,
),
body: _getScreen(selectedIndex),
),
),
],
],
onPopPage: (route, result) {
if (result == "return") {
print("return ");
return route.didPop(true);
}
resourceViewState.selectedIndex = DEFAULT_PAGE;
notifyListeners();
return route.didPop(false);
},
);
}
I want to navigate the user from an Alert Dialog to the “Select Resource” page when a button is clicked in the dialog. To do so I must (see 1, 2, 3 in diagram).
3 seems to be handled by the base router if the user pops from the default screen, but I also need to do so from an Alert Dialog.
Pop the Alert Dialog
Pop the Material Design Drawer
Pop from the Nested Router to the Base Router.
This has the effect shown by the red arrow in the diagram.
I can simply use Navigator.of(context).pop() for the first 2. I am pretty sure that the Navigator used here is different from that used by the Nested Router (would love some details here). I believe this is the case, because onPopPage is not called for the NestedRouter on these events.
For 3) I have tried this strategy:
a) Call navKey.currentState!.pop(“disconnected”) from the Navigation Drawer. I pass in the navKey of the Nested Router as shown in the code above.
b) Now the onPopPage listener registered with the nested router receives the result from this pop event.
onPopPage: (route, result) {
if (result == "return") {
print("return ");
return route.didPop(true);
}
resourceViewState.selectedIndex = DEFAULT_PAGE;
notifyListeners();
return route.didPop(false);
},
When I see result == "return" then I should navigate from the “Resource Dashboard” Page to the “Select Resource” Page. But I am not sure how to do it nor if it is even a good strategy to use different views on the same route (tangent).
This is a working solution. I pass a reference to the parent navigator key, then call pop from it. I would prefer for it to be entirely declarative but I am not sure how to implement with the nested navigator pattern.
class ResourcePage extends StatefulWidget {
ResourcePage({
required this.resourceViewState,
required this.navigatorKey,
});
final ResourceViewState resourceViewState;
final GlobalKey<NavigatorState> navigatorKey;
#override
_ResourcePageState createState() => _ResourcePageState();
}
class _ResourcePageState extends State<DevicePage> {
late DeviceDelegate _routerDelegate;
late ChildBackButtonDispatcher _backButtonDispatcher;
#override
void didChangeDependencies() {
super.didChangeDependencies();
// Defer back button dispatching to the child router
_routerDelegate = InnerRouterDelegate(parentNavigatorKey: widget.navigatorKey, resourceViewState: widget.resourceViewState);
_backButtonDispatcher = Router.of(context).backButtonDispatcher!.createChildBackButtonDispatcher();
_backButtonDispatcher.takePriority();
}
#override
void didUpdateWidget(covariant DevicePage oldWidget) {
super.didUpdateWidget(oldWidget);
_routerDelegate.state = widget.resourceViewState;
}
#override
Widget build(BuildContext context) {
return Router(
routerDelegate: _routerDelegate,
backButtonDispatcher: _backButtonDispatcher,
);
}
}
// InnerRouterDelegate Snippets
// Constructor
InnerRouterDelegate({
required this.resourceViewState,
required this.parentNavigatorKey,
}) {
resourceViewState.addListener(notifyListeners); // See Nested Navigation Example
}
// On Pop Page
onPopPage: (route, result) {
if (result == "return") {
parentNavigatorKey.currentState!.pop();
return route.didPop(true);
}
resourceViewState.selectedIndex = DEFAULT_PAGE;
notifyListeners();
return route.didPop(false);
},
Original Answer
I'm using the Getx State Management on Flutter.
Simplifying as much as possible:
I build a GetxController to control my Page, and in this controller i have a StatefulWidget instance that evoque http requests.
class MyController extends GetxController {
Player player;
}
class Player extends StatefulWidget {
PlayerState state;
#override
PlayerState createState() {
state = PlayerState();
return state;
}
}
class PlayerState extends State<Player> {
void methodName async() {
futureRequest().then((data) {
// when the error ocurrs
setState(() {});
});
}
}
The problem occurs when the user closes the mobile page, triggering the controller's close method, before the end of the request.
That way, when setState is triggered, there is no more page instance and the error occurs.
I believe that the solution would be to interrupt all requests related to this GetxController and "delete" this instance of StatefulWidget at the moment the controller close method was called.
I don't know if this would be right, and if it's how to do it ..
==================================================================
Updated Answer
The main problem was that the async request in getDetails() method, return a response even after the controller is disposed, even using GetBuilder, and this response carried a url from a video that is started by the videoPlayerController (a video_player plugin instance).
So, the user is in another screen but keep listen to the video that is playing on background.
As a workaround and thinking in apply good practices to the code, i make a refactor to use only stateless widgets, following the GetX rules. I solved the problem, but i had to convert the Future's to Stream's
The binding is being created with Get.lazyPut() to perform dependencies injection:
class Binding implements Bindings {
Get.lazyPut<PlayerController>(() {
return PlayerController(videoRepository: VideoRepository(VideoProvider(Dio())));
});
}
This binding is linked to the page router, based on GetX documentation.
class AppPages {
static final routes = [
GetPage(name: Routes.MyRoute, page: () => MyPage(), binding: MyBinding()),
];
}
To prevent the controller to make actions even before it is disposed, i have to created a Stream and cancel it on controller dispose.
class MyController extends GetxController {
MyController({#required this.repository}) : assert(repository != null);
StreamSubscription<bool> stream;
// Instance of plugin video_player
VideoPlayerController videoPlayerController;
#override
void onClose() {
if (streamGetVideo != null) streamGetVideo.cancel();
super.onClose();
if (videoPlayerController != null) videoPlayerController?.dispose();
}
// This is the method called by the user on screen
void loadVideo() {
stream = getDetails().asStream().listen((bool response) {
// This code is canceled on onClose() method by the stream
if (response) update();
});
}
Future<bool> getDetails() async {
return await repository.getDetails().then((data) async {
videoPlayerController = VideoPlayerController.network(data);
initFuture = videoPlayerController.initialize();
await initFuture.whenComplete(() { return true; });
});
}
}
I think that Flutter/GetX should have a better way to do this, without these workarounds that i made. If anyone has a better approach or a hint, i'm open to suggestions.
One solution could be to wrap your setState with
if(mounted){
setState(() {});
}
GetBuilder + update()
In GetX using a GetBuilder with update() takes care of that lifecycle checking / handling so you don't have to do it.
Below is an example of a screen/route being closed prior to an HTTP call finishing & calling setState(), without an exception thrown.
(On the 2nd screen, click the Go Back! button fast to simulate an already disposed StatefulWidget.)
Below, an update() call is used to update the screen, instead of setState(), but they are the same in a GetBuilder. GetBuilder is (extends) a StatefulWidget.
GetBuilder adds listeners to the Controller you pass it, either through init: constructor arg or via the GetBuilder<Type> parameter if the Controller was initialized elsewhere/earlier.
That listener will be disposed if the StatefulWidget (i.e. GetBuilder) is disposed.
(See GetBuilder's dispose() function for some wizardry. While adding a listener, the returned value from adding that listener, is a function to dispose/unsubscribe from that listen. Pretty clever.)
So the GetBuilder/StatefulWidget will never have its update() / setState() called if that widget has been disposed because the listener for those calls has been disposed. So a slow returning HTTP call won't attempt to update/setState a widget that no longer exists in the widget tree.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class HttpX extends GetxController {
String slowValue = 'loading...';
#override
void onInit() {
slowCall();
}
/// Simulate a slow, long running HTTP call
Future<void> slowCall() async {
slowValue = 'Slow call STARTED!';
print(slowValue);
update(); // update the screen to show started message
await Future.delayed(Duration(seconds: 5), () {
slowValue = 'Slow call FINISHED!';
print(slowValue);
update(); // won't call setState() if GetBuilder is disposed
});
}
}
class GetXDisposePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('awaiting http call to finish'),
RaisedButton(
child: Text('Go Call Page'),
onPressed: () => Get.to(SlowCallPage()),
// using Get.to ↑ requires GetMaterialApp in place of MaterialApp in MyApp
)
],
),
),
);
}
}
class SlowCallPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Dispose - Go Back!'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GetBuilder<HttpX>(
init: HttpX(), // fake slow http call starts on init
builder: (hx) => Text(hx.slowValue),
),
RaisedButton(
child: Text('Go Back!'),
onPressed: () => Get.back(),
),
],
),
),
);
}
}
I'm trying to create a modal bottom sheet using showModalBottomSheet, which wil display a form to register a todo item. The idea is that once the todo item is registered, I want to display a check icon from some seconds and then automatically close the sheet.
here is the snippet:
FloatingActionButton _floatingActionButton(BuildContext context) {
return FloatingActionButton(
child: Icon(Icons.add),
onPressed: () async {
await _showBottomSheet(
context: context,
content: CreateTodoForm(
onClose: () {
...
Navigator.pop(context);
},
),
);
},
);
}
and inside the CreateTodoForm widget:
class _CreateTodoFormState extends State<CreateTodoForm> {
TextEditingController titleController = TextEditingController();
bool completed = false;
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => getIt<TodoFormBloc>(),
child: BlocBuilder<TodoFormBloc, ITodoFormState>(
builder: (context, state) {
...
if (state is SubmittedTodo) {
Future.delayed(Duration(seconds: 2), widget.onClose);
return Container(
height: 127,
child: Icon(Icons.check, size: 50, color: Colors.white),
);
}
...
},
),
);
}
Has you can see, when the state is SubmittedTodo (todo was submitted successfully) I return a container with the check icon, and after 2 seconds I call the onClose Function which is a call to Navigator.pop(context) to close the sheet.
This works great but it has a problem... if the user taps the < button on the device, or swipe the sheet down to dismiss it, before the 2 seconds are completed, then the sheet closes due to the user action, and then the future completes and it basically closes the app (the app get full black screen).
So my quiestion is how can I close the sheet automatically after some time safely without worring about what the user does.
Probably this is happening because of Navigator.pop(context); getting called after you click the back button which cause two pop. and the black screen is shown because there is no other screen to navigate back to.
As a solution i propose wrapping your form widget by WillPopScope and then you will get notified that the user clicked on the back button. here you can close your form by calling onClose
I have a page called appointments When i click on appointments(i pass the reports to the next page) the reports are displayed on the reports page,When i add a new report to the page and when i clicked the device back button and again go the reports page the updated files are not seen .I again have to load the entire page then i'm able to view the reports.
I'm loading the appointments in the initState() of the page into FutureBuilder.
Can Anyone suggest me how to refresh the data when i click my back button from the device
You can do like this:
In your report page, when button is clicked, open Add Report Page. When the back button is pressed, it will call the _refreshData method.
Navigator.pushNamed(context, AddReportPage.ROUTE).then((onValue) {
_refreshData()
});
The _refreshData is used to load the entire page, which you use in initState.
To open new page you should use next
void _openButtonHandler() {
Navigator.of(context).pushNamed("route123").then((result) {
if (result != null) {
refreshCall();
}
}).catchError((error) {});
}
The back button method should be like this:
Navigator.pop(context, {"param1": "value1"});
You need to use RefreshIndicator for this scenario.Your refresh indicator widget will be the parent of AppointmentsPage.We will assign a key so that we can call it from anywhere and when reports page is closed we will refresh page using this key.I have created a small example along with comments to demonstrate you how it will be done.
class AppointmentsPage extends StatefulWidget {
#override
_AppointmentsPageState createState() => _AppointmentsPageState();
}
class _AppointmentsPageState extends State<AppointmentsPage> {
Future appointments;
var refreshKey = GlobalKey<RefreshIndicatorState>();
#override
void initState() {
super.initState();
//Your method to fetch appointments.
getAppointments();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: RefreshIndicator(
key: refreshKey,
color: Colors.black,
onRefresh: () async {
//Call appointments future and fetch appointments.When they are fetched
//call setState to refresh page
getAppointments();
await appointments;
setState(() {});
},
child: Column(
children: <Widget>[
FutureBuilder(),
RaisedButton(
child: Text("Go To Report"),
onPressed: () async {
//Use await and then navigate to report page
await Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ReportPage(report: report));
);
//After popped back from report page call refresh indicator to refresh page
refreshKey.currentState.show();
},
)
],
),
),
);
}
}