I'm trying to find a way to show notifications (like SnackBar) in the application that are initiated from outside of user interface.
Overall setup is this:
My application on the top has blocs defined:
Widget build(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider(create: (_) => WalletCubit()),
BlocProvider(create: (_) => NotificationCubit())
],
child: MaterialApp(...)
);
NotificationCubit is responsible for emiting notification states with only method notify:
class NotificationCubit extends Cubit<NotificationState> {
NotificationCubit() : super(NotificationInitial());
void notify(String message) {
emit(NotificationState(message));
}
}
Notifications are supposed to be shown via BlocConsumer:
class _MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) => BlocConsumer<NotificationCubit,
NotificationState>(
listener: (context, state) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(state.message)));
},
builder: (context, state) => BlocBuilder<NavigationBloc, NavigationState>(
...
)
);
Another blocs have callbacks that are called by background processes like getting data from the network where notification have to be edited:
class WalletCubit extends Cubit<WalletState> {
WalletCubit(backgroundProvider) {
backgroundProvider.subscribe(_callback);
}
void _callback(String message) {
NotificationCubit().notify(message);
}
}
This code doesn't really work. I see that instance of NotificationCubit is created but the state emited never reaches the code in p.3. I assume I shouldn't create new instance but use one from the building context.
So I've modified blocs in p.1 this way:
Widget build(BuildContext context) => MultiBlocProvider(
providers: [
BlocProvider(create: (_) => NotificationCubit()),
BlocProvider(create: (context) => WalletCubit(context))
],
child: MaterialApp(...)
);
and calling notify() from p.4 this way:
class WalletCubit extends Cubit<WalletState> {
BuildContext _context;
WalletCubit(this._context, backgroundProvider) {
backgroundProvider.subscribe(_callback);
}
void _callback(String message) {
_context.read<NotificationCubit>().notify(message);
}
}
This approach works. However it seems to me it creates tight coupling of blocs with Flutter framework. I have an understanding that all blocs should do is emiting own states.
So my question is: what is the "proper" way to allow all blocs sending application global notifications?
Related
I'm working with Bloc and Hydrated Bloc and at some point in my app I want to store a boolean variable "firstTime" in a Hydrated Bloc to know if it's the first time my user is using the app. If it is the case, I redirect the user to a on-boarding page (called IntroPage), and if not, the login screen is displayed.
I use a BlocListener to listen to the changes of "firstTime", so once my user has finished navigating the on-boarding page, it redirects to the login screen.
#override
Widget build(BuildContext context) {
return MaterialApp(
...
builder: (context, child) {
return BlocListener<UserPreferencesBloc, UserPreferencesState>(
listener: (context, state) {
if (state.firstTime) {
_navigator.pushAndRemoveUntil<void>(
IntroPage.route(),
(route) => false,
);
}
},
child: child,
);
},
onGenerateRoute: (_) => SplashPage.route(),
);
}
The main problem is that if there's no change in the state of the Bloc, it does not fire the BlocListener part. The user never access the IntroPage.
Is there a way to make it so I can get into that listener just after its initialization, even without any change in the state of the Bloc ? Or is there another way to do that (that doesn't involve the use of Shared Preferences or other packages) ?
Edit : Here is the code for the Bloc :
class UserPreferencesBloc
extends HydratedBloc<UserPreferencesEvent, UserPreferencesState> {
UserPreferencesBloc() : super(const UserPreferencesState()) {
on<UserPreferencesFirstTimed>(_onFirstTime);
}
void _onFirstTime(
UserPreferencesFirstTimed event,
Emitter<UserPreferencesState> emit,
) async {
emit(state.copyWith(firstTime: event.firstTime));
}
#override
UserPreferencesState? fromJson(Map<String, dynamic> json) {
return UserPreferencesState(firstTime: json['firstTime'] as bool);
}
#override
Map<String, dynamic>? toJson(UserPreferencesState state) => {
'firstTime': state.firstTime,
};
}
And here is the state :
part of 'user_preferences_bloc.dart';
class UserPreferencesState extends Equatable {
const UserPreferencesState({
this.firstTime = true,
});
final bool firstTime;
UserPreferencesState copyWith({
bool? firstTime,
}) {
return UserPreferencesState(
firstTime: firstTime ?? this.firstTime,
);
}
#override
List<Object> get props => [firstTime];
}
And the Bloc is initialized in the app.dart file, at the start of the application :
class App extends StatelessWidget {
const App({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiRepositoryProvider(
providers: ... //not shown in this piece of code
child: MultiBlocProvider(
providers: [
...
BlocProvider(create: (_) => UserPreferencesBloc())
],
child: AppView(),
),
);
}
}
It is by design so that BlocListener is only triggered once per state change.
But there are of course ways to do what you are after. If you'd show how you provide/create the bloc and also the definition of the state it could help...
But you could for instance let firstTime be nullable and use the cascade notion operator (..) when creating the bloc to immediately call a method in the bloc that sets the value of firstTime to true/false after initialization.
Edit:
Obviously hard from here to write all the changes you'd have to make, but here is the main idea:
Change: final bool firstTime; to bool? firstTime; and handle the null cases where applicable.
On creation, change:
BlocProvider(create: (_) => UserPreferencesBloc())
to:
BlocProvider(create: (_) => UserPreferencesBloc()..onFirstTime())
Write the method onFirstTime() something like this:
void onFirstTime() async {
emit(state.copyWith(firstTime: state.firstTime ?? true));
}
And remove the on<UserPreferencesFirstTimed>(_onFirstTime); part as well as this.firstTime = true,
In Flutter, when I call the cubit method below, the state is getting emitted twice.
Can someone help me find what's wrong with my code?
Cubit class:
class AlertCubit extends Cubit<AlertState> {
AlertCubit() : super(AlertInitial());
void alert(InfoAlertData alertInfo) {
emit(AlertInitial());
**emit(NewAlert(alertInfo));**
}
}
Where I call this function from another cubit:
class CustomersCubit extends Cubit<CustomersState> {
CustomersCubit(
{required this.alertCubit,
})
: super(CustomersInitial());
final AlertCubit alertCubit;
///removed irrelevant code
void markMissing(int loanId) async {
if (collectionCubit.active) {
} else {
**alertCubit.alert(InfoAlerts.startCollect);**
}
}
///removed irrelevant code
}
I passed cubit as a parameter in main:
///removed irrelevant code
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => AlertCubit(),
),
BlocProvider(
create: (BuildContext cusContext) => CustomersCubit(
**alertCubit: BlocProvider.of<AlertCubit>(cusContext),**
),
),
///removed irrelevant code
I am new to flutter and bloc; any help is appreciated
I have defined the following cubit.
#injectable
class AuthCubit extends Cubit<AuthState> {
final IAuthService _authService;
AuthCubit(this._authService) : super(const AuthState.initial());
void authCheck() {
emit(_authService.signedInUser.fold(
() => AuthState.unauthenticated(none()),
(user) => AuthState.authenticated(user),
));
}
}
But the BlocListener which listens to this bloc is not getting invoked even after emit is called. But everything works as expected when I add a zero delay before the emit call.
Future<void> authCheck() async {
await Future.delayed(Duration.zero);
emit(_authService.signedInUser.fold(
() => AuthState.unauthenticated(none()),
(user) => AuthState.authenticated(user),
));
}
I tried out this delay because for other events which made some backend call (with some delay) emit worked perfectly. But I'm pretty sure this is not how it should work. Am I missing something here?
EDIT:
Adding the SplashPage widget code which uses BlocListener.
class SplashPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocListener<AuthCubit, AuthState>(
listener: (context, state) {
print(state);
},
child: Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
);
}
}
Place where authCheck() is called,
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<AuthCubit>(
create: (_) => getIt<AuthCubit>()..authCheck(),
),
],
child: MaterialApp(
....
),
);
}
}
and the AuthState is a freezed union
#freezed
abstract class AuthState with _$AuthState {
const factory AuthState.initial() = _Initial;
const factory AuthState.authenticated(User user) = _Authenticated;
const factory AuthState.unauthenticated(Option<AuthFailure> failure) = _Unauthenticated;
const factory AuthState.authInProgress() = _AuthInProgress;
}
Also, when I implemented a bloc (instead of Cubit) with the same functionality, everything worked as expected.
Without the delay the emit is called directly from the create method of the provider. This means that the listener is not (completely) built yet and thus there is no listener to be called when you emit the state.
So by adding the delay you allow the listener to subscribe to the stream first and thus it gets called when you emit the new state.
For me, the delay does not work perfectly. So I found this solution, maybe help someone:
#override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback((_) async {
await myCubit.doSomethingFun();
});
}
And #Pieter is right, listener only be invoked when the widget is built.
I'm implementing a chat-based app in Flutter. I was thinking of using Provider package to create two main notifiers: UserService and ChatService. The first one handles the signIn (and all the other functions user-related), while the latter handles chat specific functions. However, the chatService needs to access the UserService for some functionalities. I tried to use ProxyProvider and this is the code:
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<UserService>(builder: (_) => UserService.instance()),
ChangeNotifierProxyProvider<UserService, ChatService>(builder: (_, user, chatService) => ChatService.init(user))
],
child: MaterialApp(
...
),
);
}
}
However, when I run the app, flutter throws this error:
Tried to use Provider with a subtype of Listenable/Stream (ChatService).
This is likely a mistake, as Provider will not automatically update dependents
when ChatService is updated. Instead, consider changing Provider for more specific
implementation that handles the update mechanism, such as:
ListenableProvider
ChangeNotifierProvider
ValueListenableProvider
Thank you!
It's not clear which "architecture" you are going to use, Provider is simply a mechanism to retrieve objects in the widget tree in a safe way.
Assuming you mean UserService and ChatService, and these are ChangeNotifiers (could be BLoC or anything else) - here's an example of how you'd hook them up with Provider:
main() {
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<UserService>(create: (_) => UserService()),
ChangeNotifierProxyProvider<UserService, ChatService>(
create: (_) => ChatService(),
update: (_, userService, chatService) => chatService..userService= userService
),
],
child: MyApp(),
));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<ChatService>(
builder: (context, chatService, _) => Text(chatService.currentUser.lastMessage) // or whatever you need to do
);
}
}
When using flutter bloc what is the recommendation, is it recomended for each page to have its own bloc or can i reuse one block for multiple pages, if so how?
I think that the best solution is to have one BLoC per page. It helps you to always know in which state each screen is just by looking at its BLoC. If you want to show the state of each of your tabs independently you should create one BLoC for each tab, and create one Repository which will handle fetching the data. But if the state of every tab will be the same, (for example you fetch data only once for all of the screens, so you don't show loading screen on every tab) then I think that you could create just one BLoC for all of this tabs.
It is also worth to add, that BLoCs can communicate with each other. So you can get state of one BLoC from another, or listen to its state changes. That could be helpful when you decide to create separate BLoCs for tabs.
I have addressed this topic in my latest article. You can check it out if you want to dive deeper.
There are no hard-set rules about this. It depends on what you want to accomplish.
An example: if each page is "radically" from each other, then yes, a BLoC per page makes sense. You can still share an "application-wide" BLoC between those pages if some kind of sharing or interaction is required between the pages.
In general, I've noticed that usually a BLoC "per page" is useful as there are always specific things related for each page that you handle within their BLoC. You can the use a general BLoC to share data or some other common thing between them.
You can combine the BLoC pattern with RxDart to handle somewhat more complex interaction scenarios between a BLoC and the UI.
Sharing a BLoC is fairly simple, just nest them or use a MultiProvider (from the provider package):
runApp(
BlocProvider(
builder: (_) => SettingsBloc(),
child: BlocProvider(
builder: (_) => ApplicationBloc(),
child: MyApp()
)
)
);
and then you can just retrieve them via the Provider:
class MyApp extends ... {
#override
Widget build(BuildContext context) {
final settingsBloc = Provider.of<SettingsBloc>(context);
final appBloc = Provider.of<ApplicationBloc>(context);
// do something with the above BLoCs
}
}
You can share different bloc's in different pages using BlocProvider.
Let's define some RootWidget that will be responsible for holding all Bloc's.
class RootPage extends StatefulWidget {
#override
_RootPageState createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
NavigationBloc _navigationBloc;
ProfileBloc _profileBloc;
ThingBloc _thingBloc;
#override
void initState(){
_navigationBloc = NavigationBloc();
_thingBloc = ThingBloc();
_profileBloc = ProfileBloc();
super.initState();
}
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<NavigationBloc>(
builder: (BuildContext context) => _navigationBloc
),
BlocProvider<ProfileBloc>(
builder: (BuildContext context) => _profileBloc
),
BlocProvider<ThingBloc>(
builder: (BuildContext context) => _thingBloc
),
],
child: BlocBuilder(
bloc: _navigationBloc,
builder: (context, state){
if (state is DrawProfilePage){
return ProfilePage();
} else if (state is DrawThingsPage){
return ThingsPage();
} else {
return null
}
}
)
)
}
}
And after that, we can use any of bloc from parent and all widgets will share the same state and can dispatch event on the same bloc
class ThingsPage extends StatefulWidget {
#override
_ThingsPageState createState() => _ThingsPageState();
}
class _ThingsPageState extends State<ThingsPage> {
#override
void initState(){
_profileBloc = BlocProvider.of<ProfileBloc>(context);
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
child: BlocBuilder(
bloc: _profileBloc,
builder: (context, state){
if (state is ThingsAreUpdated){
return Container(
Text(state.count.toList())
);
} else {
return Container()
}
}
)
);
}
}