Inicializando o gerenciamento de estado do aplicativo Flutter com Riverpod

Tempo de leitura: 9 minutes

 

Considere isso uma introdução à arquitetura de estado em aplicativos Flutter. Mostrarei como inicializar a inicialização do provedor, gerando provedores e notificadores e algumas práticas recomendadas de arquitetura de estado.

 

O que estou usando

Estes são os pacotes que estou usando:

Flutter Riverpod, este pacote também importa o pacote riverpod e o pacote state notifier.

Riverpod generator

Riverpod Annotation

E o código fonte deste artigo pode ser encontrado neste repo, que é um aplicativo fólio que estou finalizando:

 

 

Agora, mostrarei primeiro a lógica de bootstrap que inicializa todos os serviços do aplicativo.

A lógica Bootstrap para inicializar serviços de aplicativos

Então, primeiro precisamos inicializar a lógica dos provedores:

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:demo/src/app_systems/services/dark_mode_service.dart';
import 'package:demo/src/app_systems/services/logger_service.dart';
import 'package:demo/src/app_systems/services/scaffold_key_services.dart';
import 'package:demo/src/app_systems/services/scroll_service_provider.dart';
import 'package:demo/src/app_systems/services/shared_prefs_service.dart';

/// Triggered from bootstrap() to complete futures.
Future<void> appInitProviders(ProviderContainer container) async {
  /// Core
  await container.read(sharedPrefsServiceProvider.future);
  container.read(darkModeServiceProvider);
  container.read(loggerServiceProvider);
  container.read(scaffoldMessengerKeyServicePod);
  container.read(scaffoldMessengerServicePod);
  container.read(scrollServiceProvider);
}

Sim, ProviderScope usa isso internamente. Porém, nós o usamos separadamente aqui para que possamos inicializar os serviços do aplicativo. Consulte o documento da API:

Em seguida, precisamos do bootstrap que usa a lógica do provedor de inicialização acima:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:folio/src/app_systems/logging/application_logger.dart';
import 'package:folio/src/app_systems/providers/app_init_providers.dart';
import 'package:folio/src/app_systems/services/logger_service.dart';

import 'package:folio/src/domain/models/asset_list.dart';
import 'package:url_strategy/url_strategy.dart';

/// Bootstrap a lógica de inicialização incluindo provedores.
/// As melhores práticas são usar ProviderContainer apenas para inicialização e usar
/// ref.watch e ref.read emparelhados com os widgets do consumidor no nível da tela.
// ignore: prefer-static-class
Future<ProviderContainer> appBootstrap() async {
  // Get binding of Flutter Engine for loading hooks
  final binding = WidgetsFlutterBinding.ensureInitialized();

  ApplicationLogger.init(true);

  // Cache de imagens na pasta assets usando o binding do Flutter Engine
  // ciclo de vida.
  binding.deferFirstFrame();
  binding.addPostFrameCallback((_) {
    final Element? context = binding.renderViewElement;
    if (context != null) {
      for (final asset in assetList) {
        precacheImage(
          AssetImage(asset),
          context,
        );
      }
    }
    binding.allowFirstFrame();
  });

  setPathUrlStrategy();

  final container = ProviderContainer(
    // apenas para Flutter, aplicações dart requerem ouvir o container para obter mudanças como
    // ProviderObserver se conecta ao ciclo de vida do widget.
    observers: [_Logger()],
  );

  await appInitProviders(container);

  // definir o appLogger global
  appLogger = container.read(loggerServiceProvider);

  return container;
}

class _Logger extends ProviderObserver {
  @override
  void didUpdateProvider(
    ProviderBase<dynamic> provider,
    Object? previousValue,
    Object? newValue,
    ProviderContainer container,
  ) {
     // em termos de observabilidade, não queremos mensagens de log do provedor
     // produção, portanto, denugPrint é usado, pois é removido no modo de liberação    debugPrint(
      '''
      {
      "provider": "${provider.name ?? provider.runtimeType}",
      "newValue": "$newValue"
      }''',
    );
  }
}

Ele retorna um Future que, por acaso, é do tipo ProviderContainer. Depois que a instância do ProviderContainer é criada, eu a uso para inicializar os provedores que são os serviços do aplicativo. O registrador ProviderObserver usa o método debugPrint, pois só quero registros das alterações de valores do provedor no modo de depuração.

Então, no principal, tenho isso:

import 'dart:async';
import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:folio/src/app_systems/providers/app_bootstrap.dart';

import 'package:folio/src/my_app.dart';

void main() async {
  runZonedGuarded<Future<void>>(
      () async {
        runApp(
          UncontrolledProviderScope(
            container: await appBootstrap(),
            child: MyApp(),
          ),
        );
      },
      (
        Object error,
        StackTrace stack,
      ) {
        // Se estiver usando um serviço de erro de terceiros, chamaremos essa API aqui para passar o erro para
      log(
        error.toString(),
        stackTrace: stack,
      );

      },
      zoneSpecification: ZoneSpecification(
      // Interceptar todas as chamadas de impressão
      print: (
        self,
        parent,
        zone,
        line,
      ) {
        // Inclua um registro de data e hora e o nome do aplicativo
        final messageToLog = "[${DateTime.now()}] Folio $line $zone";

        // Também imprima a mensagem no "Console de depuração"
        // mas é apenas uma mensagem informativa e não contém
        // coisas proibidas pela privacidade
        parent.print(
          zone,
          messageToLog,
        );
      },
    ),

  );

  
}

O uso de UncontrolledProviderScope insere o ProviderContainer na árvore de widgets, razão pela qual podemos usar o widget Consumer para obter uma referência a qualquer provedor. Consulte o documento da API:

 

Agora, vou lhe mostrar como gerar notificadores do Riverpod.

Geração de notificadores do Riverpod

Veja a seguir como gerar um notificador de modo escuro:

import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';

part 'dark_mode_service.g.dart';

@riverpod
class DarkModeService extends _$DarkModeService {
  late SharedPreferences prefs;

  @override
  bool build(){
    _init();

    return false;
  }

  /// O uso é
  /// ```
  /// Switch(
  ///               value: darkMode,
  ///               onChanged: (val) {
  ///                 ref.read(darkModeProvider.notifier).toggle();
  ///              },
  /// ```
  /// Eu acho.
  Future<void> toggle() async {
    state = !state;
    prefs.setBool(
      "darkMode",
      state,
    );
  }

  Future _init() async {
    prefs = await SharedPreferences.getInstance();
    final darkMode = prefs.getBool("darkMode");
    state = darkMode ?? false;
  }
}

O método build é o que estaria em um construtor do notificador. O extends _$DarkModeService informa ao Riverpod Generator que estamos falando de um notificador. E isso não apenas gerará um notificador, mas também um provedor:

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'dark_mode_service.dart';

// **************************************************************************
// RiverpodGenerator
// **************************************************************************

String _$darkModeServiceHash() => r'59cc692b9031d13be1caad72f33c92262243cdc3';

/// See also [DarkModeService].
@ProviderFor(DarkModeService)
final darkModeServiceProvider =
    AutoDisposeNotifierProvider<DarkModeService, bool>.internal(
  DarkModeService.new,
  name: r'darkModeServiceProvider',
  debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
      ? null
      : _$darkModeServiceHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

typedef _$DarkModeService = AutoDisposeNotifier<bool>;
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions

E o outro notificador padrão que estará em cada aplicativo flutter é aquele que lida com localidade, primeiro nosso modelo de localidade:

// ignore_for_file: no_leading_underscores_for_local_identifiers

import 'dart:developer';
import 'dart:ui';

import 'package:folio/src/app_systems/json_local_sync.dart';
import 'package:folio/src/domain/models/locale_json_converter.dart';
import 'package:folio/src/domain/models/persistent_state.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'locale_state.freezed.dart';
part 'locale_state.g.dart';


// Local de reserva
// ignore: prefer-static-class
const Locale fallbackLocale = Locale('en', 'US',);


@freezed
class LocaleState with _$LocaleState, PersistentState<LocaleState> {
  static const _localStorageKey = 'persistentLocale';

  const factory LocaleState({
    @LocaleJsonConverter() @Default(fallbackLocale) @JsonKey() Locale locale,
  }) = _LocaleState;

  // Permitir getters / setters personalizados
  const LocaleState._();

  // Para o gerador de código toJson / fromJson json_serializable integrado ao Riverpod
  factory LocaleState.fromJson(Map<String, dynamic> json) =>
      _$LocaleStateFromJson(json);

  

  /// Local Save
  /// Salva as configurações no armazenamento persistente.
  @override
  Future<bool> localSave() async {
    final Map<String, dynamic> value = toJson();
    try {
      return await JsonLocalSync.save(key: _localStorageKey, value: value,);
    } catch (e) {
      log(e.toString());

      return false;
    }
  }

  /// Local Delete
  /// Exclui as configurações do armazenamento persistente.
  @override
  Future<bool> localDelete() async {
    try {
      return await JsonLocalSync.delete(key: _localStorageKey);
    } catch (e) {
      log(e.toString());
      
      return false;
    }
  }

  /// Criar as configurações a partir do armazenamento persistente
  /// (O Static Factory Method suporta leitura assíncrona do armazenamento).
  @override
  Future<LocaleState?> fromStorage() async {
    try {
      final _value = await JsonLocalSync.get(key: _localStorageKey);
      if (_value == null) {
        return null;
      }

      return LocaleState.fromJson(_value); 

    } catch (e) {
      rethrow;
    }
  }
  
}

E uso o Freezed para torná-lo serializável e imutável. Então, para gerar o notificador que usa LocaleState, eu tenho:

import 'dart:developer';

import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:folio/src/app_systems/services/platform_locale_service.dart';

import 'package:folio/src/app_systems/services/supported_locales_service.dart';
import 'package:folio/src/domain/models/locale_state.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'locale_state_service.g.dart';

@riverpod
class LocaleStateService extends _$LocaleStateService {

  @override
  LocaleState build() =>  const LocaleState();

  /// Initialize Locale
  /// Pode ser executado na inicialização para estabelecer o local inicial a partir do armazenamento ou da plataforma
  /// 1. Tenta restaurar o locale do armazenamento.
  /// 2. se não houver locale no armazenamento, tenta definir o local a partir das configurações da plataforma.
  Future<void> initLocale() async {
    // Tentativa de restauração a partir do armazenamento
    final bool fromStorageSuccess =
        await ref.read(localeStateServiceProvider.notifier).restoreFromStorage();

    // Se a restauração do armazenamento não funcionou, configure na plataforma
    if (!fromStorageSuccess) {
      ref
          .read(localeStateServiceProvider.notifier)
          .setLocale(ref.read(platformLocaleServiceProvider));
    }
  }

  //ignorar: comentário-formato
  /// Definir localidade.
  /// Tenta definir a localidade se ela estiver em nossa lista de localidades suportadas.
  /// SE NÃO: obtenha o primeiro locale que corresponda ao nosso código de idioma e defina-o.
  /// ELSE: não faça nada.
  void setLocale(Locale locale) {
    final List<Locale> supportedLocales = ref.read(supportedLocalesServiceProvider);

    // Defina a localidade se ela estiver em nossa lista de localidades compatíveis
    if (supportedLocales.contains(locale)) {
      // Atualizar estado
      state = state.copyWith(locale: locale);

      // Salvar na persistência
      state.localSave();

      return;
    }

    // Obtenha a localidade de idioma mais próxima e defina-a em seu lugar
    final Locale? closestLocale = supportedLocales.firstWhereOrNull(
      (supportedLocale) => supportedLocale.languageCode == locale.languageCode,
    );
    if (closestLocale != null) {
      // Atualizar estado
      state = state.copyWith(locale: closestLocale);

      // Salvar na persistência
      state.localSave();

      return;
    }

    // Caso contrário, não faça nada e continuaremos com o locale padrão
    return;
  }

  /// Restaurar localidade do armazenamento.
  Future<bool> restoreFromStorage() async {
    try {
      log("Restoring LocaleState from storage.");
      // Tentativa de obter o usuário do armazenamento
      final LocaleState? myState = await state.fromStorage();

      // Se o usuário for nulo, não há nenhum usuário para restaurar
      if (myState == null) {
        return false;
      }

      log("State found in storage: ${myState.toJson()}");

      // Definir estado
      state = myState;

      return true;
    } catch (e, s) {
      log("Error$e");
      log(s.toString());

      return false;
    }
  }
 
}

E quando isso é gerado, ele nos fornece o provedor:

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'locale_state_service.dart';

// **************************************************************************
// RiverpodGenerator
// **************************************************************************

String _$localeStateServiceHash() =>
    r'843e29a60f9dcd5bc0e5def1d2e0af62dddfb025';

/// See also [LocaleStateService].
@ProviderFor(LocaleStateService)
final localeStateServiceProvider =
    AutoDisposeNotifierProvider<LocaleStateService, LocaleState>.internal(
  LocaleStateService.new,
  name: r'localeStateServiceProvider',
  debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
      ? null
      : _$localeStateServiceHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

typedef _$LocaleStateService = AutoDisposeNotifier<LocaleState>;
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions

Agora vamos ver um exemplo mais complicado, a navegação.

 

Provedores de navegação

Aqui estão as rotas do aplicativo, tenha isso em mente antes que o construtor do roteador go mude para oferecer suporte ao ShellRoute, que deve estar em vigor em abril:

import 'package:flutter/material.dart';
import 'package:folio/src/books_feature/books_screen.dart';
import 'package:folio/src/home_feature/home_screen.dart';
import 'package:folio/src/info_feature/info_screen.dart';
import 'package:folio/src/lab_feature/lab_screen.dart';
import 'package:folio/src/showcase_feature/showcase_screen.dart';

import 'package:folio/src/uikits_feature/uikits_screen.dart';
import 'package:go_router/go_router.dart';

part 'routes.g.dart';


/// Essa é a declaração de rota shell das rotas
/// associadas ao scaffold compartilhado.
///
@TypedGoRoute<HomeRoute>(
  path: "/",
  routes: [
    TypedGoRoute<BooksRoute>(path: BooksRoute.path),
    TypedGoRoute<InfoRoute>(path: InfoRoute.path),
    TypedGoRoute<HomeRoute>(path: HomeRoute.path),
    TypedGoRoute<LabRoute>(path: LabRoute.path),
    TypedGoRoute<ShowcaseRoute>(path: ShowcaseRoute.path),
    TypedGoRoute<UIKitsRoute>(path: UIKitsRoute.path),
  ],
)
class HomeRoute extends GoRouteData {
  static const path = 'home';
  const HomeRoute();

  // todos os redirecionamentos de registro e login de usuários devem ser feitos aqui
  // e ser emparelhados com um ouvinte de notificação obrigatório

  @override
  Widget build(
    BuildContext context,
    GoRouterState state,
  ) {
    return HomeScreen();
  }
}

class LabRoute extends GoRouteData {
  static const path = 'lab';
  const LabRoute();

  @override
  Widget build(
    BuildContext context,
    GoRouterState state,
  ) {
    return LabScreen();
  }
}

class BooksRoute extends GoRouteData {
  static const path = 'books';
  const BooksRoute();

  @override
  Widget build(
    BuildContext context,
    GoRouterState state,
  ) {
    return BooksScreen();
  }
}

class InfoRoute extends GoRouteData {
  static const path = 'info';
  const InfoRoute();

  @override
  Widget build(
    BuildContext context,
    GoRouterState state,
  ) {
    return InfoScreen();
  }
}

class ShowcaseRoute extends GoRouteData {
  static const path = 'showcase';
  const ShowcaseRoute();

  @override
  Widget build(
    BuildContext context,
    GoRouterState state,
  ) {
    return ShowcaseScreen();
  }
}

class UIKitsRoute extends GoRouteData {
  static const path = 'uikits';
  const UIKitsRoute();

  @override
  Widget build(
    BuildContext context,
    GoRouterState state,
  ) {
    return UIKitsScreen();
  }
}

Agora geramos o notificador com isto:

import 'package:flutter/material.dart';
import 'package:folio/src/app_systems/router/routes.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'router_service_notifier.g.dart';

/// Esse notificador foi criado para implementar o [Listenable] que nosso [GoRouter] precisa.
///
/// Nosso objetivo é acionar redirecionamentos sempre que for necessário.
/// Isso é feito chamando nosso (único) listener toda vez que quisermos notificar algo.
/// Isso permite centralizar a lógica de redirecionamento global nessa classe.
/// Nesse caso simples, apenas ouvimos as alterações de autenticação.
///
/// OBSERVAÇÃO.
/// Isso pode parecer muito complicado à primeira vista;
/// Em vez disso, esse método tem como objetivo seguir algumas boas práticas:
/// 1. não requer que enviemos nenhum parâmetro `ref`
/// 2. Ele funciona como um substituto completo para o [ChangeNotifier] (é uma implementação do [Listenable])
/// 3. Permite ouvir vários provedores, se necessário (agora temos um [Ref]!).
@riverpod
class RouterServiceNotifier extends _$RouterServiceNotifier implements Listenable {
  VoidCallback? routerListener;

  /// Nossas rotas de aplicativos. Obtidas por meio da geração de código.
  List<GoRoute> get routes => $appRoutes;

  @override
  Future<void> build() async {
    ref.listenSelf((
      _,
      __,
    ) {
      if (state.isLoading) return;
      routerListener?.call();
    });
  }

  /// Adiciona o ouvinte do [GoRouter] conforme especificado por seu [Listenable].
  /// [GoRouteInformationProvider] usa esse método na criação para lidar com seu
  /// interno [ChangeNotifier].
  /// Confira a implementação interna de [GoRouter] e
  /// [GoRouteInformationProvider] para ver isso em ação.
  @override
  void addListener(VoidCallback listener) {
    routerListener = listener;
  }

  /// Remove o ouvinte do [GoRouter] conforme especificado por seu [Listenable].
  /// [GoRouteInformationProvider] usa esse método quando descarta,
  /// para que ele remova seu callback quando destruído.
  /// Confira a implementação interna de [GoRouter] e
  /// [GoRouteInformationProvider] para ver isso em ação.
  @override
  void removeListener(VoidCallback listener) {
    routerListener = null;
  }
}

Algumas observações: reimplementei o ChangeNotifier, pois todos os ChangeNotifiers são Listenables. A vantagem é que posso ouvir vários provedores. Então, o provedor que usa esse notificador é:

import 'package:flutter/material.dart';
import 'package:folio/src/app_systems/router/app_router_observer.dart';
import 'package:folio/src/app_systems/router/routes.dart';
import 'package:folio/src/app_systems/services/router_service_notifier.dart';
import 'package:folio/src/shared_scaffold.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'router_service.g.dart';

final _rootRouterKey = GlobalKey<NavigatorState>(debugLabel: 'routerKey');

final _shellRouterKey = GlobalKey<NavigatorState>(debugLabel: 'shellRouterKey');

/// Esse provedor simples armazena em cache nosso GoRouter.
/// Por padrão, esse provedor nunca será reconstruído.
@riverpod
// ignore: prefer-static-class
GoRouter routerService(RouterServiceRef ref) {
  final notifier = ref.watch(routerServiceNotifierProvider.notifier);

  return GoRouter(
    routes: [
      ShellRoute(
        builder: (
          BuildContext context,
          GoRouterState state,
          Widget child,
        ) {
          return SharedScaffold(child: child);
        },
        observers: [AppRouterObserver()],
        routes: notifier.routes,
        navigatorKey: _shellRouterKey,
      ),
    ],
    refreshListenable: notifier,
    initialLocation: HomeRoute.path,
    debugLogDiagnostics: true,
    navigatorKey: _rootRouterKey,
  );
}

Em seguida, isso é usado no método MaterialApp.router como:

// ignore_for_file: no_leading_underscores_for_local_identifiers

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'package:folio/src/app_systems/services/dark_mode_service.dart';

import 'package:folio/src/app_systems/services/locale_state_service.dart';
import 'package:folio/src/app_systems/services/router_service.dart';

import 'package:folio/src/app_systems/services/supported_locales_service.dart';

import 'package:folio/src/app_systems/themes/colors/brand_fcs.dart';


/// O Widget que configura seu aplicativo.
class MyApp extends ConsumerWidget {
  @override
  Widget build(
    BuildContext context,
    WidgetRef ref,
  ) {
    final router = ref.watch(routerServiceProvider);

    final appDarkThemeMode = ref.watch(darkModeServiceProvider);

    final List<Locale> _supportedLocales = ref.read(supportedLocalesServiceProvider);

    // Observe o local atual e reconstrua-o ao mudar
    final Locale _locale = ref.watch(localeStateServiceProvider).locale;

    return MaterialApp.router(
      routerConfig: router, 
      // O Widget que configura seu aplicativo.
      theme: brandThemeDataLight, 
      darkTheme: brandThemeDataDark, 
      themeMode: appDarkThemeMode ? ThemeMode.dark : ThemeMode.light, 
      locale: _locale, 
      localizationsDelegates: AppLocalizations.localizationsDelegates, 
      supportedLocales: _supportedLocales, 
      debugShowCheckedModeBanner: false, 
      restorationScopeId: "Folio",
    );
  }
}

O que acontece com os provedores que encaminham para outros provedores?

 

Provedores que fazem referência a outros provedores

O primeiro caso de uso é o Scroll Controller, que por acaso é um ChangeNotifier legado:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// ChangeNotifierProvider. O uso é esse em um widget de consumidor:
/// ```
/// final sc = ref.watch(scrollControllerProvider);
/// ```
///
/// Precisa ser implementado manualmente, pois é legado e não pode ser gerado
/// pelo gerador riverpod.
///
final scrollServiceProvider =
    ChangeNotifierProvider.autoDispose((ref) => ScrollController());

Usamos apenas o ChangeNotifierProvider no riverpod que dá suporte a essas coisas antigas.

Agora, para o provedor que se refere a outro caso de uso de provedor das chaves scaffold:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// Um provedor cujo valor é a chave global do scaffold messenger passada para
/// [MaterialApp].
///
// ignore: prefer-static-class
final scaffoldMessengerKeyServicePod = Provider((ref) {
  return GlobalKey<ScaffoldMessengerState>();
});

/// Retorna o scaffold messenger associado ao [scaffoldMessengerKeyPod].
/// E a chamada será:
/// ```
/// final scaffoldMessenger = ref.read(scaffoldMessengerPod);
///
///scaffoldMessenger.showSnackBar(
///  SnackBar(
///    content: Text('Some message'),
///  ),
///);
/// ```
// ignore: prefer-static-class
final scaffoldMessengerServicePod = Provider((ref) {
  return ref.watch(scaffoldMessengerKeyServicePod).currentState!;
});

À medida que o riverpod avança em direção à versão 3.0.0, espero que em algum momento não seja mais necessário recorrer à criação manual de provedores ou notificadores.

Pensamentos

Essa é a forma de iniciar o gerenciamento de estado do aplicativo flutter com o riverpod. É um pouco mais fácil do que lidar com os vários provedores implementados no pacote Provider, embora o Riverpod seja do mesmo autor do pacote Provider.