Salta al contenuto principale

Combinare stati di provider

Assicurarsi di aver letto prima la sezione Providers. In questa guida impareremo a combinare gli stati dei provider.

Combinare stati di provider

Precedentemente abbiamo visto come creare un semplice provider. Ma la realtà è che in molte situazioni un provider vorrà leggere lo stato di un altro provider.

Per fare ciò, possiamo usare l'oggetto ref passato alla callback del nostro provider e usare il metodo watch.

Come esempio, consideriamo il seguente provider:

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

Possiamo ora creare un altro provider che consumerà cityProvider:

final weatherProvider = FutureProvider((ref) async {
// Usiamo `ref.watch` per ascoltare un altro provider, e lo passiamo al provider
// che vogliamo consumare, in questo caso: cityProvider
final city = ref.watch(cityProvider);

// Possiamo poi utilizzare il risultato per fare delle operazioni basandoci
// sul valore di `cityProvider`.
return fetchWeather(city: city);
});

Questo è quanto. Abbiamo creato un provider che dipende da un altro provider.

Domande frequenti

Cosa succede se il valore ascoltato cambia nel tempo?

Dipendentemente dal provider che si sta ascoltando, il valore ottenuto potrebbe cambiare nel tempo. Ad esempio, potresti ascoltare un StateNotifierProvider oppure il provider che si sta ascoltando è stato forzato a riaggiornarsi mediante l'uso di ProviderContainer.refresh/ref.refresh.

Usando watch, Riverpod è in grado di rilevare che il valore in ascolto è cambiato e rieseguirà automaticamente la richiamata (callback) di creazione del provider quando necessario.

Ciò può essere utile per gli stati calcolati (computed states). Per esempio, considera uno StateNotifierProvider che espone una todo-list:

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

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

Un caso d'uso comune sarebbe avere l'UI che filtra la lista dei todo per mostrare solo i todo completati/non completati.

Un modo facile per implementare questo tipo di scenario sarebbe:

  • creare un StateProvider che espone il metodo di filtro attualmente selezionato:

     enum Filter {
    none,
    completed,
    uncompleted,
    }

    final filterProvider = StateProvider((ref) => Filter.none);
  • creare un provider separato che combina il metodo di filtro e la todo-list per esporre la todo-list filtrata:

    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();
    }
    });

Quindi, la nostra UI può ascoltare filteredTodoListProvider per a sua volta ascoltare la todo-list filtrata. Con questo approccio, l'UI si aggiornerà automaticamente quando o il filtro o la todo-list cambia.

Per vedere in azione questo approccio puoi guardare il codice sorgente dell'esempio Todo List.

info

Questo comportamento non è specifico di Provider e funziona con tutti i tipi provider.

Per esempio, potresti combinare watch con FutureProvider per implementare una funzione di ricerca che supporta instantanei cambi di configurazione:

// Il filtro di ricerca corrente
final searchProvider = StateProvider((ref) => '');

/// Configurazioni che possono cambiare nel tempo
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();
});

Questo codice otterrà una lista di personaggi dal servizio e recupererà automaticamente la lista ogni volta che cambiano le configurazioni o quando cambia la query di ricerca.

Posso leggere un provider senza ascoltarlo?

A volte, si vuole leggere il contenuto di un provider ma senza ricreare il valore esposto quando il valore ottenuto cambia.

Un esempio è un Repository, il quale legge da un altro provider il token di autenticazione dell'utente. Potremmo usare watch e creare un nuovo Repository ogni volta che il token dell'utente cambia ma servirebbe a poco o niente farlo.

In questa situazione, possiamo usare read, che è simile a watch ma non provocherà la ricreazione del valore del provider che espone.

In tal caso, una pratica comune è quella di passare ref.read all'oggetto creato. L'oggetto sarà poi in grado di leggere i provider quando vorrà.

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

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

class Repository {
Repository(this.read);

/// La funzione `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);
}
}
NOTA

Puoi anche passare ref al posto di ref.read al tuo oggetto.

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

class Repository {
Repository(this.ref);

final Ref ref;
}

L'unica differenza che comporta passare ref.read è la scrittura di meno codice, inoltre, garantisce che il nostro oggetto non utilizzi mai ref.watch.

NON usare read dentro al body di un provider
final myProvider = Provider((ref) {
// É cattiva pratica chiamare `read` qui.
final value = ref.read(anotherProvider);
});

Se hai utilizzato read come tentativo di evitare rebuilds non volute del tuo oggetto, consulta Il mio provider si aggiorna troppo spesso, cosa posso fare?

Come testare un oggetto che riceve read come parametro del suo costruttore?

Se stai usando il pattern descritto in Posso leggere un provider senza ascoltarlo?, ti starai chiedendo come scrivere dei test per il tuo oggetto.

In questo scenario, considera testare il provider direttamente invece dell'oggetto puro.
Puoi fare ciò usando 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()),
);
});

Il mio provider si aggiorna troppo spesso, cosa posso fare?

Se il tuo oggetto è ricreato troppo spesso, è possibile che il tuo provider stia in ascolto di oggetti di cui non gli interessa.

Per esempio, potresti essere in ascolto di Configuration ma usare solo la proprietà host. Ascoltando l'intero oggetto Configuration, se un'altra proprietà che non sia host cambia, il tuo provider verrà ri-elaborato, il che potrebbe essere indesiderato.

La soluzione a questo problema è creare un provider separato che espone solo ciò di cui hai bisogno di Configuration (quindi host):

EVITARE di stare in ascolto dell'intero oggetto:

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

final productsProvider = FutureProvider<List<Product>>((ref) async {
// productsProvider ri-otterrà i prodotti se qualsiasi cosa
// nelle configurazioni cambia
final configs = await ref.watch(configsProvider.future);

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

PREFERIRE usare "select" quando necessiti solo di una singola proprietà dell'oggetto:

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

final productsProvider = FutureProvider<List<Product>>((ref) async {
// Ascolta solo la proprietà 'host'. Se qualcos'altro nelle configurazioni
// cambia, non causerà inutili ri-elaborazioni del nostro provider.
final host = await ref.watch(configProvider.selectAsync((config) => config.host));

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

Questo ricostruirà productsProvider solo quando host cambierà.