TL;DR: Is there a way to listen to streams and emit state using Cubit instead of BLoC?
I'm using the BLoC library for Flutter and we use Cubits for our state management. Everything is working all right so far for interactions to get data or save data but now I need to work with streams. In my case that means watching snapshots from FirebaseFirestore.
I tried searching the internet if there are ways to observe streams for Cubit rather than using BLoC but most of the results point me to BLoC. I've worked with BLoC in another project so I know how to use it for observing streams but I wanted to use Cubit instead if there is a way.
Here is an example of the code I use to observe in FireStore:
#override
Stream<Either<Failure, List<MTalk>>> watchTalk() async* {
const path ='path/to/talks';
yield* firestore
.collection(path)
.snapshots()
.map(
(snap) => right<Failure, List<MTalk>>(
snap.docs
.map(
(documentSnapshot) => MTalk.fromFirestore(documentSnapshot))
.toList(),
),
)
.onErrorReturnWith((e) {
if (e is FirestoreException) {
return left(RetrieveFailure(message: e.message));
} else {
return left(UnknownFailure(message: e.toString()));
}
});
}
}
When using BLoC, you could simply use async* and yield to return the state whenever you return data from calling watchTalk() because mapEventToState() is also a Stream that yields State. In the case of Cubit, we use emit(MyState) to get state in the UI and the functions aren't of type Stream. What I wanna know is if we can use Cubit to work with streams.
In case anyone gets confused like I did it is pretty straightforward. You can call on listen on your cubit and emit state each time you get a value from the stream. Here's an example of my code listening for changes in network connectivity using DataConnectionChecker:
///Listens to the connectivity of
Future<void> listenToConnectivity() async {
if (_internetStream != null) {
await _internetStream.cancel();
}
_internetStream = repo.isConnectedToInternet().listen((result) {
result.fold(
(failure) => _processFailure(failure: failure),
(isConnected) {
if (isConnected) {
emit(const SessionState.connected());
} else {
emit(const SessionState.disconnected());
}
},
);
});
}
Related
Problem Summary:
I'm trying to fetch a list from StateA of BlocA when I create a new bloc.
Simplified Background:
I have an overarching bloc (BlocA), that is always active in the context of this problem, and 2 screens with a corresponding bloc each (BlocB & BlocC) that gets created when routing to its associated screen and closes when routing away from its associated screen. Every time a new bloc is created it needs to fetch its data from the state of BlocA. The user might move back and forth between screens.
What I tried:
I created stream controllers in BlocA that streams relevant data to each of the blocs via a getter. At first, I tried a normal (single listner) stream which worked fine initially. However, when routing away and then back to the screen it throws an error when resubscribing to the same stream using the getter. I then instantiated the stream controller as a broadcast stream StreamController.broadcast(). The problem is then that, when subscribing to the stream, no data is passed on subscription to the stream like with a normal stream and when I try to implement an onListen callback in the broadcast constructor (to add an event to the sink) it gives me an error The instance member '_aStream' can't be accessed in an initializer. A similar error appears for state. See below:
... _aStream = StreamController.broadcast(onListen: () {return _aStream.add(state.TypeX)})
Simplified Example Code:
class BlocA extends Bloc<BlocAEvent, BlocAState> {
BlocA() : super(BlocAState()) {
on<EventA>(_onevent);
}
final StreamController<TypeX> _aStream = StreamController.broadcast();
Stream<TypeX> get aStream => _aStream.stream;
final StreamController<TypeY> _bStream = StreamController.broadcast();
Stream<TypeY> get bStream => _bStream.stream;
...
// sink.add()'s are implemented in events
}
class BlocB extends Bloc<BlocBEvent, BlocBState> {
BlocB({required this.blocA}) : super(BlocBState()) {
on<EventB>(_onEventB);
blocASubscription = blocA.aStream.listen((stream) {
if (stream != state.fieldX) {
add(EventB(fieldX: stream));
}
});
}
final BlocA blocA
late final StreamSubscription blocASubscription;
FutureOr<void> _onEventB(EventB event, Emitter<BlocBState> emit) {
emit(state.copyWith(fieldX: event.fieldX));
}
}
class BlocC extends Bloc<BlocCEvent, BlocCState> {
// similar to BlocB
}
You do not need a stream, because bloc underhood is on streams yet. You can sent everything what you want through events and states. Check the library of Angelov https://bloclibrary.dev/#/
I ended up staying with the stream controllers, as used in the example code, but created a new event for BlocA where it is triggered when the user changes between screens and sinks the appropriate state data into the stream. The event carried an index field to indicate the screen that was routed to. The event's index corresponds with the navBar index.
The event handling implementation looked like this:
FutureOr<void> _onScreenChanged(
ScreenChanged event,
Emitter<BlocAState> emit,
) async {
switch (event.index) {
case 0:
_aStream.sink.add(state.fieldX);
break;
case 1:
_bStream.sink.add(state.fieldY);
break;
default:
}
}
on screen init, I am loading my data via an externalData_bloc. On the same screen I am also rendering another widget controlled with internalData_bloc for user input which depends on the number of imported records (how many rows are needed). Of course, the rendering is faster than the data load, so I get null rows needed.
I found this nice question & answer, however, I do not get it to work. The solution says
Future loginProcess() async {
await for (var result in _userBloc.state) {
if (result.status == Status.finished) {
return;
}
}
}
I am within my repository. In here, I am also storing the external data in a variable. So within the repository class I have my function to get the number of records (properties are stored in the repository, and I want to return its length).
Future<int> getNoOfProperties(int problem) async {
LogicGraphsPStmntBloc bloc = LogicGraphsPStmntBloc();
Future propertiesLoad() async {
await for (var s in bloc) {
if (s == LogicGraphsPStmntLoadSuccess) {
return;
}
}
}
propertiesLoad();
return properties.length;
}
But no matter what I try to put in for await for (var result in XXXX) or if (s.YYY == ZZZ), it doesn't work. My understanding is, XXXX needs to be a stream which is why I used bloc = LogicGraphsPStmntBloc(). But I guess this creates another instance than the one used in my widgets. LogicGraphsPStmntBloc doesn't work either, nor do states. bloc at least doesn't throw an error already in the editor, but besides the instance issue, the if statement throws an error, where in cubit.dart StreamSubscription<State> listen(... is called with cancelOnError . Anyone having some enlightenment?
Bloc uses Stream and has a continuous flow of data. You might want to listen for changes in data instead of a finished task. Using a StreamBuilder is one way of doing this.
StreamBuilder<User>(
stream: bloc.userState, // Stream from bloc
builder: (context, AsyncSnapshot<State> snapshot) {
if (snapshot.hasData) {
// check if auth status is finished
}
}
)
I am using BLoC in flutter.
As soon as BLoC instance is created I want to make to API calls. To achieve that, I have added the following code inside the constructor.
class MyBloc extends Bloc<MyBlocEvent, MyBlocState> {
MyBloc() {
_repository = MyAccountRepository();
_myAccountList = List();
add(API1CallEevent());
add(API2CallEevent());
}
...
and the event handling part
...
#override
Stream<MyBlocState> mapEventToState(MyBlocEvent event) async* {
if (event is API1CallEevent) {
var ap1 =
await _repository.getAPI1();
----
----
}else if (event is API2CallEevent) {
var api2 =
await _repository.getAPI2();
----
---
}
}
The problem I am facing is that the API calls are not executed parallel, which means after API1CallEvent is completed then API2CallEvent get executed...
is there any way to do that in parallel?
In my opinion, doing two API calls in parallel and expecting result at the same time is not much related to BLoC.
It is better if each bloc-event triggers a specific set of actions, and events are decoupled from each other.
Additionally;
Instead of raising an event inside init block, it is better to do that when you init Bloc inside a provider. See example;
BlocProvider<AuthBloc>(
lazy: false,
create: (context) => AuthBloc(
userRepository: _userRepository,
)..add(AppStartedEvent()),
),
This generates an event right after Bloc is initialized.
A bloc basically is a state machine. It does not do parallelism, that's not what it's built for. It's sequentially going from one state into another. In doing that, it can do things in parallel internally, but it cannot (or should not) take input in parallel.
If you want one event to execute multiple awaitable actions in parallel, you can do that:
#override
Stream<MyBlocState> mapEventToState(MyBlocEvent event) async* {
if (event is CallTheAPIsEvent) {
final results = await Future.wait([
_repository.getAPI1(),
_repository.getAPI2()
]);
// do something with the results
yield ApisHaveBeenCalledState();
}
// more event handling
}
I have two BLoCs.
EstateBloc
EstateTryBloc
My Application basically gets estates from an API and displays them in a similar fashion
Now I wanted to add a sort functionality, but I could only access the List of Estates via a specific state.
if(currentState is PostLoadedState){{
print(currentState.estates);
}
I wanted to make the List of estates available for whichever bloc, that needed that list.
What I did was, I created the EstateTryBloc, which basically contains the List of estates as a state.
class EstateTryBloc extends Bloc<EstateTryEvent, List<Estate>> {
#override
List<Estate> get initialState => [];
#override
Stream<List<Estate>> mapEventToState(
EstateTryEvent event,
) async* {
final currentState = state;
if(event is AddToEstateList){
final estates = await FetchFromEitherSource(currentState.length, 20)
.getDataFromEitherSource();
yield currentState + estates;
}
}
}
As I print the state inside the bloc I get the List of estates but I dont know how I would use that List in a different bloc.
print(EstateTryBloc().state);
simply shows the initialState.
I am open for every kind of answer, feel free to tell me if a different approach would be better.
When you do print(EstateTryBloc().state); you are creating a new instance of EstateTryBloc() that's why you always see the initialState instead of the current state.
For that to work, you must access the reference for the instance that you want to get the states of. Something like:
final EstateTryBloc bloc = EstateTryBloc();
// Use the bloc wherever you want
print(bloc.state);
Right now the recommended way to share data between blocs is to inject one bloc into another and listen for state changes. So in your case it would be something like this:
class EstateTryBloc extends Bloc<EstateTryEvent, List<Estate>> {
final StreamSubscription _subscription;
EstateTryBloc(EstateBloc estateBloc) {
_subscription = estateBloc.listen((PostState state) {
if (state is PostLoadedState) {
print(state.estates);
}
});
}
#override
Future<Function> close() {
_subscription.cancel();
return super.close();
}
}
To be honest I overcomplicated things a little bit and did not recognize the real problem.
It was that I accidently created a new instance of EstateBloc() whenever I pressed on the sort button.
Anyways, thanks for your contribution guys!
Is there any elegant way to map incoming streams from a private api directly inside mapEventToState() without having to create redundant private events in the bloc?
I came with this solution. It's ok with one single stream, but with multiple streams it starts to get a mess. Thanks in advance.
// (don't mind the imports, this is the bloc file)
class ExampleBloc extends Bloc<ExampleEvent, ExampleState> {
final MyPrivateApi api = MyPrivateApi.instance; // singleton
ExampleBloc() {
// api has a stream of booleans
api.myStream.listen((b) {
// if it's true fire this event
if (b) this.add(_MyPrivateEvent());
}
#override
ExampleState get initialState => InitialExampleState();
#override
Stream<ExampleState> mapEventToState(
ExampleEvent event,
) async* {
if (event is _MyPrivateEvent) {
yield SomeState;
}
}
// private Event
class _MyPrivateEvent extends ExampleEvent {
}
As I can see, you can subscribe on event updates in your screen, and push event from screen to Bloc if need some calculations. Code will be more clean.
Your way seems to be the only way works and seems to be used - see this bloc issue: https://github.com/felangel/bloc/issues/112 and this example project: https://github.com/algirdasmac/streams_and_blocs
Just make sure to dispose the subscription that gets returned by api.myStream.listen.
Previous answer
The following DOES NOT work for infinite streams because the generator function will await until the stream finishes. This can only be used for stream the complete fast, like maybe an upload/download progress.
See accepted answers here Dart yield stream events from another stream listener and here Dart/Flutter - "yield" inside a callback function
ExampleBloc() {
_MyInitEvent();
}
#override
Stream<ExampleState> mapEventToState(
ExampleEvent event,
) async* {
if (event is _MyInitEvent) {
await for (bool b in api.myStream) {
if (b) yield SomeState;
}
}
}
Build another block that encapsulate your stream of bytes.
You can make two events (ByteRead and ByteConsume) and two states (ByteWaiting and ByteAvailable).
Byteread and ByteAvailable should have a _byte field for storing data. Your bytebloc has a subscriber listening the stream and every time it reads a byte it fires a ByteRead event.
You should also add to the bloc a consume() method that gives the last byte readed and fires the ByteConsume event.
The two states and events are mutual:
you start in bytewaiting and every time you have a ByteRead you change to ByteAvailable and
every time you have a ByteConsume you change back to ByteWaiting.