Related
Okay, so I'm building an iOS app in flutter, using firebase_auth, and was experiencing a small error.
Ordinarily with firebase_auth if you've already authorized on the device, it should just pass you through firebase_auth, and into the rest of the program. My program did not do this. It would ask for text message verification each and every time regardless.
So my first instinct as to how to resolve this issue was to verify if I was on the latest versions of firebase_auth, and other dependencies. Verified there were some that were out of date, and so updated them. (Going from 15.3 to 15.4 firebase_auth; along with firebase_core 4.3+1 - 4.4; and a few others that don't effect this)
The ONLY changes were to my pubspec.yaml, and all of a sudden, none of my code works anymore. Firebase never sends a text. Just incase it was actually completing the authorization in the background I added some more print statements, to know where in the code things are happening, but none of those messages get printed out.
I've tried testing this on both an iphone 11 pro max running ios 13.4 and an iphone 5e, running 13.3.1, and the same issue happens on both.
As this was due to the updates to the pubspec.yaml, I tried reverting those changes, (Thank you GitHub) but it hasn't effected the code, and firebase still never sends a text anymore to either phone, with no error message, or print message going to the debug screen. And ideas?
Code as follows:
class LoginPage extends StatelessWidget { // Base Scaffold for Login Page
#override
Widget build(BuildContext context) {
return new CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('Verification'),
),
child: new _PhoneLoginSection(),
);
}
}
class _PhoneLoginSection extends StatefulWidget{
#override
State<StatefulWidget> createState() => _PhoneLoginSectionState();
}
class _PhoneLoginSectionState extends State<_PhoneLoginSection>{
final TextEditingController _phoneNumberController = TextEditingController();
final FirebaseAuth _auth = FirebaseAuth.instance;
// Firebase Authorization request
Future<void> verifyPhone(context) async {
try {
final credential = PhoneAuthProvider.getCredential(
verificationId: this.verificationId,
smsCode: this.smsCode,
);
print('Already Authorized.');
final FirebaseUser user = (await _auth.signInWithCredential(credential)).user;
final FirebaseUser currentUser = await _auth.currentUser();
assert(user.uid == currentUser.uid);
IdTokenResult firebaseIdToken = await user.getIdToken();
setInfo(this.phoneNumber, firebaseIdToken);
toMap(context);
} catch (e) {
print(e);
catchError(e, context);
}
} // End of Authorization request
// SMS Verification Screen Widget
Future<bool> smsCodeDialog(BuildContext context) {
return showCupertinoDialog(
context: context,
builder: (BuildContext context) {
return new AlertDialog(
backgroundColor: currentTheme.primaryColor,
title: Text('Enter SMS Code', style: TextStyle(color: currentTheme.textTheme.textStyle.color),),
content: Column(mainAxisAlignment: MainAxisAlignment.end, children: [
Divider(),
CupertinoTextField(
placeholder: '123456',
autofocus: true,
maxLength: 6,
maxLengthEnforced: true,
textInputAction: TextInputAction.done,
keyboardType: TextInputType.number,
inputFormatters: [WhitelistingTextInputFormatter.digitsOnly],
onSubmitted: (value) {
verifySms(context, value);
},
controller: _phoneNumberController,
),
Divider(),
(errorMessage != ''
? Text(
errorMessage,
style: TextStyle(color: Colors.red),
)
: Container())
]),
contentPadding: EdgeInsets.all(16),
actions: <Widget>[
FlatButton(
child: Text('Submit',
style: TextStyle(color: currentTheme.textTheme.textStyle.color), ),
onPressed: () {
verifySms(context, _phoneNumberController.text);
},
)
],
);
});
}
// END of SMS Verification Screen Widget
// Method to call validation of phone number and credentials.
void openCodePage(context) async {
if (!debugging) {
setState((){codeSent = true;});
final PhoneCodeSent smsCodeSent = (verID, [int forceResendingToken]) async {
this.verificationId = verID;
smsCodeDialog(context).then((value) {});
};
// Complete Phone Authorization, and generate ID Token
final PhoneVerificationCompleted verificationComplete = (AuthCredential credential) async {
validationComplete = true;
print('Authorized only through SMS.');
_auth.signInWithCredential(credential);
FirebaseUser user = await _auth.currentUser();
IdTokenResult firebaseIdToken = await user.getIdToken();
setInfo(this.phoneNumber, firebaseIdToken);
toMap(context);
};
// Error handling. Method required for Firebase Phone Auth.
final PhoneVerificationFailed verificationFailed = (AuthException exception) {
print('Phone Auth Verification Failed: ${exception.message}');
setState((){
codeSent = false;
});
};
// End of Error Handling
// Automatically Sign-In after timeout. Method required for Firebase Phone Auth.
final PhoneCodeAutoRetrievalTimeout timeout = (String verID) {
setState((){
});
print('Code Auto Retreival Timed Out: ${this.verificationId}');
};
// Actual Authorization request. Method required for Firebase Phone Auth.
try {
await _auth.verifyPhoneNumber(
phoneNumber: this.phoneNumber,
timeout: const Duration(seconds: 20),
codeAutoRetrievalTimeout: timeout,
verificationCompleted: verificationComplete,
verificationFailed: verificationFailed,
codeSent: smsCodeSent,
);
} catch(e) {
print('Verify Phone Number Error: $e');
}
}
else {
toMap(context);
}
} // END openCodePage Function
// Error Handling Function for validation errors.
catchError(PlatformException error, context) {
print(error);
switch (error.code) {
case 'ERROR_INVALID_VERIFICATION_CODE':
FocusScope.of(context).requestFocus(new FocusNode());
setState(() {
errorMessage = 'Invalid Code';
});
Navigator.of(context).pop();
smsCodeDialog(context).then((value) {
});
break;
default:
setState(() {
errorMessage = error.message;
});
break;
}
}
//End Error Handling Function
// User Phone Number Input Cleaning
void validatePhone(phoneNum){
if (phoneNum.length < 10){
return;
} else if (phoneNum.length == 11) {
this.phoneNumber = '+' + phoneNum;
setNumber(phoneNum.substring(1));
} else {
this.phoneNumber = '+1' + phoneNum;
setNumber(phoneNum);
}
_phoneNumberController.text = '';
}
// End User Phone Number Input Cleaning
// User SMS Input Cleaning
void verifySms(context, code) async {
if (code.length < 6){
return;
}
this.smsCode = code;
verifyPhone(context);
}
// End SMS Input Cleaning
// Actual Transition Code. Go to Map
void toMap(context){
setHistoryList();
Navigator.of(context).pushAndRemoveUntil(
CupertinoPageRoute(
maintainState: false,
fullscreenDialog: false,
builder: (context) =>
MainAppPage()),
(Route<dynamic> route) => false
);
} // End of Transition Code
// Build base context widget, and accept user input.
#override
Widget build(BuildContext context){
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
child: const Text('Enter your phone number:',
style: TextStyle(color: null), ),
padding: const EdgeInsets.all(16),
),
CupertinoTextField(
placeholder: '(555) 555-5555',
autofocus: true, // DO NOT REMOVE, uncomment out in production environment
clearButtonMode: OverlayVisibilityMode.editing,
maxLength: 11,
maxLengthEnforced: true,
textInputAction: TextInputAction.done,
keyboardType: TextInputType.number,
inputFormatters: [WhitelistingTextInputFormatter.digitsOnly],
onSubmitted: (phoneNum){
validatePhone(phoneNum);
if (this.phoneNumber != null) {
openCodePage(context);
}
},
controller: _phoneNumberController,
),
Container(
child: CupertinoButton(
color: currentTheme.primaryContrastingColor,
pressedOpacity: 0.7,
child: Text('Get Code', style: TextStyle(color: currentTheme.textTheme.textStyle.color),
),
onPressed: (){
validatePhone(_phoneNumberController.text);
if (this.phoneNumber != null) {
openCodePage(context);
}
},
),
alignment: Alignment.bottomLeft,
padding: const EdgeInsets.all(16),
),
],
);
}
}
My first screen is a login screen and it needs to check if the user is logged in to open the home screen directly but I get an error using this check.
I'm doing the check on initState, the condition is returning true, so looks like the problem is with the Navigator.
What is the correct way to skip the first screen if the user is logged in?
Error:
I/flutter (20803): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter (20803): The following assertion was thrown building Navigator-[GlobalObjectKey<NavigatorState>
I/flutter (20803): _WidgetsAppState#8ce27](dirty, state: NavigatorState#db484(tickers: tracking 2 tickers)):
I/flutter (20803): 'package:flutter/src/widgets/navigator.dart': Failed assertion: line 2106 pos 12: '!_debugLocked':
I/flutter (20803): is not true.
I/flutter (20803): Either the assertion indicates an error in the framework itself, or we should provide substantially
I/flutter (20803): more information in this error message to help you determine and fix the underlying cause.
Code:
class LoginScreen extends StatefulWidget {
#override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _emailController = TextEditingController();
final _passController = TextEditingController();
final _formKey = GlobalKey<FormState>();
final _scaffoldKey = GlobalKey<ScaffoldState>();
#override
void initState() {
super.initState();
if(FirebaseAuth.instance.currentUser() != null){
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => HomeScreen()
));
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
body: ScopedModelDescendant<UserModel>(
builder: (context, child, model){
if(model.isLoading)
return Center(
child: CircularProgressIndicator(),
);
return Form(
key: _formKey,
child: ListView(
padding: EdgeInsets.all(16),
children: <Widget>[
SizedBox(height: 67),
Icon(Icons.chrome_reader_mode, size: 150, color: Colors.blue,),
SizedBox(height: 16,),
TextFormField(
controller: _emailController,
decoration: InputDecoration(
hintText: "Digite seu e-mail",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
fillColor: Colors.blueAccent
),
keyboardType: TextInputType.emailAddress,
validator: (text){
if(text.isEmpty || !text.contains("#"))
return "E-mail inválido!";
},
),
SizedBox(height: 16,),
TextFormField(
controller: _passController,
decoration: InputDecoration(
hintText: "Digite sua senha",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
fillColor: Colors.blueAccent
),
obscureText: true,
validator: (text){
if(text.isEmpty || text.length < 6)
return "Digite a senha!";
},
),
SizedBox(height: 16,),
FlatButton(
padding: EdgeInsets.all(13),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)
),
color: Colors.blue,
child: Text("Entrar",
style: TextStyle(
color: Colors.white,
fontSize: 20
),
),
onPressed: (){
if(_formKey.currentState.validate()){
model.signIn(
email: _emailController.text,
pass: _passController.text,
onSuccess: _onSuccess,
onFail: _onFail,
);
}
},
),
SizedBox(height: 10,),
InkWell(
onTap: (){
if(_emailController.text.isEmpty || !_emailController.text.contains("#")){
_scaffoldKey.currentState.showSnackBar(
SnackBar(content: Text("Insira um e-mail válido para recuperação",
style: TextStyle(fontSize: 14),
),
backgroundColor: Colors.redAccent,
duration: Duration(seconds: 3),
)
);
} else {
model.recoverPass(_emailController.text);
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text("O e-mail de recuperação foi enviado!",
style: TextStyle(fontSize: 14),
),
backgroundColor: Colors.green,
duration: Duration(seconds: 3),
)
);
}
},
child: Text("Esqueci minha senha",
style: TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w400
),
textAlign: TextAlign.center,
),
),
SizedBox(height: 30,),
InkWell(
onTap: (){
Navigator.of(context).push(MaterialPageRoute(
builder: (context)=> SignUpScreen())
);
},
child: Text("Não tem conta? Cadastre-se!",
style: TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w600
),
textAlign: TextAlign.center,
),
),
],
),
);
},
),
);
}
}
Well you can solve this kind of problem using another approach. Instead check if there is user logged inside your loginScreen class you can do this a step before and then decide if you will show the loginScreen if there is no user logged or show another screen, MainScreen I' am supposing, if the user is already logged.
I will put some snipet showing how to accomplish this. I hope it helps. But before I will explain you what is wrong in your source code.
if(FirebaseAuth.instance.currentUser() != null){
// wrong call in wrong place!
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => HomeScreen()
));
}
Your code is broken because currentUser() is a async function and when you make the call this function is returning a incomplete Future object which is a non null object. So the navigator pushReplacement is always been called and it's crashing because the state of your widget is not ready yet.
Well as solution you can user FutureBuilder and decide which screen you will open.
int main(){
runApp( YourApp() )
}
class YourApp extends StatelessWidget{
#override
Widget build(BuildContext context){
return FutureBuilder<FirebaseUser>(
future: FirebaseAuth.instance.currentUser(),
builder: (BuildContext context, AsyncSnapshot<FirebaseUser> snapshot){
if (snapshot.hasData){
FirebaseUser user = snapshot.data; // this is your user instance
/// is because there is user already logged
return MainScreen();
}
/// other way there is no user logged.
return LoginScreen();
}
);
}
}
Using this approach you avoid your LoginScreen class to verify if there is a user logged!
As advise you can make use of snapshot.connectionState property with a switch case to implement a more refined control.
Achieved without Firebase, but by using SharedPreferences
find code at gist
here is a simple code: Main.dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final SharedPreferences prefs = await SharedPreferences.getInstance();
var isLoggedIn = (prefs.getBool('isLoggedIn') == null) ? false : prefs.getBool('isLoggedIn');
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
home: isLoggedIn ? anotherPage() : loginPage(),
));
}
using flutter package: shared_preferences
DemoApp if you don't want to use SharedPreferences in Main()
Make use of FirebaseAuth to get the current user. If the user is not logged in the value is null otherwise, the user is logged in.
// Get the firebase user
User firebaseUser = FirebaseAuth.instance.currentUser;
// Define a widget
Widget firstWidget;
// Assign widget based on availability of currentUser
if (firebaseUser != null) {
firstWidget = Home();
} else {
firstWidget = LoginScreen();
}
// Run the app with appropriate screen
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'UniClass',
theme: ThemeData(
primaryColor: kPrimaryColor,
scaffoldBackgroundColor: Colors.white,
),
home: firstWidget,
);
I did it using Shared preferences in main.dart file.
Worked fine for me.
Widget build(BuildContext context) {
return FutureBuilder(
future: SharedPreferences.getInstance(),
builder:
(BuildContext context, AsyncSnapshot<SharedPreferences> prefs) {
var x = prefs.data;
if (prefs.hasData) {
if (x.getBool('isloggedin')) {
if (x.getString('type') == 'doctor') {
return MaterialApp(home: DrHome());
} else
return MaterialApp(home: PtHome());
}
}
return MaterialApp(home: SignIn());
});
}
I don't know if this will be of help for any other person, but my approach was to use Provider and create another class called wrapper, this class will be in charge of switching btw screens without stress... I don't know how you did your auth, But I do all my auth inside another class which I create and name AuthService inside AuthService all auths are done
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<UserModels>(context);
if (user == null) {
return LoginScreen();
} else {
return HomeScreen();
}
}
}
Now for your auth
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
//create user object based on firebase user
UserModels _userFromFirebaseUser(User user) {
return user != null ? UserModels(uid: user.uid) : null;
}
//auth change user stream
Stream<UserModels> get user {
return _auth.authStateChanges().map(_userFromFirebaseUser);
}
// sign in with email and password
Future signInUser(String email, String pwd, {context}) async {
try {
await _auth
.signInWithEmailAndPassword(email: email, password: pwd)
.then((result) {
User user = result.user;
return _userFromFirebaseUser(user);
}).catchError((err) {
if (err.code == 'user-not-found') {
Flushbar(
message: "No user found for that email.",
duration: Duration(seconds: 7),
)..show(context);
} else if (err.code == 'wrong-password') {
Flushbar(
message: "Wrong password provided for that user.",
duration: Duration(seconds: 7),
)..show(context);
} else {
Flushbar(
message: "Internal Error: Something went wrong.",
duration: Duration(seconds: 7),
)..show(context);
}
});
} catch (e) {
print(e.toString());
return null;
}
}
}
Now go to your sign-in button onpress
onPressed: () async {
var form = formKey.currentState;
if (form.validate()) {
setState(() {
_isChecking = true;
});
String email = _email.text;
String pwd = _pwd.text;
await _authService
.signInUser(email, pwd, context: context)
.then((result) {
if (result != null) {
setState(() {
_isChecking = false;
});
} else {
setState(() {
_isChecking = false;
});
}
}).catchError((error) {
setState(() {
_isChecking = false;
});
});
}
},
This should do all the job for you without you thinking about it too much
I used Flutter Secure Storage token. I will delete the token when user logoff explicitly so the null check.
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: UserSecureStorage.getAuthToken(),
builder: (BuildContext context, AsyncSnapshot<String?> token) {
return GetMaterialApp(
title: "app",
home: (token.hasData && token.data != null) ? HomePage() : LoginPage()
);
}
);
}
}
it's working for me. null safety supported.
import 'package:firebase_auth/firebase_auth.dart';
FirebaseAuth auth = FirebaseAuth.instance;
FirebaseAuth.instance
.authStateChanges()
.listen((User? user) {
if (user == null) {
print('User is currently signed out!');
} else {
print('User is signed in!');
}
});
This is what I got, after opening several web pages
Future<void> main() async{
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(); // waiting for firebase initialization to finish
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
Widget build(BuildContext context){
bool isLoggedIn = FirebaseAuth.instance.currentUser != null ? true : false; // check user logged in or not
if (isLoggedIn) { // if user is logged in, open dashboard
return MaterialApp(
home: dashboard(), //dashboard
);
} else {// if user isn't logged in, open login page
return MaterialApp(
home: loginWithGoogle(), //login page
);
}
}
}
use this, I had the same error (Navigator error called on null) and I fixed it.
In your code add await before firebase Auth
just check this solution:
#override
void initState() {
super.initState();
detectUser();
}
void detectUser() async {
setState(() {
loading = true;
});
FirebaseAuth _auth1 = await FirebaseAuth.instance;
if (_auth1.currentUser != null) {
print('email: ' + _auth1.currentUser.email);
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) => Main(),
),
);
setState(() {
loading = false;
});
} else {
//print(user1);
setState(() {
loading = false;
});
}
}
I am navigating the app screen to webview after pressing a row on listview. I have created two routes and it is navigating properly to the webview from listview.
But the height of webview is not matching to the height of device screen, i.e, it is showing the previous route (listview) when I Hot Reload the app, on the below of screen where the webview is not covering.
Below is my main.dart file.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:connectivity/connectivity.dart';
import 'package:toast/toast.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
void main() => runApp(new MyApp());
bool isData = false;
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Shop App',
theme
: new ThemeData(
primaryColor: Color.fromRGBO(58, 66, 86, 1.0), fontFamily: 'Raleway'),
home: MyHomePage(title: 'Shop App'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<bool> inputs = new List<bool>();
List list = List();
var isLoading = false;
var connectivityResult;
_fetchJSON() async {
setState(() {
isLoading = true;
});
var response = await http.get(
'http://indiagovt.org/android/flutter.php',
headers: {"Accept": "Application/json"},
);
if(response.statusCode == 200) {
list = json.decode(response.body) as List;
setState(() {
isLoading = false;
});
} else {
print('Something went wrong');
}
}
#override
void initState() {
super.initState();
setState(() {
_fetchJSON();
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
backgroundColor: Color.fromRGBO(58, 66, 86, 1.0),
appBar: new AppBar(
title: new Text('Shop App'),
),
body: isLoading ? Center (
child: CircularProgressIndicator(),
):
new ListView.builder(
itemCount: list.length,
itemBuilder: (BuildContext context, int index){
return new Card(
child: new Container(
padding: new EdgeInsets.all(10.0),
child: new Column(
children: <Widget>[
new ListTile(
title: new Text(list [index]['title']),
subtitle: new Text(list [index]['descr']),
leading: CircleAvatar(
child: Image.network(
list [index]['icon'],
),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => webView(url: list[index]['link'], title: list[index]['name']),//goes to the next page & passes value of url and title to it
),
);
},
)
],
),
),
);
}
),
);
}
}
class webView extends StatelessWidget {
final String url;
final String title;
webView({Key key, #required this.url, #required this.title}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color.fromRGBO(58, 66, 86, 1.0),
appBar: AppBar(
title: Text(title),
),
body: new MaterialApp(
routes: {
"/": (_) => new WebviewScaffold(
url: url,
appBar: new AppBar(
),
withJavascript: true,
withLocalStorage: true,
)
},
)
);
}
}
App Screenshots:
WebView Screenshot:
Please help me to fix this issue.
I'm new to flutter, and I saw many android apps can exit when double press back button.
The first time press back button, app shows a toast"press again to exit app".
The following second press, app exits.
Of course, the time between two press must be not long.
How to do it in flutter?
This is an example of my code (I've used "fluttertoast" for showing toast message, you can use snackbar or alert or anything else)
DateTime currentBackPressTime;
#override
Widget build(BuildContext context) {
return Scaffold(
...
body: WillPopScope(child: getBody(), onWillPop: onWillPop),
);
}
Future<bool> onWillPop() {
DateTime now = DateTime.now();
if (currentBackPressTime == null ||
now.difference(currentBackPressTime) > Duration(seconds: 2)) {
currentBackPressTime = now;
Fluttertoast.showToast(msg: exit_warning);
return Future.value(false);
}
return Future.value(true);
}
You can try this package.
Inside a Scaffold that wraps all your Widgets, place the DoubleBackToCloseApp passing a SnackBar:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: DoubleBackToCloseApp(
child: Home(),
snackBar: const SnackBar(
content: Text('Tap back again to leave'),
),
),
),
);
}
}
The solution below must be considered deprecated because it causes a few issues that were tackled in the package mentioned. For instance, the app closes if the snack bar was dismissed by the user (see hcbpassos/double_back_to_close_app#2).
Old answer
You can also opt for a solution involving SnackBar. It's not as simple as Andrey Turkovsky's answer, but it's quite more elegant and you won't depend on a library.
class _FooState extends State<Foo> {
static const snackBarDuration = Duration(seconds: 3);
final snackBar = SnackBar(
content: Text('Press back again to leave'),
duration: snackBarDuration,
);
DateTime backButtonPressTime;
#override
Widget build(BuildContext context) {
return Scaffold(
// The BuildContext must be from one of the Scaffold's children.
body: Builder(
builder: (context) {
return WillPopScope(
onWillPop: () => handleWillPop(context),
child: Text('Place your child here'),
);
},
),
);
}
Future<bool> handleWillPop(BuildContext context) async {
final now = DateTime.now();
final backButtonHasNotBeenPressedOrSnackBarHasBeenClosed =
backButtonPressTime == null ||
now.difference(backButtonPressTime) > snackBarDuration;
if (backButtonHasNotBeenPressedOrSnackBarHasBeenClosed) {
backButtonPressTime = now;
Scaffold.of(context).showSnackBar(snackBar);
return false;
}
return true;
}
}
Unfortunately none of them worked for me, I have written one generic class (widget) to handle double tap exit. If someone is interested
class DoubleBackToCloseWidget extends StatefulWidget {
final Widget child; // Make Sure this child has a Scaffold widget as parent.
const DoubleBackToCloseWidget({
#required this.child,
});
#override
_DoubleBackToCloseWidgetState createState() =>
_DoubleBackToCloseWidgetState();
}
class _DoubleBackToCloseWidgetState extends State<DoubleBackToCloseWidget> {
int _lastTimeBackButtonWasTapped;
static const exitTimeInMillis = 2000;
bool get _isAndroid => Theme.of(context).platform == TargetPlatform.android;
#override
Widget build(BuildContext context) {
if (_isAndroid) {
return WillPopScope(
onWillPop: _handleWillPop,
child: widget.child,
);
} else {
return widget.child;
}
}
Future<bool> _handleWillPop() async {
final _currentTime = DateTime.now().millisecondsSinceEpoch;
if (_lastTimeBackButtonWasTapped != null &&
(_currentTime - _lastTimeBackButtonWasTapped) < exitTimeInMillis) {
Scaffold.of(context).removeCurrentSnackBar();
return true;
} else {
_lastTimeBackButtonWasTapped = DateTime.now().millisecondsSinceEpoch;
Scaffold.of(context).removeCurrentSnackBar();
Scaffold.of(context).showSnackBar(
_getExitSnackBar(context),
);
return false;
}
}
SnackBar _getExitSnackBar(
BuildContext context,
) {
return SnackBar(
content: Text(
'Press BACK again to exit!',
color: Colors.white,
),
backgroundColor: Colors.red,
duration: const Duration(
seconds: 2,
),
behavior: SnackBarBehavior.floating,
);
}
}
Use this class following way:
class Dashboard extends StatelessWidget {
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: DoubleBackToCloseWidget(
child: Container(
child: Column(
children: [
const Text('Hello there'),
const Text('Hello there again'),
],
),
),
),
),
);
}
}
The first time press back button, app shows a AlertDialog"press yes to exit app and press No to can't exit application".
This is an example of my code (I've used 'AlertDialog')
#override
Widget build(BuildContext context) {
return new WillPopScope(
onWillPop: _onBackPressed,
child: DefaultTabController(
initialIndex: _selectedIndex,
length: choices.length,
child: Scaffold(
appBar: AppBar(
),
),
),
);
}
Future<bool> _onBackPressed() {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Are you sure?'),
content: Text('Do you want to exit an App'),
actions: <Widget>[
FlatButton(
child: Text('No'),
onPressed: () {
Navigator.of(context).pop(false);
},
),
FlatButton(
child: Text('Yes'),
onPressed: () {
Navigator.of(context).pop(true);
},
)
],
);
},
) ?? false;
}
This is my answer. I used AlertDialog() to achieve this
#override
Widget build(BuildContext context) {
return new WillPopScope(
onWillPop: _onBackPressed,
child: Scaffold(
appBar: AppBar(),
body: Container(),
),
);
}
Future<bool> _onBackPressed() {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Confirm'),
content: Text('Do you want to exit the App'),
actions: <Widget>[
FlatButton(
child: Text('No'),
onPressed: () {
Navigator.of(context).pop(false); //Will not exit the App
},
),
FlatButton(
child: Text('Yes'),
onPressed: () {
Navigator.of(context).pop(true); //Will exit the App
},
)
],
);
},
) ?? false;
}
Simply use double_back_to_close_app library
https://pub.dev/packages/double_back_to_close_app
Add double_back_to_close_app under dependencies in pubspec.yaml file
dependencies:
double_back_to_close_app: ^1.2.0
Here example code
import 'package:double_back_to_close_app/double_back_to_close_app.dart';
import 'package:flutter/material.dart';
void main() => runApp(Example());
class Example extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: DoubleBackToCloseApp(
snackBar: const SnackBar(
content: Text('Tap back again to leave'),
),
child: Center(
child: OutlineButton(
child: const Text('Tap to simulate back'),
// ignore: invalid_use_of_protected_member
onPressed: WidgetsBinding.instance.handlePopRoute,
),
),
),
),
);
}
}
Just move your body contents to "DoubleBackToCloseApp's" child
The best solution without using a package use System
SystemChannels.platform.invokeMethod<void>('SystemNavigator.pop');
or
SystemNavigator.pop();
Full Code
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
class ExitApp extends StatefulWidget {
final Widget child;
const ExitApp({
Key? key,
required this.child,
}) : super(key: key);
#override
_ExitAppState createState() => _ExitAppState();
}
class _ExitAppState extends State<ExitApp> {
#override
build(BuildContext context) {
DateTime timeBackPressed = DateTime.now();
return WillPopScope(
child: widget.child,
onWillPop: () async {
final differeance = DateTime.now().difference(timeBackPressed);
timeBackPressed = DateTime.now();
if (differeance >= Duration(seconds: 2)) {
final String msg = 'Press the back button to exit';
Fluttertoast.showToast(
msg: msg,
);
return false;
} else {
Fluttertoast.cancel();
SystemNavigator.pop();
return true;
}
},
);
}
}
https://pub.dev/packages/flutter_close_app
This is my solution, it is very flexible and simple, does not depend on routing navigation, any page can close the App, such as my login page, and if it is Drawer and PageView, it can also flexibly support custom conditions, and does not need to rely on native method. The following functions are supported:
✅ Press back 2 times to close app
✅ Custom time interval
✅ Customize the prompt message
✅ Customize matching conditions
✅ Support Android
✅ One click to close app
✅ Support iOS
✅ Support MacOS
✅ Support Windows
✅ Support Linux
Easy to Use and Understand, double tap to exit;
Change the duration to 10000, and short toast message time;
import 'dart:io';
bool back = false;
int time = 0;
int duration = 1000;
Future<bool> willPop() async{
int now = DateTime.now().millisecondsSinceEpoch;
if(back && time >= now){
back = false;
exit(0);
}else{
time = DateTime.now().millisecondsSinceEpoch+ duration;
print("again tap");
back = true;
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Press again the button to exit")));
}
return false;
}
return WillPopScope(
onWillPop: onWill,
child: Scaffold()
);
If you want a snackbar you should provide a scaffold key as it's related to a scaffold, so this key should make the trick of calling a snackbar outside of it's scaffold parent.
Here is a solution :
class Home extends StatelessWidget {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async{
DateTime initTime = DateTime.now();
popped +=1;
if(popped>=2) return true;
await _scaffoldKey.currentState.showSnackBar(
SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('Tap one more time to exit.',textAlign: TextAlign.center,),
duration: Duration(seconds: 2),
)).closed;
// if timer is > 2 seconds reset popped counter
if(DateTime.now().difference(initTime)>=Duration(seconds: 2)) {
popped = 0;
}
return false;
},
child: Scaffold(
key: _scaffoldKey,
appBar: AppBar(title : Text("Demo")),
body: Text("body")
);
)
}
This is my solution, you can change backPressTotal value to the number of pressed you want!
int backPressCounter = 0;
int backPressTotal = 2;
#override
Widget build(BuildContext context) {
return Scaffold(
...
body: WillPopScope(child: getBody(), onWillPop: onWillPop),
);
}
Future<bool> onWillPop() {
if (backPressCounter < 2) {
Fluttertoast.showToast(msg: "Press ${backPressTotal - backPressCounter} time to exit app");
backPressCounter++;
Future.delayed(Duration(seconds: 1, milliseconds: 500), () {
backPressCounter--;
});
return Future.value(false);
} else {
return Future.value(true);
}
}
If the condition is that the user presses only twice, you can use the first solution of course.
If you want to increase the number of times you click, you can use this solution. Where the user has to press 3 times within two seconds so he can get out
DateTime currentBackPressTime;
/// init counter of clicks
int pressCount=1;
then :
Future<bool> onWillPop() async {
DateTime now = DateTime.now();
/// here I check if number of clicks equal 3
if(pressCount!=3){
///should be assigned at the first click.
if(pressCount ==1 )
currentBackPressTime = now;
pressCount+=1;
return Future.value(false);
}else{
if (currentBackPressTime == null ||
now.difference(currentBackPressTime) > Duration(seconds: 2)) {
currentBackPressTime = now;
pressCount=0;
return Future.value(false);
}
}
return Future.value(true);
}
You can look for time duration between the two consecutive back button clicks, and if the difference is within the desired duration then exit the app.
Here is the complete code sample for the counter app, which exits the app only if the difference between two consecutive back button clicks is less than 1 second (1000 ms)
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
void showSnackBar() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
behavior: SnackBarBehavior.floating,
duration: Duration(milliseconds: 600),
margin: EdgeInsets.only(bottom: 0, right: 32, left: 32),
content: Text('Tap back button again to exit'),
),
);
}
void hideSnackBar() {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
}
DateTime oldTime = DateTime.now();
DateTime newTime = DateTime.now();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: WillPopScope(
onWillPop: () async {
newTime = DateTime.now();
int difference = newTime.difference(oldTime).inMilliseconds;
oldTime = newTime;
if (difference < 1000) {
hideSnackBar();
return true;
} else {
showSnackBar();
return false;
}
},
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
DateTime BackPressTime = DateTime.now();
#override
Widget build(BuildContext context) {
return Scaffold(
body: WillPopScope(
child: Home(),
onWillPop: exiteApp,
),
);
}
Future<bool> exiteApp() {
print("exite app");
DateTime now = DateTime.now();
if(now.difference(BackPressTime)< Duration(seconds: 2)){
return Future(() => true);
}
else{
BackPressTime = DateTime.now();
Fluttertoast.showToast(msg: "Press agin");
return Future(()=> false);
}
}
I built an app to use Shared Preferences package to store a user's chosen locale that will override whatever is the current locale by following this example.
The example worked as it should but currently I am trying to find a way to combine the shared preferences package with the tutorial so that users could save their language of choice.
This is the current code:
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_localization_intl/locale/locales.dart';
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
MyAppState createState() {
return new MyAppState();
}
}
Future<bool> saveLocalePreference(SpecifiedLocalizationDelegate delegate) async{
SharedPreferences preferences = await SharedPreferences.getInstance();
preferences.setString("delegate", delegate.toString());
return preferences.commit();
}
class MyAppState extends State<MyApp> {
bool isSaved = false;
SpecifiedLocalizationDelegate _localeOverrideDelegate;
#override
void initState() {
_localeOverrideDelegate = new SpecifiedLocalizationDelegate(null);
super.initState();
}
onLocaleChange(Locale locale) {
setState(() {
_localeOverrideDelegate = new SpecifiedLocalizationDelegate(locale);
saveLanguage(_localeOverrideDelegate);
});
}
void makeDialog(){
showDialog(
context: context,
builder: (_) => new AlertDialog(
content: new Text("Locale has been Saved!")
)
);
}
void saveLanguage(SpecifiedLocalizationDelegate delegate){
saveLocalePreference(delegate).then((bool commited){
isSaved = commited;
setState(() {
commited = true;
print(commited);
});
makeDialog();
});
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
localizationsDelegates: [
_localeOverrideDelegate,
AppLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
Locale('en', ""),
Locale('es', ""),
Locale('fr', "")
],
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(onLocaleChange: onLocaleChange),
);
}
}
typedef void LocaleChangeCallback(Locale locale);
class MyHomePage extends StatefulWidget {
final LocaleChangeCallback onLocaleChange;
MyHomePage({Key key, this.onLocaleChange}) : super(key: key);
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
Locale myLocale = Localizations.localeOf(context);
print(myLocale);
return new Scaffold(
appBar: new AppBar(
title: new Text(
AppLocalizations.of(context).title
),
),
body: new Center(
child: new Column(
children: <Widget>[
new MaterialButton(
child: new Text(AppLocalizations.of(context).buttonText),
onPressed: (){}),
new MaterialButton(
child: new Text("ENGLISH"),
onPressed: (){
widget.onLocaleChange(const Locale("en", ""));
}),
new MaterialButton(
child: new Text("SPANISH"),
onPressed: (){
widget.onLocaleChange(const Locale("es", ""));
}),
new MaterialButton(
child: new Text("FRENCH"),
onPressed: (){
widget.onLocaleChange(const Locale("fr", ""));
}),
new MaterialButton(
child: new Text("DEFAULT"),
onPressed: (){
widget.onLocaleChange(null);
}),
],
),
),
);
}
}
The problem is:
The shared preferences does not seem to work as it will still go back to the default language whenever the app restarts. I tried removing the initState() but it will return this error -
I/flutter ( 5254): The getter 'type' was called on null.
I/flutter ( 5254): Receiver: null
What can I do to make this work?
did you try ?
if(delegate!=null){
preferences.setString("delegate", delegate.toString());
}