-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[proposal] feat(bloc): cancelable async operations #3069
Comments
Hey, @felangel approach with Also it should be present on Bloc (or BlocBase) as well, because the same scenario is appearing using Bloc. Here is my rough implementation of your idea as mixin
|
Both proposals LGTM, I slight lean towards the first one with the I wonder if this shouldn't also exists on Bloc? I believe they are prone to the same issue? |
Yes, same scenario is appearing using Bloc. So the solution discussed here should exist on Bloc as well. |
mixin is used just so I will not modify sources :) |
Wouldn't this approach create the most readable code? That is, I (subjectively) think this would be the better approach in many situations. Think of blocs which utilize event concurrency — wrapping everything in If you added all your cancelable operations to a queue, as you described, and then cancel them during the bloc's Then again, maybe it's a non-issue, depending on how canceling futures works under the hood. |
I could go either way on this. Using In terms of how cancelable would work, I envisioned that operations would be added to a queue and canceled in order with respect to when it was registered within the specific event handler (as you mentioned). I don't think it would have much of an effect with regards to bloc_concurrency because transformers apply to the event handler which takes priority over a specific cancelable operation. If an event handler is canceled all corresponding cancelable operations should also be canceled for that particular event handler. The main goal is to have an explicit way to cancel pending async operations when a bloc/cubit is closed -- I'm totally open to any other suggestions if you have them 👍 |
If this or similar will be the implementation:
Then this might cause memory leaks since |
I don't think we need to worry about implementation details in this issue. I feel it would be best if we could all just focus on defining the API/behavior/usage. Once we align on that we can discuss implementation details 👍 |
I have another situation which might benefit from cancel token so it's related to this issue when defining behaviors: class TestEvent {
final String id;
TestEvent(this.id);
}
abstract class TestState {}
class TestInitial extends TestState {}
class TestLoading extends TestState {
final String id;
TestLoading({required this.id});
@override
String toString() => 'TestLoading{id: $id}';
}
class TestLoaded extends TestState {
final String id;
final Object result;
TestLoaded({required this.id, required this.result});
@override
String toString() => 'TestLoaded{id: $id}';
}
class TestBloc extends Bloc<TestEvent, TestState> {
final _random = Random();
TestBloc() : super(TestInitial()) {
on<TestEvent>(_onTestEvent);
}
Future<void> _onTestEvent(TestEvent event, Emitter<TestState> emit) async {
final id = event.id;
emit(TestLoading(id: id));
// result which is calculated by id so someResult is related only to specific id
var someResult = await Future<void>.delayed(Duration(seconds: _random.nextInt(10)));
emit(TestLoaded(id: id, result: id));
}
} Imagine a situation when some event happens: However currently we cannot cancel event '0', the output might look like this:
|
@maRci002 right now you can make use of |
@narcodico thanks it looks promising, I will take a look. Only problem Cubits doesn't benefit from this. |
I can think of two other possible solutions ( may not be great thou :) )
|
I think this would be an awesome addition. Does this mechanic in its current proposed implementation work (or would it make sense to see if we can make it work) such that we don't even fully |
What about a mixin? |
Personally, I'm feeling like it might be best to just bring back the behavior of ignoring emit-after-close as the default behavior. I can definitely see the value in throwing runtime errors if events are added after close, but for internal async work to throw runtime errors if emit happens after close, it feels like there's currently no good option to make this "more good than harm." Other frameworks have solved similar problems by allowing a check for However, if it's up to the developer to remember to do this without any static analysis checks to help them out, I expect that a lot of buggy apps will be released to production, because the behavior isn't "consistent" and may only be exposed under certain race conditions, etc. I can see it being good to add this strict check back as a developer opt-in behavior, or maybe make it the default if For example it would be really cool if; Future<void> load() async {
emit(MyState.loading);
await Future<void>.delayed(const Duration(seconds: 3));
emit(MyState.idle); // Analyzer warning: don't "emit" after "await" without checking "isClosed"
} edit: or, maybe just make the runtime error into an assert so it doesn't blow up apps in production? |
From my point of view this solution would be the best for all parties.
|
@cmc5788 @raulmabe thanks for the feedback! The reason I'm hesitant to switch to using an Assertion is because I don't think it would make a difference. Currently a I am leaning towards just reverting this behavior to just ignore emitted states if the instance is already closed. Let me know what you think and thanks again for the feedback 👍 |
Just to play devil's advocate: from the standpoint of designing a solution to be as efficient as possible, as a developer, you ideally want to check Even if the result is ignored harmlessly, the expensive part is the actual async task itself, which often involves network or database usage, parsing, possibly communicating across isolates. Or possibly even doing the work has some kind of side effect like a login API call that caches the session. To do all of that and then throw away the result isn't ideal, but whether to treat something that "might" indicate that a developer missed that kind of problem as an error or not feels like kind of a personal decision 😂 Whatever the mechanism for it happens to be, I think it's useful as a developer to have the option of being warned when you might have missed an opportunity to avoid a potentially expensive async operation. Maybe the default of throwing an error was too annoying, but I'm also not sure ignoring it with no option to make it visible is the right solution 🤔 I think in terms of priorities of behaviors with 1 being my personal favorite I'd do something like --
But it's not a strong opinion either way, since developers have the tools they need to make it work regardless. |
I feel bloc has done a great job so far not polluting the library with excessive and unneeded configurations. |
Hi 👋 How is the state of this issue? I am currently feeling obliged to constantly check |
I use Cubits and this extension has come in handy :) hope it helps! extension CubitExt<T> on Cubit<T> {
void safeEmit(T state) {
if (!isClosed) {
// ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member
emit(state);
}
}
} |
@felangel What is the status of this issue? Do you plan to revert the default behavior to ignore the emitted states if the instance is already closed? I need to know if I should update ALL my emit calls in my apps or I should wait for a package update. I'm getting crash reports in the wild and I need to do something about it. |
If developers wants to decide to ignore Personally I like the However it would be even better if analyzer could help and remove This can be achived by using /// Emits the provided [state].
@UseResult('Returns `false` if Bloc/Cubit is already closed so abort your function like this: `if (!emit(state) return;)`')
bool call(State state); class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
// non nesting version
Future<void> increment1() async {
if (!emit(0)) return;
await Future<void>.delayed(const Duration(seconds: 3));
final _ = emit(1);
}
// nested version
Future<void> increment2() async {
if (emit(0)) {
await Future<void>.delayed(const Duration(seconds: 3));
final _ = emit(1);
}
}
}
If
Unfortunetly edit: from dart 2.18 |
@felangel This would be the best approach in my opinion because it'll resemble the behavior of Flutter's |
I like the current behavior of throwing if extension CubitMaybeEmit<S> on Cubit<S> {
@protected
void maybeEmit(S state) {
if (isClosed) {
return;
}
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
emit(state);
}
} I'd like also like to bring up the topic that I feel nobody has brought up in this thread, that is: |
We are using similar way and it works fine. It would be good to have the method |
@felangel i tried the solution by you and @DenisBogatirov by using a mixin class for CancelableOperation. But it seems out that even CancelableOperation doesn't work as expected , in my code i was still executing the future and code post it even after closing it properly on bloc close , which is leading to the StateError , right now i am making it work by forking the bloc package to my internal server and not throwing the error . But see the issue opened for Cancellable Operation here Also i would love to hear any more solutions from the community |
@felangel Why not to implement it inside bloc and allow to have both In most cases doing a check for
With proposal to do
what is the benefit of it? |
Looking forward to this change. |
Description
As a developer, I want to be able to await asynchronous operations within a bloc/cubit which are automatically canceled if the instance is closed while the async operation is pending.
An example use-case is when using a cubit to fetch some data asynchronously from a screen in which a user can go back.
In this scenario, as soon as
MyScreen
is pushed onto the navigation stack, theload()
method is called on a newly created instance ofMyCubit
. It's possible that the user might get tired of waiting and press the back button beforeload()
has completed. In this case, the Future will still complete after theMyCubit
has been closed and the subsequentemit(MyState.idle)
will be evaluated which will result in aStateError
:Unhandled Exception: Bad state: Cannot emit new states after calling close
Desired Solution
It would be nice if we had a
cancelable
(open to naming suggestions) API which allowed developers to await asynchronous operations which would automatically be canceled if the bloc/cubit was closed.Alternatives Considered
Developers could also use
CancelableOperation
andCancelableCompleter
frompackage:async
to maintain a list of cancelable operations internally which are manually canceled when the instance is closed.emit could automatically ignore states after the instance has been closed (previous behavior)
Additional Context
See #2980 and #3042.
The text was updated successfully, but these errors were encountered: