Is there a way to trigger a BlocListener just after its initialization? - flutter

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,

Related

Provider to be initialized asynchronously from `initState()` but get `could not find the correct Provider`

I develop an ad app, with a message button on the detailed view.
When the user tap on it, the chats view (stateful widget) is pushed to the screen.
The initState() is there to call the asyncInitMessages() which asynchronously fetches the chats and related message from the distant database. The asyncInitMessages() belongs to the Chats class which extends ChangeNotifier.
/// A chat conversation
class Chats extends ChangeNotifier {
/// Internal, private state of the chat.
void asyncInitMessages(
{required ClassifiedAd ad,
required String watchingUserId,
required bool isOwner}) async {
// blah blah
}
}
The ClassifiedAdMessagesViewstateful widget class implementation is as follows (snipet):
#override
void initState() {
// == Fetch conversation and messages
asyncInitMessages();
}
void asyncInitMessages() async {
// === Update all messages
try {
Provider.of<Chats>(context, listen: false).asyncInitMessages(
ad: widget.ad,
watchingUserId: widget.watchingUser!.uid,
isOwner: _isOwner);
} catch (e) {
if (mounted) {
setState(() {
_error = "$e";
_ready = true;
});
}
}
}
#override
Widget build(BuildContext context) {
// <<<<<<<<<<< The exception fires at the Consumer line right below
return Consumer<Chats>(builder: (context, chats, child) {
return Scaffold(
// ... blah blah
Finally, when running ll that, I got the exception in the build at the Consumer line:
could not find the correct Provider<chats>
Help greatly appreciated.
[UPDATED]
Here is the main (very far up from the messages screen)
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
//if (Firebase.apps.isEmpty) {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// } else {
// Firebase.app(); // if already initialized, use that one
// }
if (USE_DATABASE_EMULATOR) {
FirebaseDatabase.instance.useDatabaseEmulator(emulatorHost, emulatorPort);
}
runApp(RootRestorationScope(
restorationId: 'root',
child: ChangeNotifierProvider(
create: (context) => StateModel(),
child: const App())));
}
class App extends StatefulWidget {
const App({super.key});
#override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
#override
Widget build(BuildContext context) {
return PersistedAppState(
storage: const JsonFileStorage(),
child: MultiProvider(
providers: [
ChangeNotifierProvider<ThemeModel>.value(value: _themeModel),
//ChangeNotifierProvider<AuthModel>.value(value: _auth),
],
child: Consumer<ThemeModel>(
builder: (context, themeModel, child) => MaterialApp(
// blah blah
}
}
}
And the component just on top of the
/// Classified ad detail view
class ClassifiedAdDetailView extends StatefulWidget {
final User? watchingUser;
final ClassifiedAd ad;
const ClassifiedAdDetailView(
{Key? key, required this.watchingUser, required this.ad})
: super(key: key);
#override
State<ClassifiedAdDetailView> createState() => _ClassifiedAdDetailViewState();
}
class _ClassifiedAdDetailViewState extends State<ClassifiedAdDetailView>
with TickerProviderStateMixin {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Chats(),
builder: ((context, child) => Scaffold(
// blah blah
ElevatedButton(
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ClassifiedAdMessagesView(
ad: ad,
watchingUser: widget.watchingUser)));
}),
Providers must be located in the widget tree above the widget where you want to use them with Consumer or Provider.of. When you push a new route with Navigator, it won't be add the pushed route below the widget from where you push, it will add it at the same level where home of MaterialApp is located.
(I think the error message you get also states that you can't access the providers between routes.)
In general the tree will look like this if you push some routes (check it with the Flutter Widget Inspector):
MaterialApp
home
widget1
widget2
widget21
widget22
page1
widget1
widget2
page2
page3
In your code you create the provider in ClassifiedAdDetailView and then push
ClassifiedAdMessagesView from this in the onPressed method. You won't be access this provider from ClassifiedAdMessagesView because the tree will be like (simplified):
MaterialApp
home
ClassifiedAdDetailView
ClassifiedAdMessagesView
The solution is to "lift the state up" and place the provider above every widget from where you need to access it. It can be a part of your existing Multiprovider above MaterialApp but if it is too far, you need to find a proper place that is above both ClassifiedAdDetailView and ClassifiedAdMessagesView.

Flutter awesome notifications how to fix StateError (Bad state: Stream has already been listened to.)

I am getting this error when I have signed out from my flutter app and trying to log in again:
StateError (Bad state: Stream has already been listened to.)
The code that gives me this error is on my first page:
#override
void initState() {
AwesomeNotifications().actionStream.listen((notification) async {
if (notification.channelKey == 'scheduled_channel') {
var payload = notification.payload['payload'];
var value = await FirebaseFirestore.instance
.collection(widget.user.uid)
.doc(payload)
.get();
navigatorKey.currentState.push(PageRouteBuilder(
pageBuilder: (_, __, ___) => DetailPage(
user: widget.user,
i: 0,
docname: payload,
color: value.data()['color'].toString(),
createdDate: int.parse((value.data()['date'].toString())),
documentId: value.data()['documentId'].toString(),)));
}
});
super.initState();
}
And on another page that contains the sign out code.
await FirebaseAuth.instance.signOut();
if (!mounted) return;
Navigator.pushNamedAndRemoveUntil(context,
"/login", (Route<dynamic> route) => false);
What can I do to solve this? Is it possible to stop listen to actionstream when I log out? Or should I do it in another way?
Streams over all are single use, they replace the callback hell that that ui is, at first a single use streams can seem useless but that may be for a lack of foresight. Over all (at lest for me) flutter provides all the necessary widgets to not get messy with streams, you can find them in the Implementers section of ChangeNotifier and all of those implement others like TextEditingController.
With that, an ideal (again, at least for me) is to treat widgets as clusters where streams just tie them in a use case, for example, the widget StreamBuilder is designed to build on demand so it only needs something that pumps changes to make a "live object" like in a clock, a periodic function adds a new value to the stream and the widget just needs to listen and update.
To fix your problem you can make .actionStream fit the case you are using it or change a bit how are you using it (having a monkey patch is not good but you decide if it is worth it).
This example is not exactly a "this is what is wrong, fix it", it is more to showcase a use of how pushNamedAndRemoveUntil and StreamSubscription can get implemented. I also used a InheritedWidget just because is so useful in this cases. One thing you should check a bit more is that the variable count does not stop incrementing when route_a is not in focus, the stream is independent and it will be alive as long as the widget is, which in your case, rebuilding the listening widget is the error.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(App());
const String route_a = '/route_a';
const String route_b = '/route_b';
const String route_c = '/route_c';
class App extends StatelessWidget {
Stream<int> gen_nums() async* {
while (true) {
await Future.delayed(Duration(seconds: 1));
yield 1;
}
}
#override
Widget build(BuildContext ctx) {
return ReachableData(
child: MaterialApp(
initialRoute: route_a,
routes: <String, WidgetBuilder>{
route_a: (_) => Something(stream: gen_nums()),
route_b: (_) => FillerRoute(),
route_c: (_) => SetMount(),
},
),
);
}
}
class ReachableData extends InheritedWidget {
final data = ReachableDataState();
ReachableData({super.key, required super.child});
static ReachableData of(BuildContext ctx) {
final result = ctx.dependOnInheritedWidgetOfExactType<ReachableData>();
assert(result != null, 'Context error');
return result!;
}
#override
bool updateShouldNotify(ReachableData old) => false;
}
class ReachableDataState {
String? mount;
}
// route a
class Something extends StatefulWidget {
// If this widget needs to be disposed then use the other
// constructor and this call in the routes:
// Something(subscription: gen_nums().listen(null)),
// final StreamSubscription<int> subscription;
// Something({required this.subscription, super.key});
final Stream<int> stream;
Something({required this.stream, super.key});
#override
State<Something> createState() => _Something();
}
class _Something extends State<Something> {
int count = 0;
void increment_by(int i) => setState(
() => count += i,
);
#override
void initState() {
super.initState();
widget.stream.listen(increment_by);
// To avoid any funny errors you should set the subscription
// on pause or the callback to null on dispose
// widget.subscription.onData(increment_by);
}
#override
Widget build(BuildContext ctx) {
var mount = ReachableData.of(ctx).data.mount ?? 'No mount';
return Scaffold(
body: InkWell(
child: Text('[$count] Push Other / $mount'),
onTap: () {
ReachableData.of(ctx).data.mount = null;
Navigator.of(ctx).pushNamed(route_b);
},
),
);
}
}
// route b
class FillerRoute extends StatelessWidget {
const FillerRoute({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Go next'),
// Option 1: go to the next route
// onTap: () => Navigator.of(ctx).pushNamed(route_c),
// Option 2: go to the next route and extend the pop
onTap: () => Navigator.of(ctx)
.pushNamedAndRemoveUntil(route_c, ModalRoute.withName(route_a)),
),
);
}
}
// route c
class SetMount extends StatelessWidget {
const SetMount({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Set Mount'),
onTap: () {
ReachableData.of(ctx).data.mount = 'Mounted';
// Option 1: pop untill reaches the correct route
// Navigator.of(ctx).popUntil(ModalRoute.withName(route_a));
// Option 2: a regular pop
Navigator.of(ctx).pop();
},
),
);
}
}

i have two blocs that must listen to each other, how do i pass one as a parameter to the other?

**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...

Bloc listener not invoked without a delay

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.

Trouble rebuilding a StreamProvider to update its current data

so I have aConsumer<NotificationProvider> and in its builder function a StreamProvider<List<Item>>.
Please note that the latter widget builds perfectly on its initial load.
NotificationProvider contains a list of notifications from Firebase Cloud Messaging, ergo when I receive a notification, I push something into the class' Listand then callnotifyListeners()`.
Please also note that my NotificationProvider is doing a good job because I have a counter at my AppBar and it's updating whenever I receive one.
Now on to the meat and potato.
I'm trying to rebuild the StreamProvider whenever NotificationProvider.addAlert() is called. But somehow it's not working?
I also added updateShouldNotify: (prev, next) => true, but it didn't help one bit.
Please help. Thanks!
I also added updateShouldNotify: (prev, next) => true, on the StreamProvider properties but it didn't help one bit.
return Consumer<NotificationProvider>(
builder: (context, provider, child) {
return StreamProvider<ItemsProvider>(
builder: (_) async* {
ItemsProvider _itemsProvider = Provider.of<ItemsProvider>(context);
await _itemsProvider.getItems();
yield _itemsProvider;
},
child: LeContent(),
updateShouldNotify: (prev, next) => true,
);
},
);
I expect the list to update whenever I receive an FCM notification
The builder parameter is called exactly once for the entire life of the StreamProvider.
The fact that you called Provider.of<ItemsProvider> doesn't change anything here – the method still won't be called again, even if ItemsProvider changes.
If you insist in using StreamProvider, you'll need to somehow transform the Provider.of into a stream instead.
You can use a StatefulWidget to do so. Here's an example:
class ProviderToStream<T> extends StatefulWidget {
const ProviderToStream({Key key, this.builder, this.child}) : super(key: key);
final ValueWidgetBuilder<Stream<T>> builder;
final Widget child;
#override
_ProviderToStreamState<T> createState() => _ProviderToStreamState<T>();
}
class _ProviderToStreamState<T> extends State<ProviderToStream> {
final StreamController<T> controller = StreamController<T>();
#override
void dispose() {
controller.close();
super.dispose();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
controller.add(Provider.of<T>(context));
}
#override
Widget build(BuildContext context) {
return widget.builder(context, controller.stream, widget.child);
}
}
You can then do:
ProviderToStream<Foo>(
builder: (_, stream, __) {
return StreamProvider(
builder: (_) async* {
await for (final value in stream) {
// TODO: yield something
}
}
);
}
)
I solved my own issue by having a StreamBuilder in a StatefulWidget.
I have my fetcher as a separate Future<T> method which will be called on initState and on didUpdateWidget().
Next, in my StatefulWidget class, I require something that will be used to validate on didUpdateWidget()'s oldWidget.something != widget.something.
In my case I used the length of the NoticeProvider.notices which will be incremented every time my FCM configuration triggers it.
in code:
#override
void didUpdateWidget(_Body oldWidget) {
if (oldWidget.alertLength != widget.alertLength) _fetchData();
super.didUpdateWidget(oldWidget);
}
_fetchData, will then call the api, transform the JSON, and add it to the stream using a StreamController which, in turn, update the UI.
I hope someone will be helped by this answer in the Future!