api_state 1.0.0 copy "api_state: ^1.0.0" to clipboard
api_state: ^1.0.0 copied to clipboard

A lightweight API state wrapper for Dart/Flutter state management.

api_state #

A lightweight, zero-dependency Dart package for representing API call states using sealed classes.

Built for Dart 3+ with compile-time exhaustive pattern matching. Works with any state management solution — Bloc, Riverpod, GetX, Provider, or plain setState.

States #

State Class Description
Initial ApiInitial No action taken yet
Loading ApiLoading API call in progress
Success ApiSuccess<T> Completed with data
Failure ApiFailure<T> Failed with error
Refresh ApiRefresh<T> Re-fetching while showing existing data

Installation #

dependencies:
  api_state: ^1.0.0
import 'package:api_state/api_state.dart';

Quick Start #

// Define your failure types
class ServerFailure extends Failure {
  const ServerFailure(super.message, {super.code, super.stackTrace});
}

class NetworkFailure extends Failure {
  const NetworkFailure([super.message = 'No internet connection']);
}
// Use in your state management
ApiStatus<List<User>> usersState = const ApiInitial();

Future<void> fetchUsers() async {
  usersState = const ApiLoading();

  usersState = await ApiStatus.guard(
    () => repository.fetchUsers(),
    onError: (e, st) => ServerFailure(e.toString(), stackTrace: st),
  );
}

Pattern Matching #

Dart 3 Switch Expression #

The compiler ensures you handle every case:

Widget build(BuildContext context) {
  return switch (usersState) {
    ApiInitial()              => const SizedBox.shrink(),
    ApiLoading()              => const CircularProgressIndicator(),
    ApiSuccess(:final data)   => UserList(users: data),
    ApiFailure(:final error)  => ErrorView(message: error.message),
    ApiRefresh(:final data)   => UserList(users: data, refreshing: true),
  };
}

when — Exhaustive Callbacks #

Groups initial and loading into a single callback:

final widget = usersState.when(
  initialOrLoading: () => const CircularProgressIndicator(),
  success: (users) => UserList(users: users),
  failure: (error) => ErrorView(message: error.message),
  refresh: (users) => UserList(users: users, refreshing: true), // optional
);

whenOrNull — Handle Only What You Need #

Unhandled states return null:

usersState.whenOrNull(
  success: (users) => showSnackBar('Loaded ${users.length} users'),
  failure: (error) => showSnackBar(error.message),
);

maybeWhen — With Fallback #

final message = usersState.maybeWhen(
  success: (users) => '${users.length} users',
  orElse: () => 'No data yet',
);

Convenience Getters #

usersState.isInitial;  // true if ApiInitial
usersState.isLoading;  // true if ApiLoading
usersState.isSuccess;  // true if ApiSuccess
usersState.isFailure;  // true if ApiFailure
usersState.isRefresh;  // true if ApiRefresh

usersState.hasData;    // true if success or refresh
usersState.hasError;   // true if failure

usersState.data;       // T? — data from success/refresh, null otherwise
usersState.error;      // Failure? — error from failure, null otherwise

Transform Methods #

map — Transform Data #

Convert ApiStatus<T> to ApiStatus<R> without losing state info:

final ApiStatus<List<UserModel>> models = fetchResult;
final ApiStatus<List<UserViewModel>> viewModels = models.map(
  (users) => users.map(UserViewModel.fromModel).toList(),
);
// ApiSuccess<List<UserModel>> → ApiSuccess<List<UserViewModel>>
// ApiLoading/ApiFailure/etc pass through unchanged

mapFailure — Transform Errors #

final uiState = repoState.mapFailure(
  (failure) => UserFriendlyFailure('Something went wrong'),
);

whenData — Callback for Any Data State #

Fires for both success and refresh:

usersState.whenData((users) => cache.save(users));
// Returns null for initial/loading/failure

State Transitions #

toLoading() #

Transition to loading, discarding existing data:

usersState = usersState.toLoading();

toRefresh() #

Transition to refresh, preserving existing data. Falls back to ApiLoading if no data exists:

// Pull-to-refresh: keep showing current list while re-fetching
usersState = usersState.toRefresh();
// ApiSuccess(users) → ApiRefresh(users)
// ApiInitial()      → ApiLoading() (no data to preserve)

ApiStatus.guard #

Wraps an async call and returns ApiSuccess or ApiFailure automatically:

Future<void> fetchUsers() async {
  usersState = usersState.toLoading();

  usersState = await ApiStatus.guard(
    () => repository.fetchUsers(),
    onError: (error, stackTrace) => ServerFailure(
      error.toString(),
      stackTrace: stackTrace,
    ),
  );
}

Failure #

Failure is abstract — extend it to define domain-specific error types:

class ServerFailure extends Failure {
  const ServerFailure(super.message, {super.code, super.stackTrace});
}

class NetworkFailure extends Failure {
  const NetworkFailure([super.message = 'No internet connection']);
}

class CacheFailure extends Failure {
  const CacheFailure([super.message = 'Cache read failed']);
}

class ValidationFailure extends Failure {
  final Map<String, String> fieldErrors;

  const ValidationFailure(this.fieldErrors)
      : super('Validation failed');
}

Each failure carries:

Field Type Description
message String Human-readable error message
code int? Optional error code (e.g. HTTP status)
stackTrace StackTrace? Optional stack trace for debugging

Full Example with Bloc #

class UsersCubit extends Cubit<ApiStatus<List<User>>> {
  final UserRepository _repository;

  UsersCubit(this._repository) : super(const ApiInitial());

  Future<void> fetch() async {
    emit(const ApiLoading());
    emit(await ApiStatus.guard(
      () => _repository.getUsers(),
      onError: (e, st) => ServerFailure(e.toString(), stackTrace: st),
    ));
  }

  Future<void> refresh() async {
    emit(state.toRefresh());
    emit(await ApiStatus.guard(
      () => _repository.getUsers(),
      onError: (e, st) => ServerFailure(e.toString(), stackTrace: st),
    ));
  }
}
// In your widget
BlocBuilder<UsersCubit, ApiStatus<List<User>>>(
  builder: (context, state) => switch (state) {
    ApiInitial()              => const SizedBox.shrink(),
    ApiLoading()              => const CircularProgressIndicator(),
    ApiSuccess(:final data)   => UserList(users: data),
    ApiFailure(:final error)  => ErrorView(message: error.message),
    ApiRefresh(:final data)   => UserList(users: data, refreshing: true),
  },
)

API Reference #

ApiStatus #

Method / Getter Return Type Description
isInitial, isLoading, isSuccess, isFailure, isRefresh bool State checks
hasData, hasError bool Data/error availability
data T? Data from success/refresh
error Failure? Error from failure
when(...) R Exhaustive pattern matching
whenOrNull(...) R? Partial matching, returns null
maybeWhen(...) R Partial matching with fallback
whenData(callback) R? Callback for data states
map(transform) ApiStatus<R> Transform data type
mapFailure(transform) ApiStatus<T> Transform error type
toLoading() ApiStatus<T> Transition to loading
toRefresh() ApiStatus<T> Transition to refresh (preserves data)
ApiStatus.guard(action, onError:) Future<ApiStatus<T>> Wrap async call
1
likes
160
points
98
downloads

Documentation

API reference

Publisher

verified publisherramandeep.dev

Weekly Downloads

A lightweight API state wrapper for Dart/Flutter state management.

Repository (GitHub)
View/report issues

License

MIT (license)

More

Packages that depend on api_state