skip to content

With flutter_bloc, how do RepositoryProvider and its dispose callback fit into wiring a feature's repositories and blocs?

level: seniorimportance: should knowfreq 30%

answer

  1. provides any object, not a bloc
  2. blocs get repositories by constructor
  3. dispose arrived in flutter_bloc 9.1
  4. .value never disposes
  5. MultiRepositoryProvider for several

basics

~20 s

RepositoryProvider exposes a non-bloc dependency, such as an AuthRepository, to a subtree; BlocProvider create callbacks read it and pass it to blocs. Since flutter_bloc 9.1 its dispose callback releases the repository when the provider leaves the tree.

solid answer

~40 s

`RepositoryProvider<AuthRepository>(create: (context) => AuthRepository(client), child: ...)` is a thin `Provider` for objects that are not blocs, lazy by default like BlocProvider. Blocs do not look repositories up — they have no `BuildContext`. The documented pattern is `BlocProvider(create: (context) => LoginCubit(context.read<AuthRepository>()))`: constructor injection keeps the cubit plain Dart and lets tests pass a fake. BlocProvider can close its bloc automatically because every bloc has `close()`; a repository has no common release method, so flutter_bloc 9.1 added `dispose: (repository) => repository.dispose()`. `RepositoryProvider.value` accepts no dispose and never releases anything. Scope each provider to its lifetime — app-wide repositories above `MaterialApp`, feature blocs at the feature's route — so a repository always outlives the blocs that use it. `MultiRepositoryProvider` flattens several.

code

dart · 38 lines
dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:http/http.dart' as http;

class AuthRepository {
  AuthRepository(this._client);

  final http.Client _client;

  Future<void> signIn(String email, String password) async {
    // calls the backend with _client
  }

  void dispose() => _client.close();
}

// App, LoginCubit and LoginView are declared elsewhere.
void main() {
  runApp(
    RepositoryProvider<AuthRepository>(
      create: (_) => AuthRepository(http.Client()),
      dispose: (repository) => repository.dispose(),
      child: const App(),
    ),
  );
}

class LoginPage extends StatelessWidget {
  const LoginPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => LoginCubit(context.read<AuthRepository>()),
      child: const LoginView(),
    );
  }
}

go deeper

for a junior

Recall that RepositoryProvider provides non-bloc dependencies and that blocs receive them through their constructors.

for a middle

Explain lazy creation, the 9.1 dispose callback, why .value never disposes, and how MultiRepositoryProvider flattens nesting.

for a senior

Show you scope repositories and blocs by lifetime, release resources deterministically and keep blocs testable through constructor injection.

for a principal

Decide how widget-tree providers coexist with any app-wide DI container, and who owns each resource's release across features.

## What RepositoryProvider is `RepositoryProvider<T>` in **flutter_bloc** 9.1 makes one instance of any class — typically a repository wrapping an API client, a database or a socket — available to a subtree. It extends the provider package's `Provider<T>` and adds nothing bloc-specific: - `create: (context) => T` — builds the instance, **lazily** by default (`lazy` can be set); - `dispose: (T value) => void` — optional release hook, added in flutter_bloc 9.1; - `RepositoryProvider.value(value: ...)` — exposes an existing instance, with no dispose; - `RepositoryProvider.of<T>(context)` — lookup, equivalent to `context.read<T>()`. ## Injecting repositories into blocs A bloc or cubit is plain Dart with no `BuildContext`, so it cannot look anything up. The bloc docs' pattern reads the repository in the widget that creates the bloc and passes it through the constructor: ```dart BlocProvider( create: (context) => LoginCubit(context.read<AuthRepository>()), child: const LoginView(), ) ``` Benefits: - the cubit's dependencies are explicit in its constructor; - a unit test builds `LoginCubit(FakeAuthRepository())` with no widget tree; - swapping an implementation per environment happens in one provider. ## Why dispose exists `BlocProvider` knows how to release what it created: every bloc implements `close()`. A repository has no such common interface — one closes an HTTP client, another cancels a stream, another does nothing. Before 9.1, `RepositoryProvider` had no hook, so teams released resources elsewhere or not at all. Now: ```dart RepositoryProvider<AuthRepository>( create: (_) => AuthRepository(http.Client()), dispose: (repository) => repository.dispose(), child: const App(), ) ``` The callback runs when the provider is removed from the tree, and only if `create` actually ran — a lazy provider never looked up has nothing to dispose. ## Scoping by lifetime | Object | Provider | Scope | Released by | |---|---|---|---| | `AuthRepository` | `RepositoryProvider` | above `MaterialApp` | its `dispose` callback | | `LoginCubit` | `BlocProvider(create:)` | login route | automatic `close()` | | cubit borrowed by a dialog | `BlocProvider.value` | dialog route | its original owner | | repository from a DI container | `RepositoryProvider.value` | anywhere | the container | Placing the repository provider **above** every bloc that reads it guarantees two things: the lookups in `create` succeed, and the repository lives at least as long as its consumers. ## MultiRepositoryProvider Like `MultiBlocProvider`, `MultiRepositoryProvider(providers: [...], child: ...)` flattens nesting, and any `child` given to a listed provider is ignored. A common app root is a `MultiRepositoryProvider` of repositories wrapping a `MultiBlocProvider` of app-wide blocs. ## Pitfalls 1. Constructing a repository inside a `build` method without a provider: it is rebuilt on every build and never released. 2. Passing a `dispose` expectation to `RepositoryProvider.value`: it has no such parameter; the owner of the instance releases it. 3. Providing the repository below the route that needs it, so `context.read` in that route's `create` fails to find it. 4. Having blocs depend on other blocs' internals instead of on shared repositories; the docs direct shared data through repositories.

  • Why not let LoginCubit call RepositoryProvider.of itself?
    A cubit has no BuildContext, so it cannot. Even if one were passed in, the cubit would depend on the widget tree, tests would need a widget harness, and the dependency would be hidden. Constructor injection from BlocProvider's create keeps the cubit plain Dart with an explicit dependency.
  • Is dispose called for a lazy RepositoryProvider that nothing ever read?
    No. With lazy creation, create runs only on the first lookup; if nothing looked the repository up, no instance exists, and there is nothing for dispose to receive. Resources are only acquired, and so only released, once someone uses the repository.

saying these in an interview costs you the question

  • RepositoryProvider closes repositories automatically like BlocProvider closes blocs
  • Blocs should look up repositories themselves with RepositoryProvider.of
  • RepositoryProvider.value calls dispose when removed
  • A repository can live below the blocs that read it in create
  • RepositoryProvider only accepts classes that extend a Repository base type