Aller au contenu principal

Combiner des providers

Avant de commencer, assurez-vous de lire la section sur les providers.

Combiner des providers

Nous avons vu précédemment comment créer un simple provider. Mais la réalité est la suivante, dans de nombreuses situations, un provider pourra vouloir lire l'état d'un autre provider.

Pour ce faire, nous pouvons utiliser l'objet ref passé au callback de notre provider, et utiliser sa méthode watch.

À titre d'exemple, considérons le provider suivant :

final cityProvider = Provider((ref) => 'London');

Nous pouvons maintenant créer un autre provider qui va consommer notre cityProvider :

final weatherProvider = FutureProvider((ref) async {
// On utilise `ref.watch` pour écouter un autre provider,
// et on lui passe le provider que l'on veut consommer.
// Ici : cityProvider
final city = ref.watch(cityProvider);

// On peut ensuite utiliser le résultat pour faire quelque chose en
// fonction de la valeur de `cityProvider`.
return fetchWeather(city: city);
});

Et voilà. Nous avons créé un provider qui dépend d'un autre provider.

FAQ

Que faire si la valeur écoutée change au fil du temps ? Et voilà. Nous avons créé un provider qui dépend d'un autre provider.

Selon le provider que vous écoutez, la valeur obtenue peut changer au fil du temps.

Par exemple, vous pourriez être à l'écoute d'un StateNotifierProvider, ou le provider écouté a été forcé de se rafraîchir à l'aide de ProviderContainer.refresh/ref.refresh.

En utilisant watch, Riverpod est capable de détecter que la valeur écoutée a changé et réexécutera automatiquement le provider si nécessaire.

Cela peut être utile pour les états (states) calculés. Par exemple, considérons un StateNotifierProvider qui expose une todo-list :

class TodoList extends StateNotifier<List<Todo>> {
TodoList(): super(const []);
}

final todoListProvider = StateNotifierProvider((ref) => TodoList());

Un cas d'utilisation commun serait de faire en sorte que l'interface utilisateur (UI) filtre la liste des tâches (todos) pour n'afficher seulement les tâches terminées/non terminées.

Une façon simple de mettre en place un tel scénario serait de :

  • créer StateProvider, qui présente la méthode de filtrage actuellement sélectionnée :

     enum Filter {
    none,
    completed,
    uncompleted,
    }

    final filterProvider = StateProvider((ref) => Filter.none);
  • créer un provider indépendant qui combine la méthode de filtrage et la todo-list pour exposer la todo-list filtrée :

    final filteredTodoListProvider = Provider<List<Todo>>((ref) {
    final filter = ref.watch(filterProvider);
    final todos = ref.watch(todoListProvider);

    switch (filter) {
    case Filter.none:
    return todos;
    case Filter.completed:
    return todos.where((todo) => todo.completed).toList();
    case Filter.uncompleted:
    return todos.where((todo) => !todo.completed).toList();
    }
    });

Ensuite, notre interface utilisateur (UI) peut écouter filteredTodoListProvider pour écouter la todo-list filtrée. En utilisant cette approche, l'interface sera automatiquement mise à jour lorsque le filtre ou la liste de tâches changera. ou la todo-list change.

Pour voir cette approche en action, vous pouvez consulter le code source de exemple de Todo List.

info

Ce comportement n'est pas spécifique à Provider, et fonctionne avec tous les providers.

Par exemple, vous pouvez combiner watch et FutureProvider pour implémenter une fonction de recherche ou un mode de changement de live-configuration.

// Le filtre de recherche actuel
final searchProvider = StateProvider((ref) => '');

/// Des configurations qui peuvent évoluer dans le temps
final configsProvider = StreamProvider<Configuration>(...);

final charactersProvider = FutureProvider<List<Character>>((ref) async {
final search = ref.watch(searchProvider);
final configs = await ref.watch(configsProvider.future);
final response = await dio.get('${configs.host}/characters?search=$search');

return response.data.map((json) => Character.fromJson(json)).toList();
});

Ce code récupère une liste de caractères à partir du service, et récupère à nouveau automatiquement la liste, chaque fois que les configurations changent ou que la requête de recherche change.

Peut-on lire un provider sans l'écouter ?

Parfois, on souhaite lire le contenu d'un provider, mais sans recréer la valeur exposée lorsque la valeur obtenue change.

Un exemple serait un Repository, qui lit depuis un autre provider le jeton de l'utilisateur pour l'authentification. Nous pourrions utiliser watch et créer un nouveau Repository chaque fois que le jeton de l'utilisateur change, mais il y a peu ou pas d'utilité à faire cela.

Dans cette situation, nous pouvons utiliser read, qui est similaire à watch, mais qui n'obligera pas le provider à recréer sa valeur exposée lorsque la valeur obtenue change.

Dans ce cas, une pratique commune est de faire passer ref.read à l'objet créé. Cet objet sera alors capable de lire les providers quand il le souhaite.

final userTokenProvider = StateProvider<String>((ref) => null);

final repositoryProvider = Provider((ref) => Repository(ref.read));

class Repository {
Repository(this.read);

/// La fonction `ref.read`
final Reader read;

Future<Catalog> fetchCatalog() async {
String token = read(userTokenProvider);

final response = await dio.get('/path', queryParameters: {
'token': token,
});

return Catalog.fromJson(response.data);
}
}
remarque

Vous pouvez aussi passer le ref au lieu de ref.read à votre objet :

final repositoryProvider = Provider((ref) => Repository(ref));

class Repository {
Repository(this.ref);

final Ref ref;
}

La seule différence que le passage de ref.read apporte est un code un peu moins verbeux et la garantie que notre objet n'utilise jamais ref.watch.

NE PAS appelez read à l'intérieur du provider.
final myProvider = Provider((ref) {
// Mauvaise pratique d'appeler `read` ici.
final value = ref.read(anotherProvider);
});

Si vous avez utilisé read pour tenter d'éviter les reconstructions non souhaitées de votre objet, reportez-vous à Mon provider fait des mises à jour trop souvent, que puis-je faire ? Mauvaise pratique pour appeler read ici

Comment tester un objet qui reçoit read comme paramètre de son constructeur ?

Si vous utilisez le modèle décrit dans Peut-on lire un provider sans l'écouter ?, vous vous demandez peut-être comment écrire des tests pour votre objet.

Dans ce scénario, considérez tester le provider directement au lieu de l'objet brut. Vous pouvez le faire en utilisant la classe ProviderContainer :

final repositoryProvider = Provider((ref) => Repository(ref.read));

test('fetches catalog', () async {
final container = ProviderContainer();
addTearDown(container.dispose);

Repository repository = container.read(repositoryProvider);

await expectLater(
repository.fetchCatalog(),
completion(Catalog()),
);
});

Mon provider fait des mises à jour trop souvent, que puis-je faire ?

Si votre objet est recréé trop souvent, il y a de fortes chances que votre provider soit en train d'écouter des objets dont il ne se soucie pas.

Par exemple, vous pouvez écouter un objet Configuration, mais n'utiliser que sa propriété host. En écoutant l'ensemble de l'objet Configuration, si une propriété autre que host est modifiée, votre provider sera toujours réévalué. Ce qui peut être indésirable.

La solution à ce problème est de créer un provider séparé qui expose seulement ce dont vous avez besoin dans Configuration (donc host).

ÉVITEZ l'écoute de l'objet entier :

final configsProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
// Le ProductsProvider devra récupérer les produits si
// un élément de la configuration change.
final configs = await ref.watch(configsProvider.future);

return dio.get('${configs.host}/products');
});

PREFEREZ n'écouter que ce que vous utilisez :

final configsProvider = StreamProvider<Configuration>(...);

/// Un provider qui n'expose que l'hôte actuel.
final _hostProvider = FutureProvider<String>((ref) async {
final config = await ref.watch(configsProvider.future);
return config.host;
});

final productsProvider = FutureProvider<List<Product>>((ref) async {
// Écoute uniquement le host. Si quelque chose d'autre dans les configurations change,
// cela ne réévaluera pas inutilement notre provider.
final host = await ref.watch(_hostProvider.future);

return dio.get('$host/products');
});