Flutter Trigger bloc event again - flutter

On my first screen, I fill out the form and then click the next button it added SubmitDataEvent() to the bloc. Then, the BolcListner listing and when it comes to SuccessSate it navigate to the next screen.
on the second screen, when I click the back button it navigates to the previous screen. After that, when I change the user-input data on the form and again click the next button now SubmitDataEvent() is not added.
I preferred some resources related to this and I understand the problem is that the state is in SuccessSate and it doesn't change to InitialState. So in dispose() I used bloc.close();
#override
void dispose() {
bloc.close();
super.dispose();
}
But still, it's not working. Also, I try with this code
#override
void dispose() {
bloc.emit(InitialState);
bloc.close();
super.dispose();
}
still, it's not working.
I used this to navigate between screens:
Navigator.popAndPushNamed()
What I want to do is:
On the first screen, when clicking on the next button SubmitDataEvent() added to the bloc and it in SuccessState it navigate to the next screen. When I click the back button on the second page it navigates again to the first screen. Now when I click the next button on the first screen I want to run all bloc process again.
There are no dependencies with the first and second screens.
first screen code:
...
#override
void initState() {
super.initState();
bloc = injection<SubmitPersonalDetailsBloc>();
EasyLoading.addStatusCallback((status) {
print('EasyLoading Status $status');
if (status == EasyLoadingStatus.dismiss) {
_timer?.cancel();
}
});
}
#override
void dispose() {
_scrollController.dispose();
bloc.close();
super.dispose();
}
#override
Widget buildView(BuildContext context) {
return Scaffold(
body: BlocProvider<SubmitPersonalDetailsBloc>(
create: (_) => bloc,
child: BlocListener<SubmitPersonalDetailsBloc,
BaseState<PersonalDetailsState>>(
listener: (context, state) {
if (state is LoadingSubmitPersonalDetailsState) {
EasyLoading.show(status: 'Submitting Data');
}
if (state is SubmitPersonalDetailsSuccessState) {
setState(() {
submitPersonalDetailsResponseEntity =
state.submitPersonalDetailsResponseEntity;
});
if (submitPersonalDetailsResponseEntity!.responseCode == "00") {
EasyLoading.showSuccess('Done!');
//Navigate next screen
EasyLoading.dismiss();
}
} else if (state is SubmitPersonalDetailsFailedState) {
EasyLoading.showError(state.error); }
},
....

The problem is on Dependency Injection, Once it creates an instance the parameters don't change. So when navigating to the next screen have to reset that instance.
#override
void dispose() {
_scrollController.dispose();
bloc.close();
injection.resetLazySingleton<SubmitPersonalDetailsBloc>(); // here reset the instance
super.dispose();
}

You can try with this snippet
if (result.responseCode == APIResponse.RESPONSE_SUCCESS) {
yield SubmitPersonalDetailsSuccessState(
submitPersonalDetailsResponseEntity: r);
} else
yield SubmitPersonalDetailsFailedState(error: r.responseMsg);
}

Related

Detect if current Widget/State is visible in page route?

I want to add a Listener when the widget/state is visible and remove myself from the Listener when I leave the route.
But if the user clicks on the back button and returns to the current widget/state, I want to do the same thing again.
Currently initState, didChangeDependencies, didUpdateWidget and build are NOT called when the user clicks back from the next page, therefore I cannot detect when the user is returning and the widget was loaded from cache.
After much poking around the API, I've discovered that ModalRoute and RouteObserver is what I want.
https://api.flutter.dev/flutter/widgets/ModalRoute-class.html
https://api.flutter.dev/flutter/widgets/RouteObserver-class.html
If I just want to check if the current route is active I can call isCurrent on ModalRoute.of(context):
void onNetworkData(String data) {
if (ModalRoute.of(context).isCurrent) {
setState(() => list = data);
}
}
If I want to listen to route load/unload, I just create it and serve it up the hood with Provider like this:
class HomePage extends StatelessWidget {
final spy = RouteObserver<ModalRoute<void>>();
build(BuildContext context) {
return Provider<RouteObserver<ModalRoute<void>>>.value(
value: spy,
child: MaterialApp(
navigatorObservers: [spy],
),
);
}
}
Then somewhere in another widget:
class _AboutPageState extends State<AboutPage> with RouteAware {
RouteObserver<ModalRoute<void>>? spy;
void didChangeDependencies() {
super.didChangeDependencies();
spy = context.read<RouteObserver<ModalRoute<void>>>();
spy.subscribe(this, ModelRoute.of(context)!);
}
void dispose() {
spy.unsubscribe(this);
super.dispose();
}
void didPush() => attachListeners();
void didPopNext() => attachListeners();
void didPop() => removeListeners();
void didPushNext() => removeListeners();
attachListeners() {
}
removeListeners() {
}
}

Bloc is either calling multiple times or not getting called at all

I have 2 screens
Screen A
Screen B
I go from screen A to Screen B via the below code
onTap: () {
Navigator.pushNamed(context, '/screen_b');
},
Screen B code
class ScreenB extends StatefulWidget {
#override
_ScreenBState createState() => _ScreenBState();
}
class _ScreenBState extends State<ScreenB> {
SampleBloc sampleBloc;
#override
void initState() {
super.initState();
sampleBloc = BlocProvider.of<SampleBloc>(context);
sampleBloc.stream.listen((state) {
// this is getting called multiple times.
}
}
#override
void dispose() {
super.dispose();
sampleBloc.clear(); // If i add this, no event is firing from second time i come to the page. initState() is being called i checked so sampleBloc is not null.
}
#override
Widget build(BuildContext context) {
....
onTap: () {
sampleBloc.add(CreateSampleEvent());
},
....
}
}
When i click tap to add CreateSampleEvent to the sampleBloc in Screen B, the 'sampleBloc.stream.listen' getting fired multiples times.
Sample outcome
Step 1. First do sampleBloc.add (tap) -> sampleBloc.stream.listen
fired one time
Step 2. Go back to Screen A and come back to
Screen B Second go sampleBloc.add (tap) -> In one case i saw
the firing takes place in a pair of 2 times, and in other times the
firing takes place in pair of 4 times.
class SampleBloc extends Bloc<SampleEvent, SampleState> {
final SampleRepository sampleRepository;
SampleBloc({this.sampleRespository}) : super(null);
#override
Stream<SampleState> mapEventToState(SampleEvent event) async* {
if (event is SampleEvent) {
yield* mapSampleEventToState();
}
}
Stream<SampleEvent> mapSampleEventToState() async* {
yield SampleInProgressState();
try {
String sampleId = await sampleRepository.createSample();
if (sampleId != null) {
yield SampleCompletedState(sampleId, uid);
} else {
yield SampleFailedState();
}
} catch (e) {
print('Error: $e');
yield SampleFailedState();
}
}
Any ideas what might be going wrong ?
Since you are creating a manual subscription (by fetching the Bloc and then listening to a stream) you'll also need to manually cancel that subscription when you're done with it, otherwise, the SampleBloc will just keep getting new subscriptions and yielding events to all of them.
For that, you can either:
Save the subscription and then cancel it in the dispose() method.
SampleBloc sampleBloc;
StreamSubscription _subscription;
#override
void initState() {
super.initState();
sampleBloc = BlocProvider.of<SampleBloc>(context);
_subscription = sampleBloc.stream.listen((state) {
// this is getting called multiple times.
}
}
#override
void dispose() {
_subscription.cancel();
super.dispose();
}
You can take advantage of the BlocListener from the package which is a widget that is disposed automatically, hence, removes the subscriptions it created.
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
// do stuff here based on BlocA's state
},
child: Container(),
)

Flutter Widgets Binding Observer

In Flutter,I want to go back to the page I left the app from. But when I try to back, it always navigates to LoginPage. For example,I have 3 page.LoginPage,WorkoutPage,ProgressPage.Login page is my launcher. When I am on ProgressPage, I leave the app. But when I resume,it navigates Login Page.I used this code in login page.
Login Page
class ProgressTabState extends State with WidgetsBindingObserver{
AppLifecycleState state;
#override
void initState() {
// TODO: implement initState
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
// TODO: implement dispose
super.dispose();
WidgetsBinding.instance.removeObserver(this);
}
#override
void didChangeAppLifecycleState(AppLifecycleState appLifecycleState) {
// TODO: implement didChangeAppLifecycleState
super.didChangeAppLifecycleState(state);
state = appLifecycleState;
}
in login page I use this code to navigate to MainTab.
Navigator.push(
context,
MaterialPageRoute(
builder: (_) {
return MainTabs();
},
),
);
in Main tabs I have Tabbarviews with two page. Workoutpage and ProgressPage. This is Workout Page.
class WorkoutTabState extends State <WorkoutTab> {
#override
void initState() {
// TODO: implement initState
super.initState();
}
#override
void dispose() {
// TODO: implement dispose
super.dispose();
}
#override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
body:Text("Workout Page"),
);
}
Progress Page
class ProgressTabState extends State with WidgetsBindingObserver{
AppLifecycleState state;
#override
void initState() {
// TODO: implement initState
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
// TODO: implement dispose
super.dispose();
WidgetsBinding.instance.removeObserver(this);
}
#override
void didChangeAppLifecycleState(AppLifecycleState appLifecycleState) {
// TODO: implement didChangeAppLifecycleState
super.didChangeAppLifecycleState(state);
state = appLifecycleState;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body:Text("Progress Page"));
}}
In the login page dont use Navigator.push instead use Navigator.of(context).pushNamedAndRemoveUntil
The difference is Navigator.push will put login page as the first page so it will alway fall back to login page, after successful login you dont want that, you need to remove the login page from the routes stack, by using Navigator.of(context).pushNamedAndRemoveUntil, now when navigating between main and progress use Navigator.push
That will make the main page as the first route to fallbck to
Please read about it heare
https://api.flutter.dev/flutter/widgets/NavigatorState/pushAndRemoveUntil.html

didChangeAppLifecycleState doesn't work as expected

I hope I understand how didChangeAppLifecycleState worked correctly.
I have page A and page B . When I click the back device button from page B ( Navigator.of(context).pop(); ), I expect didChangeAppLifecycleState in pageA will get called, but it doesn't.
PageA
class _ABCState extends State<ABCrList> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
....
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
setState(() {
print(...);
});
}else{
print(state.toString());
}
}
....
This is the initState in pageA. The function used to call backend service.
#override
void initState() {
super.initState();
_bloc.getList(context); // return list and populate to ListView
});
}
The way you're thinking it is Android's way where onResume works, but in Flutter, things don't happen this way.
Generally, this gets called when the system puts the app in the background or returns the app to the foreground.
There are mainly 4 states for it:
resumed: The application is visible and responding to user input.
inactive: The application is in an inactive state and is not receiving user input.
paused: The application is not currently visible to the user, not responding user input, and running in the background.
detached: The application is still hosted on a flutter engine but is detached from any host views.
Edit:
When you're navigating to PageB from PageA, use something like:
Navigator.pushNamed(context, "/pageB").then((flag) {
if (flag) {
// you're back from PageB, perform your function here
setState(() {}); // you may need to call this if you want to update UI
}
});
And from PageB, you'll can use
Navigator.pop(context, true);

Android onResume() method equivalent in Flutter

I am working on a Flutter app and need to pop the screen. I tried initState() method but no luck. initState() gets called when I open a class for the first time.
Do we have an equivalent of Android onResume() method in Flutter?
Any ideas?
You can use the WidgetsBindingObserver and check the AppLifeCycleState like this example:
class YourWidgetState extends State<YourWidget> 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) {
if (state == AppLifecycleState.resumed) {
//do your stuff
}
}
}
Take in mind that It will called every time you open the app or go the background and return to the app. (if your widget is active)
If you just want a listener when your Widget is loaded for first time, you can listen using addPostFrameCallback, like this example:
class YourWidgetState extends State<YourWidget> {
_onLayoutDone(_) {
//do your stuff
}
#override
void initState() {
WidgetsBinding.instance?.addPostFrameCallback(_onLayoutDone);
super.initState();
}
}
Info : https://docs.flutter.io/flutter/widgets/WidgetsBindingObserver-class.html
Update: Null safety compliance
If you go to another page, then is called when you comeback
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondPage(),
),
).then((value) {
_refreshFirstPage();
});
You can accomplish this by registering a didChangeAppLifecycleState observer:
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(final AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
setState(() {
// ...your code goes here...
});
}
}
#override
Widget build(final BuildContext context) {
// ...your code goes here...
}
}
See WidgetsBindingObserver for more information.
Use focus_detector more information can see visibility_detector
Get notified every time your widget appears or disappears from the screen.
Similar to onResume()/onPause() on Android and viewDidAppear()/viewDidDisappear() on iOS.
Focus Detector fires callbacks for you whenever something happens to take or give your widget focus. Such an event might be, for instance, the user:
Navigating to/from another screen;
Turning the device’s screen on/off while your widget is visible;
Switching to/from another app while your widget is visible;
Scrolling your widget in/out the screen;
#override
Widget build(BuildContext context) =>
FocusDetector(
onFocusLost: () {
logger.i(
'Focus Lost.'
'\nTriggered when either [onVisibilityLost] or [onForegroundLost] '
'is called.'
'\nEquivalent to onPause() on Android or viewDidDisappear() on iOS.',
);
},
onFocusGained: () {
logger.i(
'Focus Gained.'
'\nTriggered when either [onVisibilityGained] or [onForegroundGained] '
'is called.'
'\nEquivalent to onResume() on Android or viewDidAppear() on iOS.',
);
},
onVisibilityLost: () {
logger.i(
'Visibility Lost.'
'\nIt means the widget is no longer visible within your app.',
);
},
onVisibilityGained: () {
logger.i(
'Visibility Gained.'
'\nIt means the widget is now visible within your app.',
);
},
onForegroundLost: () {
logger.i(
'Foreground Lost.'
'\nIt means, for example, that the user sent your app to the background by opening '
'another app or turned off the device\'s screen while your '
'widget was visible.',
);
},
onForegroundGained: () {
logger.i(
'Foreground Gained.'
'\nIt means, for example, that the user switched back to your app or turned the '
'device\'s screen back on while your widget was visible.',
);
},
child: Container(),
);