I have two classes MainScreen and SearchScreen which uses nested function with a boolean value
For example, here in MainScreen, I have a function showMap, its return value is obtained from SearchScreen
Future showMap(bool checkValue) async {
try {
//do something
}
//somewhere in the widgets
#override
Widget build(BuildContext context) {
super.build(context);
//
GestureDetector(
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SearchScreen(
showMapFunction: showMap)));)
}
}
NoW in my SearchScreen I have
class SearchScreen extends StatefulWidget {
final Function showMapFunction;
const SearchScreen({
this.showMapFunction
}); //,this.String});
#override
_SearchScreenState createState() => _SearchScreenState();
}
///
#override
#mustCallSuper
Widget build(BuildContext context) {
GestureDetector(
onTap: () async {
Navigator.pop(
//send back data
// context,
widget.showMapFunction(true));
},
child: Icon(Icons.arrow_back)),
}
This works fine, when I navigate back to MainScreen the function showMap is called, Is there any other way to do this perhaps with provider package or sth? This causes a lot of rebuilds to my widget tree.
What you can do is to await the result of the navigator, like the one used in the Flutter Cookbook.
void _navigateToSearch(BuildContext context) async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SearchScreen())),
);
}
And then, when you pop the Search Screen, do the following
GestureDetector(
onTap: () async => Navigator.pop(true),
child: Icon(Icons.arrow_back),
),
I also suggest using WillPopScope.
Related
I received the error of `A ValueNotifier was used after being disposed.
Step to reproduce the error:
I navigate to menu.dart from homePage.dart.
Then, I go back from menu.dart to homePage.dart.
I navigate again to menu.dart. The error happened.
Error message
FlutterError (A `ValueNotifier<bool>` was used after being disposed.
Once you have called `dispose()` on a ValueNotifier<bool>, it can no longer be used.)
clearNotifier.dart
import 'package:flutter/material.dart';
ValueNotifier<bool> cancelListen =ValueNotifier(false);
homePage.dart
import 'package:project/pages/MenuFrame.dart';
...
IconButton(
icon: Image.asset('assets/image.png'),
iconSize: 50,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ChangeNotifierProvider<ValueNotifier>(
create: (_) => cancelListen,
child: MenuFrame(
userId: widget.userId
)
),
// MaterialPageRoute(
// builder: (BuildContext context) => MenuFrame(
// userId: widget.userId,
// ),
),
)
.then(
);
},
)
menu.dart
import 'package:project/class/clearNotifier.dart';
class MenuFrame extends StatefulWidget {
const MenuFrame({Key key, this.userId}) : super(key: key);
final String userId;
#override
_MenuFrame createState() => _MenuFrameState();
}
#override
void dispose() {
cancelListen?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: cancelListen,
builder: (BuildContext context, bool toClearListen,Widget child){
....
});
}
How can I rebuild the ValueNotifier once it has been disposed?
When you navigate from menu.dart back to homePage.dart, it call dispose function in menu.dart and your variable cancelListen was disposed. Therefore, when you navigate again to menu.dart, it will throw an error as you see.
Suggestion:
Do not pass variable cancelListen like that. You should create another ValueNotifier variable, I would temporary call it _cancelNotifier. You will pass the current value to homePage.dart:
MenuFrame(
userId: widget.userId,
value: cancelListen.value,
)
...............
late ValueNotifier<bool> _cancelNotifier;
initState() {
_cancelNotifier = ValueNotifier<bool>(widget.value);
}
I would like to update a page other than the one in which the user presses the button, I know that to update the page itself just use the
setState (() {})
but what I want to do is update the HomePage when the user presses a button that is on the main.dart page. How can I solve?
What I thought and that it works but badly is to use the
Navigator.push (context, MaterialPageRoute (builder: (context) => const MyApp ()));
when the user presses the button, and doing so it works because it updates my MyApp (main.dart), the problem is that when they press the button in this way you see the same animation as when you open the app and it is very ugly.
you can pass a function with setState in it to the other page and run the function after click the buttton
something like this:
class Startpage extends StatefulWidget {
const Startpage({super.key});
#override
State<Startpage> createState() => StartpageState();
}
class StartpageState extends State<Startpage> {
#override
Widget build(BuildContext context) {
return TextButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondPage(updateStartPage: () {
setState(() {
//add your logic
});
})),
),
child: const Text("navigate to second page"));
}
}
class SecondPage extends StatelessWidget {
final Function updateStartPage;
const SecondPage({super.key, required this.updateStartPage});
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => updateStartPage(),
child: const Text("update parent"),
);
}
}
The used Getx Arguments are cleared after the showDialog method is executed.
_someMethod (BuildContext context) async {
print(Get.arguments['myVariable'].toString()); // Value is available at this stage
await showDialog(
context: context,
builder: (context) => new AlertDialog(
//Simple logic to select between two buttons
); // get some Confirmation to execute some logic
print(Get.arguments['myVariable'].toString()); // Variable is lost and an error is thrown
Also I would like to know how to use Getx to show snackbars without losing the previous arguments as above.
One way to do this is to duplicate the data into a variable inside the controller and make a use from it instead of directly using it from the Get.arguments, so when the widget tree rebuild, the state are kept.
Example
class MyController extends GetxController {
final myArgument = ''.obs;
#override
void onInit() {
myArgument(Get.arguments['myVariable'] as String);
super.onInit();
}
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: Center(child: Obx(() => Text(controller.myArgument()))),
),
);
}
}
UPDATE
Since you are looking for solution without page transition, another way to achieve that is to make a function in the Controller or directly assign in from the UI. Like so...
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 2 (If you don't use GetView)
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends StatelessWidget {
final controller = Get.put(MyController());
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 3 (NOT RECOMMENDED)
If you really really really want to avoid using Controller at any cost, you can assign it to a normal variable in a StatefulWidget, although I do not recommend this approach since it was considered bad practice and violates the goal of the framework itself and might confuse your team in the future.
class MyPage extends StatefulWidget {
const MyPage({ Key? key }) : super(key: key);
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
String _myArgument = 'empty';
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Text(_myArgument),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
setState(() {
_myArgument = Get.arguments['myVariable'] as String;
});
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(_myArgument); // This should work
}
}
So I'm basically trying to check if users have seen an Intro page in my flutter app. If they already have seen it, I want them to be directed to the Login() page. Else, I want them to be directed to the IntroScreen() page.
However, I am getting the following error: Unhandled Exception: Navigator operation requested with a context that does not include a Navigator. E/flutter (13982): The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.
This is my code:
void main() => runApp(CheckSplash());
//check if the intro screen has already been seen
class CheckSplash extends StatefulWidget {
#override
_CheckSplashState createState() => _CheckSplashState();
}
class _CheckSplashState extends State<CheckSplash> {
bool _introseen=true;
Future checkIntroSeen() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool _introseen = (prefs.getBool('seen') ?? false);
if (_introseen) {
Navigator.of(context).pushReplacement(
new MaterialPageRoute(builder: (BuildContext context) => Login()));
} else {
//await prefs.setBool('seen', true);
Navigator.of(context).pushReplacement(
new MaterialPageRoute(builder: (BuildContext context) => new IntroScreen()));
}
}
#override
void initState() {
super.initState();
new Timer(new Duration(milliseconds: 200), () {
checkIntroSeen();
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: appTheme,
debugShowCheckedModeBanner: false,
home: Builder(
builder: (context) => Scaffold(
resizeToAvoidBottomPadding: false,
body: Center(child: CircularProgressIndicator(valueColor: new AlwaysStoppedAnimation<Color>(Colors.red[300])))
)
)
);
}
}
So I solved the problem by using the flutter GET library. Link: flutter get library
Using this library, one doesn't need the context to navigate between pages and thus it is a full proof method to navigate from anywhere in flutter.
I Have tried the easiest way. Create a function for your code after your contextBuilder. Then, Just put the context
as a function argument:
Widget build(BuildContext context) {
return ....(your code)
}
yourFunction(context) {
Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(builder: (context) => LoginScreen()));
}
It is worked as expected for me. Thanks!
context inside initState is available inside SchedulerBinding.instance.addPostFrameCallback. This function fired after widget is built.
#override
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) {
new Timer(new Duration(milliseconds: 200), () {
checkIntroSeen();
});
});
}
I created Flutter app for Android. This my user case:
I'm on the widgetA.
I'm click button and and go to the widgetB. (A->B)
I'm click hardware back button ant return to widgetA.
Now I'm on widgetA and I need any event so that I can update my WidgetA.
Any advices?
You could do something similar to what is called on Android startActivityForResult().
If you are on WidgetA push a new WidgetB and wait for a result.
From WidgetB detect when the widget will pop and pop manually sending the result.
WidgetA:
class WidgetA extends StatefulWidget {
#override
State<StatefulWidget> createState() => WidgetAState();
}
class WidgetAState extends State<WidgetA> {
Future<String> _result;
Future<String> _startWidgetForResult(BuildContext context) async {
String result = await Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => WidgetB(context: context),
),
);
return result;
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.subdirectory_arrow_right),
onPressed: () => _result = _startWidgetForResult(context),
),
body: FutureBuilder(
future: _result,
builder: (context, snapshot) {
return Center(child: Text('${snapshot.data}'));
},
),
);
}
}
WidgetB:
class WidgetB extends StatelessWidget {
final BuildContext context;
WidgetB({this.context});
Future<bool> _onWillPop() async {
Navigator.of(context).pop("New data");
return false;
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.subdirectory_arrow_left),
onPressed: () => Navigator.of(context).pop("New data"),
),
body: Center(child: Text('Press back to send new data')),
),
);
}
}
I guess you can wrap your widgetB in WillPopScope widget. Or you can set a key press handler with SystemChannels.keyEvent.setMessageHandler(...), but this isn't a recommended way to go.
You can use RouteObserver for that
https://api.flutter.dev/flutter/widgets/RouteObserver-class.html