Repo Generator with Riverpod

pub package

A build-time code generator that creates Riverpod providers from repository interfaces. Each query method on your repository becomes a FutureProvider, StreamProvider, or Provider — with optional .family and .autoDispose.

Mutations are never turned into providers (see Mutations vs queries).

Table of contents

Installation

Add to dev_dependencies (this is a code generator, not a runtime dependency):

dev_dependencies:
  riverpod_repo: ^5.1.1
  build_runner: ^2.15.0
  riverpod_annotation: ^4.0.2
  riverpod_generator: ^4.0.3

Your app still needs riverpod (or flutter_riverpod) at runtime.

The builder is applied automatically to packages that depend on riverpod_repo (auto_apply: dependents). To customize exclusions, add a build.yaml:

targets:
  $default:
    builders:
      riverpod_repo:
        enabled: true
        generate_for:
          exclude:
            - "**/*.repo.g.dart"

Run code generation:

dart run build_runner build

Quick start

1. Define the repository

// lib/data/book_repo.dart

import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_repo/riverpod_repo.dart';

part 'book_repo.g.dart';

@Riverpod(keepAlive: true)
BookRepo bookRepo(Ref ref) => BookRepoImpl();

@riverpodRepo
abstract class BookRepo {
  Future<List<Book>> getBooks({String search = ''});
  Future<Book> getBook(String id);
  Future<void> deleteBook(String id); // mutation — no provider generated
}

Import annotations from either:

  • package:riverpod_repo/riverpod_repo.dart (recommended), or
  • package:riverpod_repo/annotations.dart

2. Implement the repository

class BookRepoImpl implements BookRepo {
  @override
  Future<List<Book>> getBooks({String search = ''}) async { ... }

  @override
  Future<Book> getBook(String id) async { ... }

  @override
  Future<void> deleteBook(String id) async { ... }
}

3. Run the builder

dart run build_runner build

This creates book_repo.repo.g.dart — a standalone library you import in widgets or other providers:

import 'book_repo.repo.g.dart';

Note (5.0.0+): Repository providers are built with the public Riverpod API (FutureProvider, StreamProvider, Provider). They are not processed by riverpod_generator. Only your hand-written @Riverpod function needs a part '*.g.dart'; directive.

Generated output

For each query method, the generator emits a provider that delegates to your repository instance:

// book_repo.repo.g.dart (generated)

import 'package:riverpod/riverpod.dart';
import 'book_repo.dart';

export 'book_repo.dart';

final bookRepoGetBooksProvider =
    FutureProvider.autoDispose.family<List<Book>, ({String search})>((ref, arg) {
  return ref.watch(bookRepoProvider).getBooks(search: arg.search);
});

final bookRepoGetBookProvider =
    FutureProvider.autoDispose.family<Book, String>((ref, id) {
  return ref.watch(bookRepoProvider).getBook(id);
});

/// Invalidates every generated query provider for BookRepo.
void invalidateBookRepoProviders(Ref ref) {
  ref.invalidate(bookRepoGetBooksProvider);
  ref.invalidate(bookRepoGetBookProvider);
}

Provider kind is inferred from the return type:

Repository method return type Generated provider
Future<T> FutureProvider
Stream<T> StreamProvider
anything else Provider

Parameters map to .family:

Method signature Family argument
no parameters plain provider
one positional parameter that type directly, e.g. family<T, String>
named and/or multiple parameters Dart record, e.g. (search: 'dart')

Using providers in the UI

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'book_repo.repo.g.dart';

class BookList extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final books = ref.watch(
      bookRepoGetBooksProvider((search: '')),
    );

    return books.when(
      loading: () => const CircularProgressIndicator(),
      error: (err, _) => Text('Error: $err'),
      data: (list) => ListView(
        children: [for (final b in list) Text(b.title)],
      ),
    );
  }
}

Mutations vs queries

Providers are generated only for reads. A method is skipped when any of these apply:

Rule Example
Returns Future<void> or Stream<void> Future<void> deleteBook(String id)
Annotated with @repoMutation or @ignoreRepo @repoMutation Future<User> archiveUser()
Name starts with a mutation prefix createOrder, updateProfile, deleteItem, setTheme, saveDraft, addComment, removeTag, putFile, postMessage

Force a query that looks like a mutation

@repoQuery
Future<DeletedItemsReport> deleteReport({DateTime? since});

Or with an explicit constructor when you need parameters on the annotation:

@RepoQueryAnnotation()
Future<DeletedItemsReport> deleteReport({DateTime? since});

Refresh reads after a mutation

Call the generated invalidation helper (name: invalidate + repo class name + Providers):

Future<void> onDeleteBook(WidgetRef ref, String id) async {
  await ref.read(bookRepoProvider).deleteBook(id);
  invalidateBookRepoProviders(ref);
}

Do not wrap mutations in FutureProvider — call repository methods directly via ref.read(bookRepoProvider).deleteBook(...).

Keep-alive vs auto-dispose

By default, generated providers use .autoDispose (cache cleared when nothing listens).

Keep every query provider alive for the repository:

@RiverpodRepoAnnotation(keepAlive: true)
abstract class BookRepo { ... }

Override a single method back to auto-dispose:

@RiverpodRepoAnnotation(keepAlive: true)
abstract class BookRepo {
  @RepoQueryAnnotation(keepAlive: false)
  Future<List<Book>> searchBooks(String query);
}

Use @RepoQueryAnnotation(keepAlive: false) (not @repoQuery(keepAlive: false)) — Dart only allows const constructor invocations on annotations when parameters are passed.

Annotations reference

Annotation Target Purpose
@riverpodRepo class Shorthand for @RiverpodRepoAnnotation() — enable generation
@RiverpodRepoAnnotation({keepAlive}) class Enable generation; default keepAlive for all queries
@repoQuery method Force provider generation (overrides mutation name heuristic)
@RepoQueryAnnotation({keepAlive}) method Force generation; optional per-method keepAlive
@repoMutation / @ignoreRepo method Skip provider generation

Provider naming rules

  1. Repository class name drives generated provider names: BookRepo + getBooksbookRepoGetBooksProvider
  2. Your @Riverpod function must return that class and use camelCase of the class name: BookRepobookRepobookRepoProvider
  3. Import *.repo.g.dart for generated query providers; keep part '*.g.dart' only on the source file for your @Riverpod provider

Migration

From 4.x → 5.0

  • Output is now a single *.repo.g.dart file (no *.repo.dart part file)
  • Remove part 'foo.repo.dart'; from source files
  • Import foo.repo.g.dart where you use generated providers
  • Parameterized methods now use .family (records for named/multi-arg methods)

From 5.0 → 5.1

  • Mutation methods no longer get providers — add @repoQuery if a read was skipped
  • Use invalidate{RepoName}Providers(ref) after writes
  • Package is pure Dart (no Flutter SDK required in pubspec.yaml)
  • Annotations are exported from package:riverpod_repo/riverpod_repo.dart

Example project

See example/ for a full repository with:

  • class-level keepAlive
  • per-method @RepoQueryAnnotation(keepAlive: false)
  • a Future<void> mutation (deleteBook) with no generated provider
  • custom model imports (Student, Country, Hello)

Run generation from the repo root:

dart run build_runner build

License

MIT — see LICENSE.

Support

Questions: dilan@dilan.me

Libraries

annotations
Annotations for riverpod_repo.
riverpod_repo
Build-time code generator for Riverpod repository providers.