Initiate global bloc on Logout - flutter

I'm providing bloc at top of all view to access at globally. When I'm doing logout and re-login without closing app, event not called because it is already initiate. Here, I want to refresh bloc on logout, so that FetchList and UpdateCount events got called when user logged In without closing app. How I can achieve this?
class DemoApp extends StatelessWidget {
const DemoApp({super.key});
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => AppBloc()..add(const UpdateFcmToken())),
BlocProvider(
create: (_) => getIt<ListBloc>()
..add(const FetchList())
..add(const UpdateCount()),
),
],
child: const AppView(),
);
}
}
For workaround, I add those event at login as well, but it will cause multiple request for fresh login.

Related

Flutter page does not use new Provider after the old one gets disposed

I have a MultiProvider set up on top of my MaterialApp
class Bell extends StatelessWidget {
const Bell({Key? key}) : super(key: key);
static User user = FirebaseAuth.instance.currentUser!;
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => MediaManager()), <- This provider
],
When I logout, I dispose of the MediaManager() provider with
await Authentication().signOut(context: context);
mediaManager.dispose();
And when I login again I use a navigator to create a new provider like this
navigator.pushAndRemoveUntil(
MaterialPageRoute(
builder: (_) => const Bell(),
),
(_) => false,
);
The problem is that if I navigate to a page that uses the MediaManager provider, it still shows the data of the old provider which got disposed and nothing works. Navigating to the main StatelessWidget creates a new one. How do I tell the app to use the new one?

Flutter Provider and Navigator

so This is my first try with Flutter. I come from Angular where dependency injection made routing a breeze. Just injecting the Router to any service would just do the trick.
Here is my scenario for flutter app:
In '/login' page I click Login button. This calls method from Auth provider, where after validating credentials Application should be routed to '/home'.
It's just something I can't wrap my head around even though I read all possible threads in the freaking internet. Everybody said one shoud wrap MaterialApp with ChangeNotifierProvider which I did - what am I doing wrong?
Here is my code. Can you point me to the right direction?
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => Auth(context: context),
child: MaterialApp(
initialRoute: "/login",
routes: {
"/login": (context) => LoginPage(),
"/home": (context) => HomePage()
},
),
);
}
}
//This is my Login page Widget - After successful login I want to be able to use Navigator to push to '/home' route
class LoginPage extends StatelessWidget {
#override
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Login"),
),
body: ElevatedButton(
//
onPressed: () => {
// here I want to use login method on Auth provider. Which should validate credentials and if valid redirect to home page
Provider.of<Auth>(context, listen: false).login()
},
child: Text("login"),
));
}
}
//This is my Home page Widget
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Scaffold(appBar: AppBar(title: Text("Home"))),
);
}
}
//This is my auth controller where After successful login I want to redirect to HomePage
class Auth extends ChangeNotifier {
BuildContext context;
Auth({
required this.context,
});
// This is login method of Auth provider -
login() {
//Here after validating the credentials (eg user / password) I want to redirect to '/home' route.
Navigator.of(context).pushNamed("/home");
}
}
I have a better solution to change your pages route based on authentication status, do code like this in your route table in MaterialApp:
route:{
'/': (context) => Consumer<AuthProvider>(
builder: (context, value, child) => value.isAuth()
? yourMainPageAfterAuth()
: AuthScreen()}
single slash will be considered as your home page, so validate your authentication in your auth provider then route to your home page after authentication
Navigator.of(context).pushNamed('/');

How to globally listen from a flutter bloc?

I'm implementing notifications on my app.
I have a Cubit that will emit states when the app receives a new notification.
This is my main:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
...
BlocProvider<NotificationCubit>(
create: (context) => sl<NotificationCubit>())
],
child: MaterialApp(
home: Splash(),
onGenerateRoute: Routes.sailor.generator(),
navigatorKey: Routes.sailor.navigatorKey,
));
}
}
I'm using get_it for dependency injection.
I tried to add BlocBuilder<NotificationCubit, NotificationState> to my home screen and it works every time the user receives the notification.
My goal is to handle the notification globally. I tried to add a listener when I create the cubit, but this doesn't work:
BlocProvider<NotificationCubit>(
create: (context) => sl<NotificationCubit>()..listen((state) {
if (state is NotificationReceived){
print("Notificaton received");
}
}))
I have heard that using get_it with bloc is not a good idea...
Especially in your case, where you are providing a bloc for the whole widget tree. Try to provide bloc without using get_it because you will have access to it in the whole app anyways.
Here there is an interesting video about it:
https://youtu.be/THCkkQ-V1-8?t=6393
To globally listen to a bloc you need to put it above MaterialApp
return MultiBlocProvider(
providers: [
...
BlocProvider<NotificationCubit>(
create: (context) => NotificationCubit()),
],
child: Builder(
builder: (context) =>
BlocListener<NotificationCubit, NotificationState>(
listener: (context, state) {
...
},
child: MaterialApp(
...
)
)),
);
Remember to use Builder or create another stateless class after using BlocProvider

How to create a BlocListener that can listen to all pages in flutter with access to MaterialApp context?

I'm trying to create a BlocListener that has the ability to listen to all pages/routes throughout the app just like how you can access a Bloc or a Provider all throughout the app if they are defined at root-level like in the code below
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<IdentityTokenProvider>(
create: (_) => IdentityTokenProvider(),
),
],
child: MultiBlocProvider(
providers: [
BlocProvider<AuthBloc>(
create: (_) => AuthBloc(),
),
],
child: MaterialApp(
debugShowCheckedModeBanner: AppConfig.DEBUGGABLE,
theme: ThemeData(
// fontFamily: CustomFontStyle.montserrat,
),
home: AuthListener(
child: Center(
child: const MainApp(),
),
),
),
),
),
);
As you can see, I have providers, blocs, and one listener. I have no problem accessing the blocs and providers in other pages. My problem is the auth listener. I lose access to the AuthListener once I move to a different page (by removing stack) , because it is inside the MaterialApp. However, in this instance, I need that specific listener (AuthListener) to be inside a MaterialApp, because it consists of code that uses page navigations (which doesn't work if the implementation is done outside/above the widget tree of a MaterialApp), and makes us of the MaterialApp context for showing dialogs.
My implementation of page routing which removes the stack, which is another cause of losing access to the AuthListener
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => route),
(Route<dynamic> route) => false);
Why do I remove the route/page stack when moving to a different page?
I specifically use this after authentication. You don't really want a user to be able to press back button after logging in, andredirect the user back to the login page right? Usually back button should hide/close the app when they are logged in.
My AuthListener implementation
class AuthListener extends StatefulWidget {
final Widget child;
const AuthListener({Key key, #required this.child}) : super(key: key);
#override
_AuthListenerState createState() => _AuthListenerState();
}
class _AuthListenerState extends State<AuthListener> {
#override
Widget build(BuildContext context) {
return BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is AuthAuthenticated) {
PageRouterController.pushAndRemoveStack(context, const EcomPage());
} else if (state is AuthUnauthenticated) {
PageRouterController.pushAndRemoveStack(context, const LoginPage());
}
},
child: widget.child,
);
}
}
Is there a different way around this?
So I ended up defining a
static final GlobalKey<NavigatorState> navigatorKey = new GlobalKey();
and used it in my MaterialApp
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: App.DEBUGGABLE,
theme: ThemeData(
// fontFamily: CustomFontStyle.montserrat,
),
navigatorKey: App.navigatorKey,
home: Center(
child: const LoginPage(),
),
);
}
So then, whenever I have to navigate in cases where the implementation is outside the MaterialApp (in my case via the AuthListener which is found at root-level, above the MaterialApp), I can navigate via
App.navigatorKey.currentState.pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => route),
(Route<dynamic> route) => false);
Which means I can finally have access to the MaterialApp navigator and context even with the listener outside the MaterialApp which allows me to do both navigation and showing of dialogs

BlocProvider.of() called with a context that does not contain a Bloc - even that it does

First of, I do know how BLoC suppose to work, the idea behind it and I know the difference between BlocProvider() and BlocProvider.value() constructors.
For simplicity, my application has 3 pages with a widget tree like this:
App() => LoginPage() => HomePage() => UserTokensPage()
I want my LoginPage() to have access to UserBloc because i need to log in user etc. To do that, I wrap LoginPage() builder at App() widget like this:
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
home: BlocProvider<UserBloc>(
create: (context) => UserBloc(UserRepository()),
child: LoginPage(),
),
);
}
}
That obviously works just fine. Then, if User logs in successfully, he is navigated to HomePage. Now, I need to have access to two different blocs at my HomePage so I use MultiBlocProvider to pass existing UserBloc further and create a brand new one named DataBloc. I do it like this:
#override
Widget build(BuildContext context) {
return BlocListener<UserBloc, UserState>(
listener: (context, state) {
if (state is UserAuthenticated) {
Navigator.of(context).push(
MaterialPageRoute<HomePage>(
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider.value(
value: BlocProvider.of<UserBloc>(context),
),
BlocProvider<DataBloc>(
create: (_) => DataBloc(DataRepository()),
),
],
child: HomePage(),
),
),
);
}
},
[...]
This also works. Problem happens when from HomePage user navigates to UserTokensPage. At UserTokensPage I need my already existing UserBloc that I want to pass with BlocProvider.value() constructor. I do it like this:
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: Text('My App'),
actions: <Widget>[
CustomPopupButton(),
],
),
[...]
class CustomPopupButton extends StatelessWidget {
const CustomPopupButton({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return PopupMenuButton<String>(
icon: Icon(Icons.more_horiz),
onSelected: (String choice) {
switch (choice) {
case PopupState.myTokens:
{
Navigator.of(context).push(
MaterialPageRoute<UserTokensPage>(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<UserBloc>(context),
child: UserTokensPage(),
),
),
);
}
break;
case PopupState.signOut:
{
BlocProvider.of<UserBloc>(context).add(SignOut());
Navigator.of(context).pop();
}
}
},
[...]
When I press button to navigate to MyTokensPage i get error with message:
════════ Exception caught by widgets library ═══════════════════════════════════════════════════════
The following assertion was thrown building Builder(dirty):
BlocProvider.of() called with a context that does not contain a Bloc of type UserBloc.
No ancestor could be found starting from the context that was passed to BlocProvider.of<UserBloc>().
This can happen if:
1. The context you used comes from a widget above the BlocProvider.
2. You used MultiBlocProvider and didn't explicity provide the BlocProvider types.
Good: BlocProvider<UserBloc>(create: (context) => UserBloc())
Bad: BlocProvider(create: (context) => UserBloc()).
The context used was: CustomPopupButton
What am I doing wrong? Is it because i have extracted PopupMenuButton widget that somehow loses blocs? I don't understand what I can be doing wrong.
You can just wrap the Blocs you need to access through out the app by wrapping it at the entry point of the app like this
runApp(
MultiBlocProvider(
providers: [
BlocProvider<UserBloc>(
create: (context) =>
UserBloc(UserRepository()),
),
],
child: App()
)
);
}
and you can access this bloc at anywhere of your app by
BlocProvider.of<UserBloc>(context).add(event of user bloc());
EDIT 10/03/2022
Since this thread became very popular I feel I need to add some comments.
This is valid solution if your goal is to use blocs that are not provided above your MaterialApp widget, but instead being declared somewhere down the widget tree by wrapping your widget (eg. some page) with BlocProvider making it possible for that widget to access the bloc.
It is easier to avoid problems by declaring all your blocs in MultiBlocProvider somewhere up the widget tree (like I said before), but this topic was not created with that in mind. Feel free to upvote and use this aproach described in Amesh Fernando response but do that knowing the difference.
I fixed it. Inside App widget i create LoginPage with
home: BlocProvider<UserBloc>(
create: (context) => UserBloc(UserRepository()),
child: LoginPage(),
At LoginPage I simply wrap BlocBuilders one into another
Widget build(BuildContext context) {
return BlocListener<UserBloc, UserState>(
listener: (context, state) {
if (state is UserAuthenticated) {
Navigator.of(context).push(
MaterialPageRoute<HomePage>(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<UserBloc>(context),
child: BlocProvider<NewRelicBloc>(
create: (_) => NewRelicBloc(NewRelicRepository()),
child: HomePage(),
),
),
),
);
}
},
[...]
PopupMenuButton navigates User to TokenPage with
Navigator.of(context).push(
MaterialPageRoute<UserTokensPage>(
builder: (_) => BlocProvider.value(
value: BlocProvider.of<UserBloc>(context),
child: UserTokensPage(),
),
),
);
And that solved all my problems.
Solution
Method A: Access UserBloc provider instance directly without passing it
I prefer this solution since it requires less code.
A.1 Wrap CustomPopupButton instance with provider Consumer so it rebuilds itself whenever UserBloc notifies listeners of value changes.
Change this:
actions: <Widget>[
CustomPopupButton(),
],
To:
actions: <Widget>[
Consumer<UserBloc>(builder: (BuildContext context, UserBloc userBloc, Widget child) {
return CustomPopupButton(),
});
],
A.2 Change Provider instance invocation inside the stateless widget to disable listening to value changes -- "listening" and resulting "rebuilds" are already done by Consumer.
A.2.1 Change this:
value: BlocProvider.of<UserBloc>(context),
To:
value: BlocProvider.of<UserBloc>(context, listen: false),
A.2.2 And change this:
BlocProvider.of<UserBloc>(context).add(SignOut());
To:
BlocProvider.of<UserBloc>(context, listen: false).add(SignOut());
Method B: pass UserBloc provider instance
Same thing as Method A, but:
In A.1 you'd pass userBloc like this: return CustomPopupButton(userBloc: userBloc),.
You'd declare final UserBloc userBloc; member property inside CustomPopupButton.
In A.2 you'd do this: userBloc.add(SignOut()); instead of BlocProvider.of<UserBloc>(context, listen: false).add(SignOut());
Explanation
flutter_bloc is using Provider, to be aware what's going on it's better understand Provider. Please refer to my answer here to understand my answer to your question, and to understand Provider and listen flag better.
Change name of context in builder whether in bottomSheet or materialPageRoute.
So that bloc can access parent context through context
unless it's going to take context from builder (bottom sheet). This can lead
to an error which you can't reach the instance of bloc .
showModalBottomSheet(
context: context,
builder: (context2) { ===> change here to context2
BlocProvider.value(
value: BlocProvider.of<BlocA>(context),
child: widgetA(),
),
}
You need to either decompose your widget into two widgets (which I recommend for testability reasons) or use a Builder widget to get a child context.
class MyHomePage extends StatelessWidget { #override Widget build(BuildContext context) { return BlocProvider( create: (_) => TestCubit(), child: MyHomeView(), ); } } class MyHomeView extends StatelessWidget { #override Widget build(BuildContext context) { return Scaffold( body: Center( child: RaisedButton(onPressed: () => BlocProvider.of<TestCubit>(context)...) ), ); } }
source: solved by Felix Angelov, https://github.com/felangel/bloc/issues/2064
you don't have to use BlocProvider.value() to navigate to another screen, you can just wrap MaterialApp into BlocProvider as a child of it