Multiple navigators in Flutter causes problem with pop - flutter

I'm struggling in a Flutter situation with multiple navigators. Concept here is a page which triggers a modal with his own flow of multiple pages. Inside the modal everything is going swiftly (navigation pushes/pops are working), but if the modal is dismissed it removes every page of the lowest navigator. I've looked at the example of https://stackoverflow.com/a/51589338, but I'm probably missing something here.
There's a wrapper Widget inside a page which is the root of the application.
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Padding(
padding: EdgeInsets.only(top: 10, bottom: 20),
child: FlatButton(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Open modal',
),
],
),
),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
settings: RouteSettings(name: '/modal'),
builder: (context) => Modal(),
),
);
},
),
),
);
}
}
The modal initiates a Scaffold with his own navigator to handle its own flow.
class Modal extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: new Navigator(
initialRoute: FirstModalPage.route().toString(),
onGenerateRoute: (routeSettings) {
final path = routeSettings.name;
if (path == '/secondmodalpage') {
return MaterialPageRoute(
settings: routeSettings,
builder: (_) => SecondModalPage(),
);
}
return new MaterialPageRoute(
settings: routeSettings,
builder: (_) => FirstModalPage(),
);
},
),
);
}
}
The pages inside the modal are below with a close button which should remove the modal from the root navigator. But for some reason if I call pop() it removes all pages from the widget tree. And popUntil((route) => route.isFirst) doesn't do anything at all. If I'm looking at the widget via the Widget Inspector it does say that the Wrapper and Modal are on the same level.
class FirstModalPage extends StatelessWidget {
static Route route() {
return MaterialPageRoute<void>(builder: (_) => FirstModalPage());
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Padding(
padding: EdgeInsets.only(
top: 30,
right: 20,
bottom: 10,
left: 20,
),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
},
child: Text('Back'),
),
Text(
'First modal page title'
),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
Navigator.of(context, rootNavigator: true).popUntil(
(route) => route.isFirst,
);
},
child: Text('Close'),
),
],
),
),
FlatButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => SecondModalPage()),
);
},
child: Text(
'Second modal page'
),
),
],
),
);
}
}

You'd need to use a Navigator key to keep tab on which Navigator you need to do an action with.
final _mainNavigatorKey = GlobalKey<NavigatorState>();
Navigator(
key: _mainNavigatorKey,
...
),
To use the Navigator Key, you can call _mainNavigatorKey.currentState.pop();

Related

Bloc provider above OverlayEntry flutter

I am having some problems with my flutter app. I am trying to add an overlay like this in the photo below:
And it works just fine, I am able to open it on long press and close it on tap everywhere else on the screen.
The problem is that those two buttons - delete and edit - should call a bloc method that then do all the logic, but I do not have a bloc provider above the OverlayEntry. This is the error:
Error: Could not find the correct Provider<BrowseBloc> above this _OverlayEntryWidget Widget
This happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:
- You added a new provider in your `main.dart` and performed a hot-reload.
To fix, perform a hot-restart.
- The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
- You used a `BuildContext` that is an ancestor of the provider you are trying to read.
Make sure that _OverlayEntryWidget is under your MultiProvider/Provider<BrowseBloc>.
This usually happens when you are creating a provider and trying to read it immediately.
For example, instead of:
```
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// Will throw a ProviderNotFoundError, because `context` is associated
// to the widget that is the parent of `Provider<Example>`
child: Text(context.watch<Example>().toString()),
);
}
```
consider using `builder` like so:
```
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// we use `builder` to obtain a new `BuildContext` that has access to the provider
builder: (context, child) {
// No longer throws
return Text(context.watch<Example>().toString());
}
);
}
```
If none of these solutions work, consider asking for help on StackOverflow:
https://stackoverflow.com/questions/tagged/flutter
I've already encountered this error but this time I'm in a bit of trouble because I'm working with an overlay and not a widget.
This is my code:
late OverlayEntry _popupDialog;
class ExpenseCard extends StatelessWidget {
const ExpenseCard({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocConsumer<AppBloc, AppState>(
listener: (context, state) {},
buildWhen: (previous, current) => previous.theme != current.theme,
builder: (context, state) {
return Column(
children: [
GestureDetector(
onLongPress: () {
_popupDialog = _createOverlay(expense);
Overlay.of(context)?.insert(_popupDialog);
},
child: Card(
...some widgets
),
),
const Divider(height: 0),
],
);
},
);
}
}
OverlayEntry _createOverlay(Expenses e) {
return OverlayEntry(
builder: (context) => GestureDetector(
onTap: () => _popupDialog.remove(),
child: AnimatedDialog(
child: _createPopupContent(context, e),
),
),
);
}
Widget _createPopupContent(BuildContext context, Expenses e) {
return GestureDetector(
onTap: () {},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: MediaQuery.of(context).size.width * 0.9,
decoration: BoxDecoration(
color: LocalCache.getActiveTheme() == ThemeMode.dark ? darkColorScheme.surface : lightColorScheme.surface,
borderRadius: const BorderRadius.all(Radius.circular(16)),
),
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...some other widgets
],
),
),
SizedBox(
width: 256,
child: Card(
child: Column(
children: [
InkWell(
onTap: () {
_popupDialog.remove();
// This is where the error is been thrown
context.read<BrowseBloc>().add(SetTransactionToEdit(e));
showBottomModalSheet(
context,
dateExpense: e.dateExpense,
total: e.total,
transactionToEdit: e,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
children: [Text(AppLocalizations.of(context).edit), const Spacer(), const Icon(Icons.edit)],
),
),
),
const Divider(height: 0),
InkWell(
onTap: () {
_popupDialog.remove();
// This is where the error is been thrown
context.read<BrowseBloc>().add(DeleteExpense(e.id!, e.isExpense));
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
children: [Text(AppLocalizations.of(context).delete), const Spacer(), const Icon(Unicons.delete)],
),
),
),
],
),
),
),
],
),
);
}
How can I add the bloc provider above my OverlayEntry? Is this the best course of action?
Thank you to everyone that can help!
Wrap your widget that you use in OverlayEntry in BlocProvider.value constructor and pass the needed bloc as an argument to it, like so
OverlayEntry _createOverlay(Expenses e, ExampleBloc exampleBloc) {
return OverlayEntry(
builder: (context) => GestureDetector(
onTap: () => _popupDialog.remove(),
child: BlocProvider<ExampleBloc>.value(
value: exampleBloc,
child: AnimatedDialog(
child: _createPopupContent(context, e),
),
),
),
);
}
I have found a solution starting from the answer of Olga P, but changing one thing. I use the BlocProvider.value but I am passing as an argument to the method the context and not the bloc itself. This is the code:
OverlayEntry _createOverlay(Expenses e, BuildContext context) {
return OverlayEntry(
builder: (_) => GestureDetector(
onTap: () => _popupDialog.remove(),
child: BlocProvider<BrowseBloc>.value(
value: BlocProvider.of(context),
child: AnimatedDialog(
child: _createPopupContent(context, e),
),
),
),
);
}
With this change the two methods - edit and delete - work perfectly. Thanks to everyone who replied, I learned something today too!
The problem is that you are using a function and not a widget. So you can either modify _createOverlay to be stateless or stateful widget, or you can pass the bloc as an argument to the function.
In the latter case this would be _createOverlay(expense, context.read<AppBloc>())

Navigator.pop seems to be triggered on orientation change

I have a page where the camera starts and I can record a video. The problem is when I rotate the camera. Then I end up in the main page. This also happens if I navigate to other pages and the change orientation.
Before adding the camera feature I enforced portrait mode so I did not discover this behaviour.
How can I avoid Navigator.pop when the orientation changes?
I am btw a rookie in Flutter.
EDIT: I have tried some different approaches, like adding WillPopScope. However, that is not triggered when rotating the camera.
It does work to navigate to the camera view using this approach:
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => CameraView()),
(Route<dynamic> route) => false,
);
But then there is no back button...
How can this be so complicated?
This is how I add navigation in the HomePage:
Widget startResultButton() {
return Padding(
padding: const EdgeInsets.all(5),
child: FractionallySizedBox(
widthFactor: _buttonWidthFactor,
heightFactor: _buttonHeightFactor,
child: TextButton(
style: getStyle(buttonColor),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return const ResultsMenuView();
},
),
).then((value) => _updateDisplay());
},
child: Row(
children: [
Flexible(child: getSvg("prev_sessions.svg")),
const Flexible(
child: Text(
"Analyse results",
textScaleFactor: textFactor,
)),
],
mainAxisAlignment: MainAxisAlignment.start,
),
),
),
);
}
And the ResultMainView looks like:
class ResultsMenuView extends StatelessWidget {
const ResultsMenuView({Key? key}) : super(key: key);
final double _buttonWidth = 0.9;
final double _heightFactor = 0.6;
Widget getBody() {
var sessionChart = Flexible(
child: NavigationButton(
"Session Overview",
buttonColor,
const SessionOverviewChart(),
_buttonWidth,
_heightFactor,
"overall_results.svg"),
);
var detailedChart = Flexible(
child: NavigationButton(
"Detailed Scores",
buttonColor,
const DetailedChartView(),
_buttonWidth,
_heightFactor,
"detailed_results.svg"),
);
var tableView = Flexible(
child: NavigationButton("History", buttonColor, const TableView(),
_buttonWidth, _heightFactor, "prev_sessions.svg"),
);
var trendCurveButton = Flexible(
child: NavigationButton(
"Trend Curve",
buttonColor,
const TrendCurveView(),
_buttonWidth,
_heightFactor,
"trend_curve.svg"),
);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [sessionChart, detailedChart, trendCurveButton, tableView],
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: buttonColor,
title: const Text("Results Menu"),
),
body: SafeArea(
child: getBody(),
),
backgroundColor: backgroundColor,
);
}
}

How to make animation like i attach a video

I want this type of bottom menu in flutter. I scroll anywhere on screen bottom menu will appear. And when I scroll vertically down it will disappear and on slide up it will appear
For that you can use showModalBottomSheet for this type of widgets.
Sample code:
class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
child: const Text('showModalBottomSheet'),
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return Container(
height: 200,
color: Colors.amber,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('Modal BottomSheet'),
ElevatedButton(
child: const Text('Close BottomSheet'),
onPressed: () => Navigator.pop(context),
)
],
),
),
);
},
);
},
),
);
}
}
Here are the good package which is useful to get pages as you want:
modal_bottom_sheet
How you can use it:
showMaterialModalBottomSheet(
context: context,
builder: (context) => Container(),
)
For more attributes, you can visit documentation here
Example Link : Modal Bottom Sheet

Navigator.of(context).pop(); makes the whole screen disappear, not the popup

In my flutter app I want to show the popup with two buttons when user presses a button, I'm doing it with the following code:
class ProfileScreen extends StatefulWidget {
#override
_ProfileScreenState createState() {
return _ProfileScreenState();
}
}
class _ProfileScreenState extends State<ProfileScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 400),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
...[
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: ElevatedButton(
onPressed: () {
showAlertDialog(context);
},
child: Text('Remove account'),
),
),
and the code for showAlertDialog is as follows:
showAlertDialog(BuildContext context) {
// set up the buttons
Widget cancelButton = FlatButton(
child: Text("Cancel"),
onPressed: () {
Navigator.of(context).pop();
},
);
Widget continueButton = FlatButton(
child: Text("Continue"),
onPressed: () {},
);
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text("AlertDialog"),
content: Text("Would you like to continue learning how to use Flutter alerts?"),
actions: [
cancelButton,
continueButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
It works, the popup shows correctly, but when I click cancel, popup stays up front, but the screen beneath it goes away (and it stays black). Why so? And how could I fix it? Thanks!
Navigator.of(context, rootNavigator: true).pop();

Use nested Navigator with WillPopScope in Flutter

In my application, I want to have a custom Navigation (only change part of the screen and keep an history of what I'm doing inside it).
For that purpose, I am using a Navigator and it's working fine for simple navigation.
However, I want to handle the back button of Android.
There is a problem with it in Flutter apparently which forces me to handle the backbutton in the parent widget of the Navigator :
https://github.com/flutter/flutter/issues/14083
Due to that, I need to retrieve the instance of my Navigator in the children and call pop() on it. I am trying to use a GlobalKey for this.
I am trying to make it work for a while now and made a sample project just to test this.
Here is my code :
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(title: 'Navigation Basics', home: MainWidget()));
}
class MainWidget extends StatelessWidget {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
#override
Widget build(BuildContext context) {
return SafeArea(
child: WillPopScope(
onWillPop: () => navigatorKey.currentState.maybePop(),
child: Scaffold(
body: Padding(
child: Column(
children: <Widget>[
Text("Toto"),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: RaisedButton(
child: Text('First'),
onPressed: () {
navigatorKey.currentState.pushNamed('/first');
// Navigator.push(
// context,
// MaterialPageRoute(builder: (context) => SecondRoute()),
// );
},
)),
Expanded(
child: RaisedButton(
child: Text('Second'),
onPressed: () {
navigatorKey.currentState.pushNamed('/second');
},
))
],
),
Expanded(
child: Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(color: Colors.red),
),
ConstrainedBox(
constraints: BoxConstraints.expand(),
child: _getNavigator()),
],
)),
],
),
padding: EdgeInsets.only(bottom: 50),
))));
}
Navigator _getNavigator() {
return Navigator(
key: navigatorKey,
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
switch (settings.name) {
case '/':
builder = (BuildContext _) => FirstRoute();
break;
case '/second':
builder = (BuildContext _) => SecondRoute();
break;
default:
throw new Exception('Invalid route: ${settings.name}');
}
return new MaterialPageRoute(builder: builder, settings: settings);
});
}
}
class FirstRoute extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
RaisedButton(
child: Text("GO TO FRAGMENT TWO"),
onPressed: () => Navigator.of(context).pushNamed("/second"),
)
],
),
decoration: BoxDecoration(color: Colors.green),
);
}
}
class SecondRoute extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
RaisedButton(
child: Text("GO TO FRAGMENT ONE"),
onPressed: () => Navigator.of(context).pop(),
)
],
),
decoration: BoxDecoration(color: Colors.blue),
);
}
}
This is however not working as I would like. The default Navigator seem to still be used : after opening the SecondRoute and pressing the Android back button, it just leaves the app instead of just going back to the first route.
How can I achieve what I want?
Following the documentation of onWillPop:
/// Called to veto attempts by the user to dismiss the enclosing [ModalRoute].
///
/// If the callback returns a Future that resolves to false, the enclosing
/// route will not be popped.
final WillPopCallback onWillPop;
your handler should indicate that the enclosing route should not be closed, hence returning false will resolve your issue.
changing your handler to this works:
onWillPop: () async {
navigatorKey.currentState.maybePop();
return false;
},