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

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

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'),
    };
  },
)
2
likes
150
points
371
downloads

Documentation

API reference

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

License

MIT (license)

Dependencies

flutter, flutter_bloc, meta

More

Packages that depend on async_cubits