I'm dealing with back actions. I'm not able to achieve good results with WillPopScope() (it's only called with top app return button but not called with android back button).
In my app, I have several pages and when android back button is pressed, I don't see the previous page.
For example, I have main-page1-page2-page3
If I'm in page 3 and press Android back button I return to page1, not to page2. In other cases it returns to main...How it is possible? Is there a way to define de pages "order"?
EDIT
This is my code that I shared in a previous question.
class Calendario1 extends StatelessWidget {
final List listaini;
Calendario1(this.listaini);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "ATGapp",
home:Cal1(listaini: listaini),
);
}
}
class Cal1 extends StatefulWidget {
final List listaini;
Cal1({Key? key,required this.listaini}) : super(key: key);
#override
///
_Cal1State createState() => _Cal1State();
}
class _Cal1State extends State<Cal1> {
#override
void initState() {
getImage(path1);
super.initState();
}
String url_1 = '';
getImage(String path1) async {
//String url='';
final ref1 = FirebaseStorage.instance.ref().child(path1);
var url1 = await ref1.getDownloadURL();
setState(() => url_1 = url1);
}
final FirebaseStorage storage =
FirebaseStorage.instance;
String path1 = 'fondos/mons.jpeg';
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: ()async{
print('Bck button pressed');
return false;
},
child: Scaffold(
body: Column(...//and so on
add Navigator.pop() inside onWillPop and return true.
onWillPop: () async {
// do something here
return true;
},
return new WillPopScope(
onWillPop: _willPopCallback,
child: new Scaffold(
//then the rest of your code...
Thank your for all your help.
The final solution has been a new structure of the app pages. Until now, every page was a different dart file and with this structure, the behaviour of willpopscope and any other solutions didn't work for me.
Now, all pages are in the same file ordered by routes definitions in MaterialApp. Now, I'm able to catch the android back button click and work with it.
I hope this can be helpful for others.
Related
I'm building a login screen. On that screen, I have a "login" button. After this button is pressed, my app connects to the internet in order to check if the user can be logged in. I want navigating backwards (physical button on Android, swipe-back on iOS) to be disabled while this loading is happening.
To achieve this, I should be able to wrap my screen in a WillPopScope widget, and have its onWillPop parameter look like this:
return WillPopScope(
onWillPop: () async => isLoading ? false : true,
child: child,
);
(isLoading is whether or loading is happening, and if navigation should be blocked)
However, this just universally blocks all navigation no matter if isLoading is true or false.
I've also tried this:
return isLoading
? WillPopScope(
onWillPop: () async => false,
child: child,
)
: child;
This works, however, doing it this way will block all animations in the child, which basically renders the solution useless.
Is there anyway to get the first method to work? Or, is there another way all together?
Thanks.
Figured it out. Use this package: https://pub.dev/packages/back_button_interceptor/example.
Create a widget that wraps your screen (Scaffold), using it like this:
import 'package:back_button_interceptor/back_button_interceptor.dart';
import 'package:flutter/material.dart';
class NavBlocker extends StatefulWidget {
const NavBlocker({
super.key,
required this.blocking,
required this.child,
});
final bool blocking;
final Widget child;
#override
State<NavBlocker> createState() => _NavBlockerState();
}
class _NavBlockerState extends State<NavBlocker> {
#override
void initState() {
super.initState();
BackButtonInterceptor.add(myInterceptor);
}
#override
void dispose() {
BackButtonInterceptor.remove(myInterceptor);
super.dispose();
}
bool myInterceptor(bool stopDefaultButtonEvent, RouteInfo info) {
return widget.blocking;
}
#override
Widget build(BuildContext context) {
return widget.child;
}
}
Where blocking specifies whether or not navigation should be blocked or not.
This solution enables animations to work, too!
i want to know the screen(StatelessWidget) is in front or background.
for now, a screen in background shows AlertDialog, but i want to show the dialog only when the screen is in front.
is there any way to do that?
here's my code using hooks_riverpod.
i omitted some lines because of "mostly code" warning.
final _provider = provider801x;
class MyPage801x extends HookConsumerWidget {
MyPage801x({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final String? message = ref.watch(_provider).message;
if (message != null) {
ref.read(_provider).done();
showResult(context, message); //don't want to show when in background.
}
return Scaffold(
appBar: AppBar(
title: Text('main screen'),
),
body: Container(),
);
}
Future<void> showResult(BuildContext context, String message) async {
Future.delayed(Duration(milliseconds: 500), () async {
await showOKOnlyDialog(context, message: message);
});
}
}
final provider801x = ChangeNotifierProvider.autoDispose((ref) => Notifier801x(ref));
class Notifier801x extends ChangeNotifier {
Notifier801x(this._ref) {
_iapManager = IAPManager4Android();
_refreshDataset();
_iapListener = _iapManager.onChangedIAP.listen((IAPTransactionStateEnum state) async {
_message = state.message! + ' #801x';
notifyListeners();
});
}
String? _message = null;
String? get message => _message;
void done() {
_message = null;
}
void _refreshDataset() {
//fetch data
notifyListeners();
}
}
this code worked!
if (ModalRoute.of(context)?.isCurrent ?? false) {
showResult(context, message);
}
I got this answer from the page below. thank you all!
Flutter | How to know if widget is on top of navigation stack (visible)
You could use mounted for this case.
mounted is true if the widget is visible to the user or technically on the top of the stack of screens otherwise will be false.
So, you can use it like this way
if(mounted){
//do your stuff like show dialog or snackbar
}
Use a static string (or DI ) in main function , set it to class name in build function of any screen. check this variable to know which screen is visible.
I'm trying to use the hive db to store simple objects in an app that has 3 main pages, selected with a nav bar in the following form (following closely the example from the flutter docs).
/// determine body widget ie page to be rendered
int _pageIndex = 0;
/// list of body widgets
static const List<Widget> _pageOption = [
KeyList(), //index 0
PersonalKey(), //index 1
Crypt(), //index 2
];
/// [index] tells body of scaffold what widget to render
void _changePage(int index) {
setState(() {
_pageIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
widget.title,
style: const TextStyle(color: cyan),
),
),
body: Center(
child: _pageOption.elementAt(_pageIndex),
),
I'm not sure how to get the hive box to open / work with each page. for instance a Box would need to used in the KeyList() page. I tried passing the box through state but this is warned against and didn't work.
I tried varies combinations of an async main function and an overriden innitState function in _KeyListState() and now I'm not getting any widgets to render.
main function (from main.dart)
void main() async {
await Hive.initFlutter();
await Hive.openBox<ContactKey>('contacts');
Hive.registerAdapter(ContactKeyAdapter());
runApp(const MyApp());
}
reference from key_list.dart
class _KeyListState extends State<KeyList> {
late final Box contactBox;
#override
void initState() {
super.initState();
contactBox = Hive.box("contacts");
// makeKeyList();
}
#override
void dispose() {
Hive.close();
super.dispose();
}
final List<ContactKey> _keys = []; // = [
// ContactKey(contactName: "cade", publicKey: "123")
// ];
void addKey(String name, String key) async {
ContactKey newContact = ContactKey(contactName: name, publicKey: key);
setState(() {
contactBox.add(newContact);
makeKeyList();
});
}
void makeKeyList() {
if (contactBox.isNotEmpty) {
for (var i = 0; i < contactBox.length; i++) {
_keys.add(contactBox.getAt(i));
}
}
}
#override
Widget build(BuildContext context) {
return Stack(
...
I'm not getting any errors or warnings but when the app is running I get the error "each child must be laid out only once" A google search on this error made it seem like a flutter bug, but if I remove the Hive code my app renders again.
I'm pretty lost right now and if anyone has any tips or sample apps that use a nav bar with hive they'd be greatly appreciated!
Thanks
My application has a bottom navigation bar, with 2 pages in the menu.
On page 1, I can fill out a form and it calculates me values that it displays to me by pushing in a 1.1 page.
On this page I have a button that allows me to redirect me to page 2 as if I clicked menu 2 of the navigation bar.
This works. My problem is how to send the data from my page 1.1 to this page 2.
The goal being that my page 2 is a form which is empty if I call it by the navigation bar but which is filled automatically if I pass by the page 1.1 in focus of the calculated values.
Here an exemple of the redirection that I do:
Here is my code :
my_app.dart :
final ThemeData _AppTheme = AppTheme().data;
final navBarGlobalKey = GlobalKey(); // => This is my key for redirect page
class MyApp extends StatefulWidget{
#override
State<StatefulWidget> createState() => MyAppState();
}
class MyAppState extends State<MyApp>{
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'App',
home: MyBottomNavigationBar(),
theme: _AppTheme,
navigatorKey: locator<NavigationService>().navigatorKey,
onGenerateRoute: Router.generateRoute,
initialRoute: HOME_ROUTE,
);
}
}
My bottom_navigation_bar.dart :
class MyBottomNavigationBar extends StatefulWidget
{
MyBottomNavigationBar({Key key}) : super(key: key);
#override
_MyBottomNavigationBarState createState() => _MyBottomNavigationBarState();
}
class _MyBottomNavigationBarState extends State<MyBottomNavigationBar>
{
int _pageIndex = 0;
final List<Widget> _pagesOption = [
page1.1(), // => Here I load direclty my page 1.1 with data for the exemple
page2(),
];
void onTappedBar(int index)
{
setState(() {
_pageIndex = index;
});
}
#override
Widget build(BuildContext context) {
return SafeArea(
child : Scaffold(
body : _pagesOption.elementAt(_pageIndex),
bottomNavigationBar: BottomNavigationBar(
key: navBarGlobalKey,
currentIndex: _pageIndex,
onTap: onTappedBar,
type: BottomNavigationBarType.fixed,
items : [
BottomNavigationBarItem(
icon : Icon(Icons.home),
title : Text('Home')
),
BottomNavigationBarItem(
icon : Icon(Icons.settings),
title : Text('Setting')
),
]
),
)
);
}
}
And here my widget submit button of page 1.1 :
Widget _getSubmitButton(){
return RaisedButton(
child: Text(
'Send'
),
onPressed: () {
final BottomNavigationBar navigationBar = navBarGlobalKey.currentWidget;
navigationBar.onTap(1); // => How to send data that I have in my page ???
},
);
}
For this, you can use Shared Preferences, the main idea is that:
Store the value of the calculated value in SharedPref from Page 1 when you're passing to Page 1.1
Let you checks for the value by default in Page 2's initState(), any changes in the Shared Preferences will be fetched in the Page 2 itself, using SharedPref get method.
WHY?
This is probably a cleaner way to achieve what you want, since in the BottomNavigationBar will not help you do this, but a Shared Preferences value will always give you that data which you can use it any time
Let's see how you can achieve this:
PAGE ONE
// Set the data of the form here
class _PageOneState extends State<PageOne>
{
void onSubmit() async{
SharedPreferences prefs = await SharedPreferences.getInstance();
//make sure you store the calculated value, if that is String
// use setString() or if it is an int, setInt()
// and then pass it to the SharedPref
// key is a string name, which is used to access
// key and store, so choose the name wisely
await prefs.setInt("key", your_calculated_value);
}
}
PAGE TWO
class _PageTwoState extends State<PageTwo>
{
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
// This will be responsible for getting the result from SharedPref
int calculated_value;
#override
void initState(){
super.initState();
// get your list here
calculated_value = _prefs.then((SharedPreferences prefs){
// here if no data is then _values will have 0
// which you can use it to check and populate data
return (prefs.getInt("key") ?? 0);
});
}
}
This is the most reasonable way of doing the thing which you want. In this manner, whenever, PageTwo will trace any values, it will reflect, else, your choice for 0 check result. Let me know, if you have any doubts in that.
In your FirstActivity
onPressed: () {
navigatePush(SecondActivity(text: "Data"));
}
In your SecondActivity
class SecondActivity extends StatefulWidget {
String text;
SecondActivity({this.text});
}
You can pass the the values as arguments when you push to your new screen. This could get messy if you're building a larger project.
A cleaner implementation would be to use a Provider. Set up the data you want in a model mixed in with ChangeNotifier and use Provider.of<*name of your class*>(context) where ever you need to use it.
I have a home page which when clicked takes me to another page through navigates, do some operations in then press the back button which takes me back to the home page. but the problem is the home page doesn't get refreshed.
Is there a way to reload the page when i press the back button and refreshes the home page?
You can trigger the API call when you navigate back to the first page like this pseudo-code
class PageOne extends StatefulWidget {
#override
_PageOneState createState() => new _PageOneState();
}
class _PageOneState extends State<PageOne> {
_getRequests()async{
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new RaisedButton(onPressed: ()=>
Navigator.of(context).push(new MaterialPageRoute(builder: (_)=>new PageTwo()),)
.then((val)=>val?_getRequests():null),
),
));
}
}
class PageTwo extends StatelessWidget {
#override
Widget build(BuildContext context) {
//somewhere
Navigator.pop(context,true);
}
}
Or you can just use a stream if the API is frequently updated, the new data will be automatically updated inside your ListView
For example with firebase we can do this
stream: FirebaseDatabase.instance.reference().child(
"profiles").onValue
And anytime you change something in the database (from edit profile page for example), it will reflect on your profile page. In this case, this is only possible because I am using onValue which will keep listening for any changes and do the update on your behalf.
(In your 1st page): Use this code to navigate to the 2nd page.
Navigator.pushNamed(context, '/page2').then((_) {
// This block runs when you have returned back to the 1st Page from 2nd.
setState(() {
// Call setState to refresh the page.
});
});
(In your 2nd page): Use this code to return back to the 1st page.
Navigator.pop(context);
use result when you navigate back from nextScreen as follow :
Navigator.of(context).pop('result');
or if you are using Getx
Get.back(result: 'hello');
and to reload previous page use this function :
void _navigateAndRefresh(BuildContext context) async {
final result = await Get.to(()=>NextScreen());//or use default navigation
if(result != null){
model.getEMR(''); // call your own function here to refresh screen
}
}
call this function instead of direct navigation to nextScreen
The solution which I found is simply navigating to the previous page:
In getx:
return WillPopScope(
onWillPop: () {
Get.off(() => const PreviousPage());
return Future.value(true);
},
child: YourChildWidget(),
or if you want to use simple navigation then:
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) =>PreviousPage() ,));
Simply i use this:
onPressed: () {
Navigator.pop(context,
MaterialPageRoute(builder: (context) => SecondPage()));
},
this to close current page:
Navigator.pop
to navigate previous page:
MaterialPageRoute(builder: (context) => SecondPage())
In FirtsPage, me adding this for refresh on startUpPage:
#override
void initState() {
//refresh the page here
super.initState();
}
For a more fine-grained, page-agnostic solution I came up with this Android Single LiveEvent mimicked behaviour.
I create such field inside Provider class, like:
SingleLiveEvent<int> currentYearConsumable = SingleLiveEvent<int>();
It has a public setter to set value. Public consume lets you read value only once if present (request UI refresh). Call consume where you need (like in build method).
You don't need Provider for it, you can use another solution to pass it.
Implementation:
/// Useful for page to page communication
/// Mimics Android SingleLiveEvent behaviour
/// https://stackoverflow.com/questions/51781176/is-singleliveevent-actually-part-of-the-android-architecture-components-library
class SingleLiveEvent<T> {
late T _value;
bool _consumed = true;
set(T val) {
_value = val;
_consumed = false;
}
T? consume() {
if (_consumed) {
return null;
} else {
_consumed = true;
return _value;
}
}
}
await the navigation and then call the api function.
await Navigator.of(context).pop();
await api call
You can do this with a simple callBack that is invoked when you pop the route. In the below code sample, it is called when you pop the route.
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => new _HomePageState();
}
class _HomePageState extends State<HomePage> {
_someFunction()async{
Navigator.of(context).push(MaterialPageRoute(builder: (_)=> PageTwo(
onClose():(){
// Call setState here to rebuild this widget
// or some function to refresh data on this page.
}
)));
}
#override
Widget build(BuildContext context) {
return SomeWidget();
}
...
} // end of widget
class PageTwo extends StatelessWidget {
final VoidCallback? onClose;
PageTwo({Key? key, this.onClose}) : super(key: key);
#override
Widget build(BuildContext context) {
return SomeWidget(
onEvent():{
Navigate.of(context).pop();
onClose(); // call this wherever you are popping the route
);
}
}