So basically I am using the showModalBottomSheet widget to show a full screen container that has a GestureDetector that runs this onTap:
onTap: () {
final String testText = "Sup";
Navigator.of(context).pop(testText);
}
This obviously returns the text when I await the result when I call showModalBottomSheet however, I would also like to set enableDrag: true so that we can swipe the modal away.
My question is:
How can I pass an argument/result back when doing a swipe to dismiss? With a function, I can simple do Navigator.of(context).pop(...) but when we swipe, there is no function and so therefore I can't figure out a way to pass arguments when we swipe to dismiss.
Thank you!
When you swipe the pop() method gets called and there isn't anyway to override it but I figured out a way to handle this scenario:
You can use then() method on showModalBottomSheet() like this:
showModalBottomSheet(context: context, builder: (context) => SecondPage()).then((value) {
str = "done";
print("data: $str");
});
Keep in mind that the value that future returns the value that gets returned in pop() method otherwise it is null.
It looks like showModalBottomSheet doesn't have a way to specify the close value. So it always returns null in that case. And there is not much you can do. But the options I see:
use result wrapper to return value by reference. Like that:
class MyResult {
int myValue;
}
class MyBottomWidget ... {
MyResult result;
MyBottomWidget(this.result);
// then you can initialize the value somewhere
// result.myValue = 5;
}
final result = MyResult();
await showModalBottomSheet(context: context, builder: (_) => MyBottomWidget(result);
// and here you can use your value
print('Result value: ${result.myValue});
another way is to return a value if the result of showModalBottomSheet is null which means a modal has been closed / dissmissed.
final result = await showModalBottomSheet(...);
if (result == null) {
// initialize the value with a value you need when modal is closed.
}
You can make a func wrapper to simplify the process:
Future<T> myShowModalBottomSheet<T>(BuildContext context, WidgetBuilder builder, T dismissValue) async {
final value = await showModalBottomSheet<T>(context: context, builder: builder);
return value ?? dismissValue;
}
or like that:
Future<T> myShowModalBottomSheet<T>(BuildContext context, WidgetBuilder builder, T Function() dismissedValueBuilder) async {
final value = await showModalBottomSheet<T>(context: context, builder: builder);
return value ?? dismissedValueBuilder();
}
the other way is to make your own shoModalBottomSheet that will allow to the specified value. The source code of the function is available so it's not that difficult to implement it. It would be the cleanest solution, but still, it has some downsides as well. First is it's much more to do. The other thing is your solution will be not in sync with the native flutter function. I.e. if flutter will change the behavior of that function or widgets you will need to update your code.
search on pub.dev for a package that will have the functionality you need.
Maybe there is some other way, but I'm not aware of it).
Use this widget and add Navigator. Pop.
->Use this WillPopScope
For More Information Visit Click Here
OR
If you want to pass data in pop. Then see Example:-
import 'package:flutter/material.dart';
void main() {
runApp(
const MaterialApp(
title: 'Returning Data',
home: HomeScreen(),
),
);
}
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Returning Data Demo'),
),
body: const Center(
child: SelectionButton(),
),
);
}
}
class SelectionButton extends StatelessWidget {
const SelectionButton({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
_navigateAndDisplaySelection(context);
},
child: const Text('Pick an option, any option!'),
);
}
// A method that launches the SelectionScreen and awaits the result from
// Navigator.pop.
void _navigateAndDisplaySelection(BuildContext context) async {
// Navigator.push returns a Future that completes after calling
// Navigator.pop on the Selection Screen.
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SelectionScreen()),
);
// After the Selection Screen returns a result, hide any previous snackbars
// and show the new result.
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text('$result')));
}
}
class SelectionScreen extends StatelessWidget {
const SelectionScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Pick an option'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () {
// Close the screen and return "Yep!" as the result.
Navigator.pop(context, 'Yep!');
},
child: const Text('Yep!'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ElevatedButton(
onPressed: () {
// Close the screen and return "Nope." as the result.
Navigator.pop(context, 'Nope.');
},
child: const Text('Nope.'),
),
)
],
),
),
);
}
}
Selection Screen:-
Navigator.pop(context, 'Yep!');
Button Code (Navigator Push Like this):-
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SelectionScreen()),
For More Information
Related
I want to call Navigator.of(context).pop() one or several times and then run a callback after navigation has completed, but I have struggled to find a neat solution. I've put together an example app to illustrate the problem I'm having:
Screens A, B, and C all access a nullable value on the Model Provider
ScreenA can set value to a non-null value
ScreenB requires value to be non-null to build
ScreenC can set value to null and pop you back to ScreenA
When you press the button on ScreenC to go back to ScreenA, it navigates successfully (the app doesn't crash) but you throw an Error because it tries to build ScreenB after the first pop.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => Model(),
child: MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const ScreenA(),
),
);
}
}
class Model extends ChangeNotifier {
int? value = 0;
Future<void> updateValue(int? newValue) async {
await Future.delayed(const Duration(milliseconds: 30));
value = newValue;
notifyListeners();
}
}
class ScreenA extends StatelessWidget {
const ScreenA({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: centredScreenContent(
[
Text('ScreenA - value: ${context.watch<Model>().value}'),
ElevatedButton(
child: const Text('Set value'),
onPressed: () => context.read<Model>().updateValue(Random().nextInt(100)),
),
ElevatedButton(
child: const Text('Go to B'),
onPressed: () async => await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => ScreenB(
nonNullValue: context.watch<Model>().value ?? (throw Error()),
),
),
),
),
],
),
);
}
}
class ScreenB extends StatelessWidget {
const ScreenB({Key? key, required this.nonNullValue}) : super(key: key);
final int nonNullValue;
#override
Widget build(BuildContext context) {
return Scaffold(
body: centredScreenContent(
[
Text('ScreenB - value: $nonNullValue'),
ElevatedButton(
child: const Text('Set value'),
onPressed: () => context.read<Model>().updateValue(Random().nextInt(100)),
),
ElevatedButton(
child: const Text('Go to C'),
onPressed: () async => await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => const ScreenC(),
),
),
),
],
),
);
}
}
class ScreenC extends StatelessWidget {
const ScreenC({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: centredScreenContent(
[
const Spacer(),
const Text('ScreenC'),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
context.read<Model>().updateValue(null);
},
child: const Text('Reset app')),
const Spacer(),
],
),
);
}
}
Widget centredScreenContent(List<Widget> widgets) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: widgets,
),
);
I've found two solutions, but neither feels neat:
Make ScreenB take a nullable value in its constructor, and have its build return something like value == null ? Container() : ActualContents(nonNullValue: value!). I don't like this though. If we know that in BAU use, ScreenB cannot be built while value == null, then we'd like to log an error if that happens in production so we can investigate the problem. We can't do this if our navigation back from ScreenC also hits this state though.
Add a sufficiently long delay to the callback so that it runs after the navigation is completed, e.g. in the example app, if you change Model.updateValue to have a 300ms delay, then it doesn't error. This also feels like an unpleasant solution, if the delay is too long we risk the app behaving sluggishly, if it's too short then we don't solve the problem at all.
I would make ScreenB(int? nullableParam) and handle the widget builder with additional assert nullableParam == null just to log the error.
But what i think the real solution you are looking for is context.read<Model>().value instead of watch - i can't think of a scenario where you want to page parameter depend on any listenable state
solution
ElevatedButton(
child: const Text('Go to B'),
onPressed: () async => await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => ScreenB(
nonNullValue: context.read<Model>().value ?? (throw Error()),
This way the page after first build will not be rebuild with null when popped.
#Edit
I see 2 problems:
passing Listenable value to Page parameter
purposely setting value to null where other part of application purposely is not handling it
The first one can be solved with the solution above
The second you have to either assure the passed value will not be null on Navigator.pop() - the solution above does that. Or handle the null value in the ScreenB widget (as you suggested with conditional build)
I implemented the alert dialog in the initstate() method but Init state is only called once. In my case I want the alert to appear automatically every time a variable value changes for exemple. ( I need it to suddenly pop up during using the app)
You could use a ValueNotifier and a ValueListenableBuilder so that every time the value in the ValueNotifier changes, the ValueListenableBuilder rebuilds and shows a dialog, like so:
class MyWidget extends StatelessWidget {
ValueNotifier<int> dialogTrigger = ValueNotifier(0);
#override
Widget build(BuildContext context) {
return Column(
children: [
TextButton(
onPressed: () {
var random = Random();
dialogTrigger.value = random.nextInt(100);
},
child: const Text('Click me and change a value')
),
Expanded(
child: ValueListenableBuilder(
valueListenable: dialogTrigger,
builder: (ctx, value, child) {
Future.delayed(const Duration(seconds: 0), () {
showDialog(
context: ctx,
builder: (ctx) {
return AlertDialog(
title: const Text('Dialog'),
content: Text('Hey! I got a $value'),
);
});
});
return const SizedBox();
})
)
]
);
}
}
(Again, I'm only adding a button to change the value, not to launch the dialog. That way you can programmatically change the value, which eventually launches the dialog). See if that works for your purposes.
I am a beginner in Flutter & Dart language and trying to figure out how to render cookie settings dialog(popup) conditionally(based on user preference) on page load. I already found some 3rd party package (sharedpreferences) to store key-value pair for the user preferences. What I want to do is to check for user preference and if not found or false (Consent not given by clicking on Deny) this popup will just keep appearing on all pages. I also want users to be able to open this cookie settings popup by clicking on a link. How can I achieve that?
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
#override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: const Center(
child: CookiesWidget(),
),
),
);
}
}
class CookiesWidget extends StatelessWidget {
const CookiesWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return TextButton(
onPressed: () => showDialog<String>(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text('Cookie Settings'),
content: const Text('This website uses cookies. Please click OK to accept.'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'Deny'),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, 'OK'),
child: const Text('OK'),
),
],
),
),
child: const Text('Show Cookie Settings'),
);
}
}
SharedPrefences + Visibility Widget would help you hide and show your cookie widget conditionally.
If you want users to open something within your app using a link, you should consider searching about deep-links.
I can't give a specific solution but resources to achieve most common cases:
A package for deep-link: uni_links
An article https://medium.com/flutter-community/deep-links-and-flutter-applications-how-to-handle-them-properly-8c9865af9283
Official Flutter guide about deep links
Shared Preferences should be the best option.
You cloud use a FutureBuilder to render (or not render) the dialog based on the Shared Preferences' data.
You should create an async function inside your class but outside your build() method:
Future<bool> cookiesAccepted() async {
var prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('cookies')) {
bool? info = prefs.getBool('cookies');
return info ?? false;
}
return false;
}
And put this inside your build() method.
FutureBuilder<bool>(
future: cookiesAccepted(),
builder: (BuildContext context, AsyncSnapshot<bool> response) {
if (response.hasData) {
if (!response.data!) { //If the cookies were not accepted
return Text("Your cookies dialog");
}
}
},
),
Remember to store data inside SharedPreferences with prefs.setBool('cookies', value);
I am building an app where I am trying to retrieve data from calling navigator.push
onTap: () async {
widget.dogData = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AddDogScreen(),
),
);
widget.setDocId(widget.dogData['docId']);
setState(() {
widget.dogData;
});
},
However, I want to link to yet another screen after AddDogScreen and pass the data by popping from that screen.
SO we would have :
Initial screen -> AddDogScreen -> Another screen (then pop back to initial screen with the data)
Then the third screen should pop back to the initial screen with the data. How can I set this up, since I want to get the data from two screens?
Thanks a lot in advance.
To do what you want to do there are two useful features in Flutter navigation:
final value = await Navigator.push() returns the value that the pushed screen returned, and you can await it to wait for the returning of the next screen.
Navigator.pop(value) allows you to return a value from a screen, while going back.
All you have to do here is play with those to do what you want.
Here is an example of the type of flow that you want:
class InitialScreen extends StatelessWidget {
onPressed(BuildContext context) async {
final List<String>? addDogScreenResult = await Navigator.of(context).push<
List<String>>(
MaterialPageRoute<List<String>>(builder: (context) => AddDogScreen()));
// Do something with the result
print("We got the values: ${addDogScreenResult}");
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => onPressed(context),
child: const Text("Click to go to AddDogScreen"))));
}
}
class AddDogScreen extends StatelessWidget {
onPressed(BuildContext context) async {
final String? anotherScreenResult = await Navigator.of(context)
.push<String>(MaterialPageRoute(builder: (context) => AnotherScreen()));
if (anotherScreenResult != null) {
const addDogScreenString = "Some value AddDogScreen generates";
// We can combine a value that we got in this screen and the return value of AnotherScreen.
final List<String> addDogScreenReturnValue = [
addDogScreenString,
anotherScreenResult
];
// We simply return the list using .pop() function
// Notice that we can type the .pop() function with the return type that we give it. It is not absolutely necessary, but it adds in clarity and safety.
Navigator.of(context).pop<List<String>>(addDogScreenReturnValue);
return;
}
Navigator.of(context).pop();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => onPressed(context),
child: const Text("Click to go to AnotherScreen"))));
}
}
class AnotherScreen extends StatelessWidget {
onPressed(BuildContext context) async {
const String anotherScreenResult = "Some value";
Navigator.of(context).pop<String>(anotherScreenResult);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => onPressed(context),
child: const Text("Click to go back to initial page"))));
}
}
Also, I see in your code widget.dogData = .... Maybe it's on purpose but if not, this is not how you should manipulate state.
State variables should be defined in the class that extends your StatefulWidget, and you should be able to read it using dogData, and update it using:
setState({
dogData = 'Something';
})
I´m trying to send data with pushName. Then i try to get this data to show in a Toast message.
PushName
Navigator.pushNamed(
context,
'/navigator',
arguments: <String, String>{
'instalation': widget.instalation,
'message': DemoLocalizations.of(context)
.text('cancel-message') +
" " +
widget.datameterValue.toString(),
},
);
Trying to retrieve data
class Navigation extends StatefulWidget {
final ConnectionPage args;
Navigation({Key key, this.message, this.instalation, this.args}) : super(key: key);
}
class _NavigationState extends State<Navigation> {
void initState() {
super.initState();
print(widget.args); //NULL
final snackBar = SnackBar(
duration: Duration(seconds: 5),
content: Text(widget.args.messsage+ '.', textAlign: TextAlign.center),
backgroundColor: Colors.red[700],
);
key.currentState.showSnackBar(snackBar);
}
}
The problem: Return null.
So: What is the right way to get data using pushName? In the documentation show how can we get data inside Scaffold but i need to get data in the initState.
UPDATE
Routes
routes: {
'/login': (context) => LoginPage(),
'/navigator': (context) => Navigation(),
'/home': (context) => HomePageScreen(),
'/connect': (context) => ConnectionPage(),
},
UPDATE 2
I try something like this
Navigator.pushNamed(
context,
'/navigator',
arguments: Navigation(
instalation: widget.instalation,
message: DemoLocalizations.of(context)
.text('cancel-message') +
" " +
widget.datameterValue.toString(),
),
);
To do this in initState You need WidgetsBinding.instance.addPostFrameCallback and ModalRoute.of(context).settings.arguments
Demo pass arguments: {'instalation': "123", "message": "456"}
You can see full code and working demo picture below
code snippet use push
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ExtractArgumentsScreen(),
// Pass the arguments as part of the RouteSettings. The
// ExtractArgumentScreen reads the arguments from these
// settings.
settings: RouteSettings(
arguments: {'instalation': "123", "message": "456"},
),
),
);
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final routeArgs1 =
ModalRoute.of(context).settings.arguments as Map<String, String>;
final instalation = routeArgs1['instalation'];
final message = routeArgs1['message'];
print('instalation ${instalation}');
print('message ${message}');
key.currentState
.showSnackBar(SnackBar(content: Text(message)));
});
}
code snippet use Navigator.pushNamed
return MaterialApp(
// Provide a function to handle named routes. Use this function to
// identify the named route being pushed, and create the correct
// Screen.
routes: {
'/extractArguments': (context) => ExtractArgumentsScreen(),
},
...
Navigator.pushNamed(
context,
ExtractArgumentsScreen.routeName,
arguments: {'instalation': "123", "message": "456"},
);
working demo
full code
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
// Provide a function to handle named routes. Use this function to
// identify the named route being pushed, and create the correct
// Screen.
routes: {
'/extractArguments': (context) => ExtractArgumentsScreen(),
},
onGenerateRoute: (settings) {
// If you push the PassArguments route
if (settings.name == PassArgumentsScreen.routeName) {
// Cast the arguments to the correct type: ScreenArguments.
final ScreenArguments args = settings.arguments;
// Then, extract the required data from the arguments and
// pass the data to the correct screen.
return MaterialPageRoute(
builder: (context) {
return PassArgumentsScreen(
title: args.title,
message: args.message,
);
},
);
}
},
title: 'Navigation with Arguments',
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home Screen'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// A button that navigates to a named route that. The named route
// extracts the arguments by itself.
RaisedButton(
child: Text("Navigate to screen that extracts arguments"),
onPressed: () {
// When the user taps the button, navigate to the specific route
// and provide the arguments as part of the RouteSettings.
Navigator.pushNamed(
context,
ExtractArgumentsScreen.routeName,
arguments: {'instalation': "123", "message": "456"},
);
/*Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ExtractArgumentsScreen(),
// Pass the arguments as part of the RouteSettings. The
// ExtractArgumentScreen reads the arguments from these
// settings.
settings: RouteSettings(
arguments: {'instalation': "123", "message": "456"},
),
),
);*/
},
),
// A button that navigates to a named route. For this route, extract
// the arguments in the onGenerateRoute function and pass them
// to the screen.
RaisedButton(
child: Text("Navigate to a named that accepts arguments"),
onPressed: () {
// When the user taps the button, navigate to a named route
// and provide the arguments as an optional parameter.
Navigator.pushNamed(
context,
PassArgumentsScreen.routeName,
arguments: ScreenArguments(
'Accept Arguments Screen',
'This message is extracted in the onGenerateRoute function.',
),
);
},
),
],
),
),
);
}
}
// A Widget that extracts the necessary arguments from the ModalRoute.
class ExtractArgumentsScreen extends StatefulWidget {
static const routeName = '/extractArguments';
#override
_ExtractArgumentsScreenState createState() => _ExtractArgumentsScreenState();
}
class _ExtractArgumentsScreenState extends State<ExtractArgumentsScreen> {
final GlobalKey<ScaffoldState> key = new GlobalKey<ScaffoldState>();
final snackBar = SnackBar(
duration: Duration(seconds: 5),
content: Text("message" + '.', textAlign: TextAlign.center),
backgroundColor: Colors.red[700],
);
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final routeArgs1 =
ModalRoute.of(context).settings.arguments as Map<String, String>;
final instalation = routeArgs1['instalation'];
final message = routeArgs1['message'];
print('instalation ${instalation}');
print('message ${message}');
key.currentState
.showSnackBar(SnackBar(content: Text(message)));
});
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
// Extract the arguments from the current ModalRoute settings and cast
// them as ScreenArguments.
final routeArgs =
ModalRoute.of(context).settings.arguments as Map<String, String>;
final instalation = routeArgs['instalation'];
final message = routeArgs['message'];
return Scaffold(
key: key,
appBar: AppBar(
title: Text(' ${routeArgs['code']} '),
),
body: Column(
children: <Widget>[
Center(
child: Text('instalation ${instalation}'),
),
RaisedButton(
onPressed: () {
key.currentState.showSnackBar(snackBar);
},
),
],
),
);
}
}
// A Widget that accepts the necessary arguments via the constructor.
class PassArgumentsScreen extends StatelessWidget {
static const routeName = '/passArguments';
final String title;
final String message;
// This Widget accepts the arguments as constructor parameters. It does not
// extract the arguments from the ModalRoute.
//
// The arguments are extracted by the onGenerateRoute function provided to the
// MaterialApp widget.
const PassArgumentsScreen({
Key key,
#required this.title,
#required this.message,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Text(message),
),
);
}
}
// You can pass any object to the arguments parameter. In this example,
// create a class that contains both a customizable title and message.
class ScreenArguments {
final String title;
final String message;
ScreenArguments(this.title, this.message);
}
So, I see you're using the simple routes approach.
In order to extract route arguments you need to supply an onGenerateRoute function to your MaterialApp (or Cupertino, I guess).
You can find an exhaustive example on how to do it here, so I won't crowd this answer more than that.
Hope this solves your problem, happy coding!