I have a custom AppBar that has a function which should handle the OnBackPressed of the screen, but unfortunately is not being triggered and dunno why.
Basically I want that on pressing the back button, to leave the current screen.
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: ComponentAppBar(
showGoBackButton: true,
onGoBackPressed: () => Get.back(),
),
body: Container(),
);
}
The custom app bar file:
import 'package:flutter/material.dart';
class ComponentAppBar extends StatefulWidget implements PreferredSizeWidget {
final bool showGoBackButton;
final Function onGoBackPressed;
const ComponentAppBar({
Key? key,
required this.showGoBackButton,
required this.onGoBackPressed,
}) : super(key: key);
#override
_ComponentAppBarState createState() => _ComponentAppBarState();
#override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
class _ComponentAppBarState extends State<ComponentAppBar> {
#override
Widget build(BuildContext context) {
return AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
//
],
),
leading: widget.showGoBackButton
? IconButton(
onPressed: () => widget.onGoBackPressed,
icon: const Icon(
Icons.arrow_back,
color: Colors.black,
),
)
: Container(),
automaticallyImplyLeading: false,
leadingWidth: !widget.showGoBackButton ? 0 : 30,
);
}
}
Pretty straight forward, yet, when pressing the back button nothing happens.
What have I debugged:
Instead of Get.back() use Navigator.pop() - same problem
Add an output onGoBackPressed: () => { print('testing') } - no output
Change onPressed: () => widget.onGoBackPressed to onPressed: () => Get.back(), it works
Change onPressed: () => widget.onGoBackPressed to onPressed: () => { print('test directly in class') }, it works
Also, when using the F5 debug tools, it shows like this, which is very strange:
It's all about callbacks.
You're receiving a callback as argument (onGoBackPressed), and you need to execute it in order for it to work. There are two ways for doing this:
onPressed: widget.onGoBackPressed, //This should work since you're assigning directly onGoBackPressed as the callback to be executed by onPressed
onPressed: ()=>widget.onGoBackPressed.call(), //This should work since you're calling onGoBackPressed callback. In other words, you're executing it. Before, you were just referencing it, without actually executing
Tip: I'd recommend you to avoid dynamics when typing arguments. So, I recommend you to type specifically what your onGoBackPressed callback will return. This way:
final void Function() onGoBackPressed;
Which is the same as
final VoidCallback onGoBackPressed;
Cheers!
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
Here goes my code ,
I am using the TextButton to update the order ,But after every change in the dropdown item, onPress is automatically invoked and the function updateOrder is automatically invoked
import 'package:admin/constants/Constants.dart';
import 'package:admin/model/order_model.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
class DetailsScreen extends StatefulWidget {
const DetailsScreen({Key? key}) : super(key: key);
#override
State<DetailsScreen> createState() => _DetailsScreenState();
}
class _DetailsScreenState extends State<DetailsScreen> {
List<String> _dropDownQuantities = [Pending, Confirmed, Rejected, Success];
late OrderModel order;
late String selectedStatus = order.status;
#override
Widget build(BuildContext context) {
order = ModalRoute.of(context)?.settings.arguments as OrderModel;
return Scaffold(
appBar: AppBar(
actions: [],
title: Text("Order Details"),
),
body: Column(children: [
Text(order.id),
DropdownButtonHideUnderline(
child: DropdownButton2<String>(
value: selectedStatus,
items: _dropDownQuantities
.map((e) => DropdownMenuItem<String>(
child: Text(e),
value: e,
))
.toList(),
onChanged: (value) {
setState(() {
selectedStatus = value;
});
},
)),
TextButton(onPressed: updateOrder(order, selectedStatus), child: Text("Confirm")),
]));
}
}
updateOrder(OrderModel order, String selected) {
print("I am executed");
}
So whenever i change the dropDown menu,
I am executed is printed in the console.
Edit:
But when i used the container with InkWell it was working fine. Why not working with TextButton ?
You are directly calling the method on build, you can create an inline anonymous function to handle this.
TextButton(
onPressed: ()=> updateOrder(order, selectedStatus),
child: Text("Confirm")),
onPressed Called when the button is tapped or otherwise activated.
While we use onPressed:method() call on every build, on dropDown onChanged we use setState, and it rebuilds the UI and onPressed:method() call again.
What we need here is to pass a function(VoidCallback) that will trigger while we tap on the button. We provide it like,
onPressed:(){
myMethod();
}
More about TextButton.
import 'package:flutter/material.dart';
import 'data.dart';
class Game extends StatefulWidget
{
Game({Key? key}) : super(key: key);
static Data data = Data();
static int counter = 0;
#override
_GameState createState() => _GameState();
}
class _GameState extends State<Game>
{
int counter2 = 0;
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Meh',
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(Game.counter.toString(), ),
Text(counter2.toString(), ),
OutlinedButton(
onPressed: () {
counter2++;
Game.counter++;
print(counter2);
print(Game.counter);
Game.data.save();
setState(() {});
},
child: Text("SAVE"),
),
OutlinedButton(
onPressed: () {
Game.data.load();
setState(() {});
},
child: Text("LOAD"),
),
],
),
),
),
);
}
}
I have to be doing something super stupid but I can't figure out what. You would expect when hitting the save button that the counter variable gets increased and displayed on screen but it's not displaying. Flutter is just ignoring the changes.
Maybe it's something wrong with my emulator? When I switch back to the editor and back to the emulator the value is showing correctly like flutter redrew itself from switching windows but not by clicking the button.
EDIT: This looks like the same issue I'm having, https://github.com/flutter/flutter/issues/17155, any ideas on how to fix it?
I tried your code, and it works for me.
It's definitely an IDE problem.
Try to update Flutter and also your IDE.
To be able to help you, please execute this command
"flutter doctor"
, and send us the result.
Cordially
You can run in https://www.dartpad.dev/, and see youself that the code is working as you expect.
I have a notifier.dart file where I have declared some ChangeNotifiers. One of which is OpacityChangeNotifier.
OpacityChangeNotifier Class:
class OpacityChangeNotifier extends ChangeNotifier {
double _opacity = 1.0;
double get opacity => _opacity;
void changeOpacity(double providedOpacity) {
_opacity = providedOpacity;
notifyListeners();
}
void printOpacity() {
print(_opacity);
}
}
This is for my coloring app where I want the user to start with an opacity of 1.0. Then he/she can change it.
Here's the opacity_picker widget
final _opacityProvider = ChangeNotifierProvider<OpacityChangeNotifier>((ref) {
return OpacityChangeNotifier();
});
class OpacityPicker extends ConsumerWidget {
const OpacityPicker({Key key}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
final opacityPicker = watch(_opacityProvider);
return Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
IconButton(
onPressed: () {
opacityPicker.changeOpacity(0.1);
},
icon: Icon(Icons.opacity, size: 20),
),
IconButton(
onPressed: () {
opacityPicker.changeOpacity(0.5);
},
icon: Icon(Icons.opacity, size: 20),
),
IconButton(
onPressed: () {
opacityPicker.changeOpacity(1.0);
},
icon: Icon(Icons.opacity, size: 20),
),
],
),
);
}
}
Now I want to use this opacity_picker inside another widget called menu_items. I've added a simple Icon button to test if the values(0.1,0.5,1.0) for opacity were getting updated or not.
IconButton(
onPressed: () {
opacity.printOpacity();
},
icon: Icon(Icons.dock_rounded),
)
But it seems the value is remaining the same which I provided as default: 1.0. Any solution on how to update the value I provided or any other way how I can change the opacity?
I'm not familiar with how you're instantiating ChangeNotifierProvider just above your OpacityPicker class. So I'm not sure if that's correct or not.
As Provider is built with InheritedWidget, any part of your app you wish to be "reactive" to changes of Provider state, needs to be a child underneath your ChangeNotifierProvider.
One way to ensure this is to wrap MyApp with your ChangeNotifierProvider. Therefore, your entire app is within the Provider's InheritedWidget scope. Remi (author of Provider) shows this in his example code:
void main() {
runApp(
/// Providers are above [MyApp] instead of inside it, so that tests
/// can use [MyApp] while mocking the providers
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
],
child: const MyApp(),
),
);
}
He's using MultiProvider but you can see that MyApp() is the child of Provider.
Are you doing something similar in your app?