Closed felangel closed 3 years ago
I Like this one <3
With the understanding that this will not impact everything that's already in place, meaning this change will not require a refactor in the presentation or the domain layer, this is epic.
To me, simplicity is elegance and this makes a lot of sense !
Instead of
@override
Stream<int> mapEventToState(CounterEvent event) async* {
if (event is Increment) yield* _mapIncrementToState(event);
}
Stream<int> _mapIncrementToState(Increment event) async* {
yield state + 1;
}
we have
on<Increment>((event, emit) => emit(state + 1));
And to me that is infinitely more elegant
This means developers must understand how to use the async, yield, and yield keywords. While these concepts are covered in the documentation, they are still fairly complex and difficult for newcomers to grasp.
That was definitely the biggest pain point while learning Dart, Flutter, and Bloc at the same time. I would often get confused as to when to use async
vs async*
and yield
vs yield*
.
In addition, developers don't have to use async generators (async, yield, yield) which can introduce complexity and undesired behavior in advanced cases.
One of the cool things about bloc is how native it felt by encouraging the use of Dart's built in generator features (despite the learning curve). I still think this is a good change, though, given how much it reduces the amount of boilerplate needed and the fact that Dart still hasn't released the fix for such a critical bug.
This allows developers to focus on the logic by directly registering an event handler for each type of event which streamlines the bloc code a bit further.
That is actually the biggest benefit of adopting this approach, to me. It feels much more declarative, cleaner, and seems to fit nicely within bloc's approach to state management.
With the proposed changes the CounterBloc would look something like:
abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0) { on<Increment>((event, emit) => emit(state + 1)); } }
That cleaned up nicely ๐ฎ
This looks clean and awesome. Just a doubt, will this be creating a subscription or listener kinda thing when the object of bloc is created?
Eg: CounterBloc _counterBloc = BlocProvider.of
CounterBloc() : super(0) {
on<Increment>(_onIncrement);
on<Decrement>(_onDecrement);
}
as there won't be any method now.
This looks clean and awesome. Just a doubt, will this be creating a subscription or listener kinda thing when the object of bloc is created? Eg: CounterBloc _counterBloc = BlocProvider.of(context); //Will this create a listener or subscription to:
CounterBloc() : super(0) { on<Increment>(_onIncrement); on<Decrement>(_onDecrement); }
as there won't be any method now.
Thanks for the feedback! It shouldn't create any additional subscriptions -- it'll just register the handler for that event type and under the hood the number of subscriptions should stay the same. Hope that helps ๐
That was pretty helpful and easy to understand
Really nice and semantic refactoring! ๐ (Feels like C/C++ enum-based-event handling)
Any way to unregister or update an event handler? ๐ค It would add more flexibility for cases when a cubit/bloc behavior might change dynamically in the run.
What would an example that includes some basic async processing look like?
Really nice and semantic refactoring! ๐ (Feels like C/C++ enum-based-event handling)
Any way to unregister or update an event handler? ๐ค It would add more flexibility for cases when a cubit/bloc behavior might change dynamically in the run.
Thanks! I thought about having on
returning something similar to a subscription so you could potentially dynamically register and unregister handlers but unless there's a compelling use case I'd prefer to keep it simple.
What would an example that includes some basic async processing look like?
It could look something like:
abstract class WeatherEvent {}
class WeatherRequested extends WeatherEvent {
WeatherRequested(this.latLng);
final LatLng latLng;
}
abstract class WeatherState {}
class WeatherInitial extends WeatherState {}
class WeatherLoadInProgress extends WeatherState {}
class WeatherLoadFailure extends WeatherState {}
class WeatherLoadSuccess extends WeatherState {
WeatherLoadSuccess(this.weather);
final Weather weather;
}
class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
WeatherBloc(this.weatherRepository) : super(WeatherInitial()) {
on<WeatherRequested>(_onWeatherRequested);
}
final WeatherRepository weatherRepository;
void _onWeatherRequested(WeatherRequested event, Emit<WeatherState> emit) async {
emit(WeatherLoadInProgress());
try {
final weather = await weatherRepository.getWeather(event.latLng);
emit(WeatherLoadSuccess(weather);
} catch (_) {
emit(WeatherLoadFailure());
}
}
}
Hope that helps ๐
Gotcha, so the idea is that on<T>
registrars would accept async
or async*
functions still to manage arbitrarily complex event handlers?
Gotcha, so the idea is that
on<T>
registrars would acceptasync
orasync*
functions still to manage arbitrarily complex event handlers?
Yeah on<T>
would allow you to register an event handler for events of type T
which could vary in complexity ๐
Amazing update and a pure example on how minimalism, even though it's tough to achieve, can be such a fresh breath of air for bloc and not only that! Thanks, @felangel!
@felangel It is very clean, I would like to see a similar way to the Freezed library
state.When/MaybeWhen
and state.Map/MaybeMap
and of course state.copyWith
too
@felangel It is very clean, I would like to see a similar way to the Freezed library
state.When/MaybeWhen
andstate.Map/MaybeMap
and of coursestate.copyWith
too
Thanks! You should still be able to use freezed
with when
, copyWith
, etc... with this approach
class MyBloc extends Bloc<MyEvent, MyState> {
MyBloc() : super(MyState.initial()) {
on<MyEvent>(_onEvent);
}
void _onEvent(MyEvent event, Emit<MyState> emit) async {
event.when(
a: () => emit(state.copyWith(...)),
b: () => emit(state.copyWith(...)),
...
}
}
@felangel I mean merge freezed magic to your magic ๐ , no problem we can still use it separately
This comment will add zero value to the proposal (which, by the way, amazing job on making the message clear!)... but I can't wait to see sealed classes/unions and data classes baked into Dart! These API changes + those language changes will be ๐ฏ!
Nice. That will make the code less boilerplate and more readable at the same time for bloc.
This will be like reading plain English!
on<Increment>(_onIncrement);
on<Decrement>(_onDecrement);
But I see that the cubit code is changed to a bloc by having an event instead of public methods
class CounterCubit extends Cubit<int> { CounterCubit() : super(0); void increment() => emit(state + 1); } Becomes ```dart abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0) { on<Increment>(_onIncrement); } void _onIncrement(Increment event, Emit<int> emit) { emit(state + 1); } }
So how is that not going to break cubits?
These changes will obviously be breaking changes that impact blocs (
cubits will remain unaffected
) so they would all be within the scope of a v8.0.0 release.Or do I miss something?
Thanks! Sorry for the confusion, the cubit code will not change at all. I was just trying to show how these changes would make it easier to convert a cubit into a bloc at a later time if you decided you wanted to switch. Let me know if that helps ๐
Understood ๐
That's great ๐
Can we also maybe rename the Emit
class to Emitter
? Honestly Emit
sounds more like a verb, like an action to be done (e.g. 'change', 'set'), not really something which provides me with that action (e.g. 'changer', 'setter') ๐
That's great ๐ Can we also maybe rename the
Emit
class toEmitter
? HonestlyEmit
sounds more like a verb, like an action to be done (e.g. 'change', 'set'), not really something which provides me with that action (e.g. 'changer', 'setter') ๐
Thanks! Emit
would just be a typedef
which would look something like:
typedef Emit<State> = void Function(State);
I like this! Mainly for reasons that others have already explained many times over now โ๏ธ
But I have a questionโhow would the library behave when multiple handlers are defined for the same event type? Would both be called (and in what order), or would one override the other? Would that be prevented somehow (I'm struggling to imagine how that could be done AOT)?
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>((event, emit) => emit(state + 1));
on<Increment>((event, emit) => emit(state - 1));
}
}
This is the only thing I can come up with that I think represents something less intuitive about this approach than having one mapEventToState
method that itself is always is called predictably (even if the internals can definitely get messy for newcomers).
Anyway, I still see this is an improvement and would be excited to work with this APIโjust curious how you'd plan to approach that!
Also kinda similar-ish question (and yes, I know that I'm offering you use cases that would likely be explicitly discouraged/practically never tried in the first place; just trying to give you stuff to consider):
I imagine this would be handled internally by registering handlers that check for a match on the event type before calling the correct callbacks, right? If so, how would the library deal with event type inheritance (because an IncrementTwice
event could return true
for both event is IncrementTwice
and event is Increment
)?
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class IncrementTwice extends Increment {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>((event, emit) => emit(state + 1));
on<IncrementTwice>((event, emit) => emit(state + 2));
}
}
I like this! Mainly for reasons that others have already explained many times over now โ๏ธ
But I have a questionโhow would the library behave when multiple handlers are defined for the same event type? Would both be called (and in what order), or would one override the other? Would that be prevented somehow (I'm struggling to imagine how that could be done AOT)?
abstract class CounterEvent {} class Increment extends CounterEvent {} class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0) { on<Increment>((event, emit) => emit(state + 1)); on<Increment>((event, emit) => emit(state - 1)); } }
This is the only thing I can come up with that I think represents something less intuitive about this approach than having one
mapEventToState
method that itself is always is called predictably (even if the internals can definitely get messy for newcomers).Anyway, I still see this is an improvement and would be excited to work with this APIโjust curious how you'd plan to approach that!
Thanks for the feedback and great question! Iโm open to suggestions but was leaning towards throwing a StateError if multiple handlers are registered for the same event type. Let me know what you think ๐
Also kinda similar-ish question (and yes, I know that I'm offering you use cases that would likely be explicitly discouraged/practically never tried in the first place; just trying to give you stuff to consider):
I imagine this would be handled internally by registering handlers that check for a match on the event type before calling the correct callbacks, right? If so, how would the library deal with event type inheritance (because an
IncrementTwice
event could returntrue
for bothevent is IncrementTwice
andevent is Increment
)?abstract class CounterEvent {} class Increment extends CounterEvent {} class IncrementTwice extends Increment {} class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0) { on<Increment>((event, emit) => emit(state + 1)); on<IncrementTwice>((event, emit) => emit(state + 2)); } }
Another great question ๐
Still working through the implementation details but ideally inheritance should be taken into consideration and all handlers of type T where T is E should be invoked when an event E is added. Hope that helps ๐
was leaning towards throwing a StateError if multiple handlers are registered for the same event type
Yeah that sounds like it would make the most sense. Restricting the ability to fall into this implementation in the first place definitely takes any of the ambiguity away, which I imagine is best ๐๐ป
ideally inheritance should be taken into consideration and all handlers of type T where T is E should be invoked when an event E is added
Interesting! I haven't thought much about it at all, but at first blush seems the most reasonable as well. And then I imagine the multiple callbacks would be called in the order that they're defined/registered?
That's all from me, I thinkโbig fan of the idea and hope I can get working with it soon!
And then I imagine the multiple callbacks would be called in the order that they're defined/registered?
Yup thatโs what I was thinking ๐
That's all from me, I thinkโbig fan of the idea and hope I can get working with it soon!
Thanks! Let me know if you have any other questions or suggestions and Iโll do my best to get an early preview of the API ready sometime this week (hopefully) so that people can start to mess around with it.
@felangel how could we avoid race conditions?
https://github.com/felangel/bloc/issues/2526#issuecomment-860886596
class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
WeatherBloc(this.weatherRepository) : super(WeatherInitial()) {
on<WeatherRequested>(_onWeatherRequested);
}
final WeatherRepository weatherRepository;
void _onWeatherRequested(WeatherRequested event, Emit<WeatherState> emit) async {
emit(WeatherLoadInProgress());
try {
final weather = await weatherRepository.getWeather(event.latLng);
emit(WeatherLoadSuccess(weather);
} catch (_) {
emit(WeatherLoadFailure());
}
}
}
This async method _onWeatherRequested
can't say when it is finished, then if I send twice the event WeatherRequested
, there is no mechanism for awaiting the end of the process of the first event.
That's great ๐ Can we also maybe rename the
Emit
class toEmitter
? HonestlyEmit
sounds more like a verb, like an action to be done (e.g. 'change', 'set'), not really something which provides me with that action (e.g. 'changer', 'setter') ๐Thanks!
Emit
would just be atypedef
which would look something like:typedef Emit<State> = void Function(State);
Right, typedef sorry ๐คญ
I've seen this kind of approach inside the framework when using StatefulBuilder (you get a setState
function in the callback, which is a typedef named StateSetter
)
https://api.flutter.dev/flutter/widgets/StatefulBuilder-class.html
But if it's not possible then no worries, great job anyway! ๐ช
Hey @felangel, just throwing in my two cents.
I may be the only dissenter but the amount of refactoring for big projects really makes me wary of this change. I feel this level of change ideally would only come if it's strictly needed. From my point of view I'm not convinced it is necessary.
To address point 1, I may be out of the loop but having nested generator like in the linked issue seems like a real anti-pattern and could be solved with some refactoring?
For 2, I am empathetic to the learning curve, but I also think Cubit is a much more approachable bloc-like thing that could be encouraged to be used for beginners. Kind of a "only use a bloc if you need to, otherwise use cubit".
For 3, I think things like the vscode extension and mason makes the boilerplate argument less impactful. I don't see the current implantation as overly verbose, and again, cubit is a nice alternative if that stuff really matters to you.
I again want to stress I'm not against it in theory; in fact, I think it's a nice readable syntax and I love that. I mainly just don't think the advantages warrant such a large breaking change. To me it seems like a bandaid solution to an edge case bug in dart, as well as syntax changes that cubit already handles well. I'm happy to be wrong about both those views though.
I also can't stress enough how grateful I am for this package and all the time everyone invests in it โค๏ธ thank you ๐
Awesome explained! Willing to see the changes soon :D
Well the proposal looks promising and easy to understand for newcomers.
Nothing against the proposal but I was just thinking that as Bloc is evolving I feel that it is moving more and more away from the BLoC pattern (Input Sink -> Output Stream).
I'm not saying that this is wrong or should be avoided, at the end of the day if the tool works it's ok but it was just a consideration. I don't know if it matters to someone or not.
@fabriziocacicia The BLoC pattern never was about "Input Sink -> Output Stream". It was about an event handler pattern base on state machines and the implementation was based on Stream
s.
@kranfix I don't think so.
The bloc
library was implemented since the beginning as a sort of a state machine, but if you saw the talk where the BLoC pattern was presented (Google IO ยด18) it was presented as a component with an input Sink
and an output Stream
.
Anyway, don't turn this issue into a debate about the essence of the BLoC pattern.
@kranfix the most important spec about the BLoC pattern was in fact relying solely on sinks and streams. ๐
@fabriziocacicia the bloc is using a Sink
for adding events and a Stream
for outputting states, how is that moving away from the BLoC pattern? ๐
I strongly agree with point of @mtwichel
For 2, I am empathetic to the learning curve, but I also think Cubit is a much more approachable bloc-like thing that could be encouraged to be used for beginners. Kind of a "only use a bloc if you need to, otherwise use cubit".
My main concerns:
mapEventToState
to on<Event>
syntax? How long would the mapEventToState
be supported?on<Event>
behave if multiple events are added to the bloc? Would the behavior be identical to how current implementation works assuming the only change would be around mapEventToState and bloc's constructor?debunce
or filter
?mapEventToState
needs to be awaited and events are being added much more frequently? See example below. The fact that the generator needs to finish processing before taking any new event into mapEventToState
has been a source of many assumptions in the past when it comes to the UI etc. I wonder if this would remain unchanged.Frankly, this change seems a bit too impacting to be worth it. Recently I had to migrate one app with dozens of bloc from version 6 to version 8 and until now I haven't migrated all the tests :/
import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
abstract class CounterEvent {}
class IncrementCounter extends CounterEvent {}
class CounterState {
const CounterState(this.value);
final int value;
}
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0));
@override
Stream<CounterState> mapEventToState(
CounterEvent event,
) async* {
if (event is IncrementCounter) {
yield* _mapIncrementCounterToState(event);
}
}
Stream<CounterState> _mapIncrementCounterToState(
IncrementCounter event) async* {
await Future.delayed(Duration(seconds: 5));
yield CounterState(state.value + 1);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: BlocProvider(
create: (context) => CounterBloc(),
child: Scaffold(body: MyHomePage()),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('${state.value}'),
ElevatedButton(
onPressed: () {
context.read<CounterBloc>().add(IncrementCounter());
},
child: Text('+1'),
),
TimerText(),
],
),
);
},
);
}
}
class TimerText extends StatefulWidget {
const TimerText({Key? key}) : super(key: key);
@override
_TimerTextState createState() => _TimerTextState();
}
class _TimerTextState extends State<TimerText> {
late Timer timer;
@override
void initState() {
super.initState();
timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {});
});
}
@override
void dispose() {
timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text('${timer.tick}');
}
}
@narcodico I thought that because the proposal removes completely any notion of Streams to the user, moving from streams to callbacks, changing completely the pattern. But, again, I'm not saying that this is bad for the package. I was just talking about the relationship with the BLoC pattern. :)
Maybe @orestesgaolin with his comment addressed with more details my concern about moving away too much from the BLoC pattern. Removing completely any notion of Sinks/Streams could be controversial. On one hand, it could lower the entry barrier for new users since they are not easy topics. On the other hand, those who are aware and have chosen the package because of its nature (i.e. a design pattern for using and handling streams) could find moving away from them problematic.
Will it affect bloc.js as well?
@kranfix @felangel
This async method _onWeatherRequested can't say when it is finished, then if I send twice the event WeatherRequested, there is no mechanism for awaiting the end of the process of the first event.
- How would the on
behave if multiple events are added to the bloc? Would the behavior be identical to how current implementation works assuming the only change would be around mapEventToState and bloc's constructor? - Would it still be possible to transform events with stream transformations like debunce or filter?
- How would event processing behave if the action in the mapEventToState needs to be awaited and events are being added much more frequently? See example below. The fact that the generator needs to finish processing before taking any new event into mapEventToState has been a source of many assumptions in the past when it comes to the UI etc. I wonder if this would remain unchanged.
These are good points.
The default behavior of bloc
today is such that I think there is (probably) no change needed because the on
handlers would queue and operate in order, because transformEvents
is implemented under the hood with asyncExpand
, which queues all work on the stream and runs it in order.
In the past it was possible to override transformEvents
to use operators (such as switchMap
or exhaustMap
) to guarantee that a new event either cancels ongoing downstream or ignores until downstream is complete to avoid most types of race conditions.
I suppose with this API change transformEvents
would stay the same and could still be used this way? It seems like a change to the behavior of the event processing logic itself might intuitively be outside the scope of this API change, but is that the case?
@felangel Oh, one point related to my above comment that this reminds me of. More fun related to how Dart streams work ๐
Here's a potential "gotcha" with this approach I've experienced related to the cancel my operation when new ones start
via switchMap
use case:
@override
Stream<Transition<Event, State>> transformEvents(
Stream<Event> events,
TransitionFunction<Event, State> transitionFn,
) {
// Assume I want new events to always cancel previous ones ...
return events.switchMap(transitionFn);
}
void _onWeatherRequested(WeatherRequested event, Emit<WeatherState> emit) async {
emit(WeatherLoadInProgress());
try {
// A new WeatherRequested event occurs while this `await` is happening ...
final weather = await weatherRepository.getWeather(event.latLng);
// At this point, since switchMap is used above, we'd expect the new event
// to cancel the currently-running operation. The downstream is canceled
// correctly, but since this just a function with an emitter, this event
// will actually be emitted even though the stream running it is canceled.
// ---
// Stream cancellation doesn't stop async functions from running for side
// effects, and in this case `emit` is a side effect, since it doesn't
// actually propagate its result by return value it will still happen.
emit(WeatherLoadSuccess(weather);
} catch (_) {
emit(WeatherLoadFailure());
}
}
Basically, the problem is that now emitting state is a side effect instead of an actual event in the stream, so it can break cancellation operators (such as switchMap
) which are commonly used if it's not implemented carefully with some test cases to prevent the undesirable behavior of emitting from an operation that's already cancelled.
You need some kind of check if I'm canceled
logic in the emitter itself. I've managed to get this working in a similar implementation but mileage may vary.
Edit:
switchMap
is one example, but without check-for-cancellation
behavior if state emission is a side-effect, any other effect of setting the state will have to guarded as well. Observer callbacks happening after a bloc is disposed, maybe? Not really sure all the implications but just something to generally be careful about.
why not this:
on<Increment>((emit) => emit(state + 1));
without the Event property?
To facilitate testing, I could pass the state in the function.
on<Increment>((emit, state) => emit(state + 1));
@jacobaraujo7 We need the event parameters like event.isDoubleIncrease
or event.city
and so on
Hey @felangel, just throwing in my two cents.
I may be the only dissenter but the amount of refactoring for big projects really makes me wary of this change. I feel this level of change ideally would only come if it's strictly needed. From my point of view I'm not convinced it is necessary.
To address point 1, I may be out of the loop but having nested generator like in the linked issue seems like a real anti-pattern and could be solved with some refactoring?
For 2, I am empathetic to the learning curve, but I also think Cubit is a much more approachable bloc-like thing that could be encouraged to be used for beginners. Kind of a "only use a bloc if you need to, otherwise use cubit".
For 3, I think things like the vscode extension and mason makes the boilerplate argument less impactful. I don't see the current implantation as overly verbose, and again, cubit is a nice alternative if that stuff really matters to you.
I again want to stress I'm not against it in theory; in fact, I think it's a nice readable syntax and I love that. I mainly just don't think the advantages warrant such a large breaking change. To me it seems like a bandaid solution to an edge case bug in dart, as well as syntax changes that cubit already handles well. I'm happy to be wrong about both those views though.
I also can't stress enough how grateful I am for this package and all the time everyone invests in it โค๏ธ thank you ๐
Thanks for the feedback, I really appreciate it! ๐
To address point 1, I may be out of the loop but having nested generator like in the linked issue seems like a real anti-pattern and could be solved with some refactoring?
In my opinion, this isn't an anti-pattern. I think it's fairly natural to decompose mapEventToState
into multiple private handlers which can each be async generators. I agree that it would be nice to have the bug fixed in Dart but it seems like we shouldn't count on that happening anytime soon.
Regarding learning curve and boilerplate, I agree that they are less important and I would not propose making breaking changes just for the sake of reducing boilerplate.
A potential alternative to make the change less breaking would be to modify the signature of mapEventToState
to look like:
void mapEventToState(Event event, Emit<State> emit) {...}
This would address the predictability issues and limit the scope of the changes. I'm not a fan of the naming and would rather have a slightly more breaking change personally in order to refine the API but I realize cost of migration is a major consideration.
The migration for the current proposal could be at least partially automated by a codemod and mapEventToState
could be deprecated to allow a smoother transition. Just want to understand if the concerns are regarding the proposal itself or mainly the impact in terms of breaking changes, thanks! ๐
@cmc5788 @kranfix @orestesgaolin regarding the behavior, I'd like to hear from you -- what do you prefer? We could retain the same behavior (asyncExpand
) but it's come up multiple times that the more natural default would be to process events as soon as they are added to avoid building up a large buffer which clogs up the bloc (if the user spams a button for example). I'd like to leave this open for discussion and gather more feedback.
@Lonli-Lokli
Will it affect bloc.js as well?
Ideally we would also migrate bloc.js to align with the current API if we decide to move forward with this proposal ๐
Thanks for the reply @felangel
In my opinion, this isn't an anti-pattern. I think it's fairly natural to decompose mapEventToState into multiple private handlers which can each be async generators. I agree that it would be nice to have the bug fixed in Dart but it seems like we shouldn't count on that happening anytime soon.
That's fair enough. I've never used it that way but I could see that being a good way to use it. This alone is probably enough reason to make the change - as you stated "one of the core principles/goals of the bloc library is to be predictable". If this bug in Dart is causing issues for predictability, I agree that should be addressed.
A potential alternative to make the change less breaking would be to modify the signature of mapEventToState...
I totally agree that if you're making the change, we should go all the way. I think modifying the signature is almost as much work as refactoring to the on<Event>
syntax.
Just want to understand if the concerns are regarding the proposal itself or mainly the impact in terms of breaking changes, thanks!
My only concern is breaking changes and how much work it will be to refactor. In the end, that's probably not a good enough reason to not do it as it's clearly cleaner code. But I'm now really convinced that the change should be made for predictability alone.
We could retain the same behavior (
asyncExpand
) but it's come up multiple times that the more natural default would be to process events as soon as they are added (...)
My point of view may be slightly biased as I used flutter_bloc from its very early versions, and before I used manual blocs, so I'm used to this queueing behavior. Thus, retaining it would be my ideal solution, even with syntax changes. In a day-to-day development it's not an issue, but I recall cases where I was actually aware and reliant on this particular approach e.g. when yielding loading state.
However, there were cases where this behavior was undesired, and thanks to introduction of Cubit I was able to achieve desired "instant" handling of events/functions.
In summary I would be really happy to see syntax change, but retaining the previous behavior would be a high priority for me. For instance, I don't recall writing tests for situations where two events would be added at the same time and 2nd of them would finish processing earlier than the order of events would suggest.
@orestesgaolin
My point of view may be slightly biased as I used flutter_bloc from its very early versions, and before I used manual blocs, so I'm used to this queueing behavior. Thus, retaining it would be my ideal solution, even with syntax changes. In a day-to-day development it's not an issue, but I recall cases where I was actually aware and reliant on this particular approach e.g. when yielding loading state.
However, there were cases where this behavior was undesired, and thanks to introduction of Cubit I was able to achieve desired "instant" handling of events/functions.
In summary I would be really happy to see syntax change, but retaining the previous behavior would be a high priority for me. For instance, I don't recall writing tests for situations where two events would be added at the same time and 2nd of them would finish processing earlier than the order of events would suggest.
Maybe I'm an outlier, but I tend to make heavy use of operators to avoid the problem of queueing up a lot of async operations in response to button presses or something like that, so I rarely accept the default behavior.
My personal preference, even if it were a little more verbose, would be to provide an API that forced a developer to semantically indicate the async processing behavior of events, instead of relying on (or even allowing) a default.
I don't really think of infinite-queueing as a reasonable default unless everything you're doing is instantaneous. It causes backpressure, and it may only appear
to be working correctly but causing the sort of bugs that manual QA testers usually expose by pressing a bunch of buttons repeatedly, or may only occur when network conditions are bad in the field ๐
So in summary, I don't know if I'd advocate for a change to default behavior, but I might advocate for not having a default behavior at all. I tend to think that developers should explicitly opt into whatever the async behavior of their event is in the case of multiple overlapping async things, to avoid unexpected behavior.
A good semantic reference for async task types might be http://ember-concurrency.com/docs/task-concurrency/
If I had to pick a default, I'd probably go with flatMap
or run the events concurrently, which lines up with the implementation for ember-concurrency as well.
So in summary, I don't know if I'd advocate for a change to default behavior, but I might advocate for not having a default behavior at all. I tend to think that developers should explicitly opt into whatever the async behavior of their event is in the case of multiple overlapping async things, to avoid unexpected behavior.
A good semantic reference for async task types might be http://ember-concurrency.com/docs/task-concurrency/
Woah, the linked documentation is awesome. Having seen that I would really love to see ability to configure how the events should be handled, but this might be a separate feature request :) So whenever I need the "current default' behavior I could provide a transformation function or a setting that would allow for that.
Hello everyone! ๐
First of all, I want to thank everyone for the amazing support and community that has grown around the bloc library! ๐๐
Context
This proposal aims to address 3 problems with the current
mapEventToState
implementation:Predictability
Due to an issue in Dart, it is not always intuitive what the value of
state
will be when dealing with nested async generators which emit multiple states. Even though there are ways to work around this issue, one of the core principles/goals of the bloc library is to be predictable. Therefore, the primary motivation of this proposal is to make the library as safe as possible to use and eliminate any uncertainty when it comes to the order and value of state changes.Learning Curve and Complexity
Writing blocs requires an understanding of
Streams
and async generators. This means developers must understand how to use theasync*
,yield
, andyield*
keywords. While these concepts are covered in the documentation, they are still fairly complex and difficult for newcomers to grasp.Boilerplate
When writing a bloc, developers must override
mapEventToState
and then handle the incoming event(s). Often times this looks something like:The important logic usually lives inside
_mapEventAToState
and_mapEventBToState
andmapEventToState
ends up mainly being setup code to handle determining which mapper to call based on the event type. It would be nice if this could be streamlined.Proposal ๐ฅ
I am proposing to remove the
mapEventToState
API in favor ofon<Event>
. This would allow developers to register event handlers by callingon<Event>
whereEvent
is the type of event being handled.on<Event>
would provide a callback(Event event, Emitter<State>) {...}
which would be invoked when an event of typeEvent
is added to the bloc. Developers could thenemit
one or more states in response to the incoming event.For example, if we look at the
CounterBloc
for reference, the current implementation might look something like:With the proposed changes the
CounterBloc
would look something like:If we wanted to support multiple events:
For more complex logic it can be refactored to look like:
These changes address the predictability issues mentioned above because it can be guaranteed that the bloc's state will update immediately when
emit
is called ensuring that cases like this behave as expected:In addition, developers don't have to use async generators (
async*
,yield
,yield*
) which can introduce complexity and undesired behavior in advanced cases.This allows developers to focus on the logic by directly registering an event handler for each type of event which streamlines the bloc code a bit further.
An added benefit is the added consistency across
Cubit
andBloc
-- both trigger state changes viaemit
and the transition fromCubit
toBloc
should become simpler.Becomes
Or as mentioned above (for simple cases)
These changes will obviously be breaking changes that impact blocs (cubits will remain unaffected) so they would all be within the scope of a v8.0.0 release.
The changes would be made in a way that only impacts the bloc
mapEventToState
implementation. The way blocs are used and tested will be 100% backward compatible which means the changes will be scoped to just themapEventToState
code and can ideally be automated via a code mod. There should be no impact to the remaining ecosystem (flutter_bloc
,bloc_test
,hydrated_bloc
,replay_bloc
, etc...).Please give this issue a ๐ if you support the proposal or a ๐ if you're against it. If you disagree with the proposal I would really appreciate it if you could comment with your reasoning.
Thanks so much for all of the continued support and looking forward to hearing everyone's thoughts on the proposal! ๐
08/31 UPDATE
Hey everyone, just wanted to give a quick update:
We currently have the v8.0.0 branch which replaces
mapEventToState
withon<Event>
; however, we were able to makeon<Event>
backward compatible withmapEventToState
๐ . You can view the changes as part of the v7.2.0 branch.The current plan is to roll out bloc v7.2.0 in the coming days which will deprecate
mapEventToState
,transformEvents
, andtransformTransitions
and will introduce the newon<Event>
API. We will have a comprehensive migration guide explaining all of the changes and how to migrate over. During this time, we encourage everyone to upgrade to bloc v7.2.0 and start to migrate blocs one at a time. In the meantime, we'll be working on development releases of bloc v8.0.0.As part of v8.0.0 all deprecated APIs from v7.2.0 will be removed and the tentative plan is to publish a stable v8.0.0 release about a month after v7.2.0 has been release. This should give everyone some time to incrementally migrate and for any adjustments to be made. In addition, v7.x.x will still receive bug fixes for the foreseeable future so there should be no pressure/urgency to jump to v8.0.0.
Let us know if you have any questions/concerns.
Thanks for everyone's feedback, patience, and continued support! ๐ ๐
09/02 UPDATE
We just published bloc v7.2.0-dev.1 which introduces the
on<Event>
API and is backwards compatible which should allow you to migrate incrementally. ๐ โจRelease Notes: https://github.com/felangel/bloc/releases/tag/bloc-v7.2.0-dev.1
Please try it out and let us know if you have any feedback/questions, thanks! ๐ ๐
09/09 UPDATE
We just published bloc v7.2.0-dev.2 ๐ Release Notes: https://github.com/felangel/bloc/releases/tag/bloc-v7.2.0-dev.2
09/10 UPDATE
We just published bloc v7.2.0-dev.3 ๐ Release Notes: https://github.com/felangel/bloc/releases/tag/bloc-v7.2.0-dev.3
09/21 UPDATE
The time has come ๐ฅ ๐ฅ ๐ฅ
bloc v7.2.0 is now out ๐
๐ฆ update now: https://pub.dev/packages/bloc/versions/7.2.0 ๐ migration guide: https://bloclibrary.dev/#/migration?id=v720