Flutter: RouteAware triggers only didPush event - flutter

Hello i'm new to flutter and although i have already searched on this topic, it is unclear to me. So here is the code:
main.dart file
RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await DotEnv().load('.env');
final UserRepository userRepository = new UserRepository();
runApp(BlocProvider(
create: (context) => AuthenticationBloc(userRepository)..add(AppStarted()),
child: App(
userRepository: userRepository
),
));
}
profile_form.dart (is on another file and imports the routeObserver from main.dart)
class ProfileForm extends StatefulWidget {
final UserRepository _userRepository;
final User _user;
ProfileForm(
{Key key, #required UserRepository userRepository, #required User user})
: assert(userRepository != null && user != null),
_userRepository = userRepository,
_user = user,
super(key: key);
State<ProfileForm> createState() => _ProfileFormState();
}
class _ProfileFormState extends State<ProfileForm> with RouteAware {
UserRepository get _userRepository => widget._userRepository;
User get _user => widget._user;
#override
void initState() {
super.initState();
}
#override
void didChangeDependencies() {
routeObserver.subscribe(this, ModalRoute.of(context));
super.didChangeDependencies();
}
#override
void didPush() {
print('didPush FirstPage');
}
#override
void didPopNext() {
print('didPopNext FirstPage');
}
#override
void didPop() {
print('didPop FirstPage');
}
#override
void didPushNext() {
print('didPushNext FirstPage');
}
#override
void dispose() {
print("dis");
routeObserver.unsubscribe(this);
super.dispose();
}
}
This form is entered through the Navigator.pushReplacementNamed(context, '/profile');
Although the trigger of didPush() event fires when i go back to another page (again from Nanigator or back button) and waiting for the didPop() event to fire it does not. What do i miss here? My main problem is that i want to save changes when the user exits the profile screen but before entering the init of another screen.

I had a similar problem so if this can help anyone, here's what caused it. The problem was that the RouteObserver that the RouteAware was subscribing to, was a different one from the one that was used by MaterialApp.
So the RouteObserver in MaterialApp was checking didPush & didPop events, but didn't have any listeners. The reason that you still get a didPush event, is because the observer always sends a didPush when you subscribe.
The root cause in our case, was that we used a Provider to provide the RouteObserver to the RouteAware widget, but everytime the MaterialApp was rebuilt, it got a new RouteObserver different from the one created by the provider.
Check if the RouteObserver you are subscribing to is the same one that's being used by MaterialApp, using the hashCode property!

Related

What Happened When I use the dispose with any Controller on Flutter?

The questions is What happened when I call the .dispose() feature with any controller(For instance TextEditingController).
there is a example code at the below:
class SearchPage extends StatefulWidget {
const SearchPage({Key? key}) : super(key: key);
#override
State<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
final TextEditingController _searchController = TextEditingController();
#override
void dispose() {
// TODO: implement dispose
super.dispose();
_searchController.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Search Page"),
),
);
}
}
I have searched the internet but I couldn't find satisfaction answer.So,What happen, when I use the _searchController.dispose()?
As far as I remember this method is being called before it gets removed from the widget tree. Like a cleanup method. So after you disposed something you can't access it anymore and it will throw a error if you still try to access it.
It checks if the object is still in use or not.
check if listeners are null.
#mustCallSuper
void dispose() {
assert(_debugAssertNotDisposed());
assert(() {
_debugDisposed = true;
return true;
}());
_listeners = _emptyListeners;
_count = 0;
}
Discards any resources used by the object. After this is called, the object is not in a usable state and should be discarded (calls to addListener will throw after the object is disposed)
Also try using super.dispos() on end part.
#override
void dispose() {
_searchController.dispose();
super.dispose();
}
State dispose
Called when this object is removed from the tree permanently.
Once you dispose of something, It is removed from the tree permanently.
You can check this question

setState is not updating the UI even though state is updating

I am trying to wait till amplify configuration is done then load the login screen. Even though state seems to be getting updated I am still getting the loadinscreen. Why is that?
I am not sure if setState is proper method on the init : Importance of Calling SetState inside initState
As per the doc : https://docs.amplify.aws/start/getting-started/integrate/q/integration/flutter/#configure-amplify
Future<void> main() async {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _isAmplifyConfigured = false;
late AmplifyAuthCognito auth;
#override
void initState() {
_initializeApp();
super.initState();
}
Future<void> _initializeApp() async {
await _configureAmplify();
setState(() {
_isAmplifyConfigured = true;
});
}
Future<void> _configureAmplify() async {
auth = AmplifyAuthCognito();
try {
await Amplify.addPlugin(auth);
await Amplify.configure(amplifyconfig);
} on AmplifyAlreadyConfiguredException {
print(
'Amplify was already configured. Looks like app restarted on android.');
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateRoute: AppRoutes.onGenerateRoute,
initialRoute: _isAmplifyConfigured
? LoginScreen.routeName
: LoadingScreen.routeName,
);
}
}
I think the issue is with you trying to reassign your initialRoute. I'm not super familiar with this property, but given the name I assume this is set once and is not rebuilt, not even when the state changes. It would make sense, also, because the rest of your code sounds like it should work.
Before trying anything else, I'd recommend you move your logic to initialize Amplify to the LoginScreen, and having its body depend on the _isAmplifyConfigured boolean value. So show spinner if it's false, and show Login fields when it's true.
Even better would be to create a HomeScreen, so you can keep this Amplify initialization at the bottom of your app's stack. And then have your HomeScreen either show the Login widgets, the home screen of your app, or a loading state.

How to access BuildContext in main() method of Flutter launcher class?

I need to access BuildContext in main() {} method of the main.dart class to show a popUp dialog in firebase FirebaseMessaging.onMessage.listen method when a new push notification is received, how to access it while there is no BuildContext in main() ?
You can't really.
In your main function in your main.dart, you have to call runApp() to launch your first widget, and within this widget, you will have access to BuildContext since all the stateful/stateless widget will receive the BuildContext through their build method.
For example you may want to do something like this:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MySuperApplication(),
);
}
class MySuperApplicationextends StatefulWidget {
const MySuperApplication({
Key? key,
}) : super(key: key);
#override
_MySuperApplicationState createState() => _MySuperApplicationState();
}
class _MySuperApplicationState extends State<MySuperApplication> {
FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
#override
Widget build(BuildContext context) {
// You have access to your BuildContext here, and you can initialize FirebaseMessaging.
_firebaseMessaging.subscribeToTopic("all");
// return any widget.
return WhatEverWidget()
}
}

Share bloc across different routes

I'm trying to share same bloc across two routes.
But when I come back from second route the bloc get automatically disposed so in the first route I find myself with all the stream closed.
For example this is the first route (HomePage) where I instantiate the bloc, download a list from api and show it in the build method.
class HomePage extends StatefulWidget {
#override
State<StatefulWidget> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
GroupsBloc _groupBloc;
#override
void initState() {
super.initState();
}
#override
void didChangeDependencies(){
super.didChangeDependencies();
_groupBloc = GroupsBloc();
_groupBloc.getAll();
}
#override
void dispose(){
_groupBloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
...
}
}
Then I navigate to a second screen where I can add an item to the list.
_onAddGroupPress(){
Navigator.of(context).push(new MaterialPageRoute(
builder: (BuildContext context) => BlocProvider<GroupsBloc>(bloc: _groupBloc, child: GroupPage()),
fullscreenDialog: true
),
);
}
In the second screen I retrieve the bloc and I use it to add an item, then I go back to Home Page.
class GroupPage extends StatefulWidget {
#override
_GroupPageState createState() => _GroupPageState();
}
class _GroupPageState extends State<GroupPage> {
FormBloc _formBloc; //another bloc
GroupsBloc _groupBloc;
#override
void initState(){
super.initState();
}
#override
void didChangeDependencies(){
super.didChangeDependencies();
_formBloc = FormBloc();
_groupBloc = BlocProvider.of<GroupsBloc>(context); //retrieve of the bloc
}
#override
void dispose() {
_formBloc?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
...
}
In the dispose method of the second screen I dispose only _formBloc bloc but _groupBloc gets disposed too, so when I come back in the first page I found myself with _groupBloc disposed and all it's stream closes.
I tought of passing the bloc to the second screen as a props but I don't know if it's the right way to do this.
It obviously depends on the scope of your Bloc, but there is nothing preventing you from sharing the same instance throughout your whole app.
ie. simply wrap your whole MaterialApp inside BlocProvider<GroupsBloc>(bloc: _groupBloc, child: MaterialApp( ... ))
if "groups" are not global to your app, you should probably just pass the bloc along to the second widget.

What is the equivalent of Android LiveData in Flutter?

Android's LiveData allows to update the UI when the activity is in an active state. So if a background operation has finished while the activity is paused, the activity won't be notified and thus the app won't crush. Can Flutter perform the same behavior?
For people interested in an equivalent of LiveData for other scenarios, I present you StreamController:
class ExampleViewModel {
StreamController<bool> loggedInStream = StreamController<bool>();
logIn() { loggedInStream.add(true); }
}
class ExampleScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() => ExampleScreenState();
}
class ExampleScreenState extends State<ExampleScreen> {
ExampleViewModel _viewModel;
BuildContext _ctx;
#override
void initState() {
super.initState();
_viewModel = ExampleViewModel()
_viewModel.loggedInStream.stream.listen( (loggedIn) {
if ( loggedIn != null && loggedIn ) {
Navigator.of(_ctx).pushReplacementNamed("/home");
}
});
}
#override
Widget build(BuildContext context) {
_ctx = context;
var loginBtn =
RaisedButton(
onPressed: _viewModel.logIn(true),
child: Text(
"LOGIN",
style: new TextStyle(
fontSize: 24.0,
)
),
color: Colors.green,
textColor: Colors.white,
);
return loginBtn;
}
#override
void dispose() {
super.dispose();
_viewModel.loggedInStream.close();
}
}
You can subscribe to it just like a LiveData, using:
loggedInStream.stream.listen( (data) { code } )
And you should clear the listeners in dispose to avoid memory leaks:
loggedInStream.close()
This code basically do the following things:
Creates a screen with a button.
Listen to a Stream (observe a LiveData).
When you click the button, it changes the value.
The listener (observer) is triggered.
Launches new screen.
You can use WidgetsBindingObserver to listen to the application state.
class AppLifecycleReactor extends StatefulWidget {
const AppLifecycleReactor({ Key key }) : super(key: key);
#override
_AppLifecycleReactorState createState() => new _AppLifecycleReactorState();
}
class _AppLifecycleReactorState extends State<AppLifecycleReactor> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
AppLifecycleState _notification;
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(() { _notification = state; });
}
#override
Widget build(BuildContext context) {
return new Text('Last notification: $_notification');
}
}
Easy: Flutterx Live Data
There is no need to observe App lifecycle: widget are built only when the app is resumend
This library integrates perfectly LiveData concepts, also is well documented.
Is developed on Flutter 1.14.x-dev you need master flutter channel at the moment
Sorry to be late for the party,
My colleague and I have developed a library that mimics the live data of android, on flutter.
Check it out:
https://pub.dev/packages/stream_live_data