Detect when we moved back to previous page in Flutter - flutter

We moved from Page1 to Page2 but now from Page2 we move back again to Page1 like this:
Navigator.of(context).pop();
How can we detect on Page1 that we went back?

Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NextPage(),
),
).then((_){
// Here you will get callback after coming back from NextPage()
// Do your code here
});

In your Page1, When you push Page2 wait for it to pop
Future<void> _goToPage2() async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Page2(),
),
);
print("Page2 is popped");
}

Another solution, which is more verbose but also more efficient if you push a lot of Routes from the same Widget, would be to create your own NavigatorObserver.
1- Create your NavigatorObserver
final routeObserver = MyRouteObserver();
class MyRouteObserver extends NavigatorObserver {
final Set<RouteAware> _listeners = <RouteAware>{};
void subscribe(RouteAware routeAware) {
_listeners.add(routeAware);
}
void unsubscribe(RouteAware routeAware) {
_listeners.remove(routeAware);
}
#override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
for (var listener in _listeners) {
listener.didPop();
}
}
}
2- Add it to your MaterialApp
return MaterialApp(
navigatorObservers: [routeObserver],
...
3- Implement RouteAware in the Widget where you want to listen to Navigation events, and subscribe/unsubscribe to your routeObserver
class _TablePageState extends State<TablePage> implements RouteAware {
#override
void initState() {
super.initState();
routeObserver.subscribe(this);
}
#override
Widget build(BuildContext context) {
return Container();
}
#override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
#override
void didPop() {
//This method will be called from your observer
}
#override
void didPopNext() {}
#override
void didPush() {}
#override
void didPushNext() {}
}

Related

Flutter show screen lock if app is resumed from inactive state

I want to show the lock screen from this Flutter extension (https://pub.dev/packages/flutter_screen_lock) when the app is resumed from a pause state.
For this I want to use the following code:
screenLock(
context: context,
correctString: '1234',
);
I tried to detect the app status with this extension (https://pub.dev/packages/is_lock_screen/example) and then display the lock screen. I tried this as follows:
void didChangeAppLifecycleState(AppLifecycleState state) async {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.inactive) {
print('app inactive, is lock screen: ${await isLockScreen()}');
} else if (state == AppLifecycleState.resumed) {
screenLock(
context: context,
correctString: '1234',
);
}
}
However, I get the following error message:
E/flutter (16888): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled exception: navigator operation was requested with a context that does not contain a navigator.
E/flutter (16888): the context used to push or remove routes from navigator must be that of a widget that is a descendant of a navigator widget.
What is the correct procedure here?
This error can still happen when you use a context that is a parent of
MaterialApp/WidgetsApp.
Wrap your widget tree in a MaterialApp or WidgetsApp.
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(home: LifeCycleManager());//your widget tree in a MaterialApp or WidgetsApp.
}
}
class LifeCycleManager extends StatefulWidget {
const LifeCycleManager({key}) : super(key: key);
_LifeCycleManagerState createState() => _LifeCycleManagerState();
}
class _LifeCycleManagerState extends State<LifeCycleManager>
with WidgetsBindingObserver {
#override
void initState() {
WidgetsBinding.instance?.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance?.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) async {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.inactive) {
print('app inactive, is lock screen: ${await isLockScreen()}');
} else if (state == AppLifecycleState.resumed) {
screenLock(
context: context,
correctString: '1234',
);
}
}
#override
Widget build(BuildContext context) {
return Container();
}
}
Use a GlobalKey you can access from anywhere to use current app context
Create the key
final GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
Pass it to your App:
new MaterialApp(
title: 'MyApp',
navigatorKey: navigatorKey,
);
Use:
screenLock(
context: navigatorKey.currentContext,
correctString: '1234',
);

after payment is successful i want to navigate to another page

i'm working on a app. after payment is successful i want to navigate to another page.
Here is the code of my app payment method in the
void handlerPaymentSuccess() { Navigator.push(context, MaterialPageRoute(builder: (context) => Itemsbuy()));
i coded this to navigate that page but after payment is success this is not showing,
but after payment done its showing only payment successful from the razorpay but its not showing my page which i navigated. after payment success it is redirecting to the second route not redirecting to the Itemsbuy();
class SecondRoute extends StatefulWidget {
#override
_SecondRouteState createState() => _SecondRouteState();}
class _SecondRouteState extends State<SecondRoute> {
Razorpay razorpay;
TextEditingController textEditingController = new TextEditingController();
#override
void initState() {
razorpay = new Razorpay();
razorpay.on(Razorpay.EVENT_PAYMENT_SUCCESS, handlerPaymentSuccess);
razorpay.on(Razorpay.EVENT_PAYMENT_ERROR, handlerPaymentError);
razorpay.on(Razorpay.EVENT_EXTERNAL_WALLET, handlerExternalWallet);
super.initState(); }
void handlerPaymentSuccess() {
Navigator.push(context, MaterialPageRoute(builder: (context) => Itemsbuy()));//Items purchased}
this is the navigation that i want to push
void handlerPaymentError()
{
print('error'); }
void handlerExternalWallet()
#override
void dispose()
{
super.dispose();
razorpay.clear(); }
var options = {
"key": "empty",
"amount": empty,
"name": 'empty',
"Description": 'empty',
};
try {
razorpay.open(options);
} catch (e) {
debugPrint('error');
}}
thank you
Use the GetX package for navigation instead of Navigator.of(context)
Example:
void handlerPaymentSuccess() {
Get.to(Itemsbuy());
}
Also make sure to use GetMaterialApp instead of MaterialApp like this:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetMaterialApp(
home: SplashScreen(),
color: violet,
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.deepPurple,
).copyWith(textTheme: GoogleFonts.montserratTextTheme()),
);
}
}
It should work.
class _Payment extends State {
bool paymentDone = false;
#override
Widget build(BuildContext context) {
if (paymentDone == true) {
return NewPage();
}
else{
return CurrentPage();
}
}
void _handlePaymentSuccess(PaymentSuccessResponse response) async {
Fluttertoast.showToast(msg: "SUCCESS: " + response.paymentId, timeInSecForIosWeb: 4);
setState(() {
paymentDone = true;
);}
}
}

dispose in flutter bloc

On the code below,
profileBloc is initialized in EditProfileScreenState's didChangeDependencies() method.
Should we be calling dispose method on EditProfileScreenState class to dispose the profileBloc ?
If so , how should the profileBloc method be disposed as ProfileBloc class extends Bloc class which doesn't have dispose method?
class Profile extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (BuildContext context) => ProfileBloc(AuthRepo()),
child: ProfileScreen(),
);
}
}
class ProfileScreen extends StatefulWidget {
#override
EditProfileScreenState createState() => EditProfileScreenState();
}
class EditProfileScreenState extends State<ProfileScreen> {
ProfileBloc profileBloc;
#override
void didChangeDependencies() {
profileBloc = BlocProvider.of<ProfileBloc>(context);
super.didChangeDependencies();
}
#override
void dispose() {
// TODO: implement dispose
//profileBloc.dispose() cannot call as ProfileBloc class doesn't have dispose method
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocConsumer<ProfileBloc, ProfileState>(
listener: (context, state) {
},
builder: (BuildContext context,ProfileState state) {
return RaisedButton(onPressed: ()=>profileBloc.add(SaveProfile("name","email")));
}
));
}
}
class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
AuthRepo authRepo;
ProfileBloc(this.authRepo) : super(ProfileSaved());
#override
Stream<ProfileState> mapEventToState(ProfileEvent event) async* {
if (event is SaveProfile) {
//Actions
}
}
}
While I was searching, I found the solution.
We don't need to initialize the profileBloc in didChangeDependencies().
We can access the add method directly from the BlocProvider using:
BlocProvider.of<ProfileBloc>(context).add(ProfileSaved())
We can remove following section from EditProfileScreenState class.
ProfileBloc profileBloc;
#override
void didChangeDependencies() {
profileBloc = BlocProvider.of<ProfileBloc>(context);
super.didChangeDependencies();
}
Moreover,
In ProfileBloc class we can use close method in case we need to cancel any streams.
#override
Future<void> close() {
//cancel streams
super.close();
}

Using FutureBuilder in main.dart

Below code always show OnboardingScreen a little time (maybe miliseconds), after that display MyHomePage. I am sure that you all understand what i try to do. I am using FutureBuilder to check getString method has data. Whats my fault ? Or any other best way for this ?
saveString() async {
final prefs = await SharedPreferences.getInstance();
prefs.setString('firstOpen', '1');
}
getString() method always return string.
getString() async {
final prefs = await SharedPreferences.getInstance();
String txt = prefs.getString('firstOpen');
return txt;
}
main.dart
home: new FutureBuilder(
future: getString(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return MyHomePage();
} else {
return OnboardingScreen();
}
})
Usually I'm using another route, rather than FutureBuilder. Because futurebuilder every hot reload will reset the futureBuilder.
There always will be some delay before the data loads, so you need to show something before the data will load.
Snapshot.hasData is showing only the return data of the resolved future.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: SplashScreen(),
);
}
}
class SplashScreen extends StatefulWidget {
#override
_SplashScreenState createState() => _SplashScreenState();
}
const isOnboardingFinished = 'isOnboardingFinished';
class _SplashScreenState extends State<SplashScreen> {
Timer timer;
bool isLoading = true;
#override
void initState() {
_checkIfFirstOpen();
super.initState();
}
Future<void> _checkIfFirstOpen() async {
final prefs = await SharedPreferences.getInstance();
var hasOpened = prefs.getBool(isOnboardingFinished) ?? false;
if (hasOpened) {
_changePage();
} else {
setState(() {
isLoading = false;
});
}
}
_changePage() {
Navigator.of(context).pushReplacement(
// this is route builder without any animation
PageRouteBuilder(
pageBuilder: (context, animation1, animation2) => HomePage(),
),
);
}
#override
Widget build(BuildContext context) {
return isLoading ? Container() : OnBoarding();
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(child: Text('homePage'));
}
}
class OnBoarding extends StatelessWidget {
Future<void> handleClose(BuildContext context) async {
final prefs = await SharedPreferences.getInstance();
prefs.setBool(isOnboardingFinished, true);
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => HomePage(),
),
);
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: RaisedButton(
onPressed: () => handleClose(context),
child: Text('finish on bording and never show again'),
),
),
);
}
}
From the FutureBuilder class documentation:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateConfig, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
So you need to create a new Stateful widget to store this Future's as a State. With this state you can check which page to show. As suggested, you can start the future in the initState method:
class FirstPage extends StatefulWidget {
_FirstPageState createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
final Future<String> storedFuture;
#override
void initState() {
super.initState();
storedFuture = getString();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: storedFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
return MyHomePage();
} else {
return OnboardingScreen();
}
});
}
}
So in your home property you can call it FirstPage:
home: FirstPage(),
Your mistake was calling getString() from within the build method, which would restart the async call everytime the screen gets rebuilt.

Is there a way to keep the splash screen as long as we need for the main screen to be ready?

In other words, instead of fixed 15 seconds, is there a way to, to tell it to stop showing when my async function is finished?
If you are using a separate screen for Splash screen, you can simply await the async function you have and the use Navigator.pushReplacement() to open your Main Screen
Example:
class SplashScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return SplashScreenState();
}
}
class SplashScreenState extends State<SplashScreen> {
#override
void initState() {
super.initState();
handleSplashscreen();
}
void handleSplashscreen() async {
// Wait for async to complete
await someAsyncFunction();
// Open Main page
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => HomePage(user: null)));
}
Future<void> someAsyncFunction() async {
// Do some Network or other stuff
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(child: Text("Loading...")),
);
}
}
If you are simply showing a loader in the same screen while async operation is being done, you can use a FutureBuilder as others suggested.
Or you can conditionally show loader and Main UI using a boolean and setState().
Example:
class MainPage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return MainPageState();
}
}
class MainPageState extends State<MainPage> {
// Boolean to show/ hide loader
bool isLoading = true;
#override
void initState() {
super.initState();
handleAsync();
}
void handleAsync() async {
// Wait for async to complete
await someAsyncFunction();
// Open Main page
setState(() {
isLoading = false;
});
}
Future<void> someAsyncFunction() async {
// Do some Network or other stuff
}
#override
Widget build(BuildContext context) {
// Using isLoading to check whether async function is complete
return isLoading
? Container(
child: Center(child: Text("Loading...")),
)
: Container(
child: Text("The Actual screen"),
);
}
}
Hope it helps.
Use FutureBuilder from here in the page and show splash screen image when the data is not loaded.