I have a code that displays a list of devices. When a user clicks on a device, they are redirected to a page with a detailed description of the device.
phone_list.dart
.....
child: TextButton(
onPressed: (){{
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>const
DeviceDescription(),
settings: RouteSettings(
arguments: phones[index],
),
.....
device_description.dart
class DeviceDescription extends StatelessWidget {
const DeviceDescription({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final device = ModalRoute.of(context)!.settings.arguments as Phone;
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(device.name),
........
),
);
}
}
But my problem is that when updating the page with a description of the device, the user is again returned to the page with a list of all devices.
Is there any way to fix this problem?
Perhaps I need to write a new route in main.dart?. Then tell me how to change the code so that information about a specific device is displayed on this page.
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)
This issue is related with github #2502.
I am using GetMaterialApp from this package.
I'm not sure if this is a bug or not.
How to make the function in the first dialog useable by using Get.toNamed()?
It happened when using the Get.toNamed().
It works fine with Navigator.push() but I need Get.toNamed for the web app.
The first page has a button that will show the first dialog.
The first dialog will show the order type button list.
When pressing an order type button, the program will find a new order of this type and open the second page with a new order data.
The second page has some work to do and this work will open the second dialog.
After finishing this work, the user will click on the back button back to the first page and find a new order again.
The problem is when the second dialog works on the second page.
The first dialog on the first page will not work.
see video example.
web example.
code example:
import 'package:flutter/material.dart';
import 'package:flutter_test_exam_bug/config/path/page_path.dart';
import 'package:get/get.dart';
Future<void> _showMyDialog({required BuildContext context, required Widget child}) async {
return showDialog<void>(
context: context,
builder: (BuildContext context) => child,
);
}
class PageTest extends StatefulWidget {
const PageTest({Key? key}) : super(key: key);
#override
_PageTestState createState() => _PageTestState();
}
class _PageTestState extends State<PageTest> {
#override
Widget build(BuildContext context) {
Widget dialog_ = Center(
child: ElevatedButton(onPressed: () => Get.toNamed(PagePath.test2), child: const Text("Open second page"))),
openDialogButton_ = ElevatedButton(
onPressed: () => _showMyDialog(context: context, child: dialog_), child: const Text("Open first dialog"));
return Scaffold(body: SafeArea(child: Center(child: openDialogButton_)));
}
}
class PageTest2 extends StatefulWidget {
const PageTest2({Key? key}) : super(key: key);
#override
State<PageTest2> createState() => _PageTest2State();
}
class _PageTest2State extends State<PageTest2> {
ButtonStyle buttonStyle = ElevatedButton.styleFrom(primary: Colors.green);
#override
Widget build(BuildContext context) {
Widget dialog_ = Center(
child: ElevatedButton(
onPressed: () => Navigator.pop(context), child: const Text("I am second dialog"), style: buttonStyle)),
openDialogButton_ = ElevatedButton(
onPressed: () => _showMyDialog(context: context, child: dialog_),
child: const Text("Open second dialog"),
style: buttonStyle);
return Scaffold(appBar: AppBar(), body: SafeArea(child: Center(child: openDialogButton_)));
}
}
I think it is a bug.
When opening a dialog, the GETX ROUTE will change to the current page again.
Follow this in https://github.com/jonataslaw/getx/issues/2502
I have created the Cubit SurveysCubit in HomePage from BlocProvider component, now, I want to access it from a new page pushed in its child body.
All works fine until the pushed page is reached here the following error is shown telling me that the SurveysBloc created on the previous page is not found :
BlocProvider.of() called with a context that does not contain a SurveysCubit.
This is the home page :
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) =>
SurveysCubit(repo: RepositoryProvider.of<SurveyRepository>(_)),
child: Builder(builder: (newContext) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => Navigator.push(
newContext,
MaterialPageRoute<void>(
builder: (BuildContext newContext) => Surveys(),
),
),
child: const Text("Surveys"),
),
],
),
),
);
}),
);
}
}
Surveys, the pushed page :
class Surveys extends StatelessWidget {
List<Survey> surveys = [];
Surveys({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
surveys = BlocProvider.of<SurveysCubit>(context).getSurveys; //this call fails
return Scaffold(
body: ListView.builder(
itemCount: surveys.length,
itemBuilder: (context, i) => Column(
children: [
ShowSurvey(survey: surveys[i]),
SizedBox(
height: 20,
),
],
),
),
);
}
}
I know I could avoid Builder widget, but I wanted to force a new context creation. I think I provided the right context, so it should work, I don't understand what's happening.
you want to share your cubit state between 2 pages you have 2 option to achieve that but first let me explain what BlocProvider.of or Navigator.of basically the "of" word,
ClassA.of means that lets search in my ancestors in context tree about an object of type ClassA.
so when you are typing Navigator.of you are getting the Navigator object from node above you in the tree, same as BlocProvider.of.
now back to your question you are providing a bloc in page HomePage, then trying to access it from Surveys page, now this Surveys page can not access it, because the bloc you are asking to get is not providing in an ancestor node of Surveys page.
to solve this you can pass the bloc instance as parameter to surveys page and wrap the hole page with BlocProvider.value
class Surveys extends StatelessWidget {
final cubit;
Surveys({Key? key, required this.cubit}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocProvider.value(
value: cubit,
child:....
);
}
}
or wrap the page directly when you are pushing it (I do not recommend this because every time you want to use this page you have to remember to wrap it with bloc provider)
Navigator.push(
newContext,
MaterialPageRoute<void>(
builder: (BuildContext newContext) => BlocProvider.value(
value: BlocProvider.of<SurveysCubit>(newContext);
child: Surveys()
),
),
),
or last Option is when you want to access you bloc in every page not only this 2 pages just wrap your MaterialApp in the main function with bloc provider.
now when ever you call BlocProvider.of(context) you will get it.
First of, if your HomePage is using SurveysCubit I'd recommend wraping it completely rather than declaring BlocProvider on top of build method. This will minimalize chance that you will run into issues like this one.
But if you wonna stay with your implementation, I have few tips.
Remove Builder from tree; it is an inline alternative to defining StatelessWidget. It's redundant here.
Even if you will pass the right context when pushing Surveys page, errors might occur because RepositoryProvider is using new throwaway context _ that clearly doesnt have repo in it, or it will but then, your implementation is missleading to people reading it.
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (newContext) => SurveysCubit(
repo: RepositoryProvider.of<SurveyRepository>(newContext),
),
child: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => Navigator.push(
newContext,
MaterialPageRoute<Surveys>(
builder: (_) => Surveys(),
),
),
child: const Text("Surveys"),
),
],
),
),
),
);
}
}
If this doesnt help, you might need to wrap MaterialPageRoute with BlocProvicer.value().
Secondly, I advice using power of bloc by using BlocBuilder inside your Survey page rather than reading cubit value once. You lose app responsiveness to state changes by fething data once. Best case scenario you want to react to changes, not reload page to get changes etc.
Consider this:
class Surveys extends StatelessWidget {
Surveys({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocBuilder<SurveysCubit, SurveysState>(
builder: (context, state) {
return ListView.builder(
itemCount: state.surveys.length,
itemBuilder: (context, i) => Column(
children: [
ShowSurvey(survey: state.surveys[i]),
const SizedBox(height: 20),
],
),
);
},
),
);
}
}
Please tell if it worked.
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);
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