Skip to content

Providers

A provider is a tool that helps manage and inject dependencies in an application, making it easier to share data or services across different parts of the app.

Providers

Declare a new provider either as a global final variable or a final static field.

Example with a global final variable:

/// global scope
final numberProvider = Provider((context) => 5);

If there is only a provider per class, you can also create a final static field. This comes down to personal preference.

class MyDatabase {
static final provider = Provider((context) => MyDatabase());
}

Injection of other providers with context

Providers can leverage the context to inject other providers. The context will be relative to the scope in which they are provided.

final doubleNumberProvider = Provider((context) {
final number = numberProvider.of(context);
return number * 2;
});

Providers with argument

Providers need to be provided before they can be injected in the widget tree. Sometimes, they need an initial argument so that they can be instantiated correctly. This is possible with Provider.withArgument.

final numberPlusArgProvider = Provider.withArgument((context, int arg) {
return 5 + arg;
});

An example where this might make more sense would be an application with multi-account support, where the database is loaded per user, and the filepath of the database contains the user ID:

class MyDatabase {
static final provider = Provider.withArgument((context, String userId) => MyDatabase.fromId(id));
}

This MyDatabase.provider has to be provided in a subtree (of the widget tree) belonging to the currently logged user.

Injection of other providers with context

Providers can both take an argument and rely on context.

final doubleNumberPlusArgProvider = Provider.withArgument((context, int arg) {
final number = numberProvider.of(context);
return number * 2 + arg;
});

Lazy creation of the values

By default, the value of a provider is created lazily: the create function is called the first time the provider is injected, and never before. If a provider is never injected, its value is never created.

Creating a value as soon as a scope is mounted

Sometimes a value has to exist even if no widget injects it yet, e.g. because it starts a subscription or warms up a cache. In that case, pass lazy: false. This can be done in three places:

  1. When inserting the provider into a ProviderScope. This only affects that scope, so the very same provider can be eager in one scope and lazy in another one:

    ProviderScope(
    providers: [
    engineProvider(lazy: false), // created as soon as the scope is mounted
    userProvider(42, lazy: false), // works for argument providers too
    settingsProvider(), // lazy
    ],
    child: const MyPage(),
    )
  2. When defining the provider. This affects every scope providing it:

    final engineProvider = Provider((_) => Engine(), lazy: false);
    final userProvider = Provider.withArgument(
    (_, int id) => User(id),
    lazy: false,
    );
  3. Globally, with DiscoConfig.lazy.

The first non-null option wins: the lazy given in the ProviderScope takes precedence over the lazy given to the provider, which takes precedence over DiscoConfig.lazy (which defaults to true). For example, engineProvider(lazy: true) makes a provider defined with lazy: false lazy in that scope only. The laziness is resolved when the ProviderScope is mounted.

The non-lazy values are created in the order in which they are declared, right after the scope is mounted and before its descendants are built. A non-lazy provider can inject the other providers of its own scope, no matter whether they are lazy or not and no matter the order in which they are declared: a value that is injected and not created yet is simply created on the fly. Every value is created only once.

Optional parameters

When defining a provider, we need to pass the positional create argument, which is a function used to generate the value contained by the provider.

There are also three optional named parameters that can be specified.

ParameterDefaultDescription
disposenullThe function to call when the scope containing the provider gets disposed. It is used to dispose correctly the value held by the provider.
lazynullWhether the value is created lazily. If null, DiscoConfig.lazy is used. It can be overridden in each ProviderScope, see Lazy creation of the values.
debugNamenullAn optional name, shown in the error messages of this library, which makes a provider easier to recognize.

Overriding a provider

A provider can be replaced by another provider of the same type through overrideWith, which is meant to be used for testing:

final myProviderOverride = myProvider.overrideWith(
Provider((context) => MyMock()),
);

The same works for providers with an argument, as long as the mock takes an argument of the same type:

final myArgProviderOverride = myArgProvider.overrideWith(
Provider.withArgument((context, int arg) => MyMock(arg)),
);

Since the override is a provider itself, it also controls how the mocked value is created and disposed. Refer to the Testing page to see how overrides are inserted into the widget tree.