Flutter Bloc to Bloc Communication: How to receive data on initial listen to broadcast stream? - flutter

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:
}
}

Related

Is it ok to return an variable from a cubit state function?

Is it ok to return a value from a Cubit state function or is it better to emit a state and use BlocListener?
Future<Game?> addGame(List<String> players, int numOfRounds) async {
try {
Game game = await repository.addGame(DateTime.now(), players, numOfRounds);
return game;
} on Exception {
emit(GamesError(message: "Could not fetch the list, please try again later!"));
}
}
The widget that calls this function adds a game and then redirects to a new page and passes the game object to it.
This works but it doesn't feel like it is the right approach. Is it ok to do this or should I be emitting a new state and using the BlocListener to redirect to the new page?
Of course, it's not.
Bloc/Cubit is the single source of truth for the widget. All data that comes to the widget should be passed via state, one source. If you return values from Cubit methods, you are breaking the whole concept of the Bloc pattern.
Bloc data flow
It is ok, but not preferred.
Presently the function addGame returns a future, so you would have to use FutureBuilder to display it's value.
Instead emit state having containing the value,Now you can use BlocListener and BlocBuilder to display the value of game produced in the function addGame. So now the purpose of using bloc makes sense.
Use code like:
Future<Game?> addGame(List<String> players, int numOfRounds) async {
try {
Game game = await repository.addGame(DateTime.now(), players, numOfRounds);
emit(GameLoaded(game: game); // 👈 Use it this way
} on Exception {
emit(GamesError(message: "Could not fetch the list, please try again later!"));
}
}

Which is the correct way to await for an Event in Flutter Bloc

my application is growing in complexity and I can't figure out how to handle the await of an Event, when I need to execute code after that Event. Now I'm just putting the code that I need after an event inside the Bloc and I now that this is not the way to do it, making my app a mess. This is how I am managing my app:
For example, if I need to add a user in my backend and after that, execute an action I do this in my view/screen:
BlocProvider.of<UserBloc>(context).add(AddUserEvent())
As events are async, I can't put the code after that line so Inside the UserBloc I am making:
on<HomeNavigationEvent>((event, emit) {
#Call backend api to create user
#Do my needed action
});
And some times this is even worst because I need to call another Bloc, so I have to pass the context to that Event, like this:
BlocProvider.of<UserBloc>(context).add(AddUserEvent(context))
on<HomeNavigationEvent>((event, emit) {
#Call backend api to create user
BlocProvider.of<OtherBloc>(event.context).add(MyNeededActionEvent())
});
So I think the answer is related with Bloc listener, but I don't know how to check for an event instead of a state I mean I can't do this because I am receiving the state but not the Event:
return BlocListener<UserBloc, UserState>(
listener: (context, state) { #I would like to have (context, state, event))
if (event is AddUserEvent) {
#DO my needed action
}
})
[EDITED]
Real case of my app:
VehiculoBloc() : super(const VehiculoState(vehicle: null)) {
on<GetCurrentVehicle>((event, emit) async {
vehicle = await api.getCurrentVehicle();
final bool showVehicleButton = vehicle != null;
BlocProvider.of<HomeBloc>(event.context).add(ShowVehicleButtonEvent(showVehicleButton ));
emit(state.copyWith(vehicle: vehicle));
});
}
The main purpose of using BLoC is to separate the UI from the state. And have a global, and immutable, state that you can access from wherever you wish. The purpose of the UI is the fire events and let the BLoC generate the Appropriante state. If you try to listen to event, why would you even use BLoC?
In situations like yours, you want to create a dedicated state for the AddUserEvent. For example AddUserStateSuccess Whenever the BLoC receives that event it will emit the corresponding state. All you have to do is listen for that state ;)
In the listener of that state you can fire other events from other BLoC as well.

How to attend best practice for not using UI code in the Controller with GetX flutter when I need to show a Dialog if my task complete.?

For a simple Email login with OTP code I have a structure as follows.
View
await _signUpCntrl.signUp(email, password);
Controller
_showOtpDialog(email);
_showOtpDialog func
return Get.dialog(
AlertDialog(
So the thing is _showOtpDialog function is inside a controller file. ie. /Controllers/controller_file.dart
I want do something like a blocListener, call the _showOtpDialog from a screen(view) file on signup success. (also relocate the _showOtpDialog to a view file)
Using GetX I have to use one of the builders either obs or getbuilder. Which is I think not a good approach to show a dialog box.
On internet it says Workers are the alternative to BlocListener. However Workers function resides on Controller file and with that the dialog is still being called on the controller file.
As OTP dialog will have its own state and a controller I wanted to put it inside a /view/viewfile.dart
How do I obtain this?
I tried using StateMixin but when I call Get.dialog() it throw an error.
visitChildElements() called during build
Unlike BLoC there's no BlocListener or BlocConsumer in GetX.
Instead GetX has RxWorkers. You can store your response object in a Rx variable:
class SomeController extends GetxController{
final response= Rxn<SomeResponse>();
Future<void> someMethod()async{
response.value = await someApiCall();
}
}
And then right before the return of your widget's build method:
class SomeWidget extends StatelessWidget{
final controller = Get.put(SomeController());
#override
Widget build(BuildContext context){
ever(controller.response, (SomeResponse res){
if(res.success){
return Get.dialog(SuccessDialog()); //Or snackbar, or navigate to another page
}
....
});
return UI();
}
First thing, you will need to enhance the quality of your question by making things more clearly. Add the code block and the number list, highlight those and making emphasize texts are bold. Use the code block instead of quote.
Seconds things, Depends on the state management you are using, we will have different approaches:
Bloc (As you already added to the question tag). By using this state management, you controller ( business logic handler) will act like the view model in the MVVM architecture. In terms of that, You will need to emit a state (e.g: Sent success event). Afterward, the UI will listen to the changes and update it value according to the event you have emitted. See this Bloc example
GetX (As your code and question pointed out): GetX will acts a little bit different. you have multiple ways to implement this:
Using callbacks (passed at the start when calling the send otp function)
Declare a general dialog for your application ( this is the most used when it comes to realization) and calling show Dialog from Bloc
Using Rx. You will define a Reactive Variable for e.g final success = RxBool(true). Then the view will listen and update whenever the success changes.
controller.dart
class MyController extends GetxController {
final success = RxBool(false);
void sendOtp() async {
final result = await repository.sendOTP();
success.update((val) => {true});
}
}
view.dart
class MyUI extends GetView<MyController> {
#override
Widget build(BuildContext context) {
ever(controller.success, (bool success) {
// This will update things whenever success is updated
if (success) {
Get.dialog(AlertDialog());
}
});
return Container();
}
}

Best practice to use mapEventToState with incoming streams?

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.

Initial value with StreamController without RxDart

I'm using StreamControllers with Flutter. I have a model with some default values. From the widgets where I'm listening to the stream I want to supply some of those default values. I can see I can set an initial value on the StreamBuilder, but I want to use data from the model inside the bloc as initial data. So as soon as someone is using the snapshot data they get the default values. I've seen RxDart has a seed value, just wondering if this is possible without replacing with RxDart?
What you are looking for is StreamController#add method,
Sends a data event.
Listeners receive this event in a later microtask.
Note that a synchronous controller (created by passing true to the
sync parameter of the StreamController constructor) delivers events
immediately. Since this behavior violates the contract mentioned here,
synchronous controllers should only be used as described in the
documentation to ensure that the delivered events always appear as if
they were delivered in a separate microtask.
happy fluttering
The default value for a stream can be specified when the class is initialized after adding a listener for that stream.
import 'dart:async';
enum CounterEvent { increase }
class CounterBloc {
int value = 0;
final _stateCntrl = StreamController<int>();
final _eventCntrl = StreamController<CounterEvent>();
Stream<int> get state => _stateCntrl.stream;
Sink<CounterEvent> get event => _eventCntrl.sink;
CounterBloc() {
_eventCntrl.stream.listen((event) {
_handleEvent(event);
});
_stateCntrl.add(value); // <--- add default value
}
void dispose() {
_stateCntrl.close();
_eventCntrl.close();
}
_handleEvent(CounterEvent event) async {
if (event == CounterEvent.increase) {
value++;
}
_stateCntrl.add(value);
}
}