Start Debugging

Migre de provider para Riverpod no Flutter (provider 6.1.5 para Riverpod 3.x)

Uma migração passo a passo do pacote provider para Riverpod 3.x em um app Flutter real: ChangeNotifierProvider para Notifier, MultiProvider para ProviderScope, context.watch para ref.watch, ProxyProvider para composição com ref.watch, além das pegadinhas de igualdade e ciclo de vida que mordem. Testado no Flutter 3.27.1, Dart 3.11, provider 6.1.5, flutter_riverpod 3.3.1.

A versão curta: adicione flutter_riverpod ao lado de provider, envolva seu app em um ProviderScope em vez de um MultiProvider e migre um recurso de cada vez começando pelas folhas da sua árvore de dependências. Cada ChangeNotifier vira um Notifier (ou AsyncNotifier para trabalho assíncrono), context.watch<T>() vira ref.watch(myProvider), Provider.of e context.read viram ref.read, e cada ProxyProvider colapsa em um simples ref.watch de outro provider. Um app de pequeno a médio porte é um trabalho de um a três dias; a parte que quebra não é a sintaxe, é que o Riverpod compara o estado por igualdade e mantém os providers vivos de forma diferente do que provider faz. Testado no Flutter 3.27.1, Dart 3.11, provider 6.1.5, flutter_riverpod 3.3.1, riverpod_annotation 2.6.1 e riverpod_generator 2.6.5.

O pacote provider (atualmente 6.1.5) tem sido a resposta padrão para gerenciamento de estado no Flutter desde 2019, e ainda funciona. Mas seu autor, Remi Rousselet, escreveu o Riverpod especificamente para corrigir os problemas estruturais do provider: estado lido através de BuildContext significa um ProviderNotFoundException em runtime em vez de um erro de compilação, o aninhamento de ProxyProvider fica ilegível passando de duas dependências, e você não pode ter dois providers do mesmo tipo sem ginástica com ValueKey. O Riverpod mantém o modelo mental (um grafo de objetos que reconstroem widgets quando mudam) e remove o acoplamento com BuildContext. Este guia é a migração mecânica, das folhas para cima, que não exige uma reescrita.

Por que migrar do provider

O que quebra

ÁreaMudançaSeveridade
Fiação da raizMultiProvider substituído por um único ProviderScopemédia
Leiturascontext.watch<T>() / Provider.of<T>(context) substituídos por ref.watch / ref.readalta
NotifiersChangeNotifier + notifyListeners() substituídos por Notifier + reatribuição de estadoalta
Semântica de reconstruçãoO Riverpod compara o estado por ==; mutação no lugar não reconstrói maisalta
ComposiçãoProxyProvider substituído por ref.watch da dependênciamédia
WidgetsStatelessWidget / StatefulWidget viram ConsumerWidget / ConsumerStatefulWidgetmédia
Ciclo de vidaprovider faz dispose quando removido da árvore; o Riverpod mantém o estado até autoDisposemédia

As duas linhas alta nas áreas de reconstrução e notifier são onde os times perdem tempo. Todo o resto é localizar e substituir.

Checklist pré-voo

Passos da migração

  1. Adicione o Riverpod ao lado do provider no pubspec.yaml. Não remova o provider ainda. Ambos os pacotes coexistem porque possuem árvores separadas; uma dada parte do estado tem exatamente um dono por vez, então migre por recurso, não por tipo.

    # pubspec.yaml. Flutter 3.27.1, Dart 3.11.
    dependencies:
      flutter:
        sdk: flutter
      provider: ^6.1.5            # keep until migration is done
      flutter_riverpod: ^3.3.1
      riverpod_annotation: ^2.6.1
    
    dev_dependencies:
      build_runner: ^2.4.13
      riverpod_generator: ^2.6.5
      custom_lint: ^0.7.0
      riverpod_lint: ^2.6.5

    Verifique: flutter pub get resolve sem conflitos de versão.

  2. Envolva a raiz do app em ProviderScope e mantenha o MultiProvider dentro dele por enquanto. O ProviderScope é onde o Riverpod armazena todo o estado dos providers. Não é uma lista de providers, é um único limite. Deixe seu MultiProvider existente embaixo dele para que as telas não migradas continuem funcionando.

    // lib/main.dart, Flutter 3.27.1
    import 'package:flutter/material.dart';
    import 'package:flutter_riverpod/flutter_riverpod.dart';
    import 'package:provider/provider.dart';
    
    void main() {
      runApp(
        ProviderScope(                       // Riverpod root
          child: MultiProvider(              // legacy, shrinks as you migrate
            providers: [
              ChangeNotifierProvider(create: (_) => CartModel()),
              ChangeNotifierProvider(create: (_) => AuthModel()),
            ],
            child: const MyApp(),
          ),
        ),
      );
    }

    Verifique: o app ainda compila e roda de forma idêntica. Nada se moveu ainda.

  3. Converta um ChangeNotifier folha em um Notifier. Escolha um model do qual nada mais depende. No provider você muta um campo e chama notifyListeners(). No Riverpod, build() retorna o estado inicial e você reatribui state para notificar. Não há notifyListeners().

    // Before: provider 6.1.5
    class CartModel extends ChangeNotifier {
      final List<Item> _items = [];
      List<Item> get items => List.unmodifiable(_items);
    
      void add(Item item) {
        _items.add(item);
        notifyListeners();
      }
    }
    // After: flutter_riverpod 3.3.1, code generation
    import 'package:riverpod_annotation/riverpod_annotation.dart';
    
    part 'cart_model.g.dart';
    
    @riverpod
    class Cart extends _$Cart {
      @override
      List<Item> build() => const [];
    
      void add(Item item) {
        state = [...state, item];   // new list, not state.add(...)
      }
    }

    Execute dart run build_runner build --delete-conflicting-outputs para gerar cartProvider. Verifique: o gerador produz cart_model.g.dart sem erros.

  4. Mude a tela que o consome para um ConsumerWidget. StatelessWidget vira ConsumerWidget e build ganha um WidgetRef ref. context.watch<CartModel>() vira ref.watch(cartProvider). Para uma chamada de método, context.read<CartModel>().add(x) vira ref.read(cartProvider.notifier).add(x).

    // Before
    class CartView extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final items = context.watch<CartModel>().items;
        return Column(children: [
          for (final i in items) Text(i.name),
          ElevatedButton(
            onPressed: () => context.read<CartModel>().add(Item('pen')),
            child: const Text('Add'),
          ),
        ]);
      }
    }
    // After
    class CartView extends ConsumerWidget {
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        final items = ref.watch(cartProvider);
        return Column(children: [
          for (final i in items) Text(i.name),
          ElevatedButton(
            onPressed: () => ref.read(cartProvider.notifier).add(Item('pen')),
            child: const Text('Add'),
          ),
        ]);
      }
    }

    Se o widget já tinha estado próprio, use ConsumerStatefulWidget mais ConsumerState, onde ref está disponível como um campo. Remova a linha ChangeNotifierProvider(create: (_) => CartModel()) do MultiProvider. Verifique: a tela se comporta da mesma forma e a lista do MultiProvider ficou uma linha menor.

  5. Substitua ProxyProvider por composição com ref.watch. Este é o passo que apaga mais código. Um ProxyProvider que constrói B a partir de A vira um provider que simplesmente observa A.

    // Before: ProxyProvider wiring
    ProxyProvider<AuthModel, ApiClient>(
      update: (_, auth, __) => ApiClient(token: auth.token),
    ),
    // After: a provider that watches its dependency
    @riverpod
    ApiClient apiClient(ApiClientRef ref) {
      final token = ref.watch(authProvider.select((a) => a.token));
      return ApiClient(token: token);
    }

    ref.watch(...select(...)) é a substituição direta para o context.select do provider, e significa que apiClient só reconstrói quando token muda, não a cada atualização de AuthModel. Verifique: os widgets dependentes reconstroem quando o provider upstream muda.

  6. Migre FutureProvider e StreamProvider para seus equivalentes do Riverpod. Os nomes são os mesmos; só a fiação difere. Um FutureProvider do provider é lido com encanamento no estilo context.watch<AsyncSnapshot>; o do Riverpod retorna um AsyncValue<T> no qual você faz switch diretamente.

    // After: flutter_riverpod 3.3.1
    @riverpod
    Future<User> currentUser(CurrentUserRef ref) {
      return ref.watch(apiClientProvider).fetchUser();
    }
    
    // in a ConsumerWidget
    final userAsync = ref.watch(currentUserProvider);
    return userAsync.when(
      data: (user) => Text(user.name),
      loading: () => const CircularProgressIndicator(),
      error: (e, _) => Text('Failed: $e'),
    );

    Verifique: os estados de carregamento e erro renderizam sem flags manuais bool isLoading. Para mais sobre esse padrão, veja o post sobre AsyncValue vinculado abaixo.

  7. Apague a dependência provider. Assim que o MultiProvider estiver vazio, remova-o do main.dart, depois retire provider: ^6.1.5 do pubspec.yaml e execute flutter pub get. O compilador vai apontar quaisquer chamadas remanescentes de context.watch/context.read/Provider.of. Verifique: o projeto compila com zero referências a package:provider.

Verificação

Execute este checklist depois do último passo, não só no fim:

Plano de rollback

Esta migração é reversível por recurso justamente porque ambos os pacotes coexistem. Se uma tela migrada se comportar mal, reverta o commit daquela tela: coloque a linha ChangeNotifierProvider de volta no MultiProvider, restaure a classe ChangeNotifier e mude o widget de volta para StatelessWidget. Como você migrou das folhas para cima e um recurso por commit, nenhum rollback toca em mais de uma tela. Não apague o provider do pubspec.yaml (passo 7) até ter confiança, já que essa é a única porta de mão única na sequência.

Pegadinhas que encontramos

Mutação no lugar para de reconstruir. Essa é a surpresa número um. No provider, _items.add(x); notifyListeners() funciona porque você controla a notificação. Em um Notifier do Riverpod, o framework reconstrói só quando state recebe um valor que não é == ao antigo. state.add(x) muta a mesma lista, a referência não muda, e nada reconstrói. Sempre atribua uma nova coleção: state = [...state, x]. O mesmo vale para objetos de model, e é por isso que estado imutável (records, copyWith ou uma classe freezed) combina naturalmente com o Riverpod.

Os providers não fazem dispose quando o widget sai da árvore. Um ChangeNotifierProvider do provider sofre dispose quando sua subárvore é removida. Um provider do Riverpod, por padrão, mantém seu estado por toda a vida do ProviderScope. Se você contava com o controller de uma tela resetando quando navega para fora, agora você precisa de autoDispose (ou, com geração de código, esse é o padrão para providers anotados a menos que você chame ref.keepAlive()). Audite qualquer provider cujo comportamento antigo dependia do dispose baseado na árvore.

ref.read dentro de build() é uma armadilha. Ler outro provider com ref.read dentro de um Notifier.build() ou de um build de widget captura o valor uma vez e nunca atualiza. Use ref.watch para qualquer coisa que deva reagir, e reserve ref.read para handlers de eventos como callbacks de botão. O riverpod_lint aponta a maioria desses para você, e é por isso que vale a pena instalar a dependência de dev já no primeiro dia.

Consumer existe em ambos os pacotes. Se você importar os dois durante a migração, Consumer fica ambíguo. O Consumer do Riverpod recebe um builder (context, ref, child); o do provider recebe (context, value, child). Prefira converter o widget inteiro para ConsumerWidget em vez de soltar um Consumer do Riverpod dentro de um widget da era do provider, e você vai evitar o conflito de import por completo.

A migração recompensa quem é chato: uma folha, um commit, rode os testes, repita. Quando você apagar o provider do pubspec.yaml, a parte arriscada já aconteceu semanas atrás em passos pequenos e reversíveis.

Relacionados

Fontes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Voltar