I need to show an alert dialog before user navigates away from current route by pressing Back button on Android devices. I tried to intercept back button behavior by implementing WidgetsBindingObserver in widget state. There is an closed issue on GitHub regarding same topic. However my code is not working as the method didPopRoute() was never called. Here is my code below:
import 'dart:async';
import 'package:flutter/material.dart';
class NewEntry extends StatefulWidget {
NewEntry({Key key, this.title}) :super(key: key);
final String title;
#override
State<StatefulWidget> createState() => new _NewEntryState();
}
class _NewEntryState extends State<NewEntry> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
Future<bool> didPopRoute() {
return showDialog(
context: context,
child: new AlertDialog(
title: new Text('Are you sure?'),
content: new Text('Unsaved data will be lost.'),
actions: <Widget>[
new FlatButton(
onPressed: () => Navigator.of(context).pop(true),
child: new Text('No'),
),
new FlatButton(
onPressed: () => Navigator.of(context).pop(false),
child: new Text('Yes'),
),
],
),
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.edit),
onPressed: () {},
),
);
}
}
I found the solution is to use WillPopScope widget. Here is the final code below:
import 'dart:async';
import 'package:flutter/material.dart';
class NewEntry extends StatefulWidget {
NewEntry({Key key, this.title}) :super(key: key);
final String title;
#override
State<StatefulWidget> createState() => new _NewEntryState();
}
class _NewEntryState extends State<NewEntry> {
Future<bool> _onWillPop() {
return showDialog(
context: context,
child: new AlertDialog(
title: new Text('Are you sure?'),
content: new Text('Unsaved data will be lost.'),
actions: <Widget>[
new FlatButton(
onPressed: () => Navigator.of(context).pop(false),
child: new Text('No'),
),
new FlatButton(
onPressed: () => Navigator.of(context).pop(true),
child: new Text('Yes'),
),
],
),
) ?? false;
}
#override
Widget build(BuildContext context) {
return new WillPopScope(
onWillPop: _onWillPop,
child: new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.edit),
onPressed: () {},
),
),
);
}
}
The back_button_interceptor package can simplify this for you and is especially useful in more complex scenarios.
https://pub.dev/packages/back_button_interceptor#-readme-tab-
Example usage:
#override
void initState() {
super.initState();
BackButtonInterceptor.add(myInterceptor);
}
#override
void dispose() {
BackButtonInterceptor.remove(myInterceptor);
super.dispose();
}
bool myInterceptor(bool stopDefaultButtonEvent) {
print("BACK BUTTON!"); // Do some stuff.
return true;
}
If you are using the GetX package and you implemented the GetMaterialApp method to initialize your app, the didPopRoute and didPushRoute methods in WidgetsBindingObserver never get called. Use the routingCallback instead, below is an example, for more info check out GetX documentation:
GetMaterialApp(
routingCallback: (routing) {
routing.isBack ? didPopRoute() : didPushRoute(routing.current);
}
)
Related
I need your help.
I'm making a function for my app that has the user add something by pressing the add button, it will then navigate to an adding page and then from the adding page it will add a new listtile in the listview. But I don't know why the text that was input by the user cannot be shown. Can anyone help me?
import 'package:flutter/material.dart';
import 'storage for each listview.dart';
import 'package:provider/provider.dart';
import 'adding page.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Storage(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage()
),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
final provider = Provider.of<Storage>(context, listen: false);
final storageaccess = provider.storage;
return Scaffold(
appBar: AppBar(
title: Text('app'),
),
body: ListView.builder(
itemCount: storageaccess.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(storageaccess[index].title),
subtitle: Text(storageaccess[index].titlediary.toString()),
onTap: () {},
onLongPress: () {
//delete function here
},
);
}),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context, MaterialPageRoute(builder: (context) => addpage()));
}, //void add
tooltip: 'add diary',
child: Icon(Icons.add),
) // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
/// this one I did not do anything first this one for later today just make UI
class Things {
String title;
DateTime titlediary;
Things({required this.title, required this.titlediary});
}
class addpage extends StatefulWidget {
#override
_addpageState createState() => _addpageState();
}
class _addpageState extends State<addpage> {
String title = '';
#override
Widget build(BuildContext context) {
final TextEditingController titleController=TextEditingController(text: title);
final formKey = GlobalKey<FormState>();
return Scaffold(
appBar: AppBar(
title: Text('enter page ',style: TextStyle(fontSize: 30),),
),
body:Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: titleController,
autofocus: true,
validator: (title) {
if (title!.length < 0) {
return 'enter a title ';
} else {
return null;
}
},
decoration: InputDecoration(
border: UnderlineInputBorder(),
labelText: 'title',
),
),
SizedBox(height: 8),
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.black),
),
onPressed: () {
if (formKey.currentState!.validate()) {
final accessthing = Things(
title: title,
titlediary: DateTime.now(),
);
final provideraccess = Provider.of<Storage>(context, listen: false);
provideraccess.add(accessthing);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>MyHomePage()));
}
},
child: Text('Save'),
),
],
),),);
}
}
class Storage extends ChangeNotifier {
List<Things> storage = [
Things(
title: 'hard code one ',
titlediary: DateTime.now(),
),
Things(
title: 'hard code two ',
titlediary: DateTime.now(),
),
Things(
title: 'hard code two ',
titlediary: DateTime.now(),
),
Things(
title: 'hard code two ',
titlediary: DateTime.now(),
),
];
void add(Things variablethings) {
storage.add(variablethings);
} notifyListeners();
}
after the user clicks the addbutton, it will send them to an adding page, then after clicking save, the data will be saved into a storage page and then the provider will add the data provided by the user, but the text will not show on the listtile.
I suspect this is happening because you are Navigating to HomePage again using,
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>MyHomePage()));
but this time it is not connected to your provider context.
[
In detail: As you have used ChangeNotifierProvider() in MyApp then connected the MyHomePage() there. But if you push again in Navigator, then fluter creates a separate instance of MyHomePage() widget. Which will not be connected to ChangeNotifierProvider() in MyApp
].
In place of this, use Navigator.of(context).pop();
And in onPressed() use this,
floatingActionButton: FloatingActionButton(
---> onPressed: () async {
---> await Navigator.push(
context, MaterialPageRoute(builder: (context) => addpage()));
---> setState((){});
},
I have an app with three different pages. To navigate between the pages I use a bottom navigation. One of the pages contains a form. Before leaving the form page, I want to display a confirmation dialog. I know that I can intercept the back button of the device with WillPopScope. This works perfectly. But WillPopScope is not triggered when I leave the page via the bottom navigation.
I have also tried using the WidgetsBindingObserver, but both didPopRoute and didPushRoute are never triggered.
Any ideas what I can do?
My form page:
class TodayTimeRecordFormState extends State<TodayTimeRecordForm> with WidgetsBindingObserver{
final formKey = GlobalKey<FormState>();
TodayTimeRecordFormState();
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
Future<bool> didPopRoute() {
//never called
_onWillPop();
}
#override
Future<bool> didPushRoute(String route){
//never called
_onWillPop();
}
#override
Widget build(BuildContext context) {
return WillPopScope(
// called when the device back button is pressed.
onWillPop: _onWillPop,
child: Scaffold(...));
}
Future<bool> _onWillPop() {
return showDialog(
context: context,
child: new AlertDialog(
title: new Text('Are you sure?'),
content: new Text('Unsaved data will be lost.'),
actions: <Widget>[
new FlatButton(
onPressed: () => Navigator.of(context).pop(false),
child: new Text('No'),
),
new FlatButton(
onPressed: () => Navigator.of(context).pop(true),
child: new Text('Yes'),
),
],
),
);
}
}
Page containing the bottom navigation:
const List<Navigation> allNavigationItems = <Navigation>[
Navigation('Form', Icons.home, Colors.orange),
Navigation('Page2', Icons.work_off, Colors.orange),
Navigation('Page3', Icons.poll, Colors.orange)
];
class HomePage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return HomePageState();
}
}
class HomePageState extends State<HomePage> {
int selectedTab = 0;
final pageOptions = [
TodayTimeRecordForm(),
Page2(),
Page3(),
),
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppConfig.instance.values.title),
),
body: pageOptions[selectedTab],
bottomNavigationBar: BottomNavigationBar(
currentIndex: selectedTab,
unselectedItemColor: Colors.grey,
showUnselectedLabels: true,
selectedItemColor: Colors.amber[800],
onTap: (int index) {
setState(() {
selectedTab = index;
});
},
items: allNavigationItems.map((Navigation navigationItem) {
return BottomNavigationBarItem(
icon: Icon(navigationItem.icon),
backgroundColor: Colors.white,
label: navigationItem.title);
}).toList(),
),
);
}
}
You can do it by modifying your BottomNavigationBar's onTap.
onTap: (int index) {
if (selectedTab == 0 && index != 0){
// If the current tab is the first tab (the form tab)
showDialog(
context: context,
child: new AlertDialog(
title: new Text('Are you sure?'),
content: new Text('Unsaved data will be lost.'),
actions: <Widget>[
new FlatButton(
onPressed: () => Navigator.pop(context), // Closes the dialog
child: new Text('No'),
),
new FlatButton(
onPressed: () {
setState(()=>selectedTab = index); // Changes the tab
Navigator.pop(context); // Closes the dialog
},
child: new Text('Yes'),
),
],
),
);
} else {
// If the current tab isn't the form tab
setState(()=>selectedTab = index);
}
},
In this code, I want to show indexed widgets by changing the index from the Navigation drawer i.e. The main MaterialApp shows widget according to the index(widgetIndex). The index is updated but the widget does not change until I hot reload it. So, I want it to reload the MyApp widget from the drawer widget.
main.dart:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:indexed/page1.dart';
import 'package:indexed/page2.dart';
import 'drawer.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
//set widgetIndex(int widgetIndex) {widgetIndex = DrawerS.widgetIndex;}
int widgetIndex = SideDrawerState.widgetIndex;
#override
Widget build(BuildContext context)
{
return MaterialApp(
home: Container(
child: IndexedStack(
index: widgetIndex,
children: <Widget>[
Page1(), //A Scaffold wid.
Page2(), //A Scaffold wid.
],
),
),
);
}
}
drawer.dart:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class SideDrawer extends StatefulWidget {
#override
SideDrawerState createState() => SideDrawerState();
}
class SideDrawerState extends State<SideDrawer> {
static int widgetIndex = 0;
#override
Widget build(BuildContext context) {
return Drawer(
child: Column(
children: <Widget>[
ListTile(
contentPadding: EdgeInsets.only(top: 50),
title: Text('1'),
onTap: () async {
setState(() => widgetIndex = 0);
Navigator.of(context).pop();
},
),
ListTile(
title: Text('2'),
onTap: (){
setState(() => widgetIndex = 1);
Navigator.of(context).pop();
},
),
],
),
);
}
}
You can create a function field in your SideDrawer that takes an index as a parameter.
Call the function passing the appropriate parameter in the onTap of each ListTile.
In your MyApp create a variable with initial value of 0 then when setting the SideDrawer add the onTap attribute then change the value of the in the setState
Like this
class MyApp2 extends StatefulWidget {
#override
MyApp2State createState() => MyApp2State();
}
class MyApp2State extends State<MyApp2> {
var widgetIndex = 0;
#override
Widget build(BuildContext context)
{
return Scaffold(
appBar: AppBar(
title: Text("Home"),
),
body: SafeArea(
child: Container(
child: IndexedStack(
index: widgetIndex,
children: <Widget>[
Text("djdhjhd"),
Text("nonono")
],
),
),
),
drawer: SideDrawer(
onTap: (index){
setState(() {
widgetIndex = index;
});
},
),
);
}
}
class SideDrawer extends StatefulWidget {
final Function(int index) onTap;
SideDrawer({this.onTap});
#override
SideDrawerState createState() => SideDrawerState();
}
class SideDrawerState extends State<SideDrawer> {
#override
Widget build(BuildContext context) {
return Drawer(
child: Column(
children: <Widget>[
ListTile(
contentPadding: EdgeInsets.only(top: 50),
title: Text('1'),
onTap: () async {
widget.onTap(0);
Navigator.of(context).pop();
},
),
ListTile(
title: Text('2'),
onTap: (){
widget.onTap(1);
Navigator.of(context).pop();
},
),
],
),
);
}
}
The output:
I hope this helps you.
This is my scenario:
The App has a Tabbar with 5 Tabs
Some Views have a detail view
Implementation of the detail view:
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (context) => RPlanDetail(lesson))),
(RPlanDetail is the detail view and lesson is the passed data)
The base view
Now to the problem:
When I return to the base view the index on the tabbar is wrong. So the tabbar indicates that you are on the first view while you are on another view.
How can I fix this?
Please let me know if you need additional information.
Thank you for your help!
I have a code demo as below:
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: TabBarDemo(),
),
);
}
class TabBarDemo extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return TabBarDemoState();
}
}
class TabBarDemoState extends State<TabBarDemo> with TickerProviderStateMixin {
static int _index = 0;
TabController _controller;
#override
void initState() {
super.initState();
_controller = TabController(
initialIndex: TabBarDemoState._index, length: 3, vsync: this);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _controller,
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike)),
],
onTap: (int index) {
TabBarDemoState._index = index;
},
),
title: Text('Tabs Demo'),
),
body: TabBarView(
controller: _controller,
children: [
Center(
child: FlatButton(
color: Colors.red,
child: Text("Go to Detail page"),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (ct) => DetailPage(TabBarDemoState._index)));
},
),
),
Center(
child: FlatButton(
color: Colors.red,
child: Text("Go to Detail page"),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (ct) => DetailPage(TabBarDemoState._index)));
},
),
),
Center(
child: FlatButton(
color: Colors.red,
child: Text("Go to Detail page"),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (ct) => DetailPage(TabBarDemoState._index)));
},
),
),
],
),
);
}
}
class DetailPage extends StatelessWidget {
DetailPage(this.index);
final int index;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text("Detail page $index"),
),
);
}
}
I hope it can help you. But I have an advise for you that you should replace Navigator.push to Navigator.pushName and use Navigate with named routes, Because it don't create a new screen. You can read more: https://flutter.dev/docs/cookbook/navigation/named-routes
Set your Tab Controller
inside 'initState();'
Never put Tab Controller in 'build(BuildContext context)'
#override
void initState() {
super.initState();
_tabController = new TabController(length: 3, vsync: this);
}
I am working on this app, where you can navigate to a screen with a stateful widget that has a bunch complex functions that run in initState().
Due to these functions, navigating to this screen takes close to two seconds after the Navigation function is triggered i.e It is very slow
My Code looks like this
#override
void initState(){
someComplexHeavyFunctions();
super.initState();
}
Is there any way to make sure the navigation has completed (fast and smmothly) before running the function in initState() and also, maybe showing a loader while the functions are still processing after the screen has been navigated to?
like this:
You can use the compute function to do the calculation in a different isolate which runs on a different thread.
You can call the initializing function and while awaiting for it, display the dialog.
This is a complete example:
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Test"),
),
body: new Center(
child: FloatingActionButton(
child: Text("Go"),
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (_) => OtherPage()));
},
),
),
);
}
}
class OtherPage extends StatefulWidget {
#override
_OtherPageState createState() => _OtherPageState();
}
class _OtherPageState extends State<OtherPage> {
bool initialized = false;
#override
void initState() {
super.initState();
initialize();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await showDialog<String>(
context: context,
builder: (BuildContext context) => new AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
CircularProgressIndicator(),
SizedBox(height: 40.0,),
Text("Performing task"),
],
),
),
);
});
}
Future<void> initialize() async {
initialized = await Future.delayed(Duration(seconds: 5), () => true);
Navigator.of(context).pop();
setState(() {});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(),
body: new Center(
child: initialized ? Text("Initialized") : Container(),
),
);
}
}