async_cubits 0.4.0 copy "async_cubits: ^0.4.0" to clipboard
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 #

style: very good analysis License: MIT

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:

  1. AsyncValue.loading()
  2. AsyncValue.data(value) on success, or AsyncValue.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:

  1. A state where isLoading is true and value holds the previous data — useful for showing existing content while reloading without a loading indicator. Set skipLoadingOnRefresh: false in AsyncValue.when to show a loading indicator instead.
  2. AsyncValue.data(newValue) on success.
  3. On failure, a state where isLoading is false, error/stackTrace are set, and value still holds the previous data — useful for showing the previous content alongside an error message (use skipError: true in AsyncValue.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'),
    };
  },
)
2
likes
150
points
154
downloads

Publisher

unverified uploader

Weekly Downloads

A set of cubits that can be used to handle async operations in a simple, consistent way.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, flutter_bloc, meta

More

Packages that depend on async_cubits