How to listen to multiple stream subscriptions in a bloc? - flutter

I'm trying to create a bloc that depends on two other blocs. For example, I have Bloc C which depends on Bloc A and Bloc B. I'm trying to do something like the following using flutter_bloc in order to achieve it :
class BlocC
extends Bloc< BlocCEvent, BlocCState> {
final BlocA blocA;
final BlocC blocB;
StreamSubscription blocASubscription;
StreamSubscription blocBSubscription;
BlocC({
#required this.blocA,
#required this.blocB,
}) : super((blocA.state is blocALoaded &&
blocB.state is blocBLoaded)
? BlocCLoaded(
blocA: (blocA.state as blocALoaded).arrayFromBlocA,
blocB:
(blocB.state as blocBLoaded).arrayFromBlocB,
)
: BlocCLoading()) {
blocASubscription = blocA.stream.listen((state) {
if (state is blocALoaded) {
add(BlocAUpdated((blocA.state as blocALoaded).arrayFromBlocA));
}
});
blocBSubscription = blocB.stream.listen((state) {
if (state is BlocBLoaded) {
add(BlocBUpdated((blocB.state as BlocBLoaded).arrayFromBlocB));
}
});
}
...
#override
Future<void> close() {
blocASubscription.cancel();
BlocBSubscription.cancel();
return super.close();
}
}
The problem is that I'm getting the following error: Bad state: Stream has already been listened to. I found information about that error in the next post.
I understand the error is happening because a stream can only listen to one bloc at a time, and not to multiple ones. In my case, the stream is already listening to blocA when I try to listen to blocB. However, I'm not sure how to fix this problem.
I will really appreciate any help on this.

You have to merge the two streams into one and act based on the event type:
import 'package:async/async.dart' show StreamGroup;
//...
final blocAStream = blocA.stream;
final blocBStream = blocB.stream;
var blocAandBStreams = StreamGroup.merge([blocAStream, blocBStream]);
blocAandBStream.listen((event){
if(event is BlocAState){
if (event is blocALoaded) { //<-- for readability
add(BlocAUpdated((blocA.state as blocALoaded).arrayFromBlocA));
}
}else if(event is BlocBState){
if (event is BlocBLoaded) {//<-- for readability
add(BlocBUpdated((blocB.state as BlocBLoaded).arrayFromBlocB));
}
}
})

I've implemented event bus pattern to communicate between blocs. The advantage of this approach is that your block instances are not coupled to each other so you don't need to
instantiate or inject their instances Manage Global Events by bloc

Related

Is it good to delegate event handling to events, when using flutter bloc?

I started to learn bloc state management recently, and my mapEventToState often getting too large, so i split event handling to another async* methods, for example:
class ClassNameBloc extends Bloc<ClassNameEvent, ClassNameState> {
ClassNameBloc(): super(ClassNameInitialState());
Stream<ClassNameStata> handleEventOne() async* {
yield ClassNameState;
...
}
#override
Stream<ClassNameState> mapEventToState(ClassNameEvent event) async* {
if (event is ClassNameEvent1) {
yield* handleEventOne();
} else if (...) {
yield ClassNameState;
...
}
}
#override
Future<void> close() async {
super.close();
}
}
But using this solution does not fix problem of large if else statements
So another solution is to delegate event handling to events, for example
class ClassNameEvent extends Equatable {
Stream<ClassNameState> handleEvent(HandleEventParams) async* {}
#override
List<Object> get props => [];
}
Each concrete event overrides handleEvent method, so in bloc i can type
class ClassNameBloc extends Bloc<ClassNameEvent, ClassNameState> {
ClassNameBloc(): super(ClassNameInitialState());
#override
Stream<ClassNameState> mapEventToState(ClassNameEvent event) async* {
yield* event.handleEvent();
}
#override
Future<void> close() async {
super.close();
}
}
I know, that its not a very good approach of event-handling, because event's main function is to notify listener about event, not handle itself
Is there another approach?
I can't answer with the truth, but I can provide some context.
Bloc library advices that:
BLoC is a design pattern that is defined by the following rules:
Input and Output of the BLoC are simple Streams and Sinks.
Dependencies must be injectable and Platform agnostic.
No platform branching is allowed.
Implementation can be whatever you want as long as you follow the above rules.
I read all https://bloclibrary.dev and couldn't find some preferences for your question. But looking at the examples on GitHub, I feel the event handling is in the Bloc class, not in the Event class.

What is the difference between "return <Stream>" and "yield* <Stream>" in flutter?

I noticed some strange behaviour of streams when working with Streams in Flutter.
Setup
An EventChannel provides a stream of events.
In my Cubit I listen on that stream and cancel the StreamSubscription in the close method of the Cubit.
The next screen also uses the same EventChannel to listen for events.
When I enter the second screen, the onCancel method in Android was called twice and therefore no events where passed through to the second Cubit.
The Scanner is a singleton, so both Cubits use the same instance.
Function for the Stream
class Scanner {
final eventChannel = EventChannel("events");
Stream<ScanEvent> getScanEvent() {
return _scanEvents.receiveBroadcastStream().map((event) => ScanEvent.fromJson(jsonDecode(event)));
}
}
Code in the Cubits
Scanner scanner = get<Scanner>();
Future<void> listenForScan() async {
_streamSubscription = _scanner.getScanEvent().listen((event) => submitSerialText(event.scanData));
}
#override
Future<void> close() {
_streamSubscription?.cancel();
return super.close();
}
Fix
When I use async* with yield* like so it works:
Fixed Function for the Stream
class Scanner {
final eventChannel = EventChannel("events");
Stream<ScanEvent> getScanEvent() async* {
yield* _scanEvents.receiveBroadcastStream().map((event) => ScanEvent.fromJson(jsonDecode(event)));
}
}
Question
Why is the stream in the first approach canceled twice?

Riverpod provider is always null

I am using riverpod for my state manegement in my flutter app.
Riverpod offers a feature for combined providers, but my dependent provider does not update and always returns null.
By clicking one of the pins (secrets) on the map, my "selectedSecretProvider" is updated (default is null). This should trigger the initialization of my audio player. And by clicking play, the sound of the current _selectedSecret should play. So my "selectedTrackProvder" is dependent on my "selectedSecretProvider":
final selectedTrackProvider = StateNotifierProvider<SelectedTrack, Track>((ref) {
Secret? selectedSecret = ref.watch(selectedSecretProvider);
return SelectedTrack(selectedSecret);
});
Here is my selectedTrack class:
class SelectedTrack extends StateNotifier<Track> {
SelectedTrack(this.selectedSecret) : super(Track.initial());
Secret? selectedSecret;
#override
void dispose() {
...
}
void initAudioPlayer() {
...
}
Future<int> play() async {
print(selectedSecret);
return ...
}
}
So why does it always print null, when clicking play?
(Btw. my bottom_panel_sheet is showing the correct data and also consumes the "selectedSecretProvider".)
I wouldn't say the way you're creating your StateNotifierProvider is wrong, but I think the following is a better approach that should solve your problem.
final selectedTrackProvider = StateNotifierProvider<SelectedTrack, Track>((ref) {
return SelectedTrack(ref);
});
class SelectedTrack extends StateNotifier<Track> {
SelectedTrack(this.ref) : super(Track.initial());
final ProviderReference ref;
Future<int> play() async {
final selectedSecret = ref.read(selectedSecretProvider);
print(selectedSecret);
return ...
}
}
This way you don't create a new StateNotifier every time the selectedSecretProvider updates, instead opting to read the current value of the selectedSecretProvider when attempting to call play.

Flutter bloc adding 2 event in same time

I wanna check users internet connection and firebase auth state changes in my app. I am using flutter bloc for my app's state management. But when call different 2 .add(event) in one initstate always the first one is run and changes states but second one didnt run didnt change state. What is the my wrong ?
my bloc:
class ControllerBloc extends Bloc<ControllerEvent, ControllerState> {
ControllerBloc() : super(ControllerInitial());
AuthApiClient _authApiClient = getIt<AuthApiClient>();
#override
Stream<ControllerState> mapEventToState(
ControllerEvent event,
) async* {
if (event is ControllInternetConnection) {
yield* internetControll();
}
if (event is ControllUserAuth) {
debugPrint("wwwwgeldi");
yield* userAuthControl();
}
// TODO: implement mapEventToState
}
Stream<ControllerState> internetControll() async* {
Stream<DataConnectionStatus> connectionState =
DataConnectionChecker().onStatusChange;
await for (DataConnectionStatus status in connectionState) {
switch (status) {
case DataConnectionStatus.connected:
debugPrint("Bağlandı");
yield InternetConnectedState();
break;
case DataConnectionStatus.disconnected:
debugPrint("Kesildi");
yield InternetConnectionLostState();
break;
}
}
}
Stream<ControllerState> userAuthControl() async* {
FirebaseAuth firebaseAuth = _authApiClient.authInstanceAl();
debugPrint("geldi");
Stream<User> authStream = firebaseAuth.authStateChanges();
_authApiClient.authInstanceAl().signOut();
await for (User authUserResult in authStream) {
if (authUserResult == null) {
yield UserAuthControlError();
}
}
}
}
my page where call my events
class _NavigationPageState extends State<NavigationPage> {
ControllerBloc controllerBloc;
#override
void initState() {
controllerBloc= BlocProvider.of<ControllerBloc>(context);
controllerBloc.add(ControllInternetConnection());
controllerBloc.add(ControllUserAuth());
super.initState();
}
If I am understanding this right, it looks to me that you are trying to solve two different problems with one BLoC. I don't see a reason why internet connection and user auth have to be in one BLoC, rather I would just separate the two in separate BLoCs.
As the discussion in this thread points out, the point of using a BLoC revolves around the idea of predictability purposes. You can override the existing BLoC event stream, but I personally think that is too complicated for what you are trying to do.
So I would suggest, either make two separate BLoCs or combine the entire process into one event, where the internet connection would be checked before the user is authenticated, you will then return different states depending on the errors.

Flutter listen Bloc state from Other Bloc

Hello I'm trying to listen state of bloc form other bloc.
I'm using this package https://pub.dev/packages/bloc
From my UserBloc I want listen AuthBloc and when It has the state AuthenticationAuthenticated the UserBloc should fire an event.
final UserRepository userRepository;
final authBloc;
StreamSubscription authSub;
UserBloc({ #required this.userRepository, #required this.authBloc}) {
authSub = authBloc.listen((stateAuth) {
//here is my problem because stateAuth, even is AuthenticationAuthenticated it return always false.
if (stateAuth is AuthenticationAuthenticated) {
this.add(GetUser()) ;
}
});
}
#override
Future<void> close() async {
authSub?.cancel();
super.close();
}
For now I have this problem:
When in debug I'm trying to print stateAuth it return:
stateAuth = {AuthenticationAuthenticated} AuthenticationAuthenticated
props = {_ImmutableList} size = 0
But stateAuth is AuthenticationAuthenticated return always false.
Is there any way for listen blocState From Other Bloc class?
Actually in one of the examples of the bloc library they listen to a Bloc (TodosBloc) from another Bloc (FilteredTodosBloc).
class FilteredTodosBloc extends Bloc<FilteredTodosEvent, FilteredTodosState> {
final TodosBloc todosBloc;
StreamSubscription todosSubscription;
FilteredTodosBloc({#required this.todosBloc}) {
todosSubscription = todosBloc.listen((state) {
if (state is TodosLoadSuccess) {
add(TodosUpdated((todosBloc.state as TodosLoadSuccess).todos));
}
});
}
...
You can check this example's explanation here.
To answer Sampir's question, yes, you're right, but sometimes you may want to do it in another way.
A bloc is something that manages an event for someone else. If you are working with ui events, your bloc manages them for your ui, but if you are working also with other kind of events (i.e. position events, or other streams events) you can have a bloc that manages your ui events and antoher bloc that manages the other kind of events (i.e. a bluetooth connection). So the first bloc must listen to the second one (i.e. because is waiting for establishing bluetooth connection). Think about an app that uses a lot of sensors, each one with its stream of data, and you'll have a chain of blocs that have to cooperate.
You can do it with multi-provider and multi-listener but your chain could be very long and writing your listener cases can be hard, or you may want to hide it from your ui, or you want to reuse it in another part of your app, so you may want to build your chain inside your blocs.
You can add a listener to a bloc almost everywhere. Using StreamSubscription, you can add a listener to every kind of streams, even the one in another bloc. The bloc must have a method to expose his stream, so you can listen to him.
Some code (I use flutter_bloc - flutter_bloc has multi-providers, but it's just for example):
class BlocA extends Bloc<EventA, StateA> {
final BlocB blocB;
StreamSubscription subscription;
BlocA({this.blocB}) {
if (blocB == null) return;
subscription = blocB.listen((stateB) {
//here logic based on different children of StateB
});
}
//...
}
class BlocB extends Bloc<EventB, StateB> {
//here BlocB logic and activities
}
You might not want your bloc to depend on another bloc with direct bloc-to-bloc dependency, instead, you might want to connect your bloc through the presentation layer.
You can use a BlocListener to listen to one bloc and add an event to another bloc whenever the first bloc changes.
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocListener<WeatherCubit, WeatherState>(
listener: (context, state) {
// When the first bloc's state changes, this will be called.
//
// Now we can add an event to the second bloc without it having
// to know about the first bloc.
BlocProvider.of<SecondBloc>(context).add(SecondBlocEvent());
},
child: TextButton(
child: const Text('Hello'),
onPressed: () {
BlocProvider.of<FirstBloc>(context).add(FirstBlocEvent());
},
),
);
}
}
The code above prevents SecondBloc from needing to know about FirstBloc, encouraging loose-coupling.
Check the official documentation for more info.
A recent update in the bloc source code requires a small change to the solution.
You now have to listen to a bloc/cubit's stream attribute, please see below example.
class FilteredTodosBloc extends Bloc<FilteredTodosEvent, FilteredTodosState> {
final TodosBloc todosBloc;
StreamSubscription todosSubscription;
FilteredTodosBloc({#required this.todosBloc}) {
todosSubscription = todosBloc.stream.listen((state) {
// ^^^^^
if (state is TodosLoadSuccess) {
add(TodosUpdated((todosBloc.state as TodosLoadSuccess).todos));
}
});
}
...
For people who don't want to disturb the bloc,
we can use this library Event Bus . It is based on stream. we can fire and listen events from anywhere between the screens.
// declare this globally
EventBus eventBus = EventBus();
// create event
class UserLoggedInEvent {
User user;
UserLoggedInEvent(this.user);
}
// listen event
eventBus.on<UserLoggedInEvent>().listen((event) {
// All events are of type UserLoggedInEvent (or subtypes of it).
print(event.user);
});
// fire event
User myUser = User('Mickey');
eventBus.fire(UserLoggedInEvent(myUser));