async_cubits 0.4.0
async_cubits: ^0.4.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
Pass the same AsyncCubitContainer to both cubits so that after a successful mutation you can call invalidate<T>() to refresh the related FutureCubit. Both cubits default to AsyncCubitContainer.defaultInstance, so for simple apps no extra setup is needed:
class GetUserCubit extends FutureCubit<User> {
GetUserCubit(this._userRepository);
final UserRepository _userRepository;
Future<void> load() => performLoad(_userRepository.getUser);
Future<void> refresh() => performRefresh(_userRepository.getUser);
}
class SaveUserCubit extends MutationCubit<User, void> {
SaveUserCubit(this._userRepository);
final UserRepository _userRepository;
@override
Future<void> mutation(User input) => _userRepository.saveUser(input);
@override
void onSuccess(void result) {
super.onSuccess(result);
invalidate<GetUserCubit>();
}
}
invalidate<T>() invalidates all registered cubits of type T. Use the optional filter parameter to target a specific instance:
invalidate<GetUserByIdCubit>(filter: (cubit) => cubit.id == userId);
For feature modules with isolated lifecycles, create a dedicated container and pass it explicitly:
final container = AsyncCubitContainer();
BlocProvider(create: (_) => GetUserCubit(_repo, container: container)),
BlocProvider(create: (_) => SaveUserCubit(_repo, container: container)),
MutationCubit emits MutationState<T>, a sealed class with four subtypes:
| State | Type | Description |
|---|---|---|
| Idle | MutationIdle |
Initial state — mutation not yet invoked |
| Loading | MutationLoading |
Mutation in progress |
| Success | MutationSuccess |
Mutation completed — holds result |
| Failure | MutationFailure |
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'),
};
},
)