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
Related
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?
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,
not sure why my ChangeNotifier isn't working.
This is my Class:
class LoadingProv with ChangeNotifier {
bool globalLoading;
void setGlobalLoading(bool truefalse) {
if (truefalse == true) {
globalLoading = true;
} else {
globalLoading = false;
}
notifyListeners();
}
bool get getGlobalLoadingState {
return globalLoading;
}
}
This is my Multiprovider in main.dart:
MultiProvider(
providers: [
ChangeNotifierProvider<MapData>(create: (ctx) => MapData()),
ChangeNotifierProvider<LoadingProv>(create: (ctx) => LoadingProv()),
],
child: MaterialApp(
This is my code in the main.dart Widget build(BuildContext context):
Consumer<LoadingProv>(builder: (context, loadingState, child) {
return Text(loadingState.getGlobalLoadingState.toString());
}),
And this is how I call setGlobalLoading:
final loadingProv = LoadingProv();
loadingProv.setGlobalLoading(true);
Unfortunately my loadingState.getGlobalLoadingState is always printed as false. But I can debug that it becomes actually true.
From my understanding, you are creating 2 LoadingProv object.
One is when initialising the Provider
ChangeNotifierProvider<LoadingProv>(create: (ctx) => LoadingProv()),
One is when some places you call
final loadingProv = LoadingProv();
So the one you updating is not the one inherit on the widget, then you cannot see the value updating the Consumer.
(1) if you want to keep create along with the create method, you should call setGlobalLoading via
Provider.of<LoadingProv>(context).setGlobalLoading(true);
(2) Or if you want to directly access the value like loadingProv.setGlobalLoading(true), you should initialise your provider like this
final loadingProv = LoadingProv();
MultiProvider(
providers: [
ChangeNotifierProvider<MapData>(create: (ctx) => MapData()),
ChangeNotifierProvider<LoadingProv>.value(value: loadingProv),
],
you can use this code to read data when change it automatically refresh the Text widget
Text(context.watch<LoadingProv>().getGlobalLoadingState.toString());
on for calling the void you can use this
context.read<LoadingProv>().setGlobalLoading(true);
**Hello I am new to flutter and bloc architecture.
I am trying to build a simple quiz app that has a timer.
On the quiz page, I have two blocs, a counter cubit to navigate to the next question, and a triviabloc for quiz activities like answer selection.
I am using MultiBlovProvider to provide the blocs.
I need each bloc to communicate with each other. Since each of the blocs is a parameter to the other, how do I pass it in the multiblocprovider
?**
var bloc = TriviaBloc();
var con = CountDownController();
// ignore: close_sinks
var cubit = CounterCubit(
bloc: bloc, controller: con);
return MultiBlocProvider(
providers: [
BlocProvider<TriviaBloc>(
create: (context) => bloc,
),
BlocProvider<CounterCubit>(
create: (context) => cubit)
],
child:
QuestionScreen(trivia: questions),
);
the cubit
class CounterCubit extends Cubit<int> {
StreamSubscription sub;
CounterCubit({this.controller, this.bloc}) : super(0) {
sub = bloc.listen((state) {
if (state is AnswerCorrect || state is AnswerNotCorrect) {
controller.pause();
}
});
}
final TriviaBloc bloc;
final CountDownController controller;
void increment() => emit(state + 1);
#override
Future<void> close() {
sub?.cancel();
return super.close();
}
#override
void onChange(Change<int> change) {
print(change);
super.onChange(change);
}
}
the bloc that must listen to the cubit
class TriviaBloc extends Bloc<TriviaEvent, TriviaState> {
StreamSubscription sub;
TriviaBloc({this.cubit}) : super(TriviaInitial()) {
sub = cubit.listen(
(state) async* {
if (state != 0) {
yield TriviaInitial();
}
},
);
}
final CounterCubit cubit;
Stream<TriviaState> mapEventToState(TriviaEvent event) async* {
if (event is AnswerCLicked) {
print(event.answer);
if (event.answer == event.correctAnswer) {
yield AnswerCorrect();
} else {
yield AnswerNotCorrect();
}
}
if (event is NoAnswerChosen) {
yield ShowAnswer();
}
}
#override
Future<void> close() {
sub?.cancel();
return super.close();
}
}
Thank you
You pass one bloc as an argument to a 2nd bloc. Now, within the 2nd bloc, you can get values from the 1st bloc's state. This is an approach for that:
if (userBloc.state is AppSettled) {
achievements = (userBloc.state as AppSettled).achievements;
userBloc is the bloc that I passed to the 2nd bloc, AppSettled is a state of userBloc, and achievements is a variable defined within that state.
In order to pass data back, you can this answer
Not sure if this is what you are looking for, but this is what I do to make sure that if the user authorization state changes they JobListCubit actually triggers a route to authorization screen.
In my main.dart:
MultiBlocProvider(
providers: [
BlocProvider<UserAuthCubit>(
lazy: true,
create: (context) => UserAuthCubit(
UserAuthRepository(),
),
),
BlocProvider<JobListCubit>(
lazy: true,
create: (context) => JobListCubit(
jobListRepository: JobListRepository(),
userAuthCubit: BlocProvider.of<UserAuthCubit>(context),
)),
....
Then in my JobListCubit:
class JobListCubit extends Cubit<JobListState>
with HydratedMixin<JobListState> {
JobListState get initialState {
return initialState ?? JobListInitial();
}
final JobListRepository jobListRepository;
final UserAuthCubit userAuthCubit;
JobListCubit({this.jobListRepository, this.userAuthCubit})
: super(JobListInitial());
...
Hope this is what you were looking for. I am a novice and it took me a lot of time to find a solution...
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.