async_cubits 0.6.0
async_cubits: ^0.6.0 copied to clipboard
A set of cubits that can be used to handle async operations in a simple, consistent way.
async_cubits #
A package that provides a set of cubits for making asynchronous operations in a simpler, consistent way.
Inspiration #
This package is inspired by Riverpod's FutureProvider and StreamProvider. The AsyncValue class is mostly the same as in the riverpod package. Please check the great work done there.
Installation 💻 #
❗ In order to start using Async Cubits you must have the Flutter SDK installed on your machine.
flutter pub add async_cubits
Usage 🚀 #
Importing the package #
import 'package:async_cubits/async_cubits.dart';
Using a FutureCubit #
FutureCubit is a cubit for async operations that return a value (e.g. fetching data from an API).
Concrete subclasses define their own load/refresh methods and call performLoad or performRefresh with the appropriate future factory.
Without args
class GetUserCubit extends FutureCubit<User> {
GetUserCubit(this._userRepository);
final UserRepository _userRepository;
Future<void> load() => performLoad(_userRepository.getUser);
Future<void> refresh() => performRefresh(_userRepository.getUser);
}
With args
class GetUserByIdCubit extends FutureCubit<User> {
GetUserByIdCubit(this._userRepository);
final UserRepository _userRepository;
Future<void> load(int id) => performLoad(() => _userRepository.getUserById(id));
Future<void> refresh(int id) => performRefresh(() => _userRepository.getUserById(id));
}
Loading data
Call load() to trigger the async operation. The cubit emits AsyncValue<T> in the following order:
AsyncValue.loading()AsyncValue.data(value)on success, orAsyncValue.error(error, stackTrace)on failure
In your UI, use BlocProvider and BlocBuilder:
BlocProvider(
create: (context) => GetUserCubit(
context.read<UserRepository>(),
)..load(),
child: BlocBuilder<GetUserCubit, AsyncValue<User>>(
builder: (context, state) {
return state.when(
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) => Text('$error'),
data: (user) => Text(user.name),
);
},
),
);
Refreshing data
Call refresh() to reload without clearing the previously loaded value. The cubit emits:
- A state where
isLoadingistrueandvalueholds the previous data — useful for showing existing content while reloading without a loading indicator. SetskipLoadingOnRefresh: falseinAsyncValue.whento show a loading indicator instead. AsyncValue.data(newValue)on success.- On failure, a state where
isLoadingisfalse,error/stackTraceare set, andvaluestill holds the previous data — useful for showing the previous content alongside an error message (useskipError: trueinAsyncValue.when).
ElevatedButton(
onPressed: () => context.read<GetUserCubit>().refresh(),
child: const Text('Refresh'),
),
Using a StreamCubit #
StreamCubit listens to an async stream of values (e.g. a websocket feed).
class NewMessageCubit extends StreamCubit<Message> {
NewMessageCubit(this._messageRepository);
final MessageRepository _messageRepository;
@override
Stream<Message> dataStream() => _messageRepository.newMessageStream();
}
StreamCubit emits AsyncValue<T>, so consuming state is identical to FutureCubit:
class NewMessageWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final state = context.watch<NewMessageCubit>().state;
return state.when(
loading: LoadingWidget.new,
error: (error, stackTrace) => ErrorWidget(error),
data: (message) => LoadedMessage(message),
);
}
}
Using a MutationCubit #
MutationCubit handles asynchronous mutations — API calls, database writes, form submissions, etc.
class SaveUserCubit extends MutationCubit<User, void> {
SaveUserCubit(this._userRepository);
final UserRepository _userRepository;
@override
Future<void> mutation(User input) => _userRepository.saveUser(input);
}
Invoke the mutation with invoke:
context.read<SaveUserCubit>().invoke(user);
Invalidating a FutureCubit after a mutation
After a successful mutation, call perform<T>() on an AsyncCubitContainer to run an operation on all registered cubits of that type.
For simple apps all cubits share AsyncCubitContainer.defaultInstance by default, so you can call perform on it directly. The static shorthand AsyncCubitContainer.performDefault does the same thing with less typing:
class SaveUserCubit extends MutationCubit<User, void> {
SaveUserCubit(this._userRepository);
final UserRepository _userRepository;
@override
Future<void> mutation(User input) => _userRepository.saveUser(input);
@override
void onSuccess(User input, void result) {
super.onSuccess(input, result);
// Using the default instance directly:
AsyncCubitContainer.defaultInstance.perform<GetUserCubit>(
runner: (c) => c.invalidate(),
);
// Or with the static shorthand:
AsyncCubitContainer.performDefault<GetUserCubit>(
runner: (c) => c.invalidate(),
);
}
}
perform<T>() runs the runner on all registered cubits of type T. Use the optional filter parameter to target a specific instance:
AsyncCubitContainer.performDefault<GetUserByIdCubit>(
runner: (c) => c.invalidate(),
filter: (c) => c.id == userId,
);
For an optimistic update before the refresh fetch completes, pass optimisticRefresh to invalidate. The input parameter is now available directly in onSuccess:
@override
void onSuccess(User input, void result) {
super.onSuccess(input, result);
AsyncCubitContainer.performDefault<GetUserCubit>(
runner: (c) => c.invalidate(
optimisticRefresh: (current) => current.copyWith(name: input.name),
),
);
}
For feature modules with isolated lifecycles, create a dedicated container and pass it explicitly to both cubits:
final container = AsyncCubitContainer();
BlocProvider(create: (_) => GetUserCubit(_repo, container: container)),
BlocProvider(create: (_) => SaveUserCubit(_repo, container: container)),
Inside the mutation cubit, hold a reference to the container and call perform on it:
class SaveUserCubit extends MutationCubit<User, void> {
SaveUserCubit(this._userRepository, this._container);
final UserRepository _userRepository;
final AsyncCubitContainer _container;
@override
Future<void> mutation(User input) => _userRepository.saveUser(input);
@override
void onSuccess(User input, void result) {
super.onSuccess(input, result);
_container.perform<GetUserCubit>(runner: (c) => c.invalidate());
}
}
MutationCubit emits MutationState<T>, a sealed class with four subtypes:
| State | Type | Description |
|---|---|---|
| Idle | MutationIdle<O> |
Initial state — mutation not yet invoked |
| Loading | MutationLoading<O> |
Mutation in progress |
| Success | MutationSuccess<I, O> |
Mutation completed — holds input and result |
| Failure | MutationFailure<O> |
Mutation failed — holds error and stackTrace |
Use pattern matching or the is checks (isLoading, isSuccess, isFailure) to react to state changes:
BlocBuilder<SaveUserCubit, MutationState<void>>(
builder: (context, state) {
return switch (state) {
MutationIdle() || MutationSuccess() => SaveButton(),
MutationLoading() => const CircularProgressIndicator(),
MutationFailure(:final error) => Text('Failed: $error'),
};
},
)