Le préchauffage des providers asynchrones
Riverpod
Commençons par définir Riverpod
L’une des fonctionnalités majeures de Riverpod est de se comporter comme un conteneur, qui permet de faire de l’injection de dépendances. Cependant, on ne peut pas le résumer à cela uniquement ; il peut également être vu comme un gestionnaire d’état réactif. Par “réactif”, j’entends le fait qu’un changement d’état peut être écouté et par exemple induire une reconstruction d’un widget.
Qu’est ce qu’un Provider
Si vous compter utiliser Riverpod vous aller rencontrer cette notion fondamentale que l’on appelle provider. On pourrait définir un provider comme une fonction qui retourne une valeur. Cette valeur retournée peut être une dépendance, un état, ou tout ce que vous voulez.
import 'package:flutter_riverpod/flutter_riverpod.dart';
class WeatherService {
String getWeatherByLocation(String location) {
if (location == "Paris") {
return "9°C";
}
return "21°C"
}
}
final weatherServiceProvider = Provider<WeatherService>((ref) => WetherService());
Pour obtenir l’instance WeatherService
depuis un widget, on peut écrire :
class WeatherWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final weatherService = ref.read(weatherServiceProvider);
return Text(weatherService.getWeatherByLocation("Paris"));
}
}
FutureProvider
Il existe plusieurs types de providers, et je voudrais m’attarder plus précisément sur le type FutureProvider
. Comme son nom l’indique, c’est un provider qui retourne une valeur de type Future
.
class LocationFactory {
Future<LocationInstance> getInstance() async {
return Future.value(LocationInstance());
}
}
final locationProvider = FutreProvider<LocationInstance>((ref) => await LocationFactory.getInstance());
Comment lire un FutureProvider depuis un widget ?
class MapsWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final AsyncValue<LocationInstance> locationService = ref.watch(locationProvider);
final myPosition = locationService.when(
data: (service) => service.findMyPosition(),
error: (error, stack) => "Location error loading",
loading: () => "Loading location service"
);
return Text(myPosition);
}
}
Qu’est ce qui se passe ici ?
Au moment où l’on cherche à utiliser le service de géolocalisation, on doit faire appel au conteneur Riverpod pour obtenir une instance de notre service via la méthode watch
.
Cependant, celui-ci n’a peut-être jamais encore été chargé depuis le lancement de notre application. Riverpod nous transmet donc un objet AsyncValue<LocationInstance>
.
L’obtention de cette instance se fera après un délai d’attente. La tentative d’obtention de l’instance pourrait échouer pour une raison quelconque. On a donc trois états potentiels à gérer.
- Disponible : L’instance peut être utilisée.
- En Chargement : L’instance est en cours de chargement:
- En Erreur : Une erreur s’est produite lors de la demande de l’instance de notre service au conteneur.
Une autre solution consisterait à forcer Riverpod à nous fournir une instance de notre service avec l’extension requireValue
comme ceci :
class MapsWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final LocationInstance locationService = ref.read(locationProvider).requireValue;
final myPosition = locationService.findMyPosition();
return Text(myPosition);
}
}
Le risque encouru est que si le service est en cours de chargement, une exception de type StateError
sera émise. De même, en cas d’échec lors de la tentative d’obtention, l’exception correspondant au problème sous-jacent sera levée.
Comment gérer cela de manière élégante ?
Préchauffage des Providers Asynchrones
Pour gérer le cas des providers asynchrones, on peut imaginer les appeler en amont du lancement de notre application, dans la méthode main()
de notre application. Cette technique peut être appelée "WarmUp"
des FutureProvider. Elle consiste plus précisément à vérifier l’état de tous nos providers asynchrones.
Une fois ceux-ci disponibles et marqués comme chargés, on peut poursuivre l’exécution de notre application. Cela implique que tant que nos providers ne sont pas “chauds”, on affiche un loader ou une animation d’attente.
void main() {
runApp(
const ProviderScope(
child: WarmUp(),
),
);
}
class WarmUp extends ConsumerStatefulWidget {
const WarmUp({super.key});
ConsumerState<WarmUp> createState() => _WarmUpState();
}
class _WarmUpState extends ConsumerState<WarmUp> {
bool warmedUp = false;
Widget build(BuildContext context) {
final logger = Logger();
if (warmedUp) {
return const MyApp();
}
final providers = <ProviderListenable<AsyncValue<Object?>>>[
locationProvider, // Add here all future provider to be ready before app is started
];
final states = providers.map(ref.watch).toList();
for (final state in states) {
if (state is AsyncError) {
logger.e("warm up async provider failed: $state");
Error.throwWithStackTrace(
state.error, state.stackTrace); // TODO show nice error screen
}
}
if (states.every((state) => state is AsyncData)) {
logger.d("warmed up all async provider done");
Future(() => setState(() => warmedUp = true));
}
return Center(
child: Loader(),
);
}
}
Conclusion
Grâce à la technique du "WarmUp"
et à l’utilisation de l’extension requireValue
, nous n’avons plus à nous soucier du chargement de nos providers asynchrones.