riverpod_crud 0.0.1 copy "riverpod_crud: ^0.0.1" to clipboard
riverpod_crud: ^0.0.1 copied to clipboard

A lightweight Riverpod CRUD helper for loading, error, refresh, pagination, create, update, and delete states.

riverpod_crud #

Overview #

riverpod_crud is a lightweight Dart package that helps you build reusable CRUD state management with Riverpod.

It is designed for apps that repeatedly need the same loading, error, refresh, pagination, create, update, and delete behavior across models such as User, Product, Post, or Order.

This package follows Clean Architecture with Repository Pattern and Riverpod-based state management.

The package is pure Dart and depends on riverpod. It can be used in Flutter apps through Riverpod providers.

Features #

  • Generic CRUD support with <T, ID>.
  • Repository-based API through CrudRepository<T, ID>.
  • Riverpod StateNotifier through CrudNotifier<T, ID>.
  • Immutable state through CrudState<T>.
  • Initial loading, refresh, and load-more flags.
  • Page-based pagination with PaginationConfig.
  • Create, update, and delete operations.
  • Optional optimistic updates with rollback on failure.
  • Readable error state and clearError().
  • reset() for returning to an empty initial state.
  • No code generation, Freezed, or build runner.

Architecture #

UI Layer
-> Riverpod Provider / Notifier Layer
-> Repository Layer
-> Data Source Layer
-> API / Database

The package provides the provider, notifier, state, and repository abstraction. Your app owns the model classes and repository implementations.

Installation #

Add the package to your pubspec.yaml:

dependencies:
  riverpod_crud: ^0.0.1

For Flutter UI code, use your normal Riverpod setup in the app.

Quick Start #

Create a model:

class User {
  final int id;
  final String name;

  const User({
    required this.id,
    required this.name,
  });
}

Create a provider:

final usersProvider = crudNotifierProvider<User, int>(
  repository: UserRepository(),
  getId: (user) => user.id,
);

Use the state and notifier:

final state = ref.watch(usersProvider);
final notifier = ref.read(usersProvider.notifier);

notifier.refresh();
notifier.loadMore();
notifier.create(user);
notifier.update(user.id, user);
notifier.delete(user.id);

Creating a Repository #

CrudRepository<T, ID> is the contract between the notifier and your data layer.

T is your model type, such as User. ID is the type of the model ID, such as int or String.

class UserRepository implements CrudRepository<User, int> {
  @override
  Future<List<User>> fetch({
    int page = 1,
    int limit = 20,
  }) async {
    return api.fetchUsers(page: page, limit: limit);
  }

  @override
  Future<User> create(User item) async {
    return api.createUser(item);
  }

  @override
  Future<User> update(int id, User item) async {
    return api.updateUser(id, item);
  }

  @override
  Future<void> delete(int id) async {
    await api.deleteUser(id);
  }
}

Your repository can use REST, GraphQL, Firebase, SQLite, Hive, or any other data source.

Creating a Provider #

Use crudNotifierProvider to create a Riverpod provider for a model.

final usersProvider = crudNotifierProvider<User, int>(
  repository: UserRepository(),
  getId: (user) => user.id,
);

The getId function tells the notifier how to find an item's unique ID. It is used when updating or deleting items from local state.

For example:

getId: (user) => user.id

Using in Flutter UI #

In Flutter, watch the provider state and read the notifier.

class UsersView extends ConsumerWidget {
  const UsersView({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(usersProvider);
    final notifier = ref.read(usersProvider.notifier);

    if (state.isLoading) {
      return const Center(child: CircularProgressIndicator());
    }

    return Column(
      children: [
        if (state.error != null)
          Text(state.error!),
        ElevatedButton(
          onPressed: notifier.refresh,
          child: const Text('Refresh'),
        ),
        Expanded(
          child: ListView.builder(
            itemCount: state.items.length,
            itemBuilder: (context, index) {
              final user = state.items[index];

              return ListTile(
                title: Text(user.name),
                trailing: IconButton(
                  icon: const Icon(Icons.delete),
                  onPressed: () => notifier.delete(user.id),
                ),
              );
            },
          ),
        ),
        ElevatedButton(
          onPressed: state.hasMore ? notifier.loadMore : null,
          child: state.isLoadingMore
              ? const Text('Loading...')
              : const Text('Load more'),
        ),
      ],
    );
  }
}

Pagination #

PaginationConfig controls page-based loading.

final usersProvider = crudNotifierProvider<User, int>(
  repository: UserRepository(),
  getId: (user) => user.id,
  paginationConfig: const PaginationConfig(
    initialPage: 1,
    limit: 10,
  ),
);

loadMore() fetches the next page only when state.hasMore is true.

The notifier sets hasMore to false when the repository returns fewer items than the configured limit.

Refresh #

Use refresh() when the user pulls to refresh or taps a refresh button.

await ref.read(usersProvider.notifier).refresh();

Refresh loads the first page again, replaces the current list, resets the page number, and updates state.isRefreshing while the request is running.

Create, Update, and Delete #

Create an item:

await ref.read(usersProvider.notifier).create(
  const User(id: 3, name: 'Linus'),
);

Update an item:

await ref.read(usersProvider.notifier).update(
  user.id,
  User(id: user.id, name: 'Updated name'),
);

Delete an item:

await ref.read(usersProvider.notifier).delete(user.id);

Created items are added to the top of the current list. Updated items are replaced by matching their ID with getId. Deleted items are removed by ID.

Optimistic Updates #

Optimistic updates are enabled by default.

final usersProvider = crudNotifierProvider<User, int>(
  repository: UserRepository(),
  getId: (user) => user.id,
  optimisticUpdate: true,
);

When optimisticUpdate is true, create, update, and delete operations update local state before the repository call finishes. If the repository throws, the notifier rolls back to the previous state and stores the error message in state.error.

Disable optimistic updates when you want the UI to change only after the repository succeeds.

final usersProvider = crudNotifierProvider<User, int>(
  repository: UserRepository(),
  getId: (user) => user.id,
  optimisticUpdate: false,
);

Error Handling #

All notifier methods catch repository errors and store a readable message in state.error.

if (state.error != null) {
  return Text(state.error!);
}

Clear the current error:

ref.read(usersProvider.notifier).clearError();

You can throw CrudException from your repository when you want full control over the message.

throw const CrudException('Unable to load users');

API Reference #

CrudRepository<T, ID> #

An abstraction for your data layer. Implement it to define how a model is fetched, created, updated, and deleted.

Methods:

  • fetch({int page = 1, int limit = 20})
  • create(T item)
  • update(ID id, T item)
  • delete(ID id)

CrudNotifier<T, ID> #

A Riverpod StateNotifier that manages CRUD state. It calls your repository and updates CrudState<T>.

Methods:

  • fetchInitial()
  • refresh()
  • loadMore()
  • create(T item)
  • update(ID id, T item)
  • delete(ID id)
  • clearError()
  • reset()

CrudState<T> #

The immutable state object used by CrudNotifier.

Fields:

  • items: current loaded items.
  • isLoading: first page is loading.
  • isRefreshing: refresh is running.
  • isLoadingMore: next page is loading.
  • error: latest readable error message.
  • page: current page number.
  • hasMore: whether another page can be loaded.

PaginationConfig #

Configures page-based loading.

const PaginationConfig(
  initialPage: 1,
  limit: 20,
)

crudNotifierProvider #

A helper that creates a StateNotifierProvider<CrudNotifier<T, ID>, CrudState<T>> and automatically calls fetchInitial().

Example #

The example/ folder contains a Flutter app with fake user CRUD:

  • Load users.
  • Refresh users.
  • Load more users.
  • Add a user.
  • Update a user.
  • Delete a user.
  • Display loading and error states.

Run it from the example folder with Flutter tooling.

Testing #

Run the package tests:

dart test

The test suite covers:

  • Initial fetch.
  • Refresh.
  • Pagination.
  • Create.
  • Update.
  • Delete.
  • Repository error handling.
  • Optimistic delete rollback.

Roadmap #

  • Cursor-based pagination.
  • Optional cache integration hooks.
  • Batch create, update, and delete helpers.
  • More examples for API and local database repositories.

Website #

For updates, support, and related package information, visit csjotlab.com.

License #

MIT License. See LICENSE for details.

2
likes
150
points
106
downloads

Documentation

API reference

Publisher

verified publishercsjotlab.com

Weekly Downloads

A lightweight Riverpod CRUD helper for loading, error, refresh, pagination, create, update, and delete states.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

riverpod

More

Packages that depend on riverpod_crud