Flutter back button - flutter

I have added following code to my main screen in order to exit app after 2 times back button taps
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
final timeGap = DateTime.now().difference(preBackpress);
final cantExit = timeGap >= const Duration(seconds: 2);
preBackpress = DateTime.now();
if(cantExit){
const snack = SnackBar(
content: Text('Press Back button again to Exit'),
duration: Duration(seconds: 2),
);
ScaffoldMessenger.of(context).showSnackBar(snack);
return false;
}else{
return true;
}
},
child: Scaffold(....)
);
}
But what I get instead is that when I hit back button it goes all the steps back to every single screen user has been visited.
The reason I added WillPopScope above was to avoid going back to visited screen and just close the app but it seems still doesn't help.
My question is: How do I exit app (when in main screen back button is tapped) without going back to visited screens?

Maybe this is not the same as your code. but I have code to close apps as you want
class ExitController extends StatefulWidget {
#override
_ExitControllerState createState() => _ExitControllerState();
}
class _ExitControllerState extends State<ExitController> {
static const snackBarDuration = Duration(milliseconds: 2000);
// set how long the snackbar appears
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
DateTime? backButtonPressTime;
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
// margin: EdgeInsets.fromLTRB(75, 0, 75, 250),
// add margin so that the snackbar is not at the bottom
duration: snackBarDuration,
backgroundColor: Colors.black87,
content: Text('Press Again to Exit',
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)),
);
Future<bool> onWillPop() async {
DateTime currentTime = DateTime.now();
bool backButtonCondition =
backButtonPressTime == null ||
currentTime.difference(backButtonPressTime!) > snackBarDuration;
if (backButtonCondition) {
backButtonPressTime = currentTime;
ScaffoldMessenger.of(context).showSnackBar(snackBar);
return false;
}
return true;
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey,
body: WillPopScope(
onWillPop: onWillPop,
child: yourPageClass(),
),
);
}
}
The snackbar will appear for 2000 milliseconds and during that time if the user press the back button again, the application will exit
Note to call ExitController class in main.dart, and wrapping your all structure(navigation class/page class/etc) with this ExitController class.
Example:
main.dart
import 'package:flutter/material.dart';
import 'package:app_name/structure.dart';
void main(){
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ExitController(),
);
}
}

WillPopScope(
onWillPop: () async {
return await showDialog(context: ctx, builder: (_) => exitAlertDialog(_));
},
child: GestureDetector(onTap: () => FocusScope.of(ctx).unfocus(), child: widget));
Widget exitAlertDialog(BuildContext context) {
return AlertDialog(
backgroundColor: plColor2,
content: Text('Are you sure you want to exit?'),
actions: <Widget>[
TextButton(child: Text('No'), onPressed: () => Navigator.of(context).pop(false)),
TextButton(child: Text('Yes, exit'), onPressed: () => SystemNavigator.pop())
],
);
}
or you can use showsnackbar as your need

Related

Main app widget: setState call -> build() builds all widgets but screen not updated

App is a simple memory/guessing game with a grid of squares. Floating action button triggers a "New game" dialog, and a Yes response triggers setState() on the main widget. The print() calls show it is building all the Tile widgets in the grid, but as it returns, the old grid values are still showing. Probably done something stupid but not seeing it. Basic code is below. TIA if anyone can see what is missing/invalid/broken/etc.
Main.dart is the usual main() that creates a stateless HomePage which creates a stateful widget which uses this State:
class MemHomePageState extends State<MemHomePage> {
GameBoard gameBoard = GameBoard();
GameController? gameController;
int gameCount = 0, winCount = 0;
#override
void initState() {
super.initState();
gameController = GameController(gameBoard, this);
}
#override
Widget build(BuildContext context) {
if (kDebugMode) {
print("MemHomepageState::build");
}
gameBoard.newGame(); // Resets secrets and grids
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: GridView.count(
crossAxisCount: Globals.num_columns,
children: List.generate(Globals.num_columns * Globals.num_rows, (index) {
int x = index~/Globals.NR, y = index%Globals.NR;
int secret = gameBoard.secretsGrid![x][y];
var t = Tile(x, y, Text('$secret'), gameController!);
gameBoard.tilesGrid![x].add(t);
if (kDebugMode) {
print("Row $x is ${gameBoard.secretsGrid![x]} ${gameBoard.tilesGrid![x][y].secret}");
}
return t;
}),
),
// Text("You have played $gameCount games and won $winCount."),
),
floatingActionButton: FloatingActionButton(
onPressed: () => newGameDialog("Start a new game?"),
tooltip: 'New game?',
child: const Icon(Icons.refresh_outlined),
),
);
}
/** Called from the FAB and also from GameController "won" logic */
void newGameDialog(String message) {
showDialog<void>(
context: context,
barrierDismissible: false, // means the user must tap a button to exit the Alert Dialog
builder: (BuildContext context) {
return AlertDialog(
title: Text("New game?"),
content: Text(message),
//),
actions: <Widget>[
TextButton(
child: const Text('Yes'),
onPressed: () {
setState(() {
gameCount++;
});
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('No'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
}
);
}
The Tile class is a StatefulWidget whose state determines what that particular tile should show:
import 'package:flutter/material.dart';
import 'gamecontroller.dart';
enum TileMode {
SHOWN,
HIDDEN,
CLEARED,
}
/// Represents one Tile in the game
class Tile extends StatefulWidget {
final int x, y;
final Widget secret;
final GameController gameController;
TileState? tileState;
Tile(this.x, this.y, this.secret, this.gameController, {super.key});
#override
State<Tile> createState() => TileState(x, y, secret);
setCleared() {
tileState!.setCleared();
}
}
class TileState extends State<Tile> {
final int x, y;
final Widget secret;
TileMode tileMode = TileMode.HIDDEN;
TileState(this.x, this.y, this.secret);
_unHide() {
setState(() => tileMode = TileMode.SHOWN);
widget.gameController.clicked(widget);
}
reHide() {
print("rehiding");
setState(() => tileMode = TileMode.HIDDEN);
}
setCleared() {
print("Clearing");
setState(() => tileMode = TileMode.CLEARED);
}
_doNothing() {
//
}
#override
Widget build(BuildContext context) {
switch(tileMode) {
case TileMode.HIDDEN:
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
),
onPressed: _unHide,
child: Text(''));
case TileMode.SHOWN:
return ElevatedButton(
onPressed: _doNothing,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
child: secret);
case TileMode.CLEARED:
return ElevatedButton(
onPressed: _doNothing,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black12,
),
child: const Icon(Icons.check));
}
}
}
it looks like you are calling the following in your build function. That would cause everything to reset everytime it builds. Perhaps it belongs in init instead?
gameBoard.newGame(); // Resets secrets and grids
The original problem is that the Tile objects, although correctly created and connected to the returned main widget, did not have distinct 'key' values so they were not replacing the originals. Adding 'key' to the Tile constructor and 'key: UniqueKey()' to each Tile() in the loop, solved this problem. It exposed a related problem but is out of scope for this question. See the github link in the OP for the latest version.

How change Future.delayed() duration, or circumvent having to do this

I have list of Dismissible widgets that I'd like to swipe left to delete. These have a an argument confirmDismis which takes a Future<bool>. I'd like to show a SnackBar once you've swiped an item allowing you to undo the delete for a few seconds. For this I have this method:
Future<bool> _confirmDismiss() {
var remove = true;
var duration = Duration(seconds: 5);
final snackbar = SnackBar(
duration: duration,
content: Text('This item was deleted'),
action: SnackBarAction(
label: 'Undo',
onPressed: () {
remove = false; //how to return false immediately?
},
),
);
_scaffoldKey.currentState.showSnackBar(snackbar);
return Future.delayed(duration,(){
return remove;
});
}
The snackbar is shown for 5 seconds, after that it disappears and the callback is triggered.
Now I'd like to return the item as soon as you hit the undo button. Right now you have to wait for the 5 seconds to pass for it to return.
I have tried timeout and Time() and many other things but I can't seem to make it work... Any ideas?
#Janneman, I don't think you need to use Future.delayed, that could be the reason why you now have to wait 5 secs. Just use the default SnackBar functions. Here is a quick example that shows SnackBar for 5 secs and quickly changes the text on Undo,
class MyApp extends StatefulWidget {
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
String text = 'Yay! A Snackbar';
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('SnackBar'),
),
body: Builder(builder: (BuildContext context) {
return Padding(
padding: EdgeInsets.all(20.0),
child: Center(
child: Column(children: <Widget>[
Text(text),
RaisedButton(
onPressed: () {
text = 'Snackbar changed';
setState(() {
});
final snackBar = SnackBar(
duration: Duration(seconds: 5),
content: Text(text),
action: SnackBarAction(
label: 'Undo',
onPressed: () {
text = 'Yay! A Snackbar';
setState(() {});
},
),
);
// Find the Scaffold in the widget tree and use
// it to show a SnackBar.
Scaffold.of(context).showSnackBar(snackBar);
},
child: Text('Show SnackBar'),
),
])));
})));
}
}
Hope this helps. Good luck!

How to set text values to change automatically in flutter?

I'm new to flutter and developing an app in which vehicle speed is shown in the floating action button in Scaffold. But I want it to change according to speed automatically so that it doesn't need to refresh/restart manually every time.
Here's my code.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
double speedInMps;
double speedInKph;
var geolocator = Geolocator();
var locationOptions = LocationOptions(accuracy: LocationAccuracy.high,
distanceFilter: 10);
Future<void> getVehicleSpeed()async{
try{
geolocator.getPositionStream((locationOptions)).listen((position) async
{
speedInMps = await position.speed;
speedInKph = speedInMps * 1.609344;
print(speedInKph.round());
});
}catch(e){
print(e);
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold( floatingActionButton: FloatingActionButton(
onPressed: () {getVehicleSpeed();
},
child: Text(speedInKph.round().toString() +'Km/h'),//Need Improvments
Here
backgroundColor: Colors.green,
),
appBar: AppBar(
title: Text('speed'),
centerTitle: true,
),
body: Center(
child: FlatButton(
onPressed: getVehicleSpeed,
child: Text(
speedInKph.toString(),
style: TextStyle(fontSize: 16.0),
),
color: Color(0xffdd4b39),
textColor: Colors.white,
padding: const EdgeInsets.all(20.0),
),
),
)
);
}
}
I have to hot reload/restart to get updated speed, but I want it to refresh speed automatically.
You need to listen location only once. So put in initState which called when widget is initialized.
#override
void initState() {
super.initState();
getVehicleSpeed();
}
And than call setState method when data is change. It will rebuild the widget.
Future<void> getVehicleSpeed() async {
try {
geolocator.getPositionStream((locationOptions)).listen((position) async {
speedInMps = position.speed;
setState(() {
speedInKph = speedInMps * 1.609344;
});
print(speedInKph.round());
});
} catch (e) {
print(e);
}
}

How to properly display a Snackbar in Flutter? [duplicate]

This question already has answers here:
Display SnackBar in Flutter
(20 answers)
Closed 10 months ago.
I am trying to show a Snackbar on click of a floatingActionbutton. But when I click on the floatingactionbutton it's not showing anything. Here is my code. I am using a StatefulWidget. I debugged and checked that the onPressed function is also getting executed but somehow the Snackbar is not visible. What can be the root cause of the issue? I feel the BuildContext I am passing has some issue.
class MyApp extends StatefulWidget{
#override
MyAppState createState() {
// TODO: implement createState
return new MyAppState();
}
}
class MyAppState extends State<MyApp>{
File _image;
String _text;
Future getImage() async {
var image = await ImagePicker.pickImage(source: ImageSource.camera);
_image = image;
final FirebaseVisionImage visionImage = FirebaseVisionImage.fromFile(_image);
final TextRecognizer textRecognizer = FirebaseVision.instance.textRecognizer();
final VisionText visionText = await textRecognizer.processImage(visionImage);
String detectedText = visionText.text;
setState(() {
_image = image;
_text = detectedText;
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: new AppBar(
title: new Text('Image Picker Example'),
),
body: new Center(
child: _image == null
? new Text('No image selected.')
: new Image.file(_image),
),
floatingActionButton: new FloatingActionButton(
onPressed: (){
showSnackBar(context);
// getImage();
},
tooltip: 'Pick Image',
child: new Icon(Icons.add_a_photo),
),
),
);
}
void showSnackBar(BuildContext context) {
final scaffold = Scaffold.of(context);
final snackBarContent = SnackBar(
content: Text("sagar"),
action: SnackBarAction(
label: 'UNDO', onPressed: scaffold.hideCurrentSnackBar),
);
scaffold.showSnackBar(snackBarContent);
}
}
That happens because the BuildContext used has not a Scaffold ancestor thus, won't be able to find it to render a SnackBar since it's up to the Scaffold to display it.
According to the of method documentation:
When the Scaffold is actually created in the same build function, the
context argument to the build function can't be used to find the
Scaffold (since it's "above" the widget being returned). In such
cases, the following technique with a Builder can be used to provide a
new scope with a BuildContext that is "under" the Scaffold:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Demo')
),
body: Builder(
// Create an inner BuildContext so that the onPressed methods
// can refer to the Scaffold with Scaffold.of().
builder: (BuildContext context) {
return Center(
child: RaisedButton(
child: Text('SHOW A SNACKBAR'),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('Hello!'),
));
},
),
);
},
),
);
}
Solution
Wrapping your FloatingActionButton in a Builder widget will make it possible in a more elegant way than using a GlobalKey, which was already mentioned by the #Epizon answer.
class MyApp extends StatefulWidget{
#override
MyAppState createState() {
// TODO: implement createState
return new MyAppState();
}
}
class MyAppState extends State<MyApp>{
final GlobalKey<ScaffoldState> _scaffoldkey = new GlobalKey<ScaffoldState>();
File _image;
String _text;
Future getImage() async {
var image = await ImagePicker.pickImage(source: ImageSource.camera);
_image = image;
final FirebaseVisionImage visionImage = FirebaseVisionImage.fromFile(_image);
final TextRecognizer textRecognizer = FirebaseVision.instance.textRecognizer();
final VisionText visionText = await textRecognizer.processImage(visionImage);
String detectedText = visionText.text;
setState(() {
_image = image;
_text = detectedText;
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
key: _scaffoldkey,
appBar: new AppBar(
title: new Text('Image Picker Example'),
),
body: new Center(
child: _image == null
? new Text('No image selected.')
: new Image.file(_image),
),
floatingActionButton: new FloatingActionButton(
onPressed: (){
showSnackBar();
// getImage();
},
tooltip: 'Pick Image',
child: new Icon(Icons.add_a_photo),
),
),
);
}
void showSnackBar() {
final snackBarContent = SnackBar(
content: Text("sagar"),
action: SnackBarAction(
label: 'UNDO', onPressed: _scaffoldkey.currentState.hideCurrentSnackBar),
);
_scaffoldkey.currentState.showSnackBar(snackBarContent);
}
}
To show SnackBar You can use like this:
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('User Logged In'),
));
Previous SnackBar was used like this:
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('Rahul Kushwaha!'),
));
Now, SnackBar is managed by ScaffoldMessenger. For details study this link
I faced this issue when trying to showSnackbar() in StatfulWidget from second level child of the parent Parent() => childLevel1 => childOfLevel1(){showing the snackbar here}
I implemented #Epizon answer but .currentState.showSnackBar(snackBarContent); was deprecated, According to flutter:
The SnackBar API within the Scaffold is now handled by the ScaffoldMessenger
read full document
so replacing this line in #Epizon answer:
_scaffoldKey.currentState!.showSnackBar(_snackBarContent);
to this:
ScaffoldMessenger.of(_scaffoldKey.currentState!.context) .showSnackBar(_snackBarContent);
will solve the deprecation issue
here is the code
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
void _showSnack(String msg) {
final _snackBarContent = SnackBar(content: Text(msg));
ScaffoldMessenger.of(_scaffoldKey.currentState!.context)
.showSnackBar(_snackBarContent);
}
return MaterialApp(
home: Scaffold(
//resizeToAvoidBottomInset: true,
key: _scaffoldKey,
body: Center(
child: ElevatedButton(
child: Text("show snackbar"),
onPressed(){_showSnack("im located in the btn's onPressed");},
);
);

How to implement a Slider within an AlertDialog in Flutter?

I am learning app development on Flutter and can't get my Slider to work within the AlertDialog. It won't change it's value.
I did search the problem and came across this post on StackOverFlow:
Flutter - Why slider doesn't update in AlertDialog?
I read it and have kind of understood it. The accepted answer says that:
The problem is, dialogs are not built inside build method. They are on a different widget tree. So when the dialog creator updates, the dialog won't.
However I am not able to understand how exactly does it have to be implemented as not enough background code is provided.
This is what my current implementation looks like:
double _fontSize = 1.0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(qt.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.format_size),
onPressed: () {
getFontSize(context);
},
),
],
),
body: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 15.0),
itemCount: 3,
itemBuilder: (context, index) {
if (index == 0) {
return _getListTile(qt.scripture, qt.reading);
} else if (index == 1) {
return _getListTile('Reflection:', qt.reflection);
} else {
return _getListTile('Prayer:', qt.prayer);
}
})
);
}
void getFontSize(BuildContext context) {
showDialog(context: context,builder: (context){
return AlertDialog(
title: Text("Font Size"),
content: Slider(
value: _fontSize,
min: 0,
max: 100,
divisions: 5,
onChanged: (value){
setState(() {
_fontSize = value;
});
},
),
actions: <Widget>[
RaisedButton(
child: Text("Done"),
onPressed: (){},
)
],
);
});
}
Widget parseLargeText(String text) {...}
Widget _getListTile(String title, String subtitle) {...}
I understand that I will need to make use of async and await and Future. But I am not able to understand how exactly. I've spent more than an hour on this problem and can't any more. Please forgive me if this question is stupid and noobish. But trust me, I tried my best.
Here is a minimal runnable example. Key points:
The dialog is a stateful widget that stores the current value in its State. This is important because dialogs are technically separate "pages" on your app, inserted higher up in the hierarchy
Navigator.pop(...) to close the dialog and return the result
Usage of async/await
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
double _fontSize = 20.0;
void _showFontSizePickerDialog() async {
// <-- note the async keyword here
// this will contain the result from Navigator.pop(context, result)
final selectedFontSize = await showDialog<double>(
context: context,
builder: (context) => FontSizePickerDialog(initialFontSize: _fontSize),
);
// execution of this code continues when the dialog was closed (popped)
// note that the result can also be null, so check it
// (back button or pressed outside of the dialog)
if (selectedFontSize != null) {
setState(() {
_fontSize = selectedFontSize;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Font Size: ${_fontSize}'),
RaisedButton(
onPressed: _showFontSizePickerDialog,
child: Text('Select Font Size'),
)
],
),
),
);
}
}
// move the dialog into it's own stateful widget.
// It's completely independent from your page
// this is good practice
class FontSizePickerDialog extends StatefulWidget {
/// initial selection for the slider
final double initialFontSize;
const FontSizePickerDialog({Key key, this.initialFontSize}) : super(key: key);
#override
_FontSizePickerDialogState createState() => _FontSizePickerDialogState();
}
class _FontSizePickerDialogState extends State<FontSizePickerDialog> {
/// current selection of the slider
double _fontSize;
#override
void initState() {
super.initState();
_fontSize = widget.initialFontSize;
}
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Font Size'),
content: Container(
child: Slider(
value: _fontSize,
min: 10,
max: 100,
divisions: 9,
onChanged: (value) {
setState(() {
_fontSize = value;
});
},
),
),
actions: <Widget>[
FlatButton(
onPressed: () {
// Use the second argument of Navigator.pop(...) to pass
// back a result to the page that opened the dialog
Navigator.pop(context, _fontSize);
},
child: Text('DONE'),
)
],
);
}
}
You just need to warp the AlertDialog() with a StatefulBuilder()