felangel / felangel/bloc

[proposal] feat(bloc): cancelable async operations

Open
#3,069 37 comments 73 reactions 1 assignee Claimed by @felangel View on GitHub
enhancement candidate feedback wanted pkg:bloc
Dominant language
Dart
Stars
12.5k
Forks
3.4k
PR merge metrics
No merged PRs in 30d

Description

**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.

```dart
static Route route() {
return MaterialPageRoute(
builder: (context) => BlocProvider(
create: (context) => MyCubit()..load(),
child: const MyScreen(),
),
);
}

enum MyState { idle, loading }

class MyCubit extends Cubit {
MyCubit() : super(MyState.idle);

Future load() async {
emit(MyState.loading);
await Future.delayed(const Duration(seconds: 3));
emit(MyState.idle);
}
}
```

In this scenario, as soon as `MyScreen` is pushed onto the navigation stack, the `load()` method is called on a newly created instance of `MyCubit`. It's possible that the user might get tired of waiting and press the back button before `load()` has completed. In this case, the Future will still complete after the `MyCubit` has been closed and the subsequent `emit(MyState.idle)` will be evaluated which will result in a `StateError`:

`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.

```dart
enum MyState { idle, loading }

class MyCubit extends Cubit {
MyCubit() : super(MyState.idle);

Future load() async {
emit(MyState.loading);
await cancelable(() => Future.delayed(const Duration(seconds: 3)));
emit(MyState.idle);
}
}
```

**Alternatives Considered**

- Developers could check if the instance has been closed after any asynchronous operations and abort:

```dart
enum MyState { idle, loading }

class MyCubit extends Cubit {
MyCubit() : super(MyState.idle);

Future load() async {
emit(MyState.loading);
await Future.delayed(const Duration(seconds: 3));
if (isClosed) return;
emit(MyState.idle);
}
}
```

- Developers could also use `CancelableOperation` and `CancelableCompleter` from `package: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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.