Start Debugging

FutureBuilder/StreamBuilder vs AsyncValue do Riverpod no Flutter: qual usar?

Use FutureBuilder ou StreamBuilder para um widget assincrono autocontido e descartavel. Recorra ao AsyncValue do Riverpod quando o resultado for compartilhado, cacheado ou mutado. Aqui esta a decisao, os detalhes traicoeiros e codigo executavel para ambos. Testado no Flutter 3.44 e flutter_riverpod 3.3.1.

Se voce esta decidindo entre os FutureBuilder / StreamBuilder integrados do Flutter e o AsyncValue do Riverpod, a resposta curta e: mantenha os builders para um widget unico e autocontido que possui um resultado assincrono descartavel, e migre para o AsyncValue do Riverpod no momento em que esse resultado for compartilhado entre telas, cacheado, atualizado ou mutado. Os builders nao sao “a versao para iniciantes” da mesma coisa. Eles sao uma primitiva de UI que se inscreve em um objeto assincrono. AsyncValue e um modelo de estado que vive fora da arvore de widgets. Este guia foi testado no Flutter 3.44 (estavel, 2026-05-18), Dart 3.12 e flutter_riverpod 3.3.1 (a linha 3.0 saiu em 2025-09-10).

Eles resolvem problemas sobrepostos em camadas diferentes

FutureBuilder e StreamBuilder sao widgets. Voce passa para cada um deles um Future ou Stream, e ele entrega ao seu callback builder um AsyncSnapshot<T> que descreve o estado de conexao atual (waiting, active, done) mais os ultimos dados ou o erro. O widget se inscreve quando e inserido, cancela a inscricao quando e removido e se reinscreve se voce passar uma instancia diferente de Future/Stream. Esse e todo o contrato. Nao ha cache, nem compartilhamento, nem memoria do resultado depois que o widget sai da arvore.

O AsyncValue<T> do Riverpod nao e um widget de forma alguma. E uma uniao selada com tres subtipos (AsyncData, AsyncLoading, AsyncError) que um provider expoe como seu valor. O trabalho assincrono roda dentro de um provider que vive fora da arvore de widgets, entao qualquer widget pode le-lo, varios widgets podem ler a mesma instancia e o resultado sobrevive a reconstrucoes e a navegacao. Voce o renderiza com value.when(...) ou um switch do Dart 3, do mesmo jeito que voce renderiza um AsyncSnapshot, mas a fonte de verdade e um provider em vez de um campo do widget.

Entao a verdadeira pergunta nao e “qual renderiza tres estados melhor”. Ambos renderizam tres estados bem. A pergunta e onde o resultado assincrono deve viver e quantas coisas precisam ve-lo.

Matriz de recursos

AspectoFutureBuilder / StreamBuilder (Flutter 3.44)AsyncValue do Riverpod (flutter_riverpod 3.3.1)
O que eUm widget que se inscreve em um Future/StreamUm tipo de estado selado exposto por um provider
Onde o resultado viveNo widget, morre quando o widget e desmontadoEm um provider, fora da arvore, sobrevive a navegacao
Compartilhar entre telasNao, cada builder reexecuta seu proprio trabalhoSim, um provider lido por muitos widgets
Cache / dedupNenhum, voce memoiza o Future voce mesmoIntegrado, o provider cacheia ate invalidar
Disparo a cada reconstrucaoSim, se o Future for criado no buildNao, o build do provider roda uma vez ate invalidar
Loading + dados anterioresManual, o snapshot perde data enquanto aguardavalue.isLoading mantem value durante a atualizacao
Mutacoes / atualizacaoReatribuir o Future e setStateref.invalidate ou AsyncValue.guard em um notifier
Testar sem um widgetDificil, precisa de pumpWidgetFacil, le o provider em um ProviderContainer simples
DependenciasZero, vem com o SDKPacote flutter_riverpod
Linhas de boilerplate para algo unicoMinimasMais configuracao para uma unica chamada descartavel

Quando FutureBuilder ou StreamBuilder e a escolha certa

Recorra aos builders integrados quando o resultado assincrono pertence genuinamente a um widget e ninguem mais precisa dele.

Aqui esta a forma correta. O Future e criado uma vez no initState, nao no build, entao o widget nao refaz o fetch a cada reconstrucao do pai.

// Flutter 3.44, Dart 3.12
class UserCard extends StatefulWidget {
  const UserCard({super.key, required this.id});
  final String id;

  @override
  State<UserCard> createState() => _UserCardState();
}

class _UserCardState extends State<UserCard> {
  late Future<User> _user;

  @override
  void initState() {
    super.initState();
    _user = api.fetchUser(widget.id); // created ONCE, not in build
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<User>(
      future: _user,
      builder: (context, snapshot) {
        return switch (snapshot) {
          AsyncSnapshot(connectionState: ConnectionState.waiting) =>
            const CircularProgressIndicator(),
          AsyncSnapshot(hasError: true, :final error) =>
            Text('Failed: $error'),
          AsyncSnapshot(hasData: true, :final data?) =>
            Text(data.name),
          _ => const SizedBox.shrink(),
        };
      },
    );
  }
}

O bug mais comum com esse widget e criar o Future inline, como future: api.fetchUser(widget.id) diretamente no build. Cada reconstrucao entao aloca um novo Future, o FutureBuilder ve uma nova identidade e reinicia a partir do estado de loading. Esse modo de falha e comum o bastante para ter seu proprio artigo: veja por que o FutureBuilder recria seu Future a cada reconstrucao para a reproducao completa e todas as variantes que o disparam.

Quando o AsyncValue do Riverpod e a escolha certa

Migre para o AsyncValue quando o resultado assincrono deixa de ser um detalhe privado de um widget.

O mesmo render de tres estados, agora com origem em um provider:

// Flutter 3.44, Dart 3.12, flutter_riverpod 3.3.1
final userProvider = FutureProvider.family<User, String>((ref, id) {
  return api.fetchUser(id); // runs once, cached per id, shared everywhere
});

class UserCard extends ConsumerWidget {
  const UserCard({super.key, required this.id});
  final String id;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(userProvider(id));
    return switch (user) {
      AsyncData(:final value) => Text(value.name),
      AsyncError(:final error) => Text('Failed: $error'),
      _ => const CircularProgressIndicator(),
    };
  }
}

Dois widgets chamando ref.watch(userProvider('42')) compartilham um fetch e um resultado cacheado. Nao ha initState, nem campo armazenado, nem disciplina de “criar o Future uma vez” para lembrar, porque o provider ja roda seu build exatamente uma vez por argumento ate ser invalidado. Para o conjunto completo de estados, mutacoes com AsyncValue.guard e manter os dados anteriores na atualizacao, veja como mostrar estados de loading e erro com AsyncValue.

O comportamento de reconstrucao e refetch que de fato decide

O desempenho nao e o eixo aqui. Ambas as abordagens renderizam na mesma taxa de frames. O que difere e quantas vezes seu trabalho assincrono roda, e isso e uma questao de correcao e de custo, nao de velocidade bruta.

Coloque um contador dentro da chamada assincrona e observe o que acontece quando o widget ao redor se reconstroi (uma troca de tema, um teclado abrindo, um setState do pai):

Se seu trabalho assincrono e uma leitura local barata, nada disso importa e o builder ganha em simplicidade. Se e uma chamada de rede, uma consulta a banco de dados ou qualquer coisa com um custo ou um limite de taxa, o cache e toda a razao pela qual o AsyncValue existe, e reimplementar o mesmo comportamento na mao em torno do FutureBuilder reimplementa uma versao pior do cache de provider do Riverpod.

O detalhe traicoeiro que decide por voce

Algumas restricoes resolvem a decisao independentemente do gosto.

Voce ja esta usando Riverpod. Se o app tem providers, nao misture FutureBuilder em uma tela que os le. Ler os dados de um provider e depois envolver um segundo FutureBuilder em torno de outra chamada assincrona te da dois ciclos de vida nao relacionados em uma tela e dois lugares onde “loading” pode ser true. Exponha a segunda chamada como um provider tambem e renderize ambos com AsyncValue. A consistencia aqui previne a classe de bug onde metade da tela fica desatualizada.

O resultado precisa sobreviver ao widget. Qualquer coisa buscada no initState morre com o State. Se o usuario navega para frente e volta e voce nao quer um spinner novo e uma chamada de rede nova toda vez, voce precisa de um cache que viva acima do widget. Isso e um provider. FutureBuilder nao pode te dar persistencia entre rotas nao importa como voce o organize.

Voce toca ref depois de um await. Essa e uma armadilha especifica do Riverpod, nao um motivo para evita-lo: se voce faz await dentro de um notifier e depois le ref depois que o widget que o disparou nao existe mais, voce encontra Cannot use "ref" after the widget was disposed. A correcao e capturar o que voce precisa antes do await. Vale a pena saber antes de se comprometer, e isso e coberto em a correcao para usar ref apos o descarte.

Voce quer explicitamente zero dependencias. Um exemplo de pacote pub, um caso de reproducao ou uma politica de equipe contra bibliotecas de gerenciamento de estado forca os builders. Essa e uma restricao legitima, e os builders sao perfeitamente capazes para UI assincrona autocontida.

StreamBuilder tem um detalhe extra

Tudo acima se aplica ao trabalho com Future. Streams adicionam um ciclo de vida de inscricao, e isso inclina a decisao um pouco mais para o Riverpod em qualquer coisa nao trivial. StreamBuilder se reinscreve quando voce passa uma nova instancia de Stream e cancela a inscricao quando sai da arvore, mas nao faz multicast: dois StreamBuilder sobre o mesmo stream de inscricao unica vao lancar um erro, porque um Stream de inscricao unica permite apenas um listener. O StreamProvider do Riverpod fica na frente do stream, entao varios widgets leem um AsyncValue sem brigar pela inscricao, e o ultimo valor e cacheado para inscritos tardios. Se um stream e exibido em exatamente um lugar, StreamBuilder esta bem. Se mais de um widget precisa dele, StreamProvider elimina o problema do listener unico por completo.

A recomendacao, com todo o contexto por tras

Por padrao, use o AsyncValue do Riverpod para qualquer resultado assincrono que seja compartilhado, cacheado, atualizado ou mutado, que em um app real e a maioria deles. Voce obtem um fetch em vez de N, cache gratis entre navegacoes, um isLoading que preserva os dados anteriores na atualizacao e logica que voce pode testar sem um widget. Mantenha FutureBuilder e StreamBuilder para UI assincrona genuinamente autocontida e descartavel: um widget folha que carrega uma coisa, mostra e esquece ao ser desmontado, especialmente em apps que nao carregam nenhuma dependencia de gerenciamento de estado. Os builders nao sao rodinhas que voce supera. Eles sao a ferramenta certa quando o resultado assincrono tem uma audiencia de um, e a ferramenta errada no momento em que tem uma audiencia de dois. Escolha por propriedade, nao por familiaridade.

Se voce ainda esta escolhendo uma abordagem de gerenciamento de estado de forma mais ampla, os trade-offs entre pacotes estao em Provider vs Riverpod vs Bloc para gerenciamento de estado no Flutter em 2026. E se sua UI assincrona continua expondo falhas, como lidar com erros de rede com elegancia em um app Flutter cobre como transformar excecoes lancadas em um estado de erro limpo em ambos os modelos.

Fontes

Comments

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

< Voltar