Por que usar o Cloud Firestore? O conjunto de tecnologias do FlutterFire , composto pelo Flutter e pelo Firebase (e, especificamente pelo Cloud Firestore), desbloqueia uma velocidade de desenvolvimento sem precedentes durante a criação e o lançamento de aplicativos. Neste artigo, vamos explorar a integração robusta dessas duas tecnologias, com foco nos testes e no uso de padrões arquitetônicos limpos. No entanto, em vez de passar direto para a implementação final, vamos percorrer o caminho, passo a passo, para que a lógica por trás de cada etapa fique clara.
O que vamos criar Para demonstrar uma forma limpa de implementar o Cloud Firestore como o back-end do aplicativo, vamos criar uma versão modificada do aplicativo contador clássico do Flutter. A única diferença é que o carimbo de data/hora de cada clique é armazenado no Cloud Firestore, e a contagem exibida é derivada do número de carimbos de data/hora mantidos. Você usará o Provider e o ChangeNotifier para manter o código de dependências e gerenciamento de estados limpo, e atualizará o teste gerado para manter o código correto .
Antes de começar Este artigo supõe que você já assistiu e seguiu as etapas deste tutorial para integrar o aplicativo ao Firebase. Resumo:
Crie um novo projeto do Flutter e nomeie-o como firebasecounter. Crie um aplicativo Firebase no Console do Firebase . Vincule o aplicativo ao iOS e/ou Android, dependendo do ambiente para desenvolvedores e do público-alvo. Observação: se você configurar o aplicativo para funcionar em um cliente Android, terá que criar um arquivo debug.keystore antes de gerar o certificado SHA1. Após a geração dos aplicativos iOS ou Android no Firebase, você estará pronto para continuar. O restante do vídeo traz um ótimo conteúdo, do qual você provavelmente precisará nos projetos reais, mas isso não é necessário para este tutorial.
Caso você encontre obstáculos Se qualquer uma das etapas deste tutorial não funcionar para você, consulte este repositório público , que detalha as mudanças em diferentes commits. Ao longo do tutorial, você encontrará links para cada commit, quando apropriado. Sinta-se à vontade para usar essa função a fim de verificar se está progredindo da forma esperada!
Criação de um gerenciador de estados simples Para começar o processo de integração do aplicativo com o Cloud Firestore, você deve primeiro refatorar o código gerado, para que o StatefulWidget inicial se comunique com uma classe separada, em vez de com seus próprios atributos. Isso permite que você, eventualmente, instrua essa classe separada a usar o Cloud Firestore.
Ao lado do arquivo main.dart gerado automaticamente para o projeto, crie um novo arquivo chamado counter_manager.dart e copie para ele o seguinte código:
class CounterManager { /// Create a private integer to store the count. Make this private /// so that Widgets can't modify it directly, but instead must /// use official methods. int _count = 0; /// Publicly accessible reference to our state. int get count => _count; /// Publicly accessible state mutator. void increment() => _count++; } Com esse código inserido, adicione a seguinte linha ao início do arquivo firebasecounter/lib/main.dart:
import 'package:firebasecounter/counter_manager.dart'; Depois, altere o código de _MyHomePageState para:
class _MyHomePageState extends State<MyHomePage> { final manager = CounterManager(); void _incrementCounter() { setState(() => manager.increment()); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('You have pushed the button this many times:'), Text( '${manager.count}', style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), ); } } Depois de salvar a mudança do código, talvez pareça que o aplicativo falhou, pois ele mostrará uma tela de erro vermelha. Isso é porque você introduziu uma nova variável, manager, cuja oportunidade de inicialização já passou. Essa é uma experiência comum no Flutter, quando a forma como o estado é inicializado muda, e a questão pode ser facilmente resolvida com uma reinicialização a quente.
Após a reinicialização a quente, você deverá voltar para o ponto inicial: na contagem 0 e capaz de clicar no botão de ação flutuante o quanto quiser.
Esse é um bom momento para executar o único teste que o Flutter fornece em qualquer novo projeto. A definição dele pode ser encontrada em test/widget_test.dart, e ele pode ser executado com o seguinte comando:
$ flutter test Se o teste for bem-sucedido, você estará pronto para continuar!
Observação: caso você encontre obstáculos nesta seção, compare suas mudanças com este commit no repositório do tutorial. Manutenção dos carimbos de data/hora A descrição inicial do aplicativo mencionava a manutenção do carimbo de data/hora de cada clique. Até agora, você não adicionou nenhuma infraestrutura para atender a esse segundo requisito, portanto, crie um novo arquivo chamado app_state.dart e adicione a seguinte classe:
/// Container for the entirety of the app's state. An instance of /// this class should be able to inform what is rendered at any /// point in time. class AppState { /// Full click history. For super important auditing purposes. /// The count of clicks becomes this list's `length` attribute. final List<DateTime> clicks; /// Default generative constructor. Const-friendly, for optimal /// performance. const AppState([List<DateTime> clicks]) : clicks = clicks ?? const <DateTime>[]; /// Convenience helper. int get count => clicks.length; /// Copy method that returns a new instance of AppState instead /// of mutating the existing copy. AppState copyWith(DateTime latestClick) => AppState([ latestClick, ...clicks, ]); } A partir deste ponto, a função da classe AppState é representar o estado do que deve ser renderizado. A classe não contém nenhum método que possa sofrer mutações, apenas um único método copyWith, que será utilizado pelas outras classes.
Com os testes em mente, você pode começar a fazer mudanças no conceito do CounterManager. Ter uma única classe não será suficiente no longo prazo, porque o aplicativo eventualmente interage com o Cloud Firestore. Mas não é preciso criar registros reais todas as vezes que um teste é executado. Para isso, é necessária uma interface abstrata que defina como o aplicativo deve se comportar.
Abra o arquivo counter_manager.dart novamente e adicione o seguinte código no início do arquivo:
import 'package:firebasecounter/app_state.dart'; /// Interface that defines the functions required to manipulate /// the app state. /// /// Defined as an abstract class so that tests can operate on a /// version that does not communicate with Firebase. abstract class ICounterManager { /// Any `CounterManager` must have an instance of the state /// object. AppState state; /// Handler for when a new click must be stored. Does not require /// any parameters, because it only causes the timestamp to /// persist. void increment(); } A próxima etapa é atualizar o CounterManager para descender explicitamente do ICounterManager. Atualize a definição dele da seguinte forma:
class CounterManager implements ICounterManager { AppState state = AppState(); void increment() => state = state.copyWith(DateTime.now()); } Neste ponto, nosso código auxiliar parece muito bom, mas o main.dart ficou para trás. Não há referência ao ICounterManager no main.dart, quando, na verdade, essa é a única classe Manager que ele deve conhecer. No main.dart, faça as seguintes mudanças:
Adicione o import que falta ao início do arquivo main.dart: import 'package:firebasecounter/app_state.dart'; 2. Atualize _MyHomePageState da seguinte forma:
class _MyHomePageState extends State<MyHomePage> { final ICounterManager manager; _MyHomePageState({@required this.manager}); void _incrementCounter() => setState(() => manager.increment()); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('You have pushed the button this many times:'), Text( '${manager.state.count}', style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: Icon(Icons.add), ), ); } } Essa mudança deve remover todas as linhas vermelhas sublinhadas do ambiente de desenvolvimento integrado de _MyHomePageState. Mas, agora, MyHomePage está reclamando, porque seu método createState() não fornece todos os argumentos necessários para _MyHomePageState. Você pode fazer com que MyHomePage solicite essa variável e transmita o objeto para sua classe baseada em estado, mas isso geraria longas cadeias de widgets solicitando e transmitindo objetos que não são realmente importantes para eles, simplesmente porque um widget descendente está exigindo e um widget ancestral está fornecendo. Claramente, isso requer uma estratégia melhor.
É aí que entra o Provider .
Como usar o Provider para acessar o estado do aplicativo O Provider é uma biblioteca que otimiza o uso do padrão InheritedWidget do Flutter. Ele permite que um widget no topo da árvore de widgets seja acessível diretamente por todos os seus descendentes. Isso pode parecer similar a uma variável global, mas a alternativa é transmitir os modelos de dados por cada um dos widgets intermediários, quando muitos deles não terão nenhum interesse intrínseco por esses modelos. Esse antipadrão no estilo de “brigada de baldes de variáveis” ofusca a separação dos interesses do aplicativo e pode tornar os layouts de refatoração desnecessariamente enfadonhos. O InheritedWidget e o Provider ignoram esses problemas permitindo que os widgets em qualquer ponto da árvore obtenham diretamente os modelos de dados de que necessitam.
Para adicionar o Provider ao aplicativo, abra o pubspec.yaml e adicione-o abaixo da seção dependencies:
dependencies: flutter: sdk: flutter # Add this provider: ^4.3.2+2 Depois de adicionar essa linha ao arquivo pubspec.yaml, execute o seguinte para fazer o download do Provider para a máquina:
$ flutter pub get Ao lado do main.dart, crie um novo arquivo chamado dependencies.dart e copie para ele o seguinte código:
import 'package:firebasecounter/counter_manager.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class DependenciesProvider extends StatelessWidget { final Widget child; DependenciesProvider({@required this.child}); @override Widget build(BuildContext context) { return MultiProvider( providers: [ Provider<ICounterManager>(create: (context) => CounterManager()), ], child: child, ); } } Algumas observações sobre o DependenciesProvider:
Ele usa o MultiProvider, apesar de ter apenas uma entrada na lista. Isso, tecnicamente, poderia ser recolhido em um único widget Provider, mas um aplicativo real provavelmente conterá muitos desses serviços, por isso geralmente é melhor começar com o MultiProvider, de qualquer forma. Ele requer um widget filho, que segue a convenção do Flutter para a composição de widgets e permite inserir esse auxiliar perto to topo da árvore de widgets, disponibilizando a instância do ICounterManager para todo o aplicativo. Em seguida, disponibilize o novo DependenciesProvider para todo o aplicativo. Uma forma simples de fazer isso é agrupar todo o widget MaterialApp com ele. Abra o main.dart e atualize o método principal da seguinte maneira:
void main() { runApp( DependenciesProvider(child: MyApp()), ); } Também é preciso importar o dependencies.dart para o main.dart:
import 'package:firebasecounter/dependencies.dart'; Como usar um widget Consumer Já vimos o widget MultiProvider em ação (que é, na verdade, apenas uma forma mais bacana de declarar uma série de widgets Provider individuais). O próximo passo é acessar o objeto ICounterManager usando o widget Consumer .
Injeção de dependências Se você já escreveu um aplicativo Flutter usando o Cloud Firestore, provavelmente descobriu que o Firestore pode tornar mais complicado escrever bons testes de unidade. Afinal, como evitar gerar registros reais no banco de dados quando uma integração do Firestore é conectada diretamente à árvore de widgets?
Se você já teve essa experiência, já sabe das limitações para a incorporação de dependências diretamente no código da IU, que são os widgets, no caso do Flutter. Esse é o poder da injeção de dependências: se os widgets aceitarem classes auxiliares que facilitam sua interação com as dependências (como o Firebase, o sistema de arquivos do dispositivo ou mesmo as solicitações de rede), você pode fornecer itens fictícios ou falsos em vez das classes reais durante os testes. Com isso, é possível testar se os widgets se comportam como deveriam sem precisar esperar por solicitações lentas de rede, encher o sistema de arquivos ou incorrer em taxas de faturamento do Firebase.
Para conseguir isso, você precisa refatorar o aplicativo para que haja um ponto limpo no qual os testes possam injetar itens falsos que imitem o comportamento real do Cloud Firestore. Felizmente, o widget Consumer é perfeito para o trabalho.
Abra o main.dart e substitua o widget MyApp pelo seguinte código:
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: Consumer<ICounterManager>( builder: (context, manager, _child) => MyHomePage( manager: manager, title: 'Flutter Demo Home Page', ), ), ); } } Além disso, importe o Provider para o início do main.dart:
import 'package:provider/provider.dart'; O agrupamento de MyHomePage em um widget Consumer permite chegar arbitrariamente alto na árvore de widgets para acessar os recursos desejados e injetá-los nos widgets que precisam deles. Esse pode parecer um trabalho desnecessário neste tutorial, porque você só precisa voltar uma camada até o MyApp(), mas isso pode abranger dezenas widgets em aplicativos reais.
Em seguida, no mesmo arquivo, faça esta edição em MyHomePage:
Observação: não se preocupe se vir uma tela vermelha depois de salvar essa mudança. Mais edições são necessárias para concluir a refatoração. class MyHomePage extends StatefulWidget { final ICounterManager manager; MyHomePage({@required this.manager, Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } Essa mudança simples de construtor permite que o código aceite a variável transmitida no snippet anterior.
Por fim, conclua a refatoração fazendo esta edição em _MyHomePageState:
class _MyHomePageState extends State<MyHomePage> { // No longer expect to receive a `ICounterManager object` @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('You have pushed the button this many times:'), Text( // Reference `widget.manager` instead of // `manager` directly '${widget.manager.state.count}', style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( // Reference `widget.manager` instead of `manager` directly onPressed: () => setState(() => widget.manager.increment()), tooltip: 'Increment', child: Icon(Icons.add), ), ); } } Observação: provavelmente será necessária uma reinicialização a quente para corrigir o aplicativo. Como você deve se lembrar, todos os objetos State contêm uma referência para seus wrappers StatefulWidget no atributo widget. Portanto, o objeto _MyHomePageState pode acessar esse novo atributo manager mudando seu código de manager para widget.manager.
Pronto! Você injetou dependências nos widgets que precisam delas, em vez de fixar implementações de produção no código.
Teste o aplicativo Se você executar o teste do Flutter agora mesmo, verá que o pacote de teste não é mais bem-sucedido. Quando você inspecionar o widget_test.dart, o motivo deve ser claro: a função de teste instancia o MyApp(), mas não o agrupa com o DependenciesProvider como você fez no código real. Por isso, o widget Consumer adicionado dentro do MyApp não consegue encontrar um Provider satisfatório em seus widgets ancestrais.
É nesse ponto que a injeção de dependências começa a demonstrar seus benefícios. Em vez de imitar o código de produção nos testes (agrupando o MyApp com o DependenciesProvider), altere o teste para inicializar o MyHomePage. Atualize o widget_test.dart da seguinte maneira:
import 'package:firebasecounter/counter_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:firebasecounter/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget( MaterialApp( home: MyHomePage( manager: CounterManager(), title: 'Test Widget', ), ), ); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); expect(find.text('1'), findsNothing); // Tap the '+' icon and trigger a frame. await tester.tap(find.byIcon(Icons.add)); await tester.pump(); // Verify that our counter has incremented. expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); }); } Com o uso direto de uma instância de MyHomePage (juntamente com o agrupamento do MaterialApp para fornecer objetos BuildContext válidos), você configurou uma integração com teste de unidade com o Cloud Firestore!
Observação: caso você encontre obstáculos nesta seção, compare suas mudanças com este commit no repositório do tutorial. Implementação do Cloud Firestore Até agora, você passou por um monte de códigos e introduziu várias classes auxiliares, mas não mudou nada no funcionamento do aplicativo. A boa notícia é que tudo está preparado para começar a escrever o código que reconhece o Cloud Firestore. Para começar, abra o pubspec.yaml e adicione estas duas linhas:
dependencies: # Add this cloud_firestore: ^0.14.1 # Add this firebase_core: ^0.5.0 flutter: sdk: flutter provider: ^4.3.2+2 Como sempre, quando se aplica mudanças ao pubspec.yaml (a menos que o ambiente de desenvolvimento integrado faça isso por você), é preciso executar o seguinte comando para fazer o download e vincular as novas bibliotecas:
$ flutter pub get Observação: se você ainda não tiver criado o banco de dados, acesse o Console do Firebase do projeto, clique na guia Cloud Firestore e clique no botão Create Database . A espera pelo Firebase A primeira etapa para usar com êxito o Cloud Firestore é inicializar o Firebase e, o mais importante, não tentar usar nenhum recurso do Firebase até que essa tarefa tenha sido realizada com êxito . Por sorte, você pode conter essa lógica com um StatefulWidget em vez de espalhar isso por todo o código.
Crie um novo arquivo em firebasecounter/lib/firebase_waiter.dart e adicione o seguinte código:
import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; class FirebaseWaiter extends StatefulWidget { final Widget Function(BuildContext) builder; final Widget waitingChild; const FirebaseWaiter({ @required this.builder, this.waitingChild, Key key, }) : super(key: key); @override _FirebaseWaiterState createState() => _FirebaseWaiterState(); } class _FirebaseWaiterState extends State<FirebaseWaiter> { Future<FirebaseApp> firebaseReady; @override void initState() { super.initState(); firebaseReady = Firebase.initializeApp(); } @override Widget build(BuildContext context) => FutureBuilder<FirebaseApp>( future: firebaseReady, builder: (context, snapshot) => // snapshot.connectionState == ConnectionState.done ? widget.builder(context) : widget.waitingChild, ); } Essa classe usa o padrão do Flutter de aproveitar determinados widgets para lidar completamente com uma dependência ou um problema específico dentro do aplicativo. Para usar esse widget FirebaseWaiter, volte ao main.dart e aplique a seguinte mudança em MyApp:
// Add this import at the top import 'package:firebasecounter/firebase_waiter.dart'; // Replace `MyApp` with this class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: FirebaseWaiter( builder: (context) => Consumer<ICounterManager>( builder: (context, manager, _child) => MyHomePage( manager: manager, title: 'Flutter Demo Home Page', ), ), // This is a great place to put your splash page! waitingChild: Scaffold( body: const Center(child: CircularProgressIndicator()), ), ), ); } } Agora, o aplicativo pode aguardar pela inicialização do Firebase, mas pode ignorar esse processo durante os testes simplesmente não utilizando o FirebaseWaiter.
Observação: as mudanças acima podem fazer com que o Flutter reclame pela ausência de plug-ins do Firebase. Se isso acontecer, encerre completamente o aplicativo e reinicie a depuração, para que o Flutter possa instalar todas as dependências específicas de plataforma. Como obter dados do Cloud Firestore Primeiro, importe o Cloud Firestore adicionando a seguinte linha ao início do counter_manager.dart:
import 'package:cloud_firestore/cloud_firestore.dart'; Em seguida, também no counter_manager.dart, adicione a seguinte classe:
class FirestoreCounterManager implements ICounterManager { AppState state; final FirebaseFirestore _firestore; FirestoreCounterManager() : _firestore = FirebaseFirestore.instance, state = const AppState() { _watchCollection(); } void _watchCollection() { // Part 1 _firestore .collection('clicks') .snapshots() // Part 2 .listen((QuerySnapshot snapshot) { // Part 3 if (snapshot.docs.isEmpty) return; // Part 4 final _clicks = snapshot.docs .map<DateTime>((doc) { final timestamp = doc.data()['timestamp']; return (timestamp != null) ? (timestamp as Timestamp).toDate() : null; }) // Part 5 .where((val) => val != null) // Part 6 .toList(); // Part 7 state = AppState(_clicks); }); } @override void increment() { _firestore.collection('clicks').add({ 'timestamp': FieldValue.serverTimestamp(), }); } } Observação: essa classe é quase correta, mas cria um bug que será explorado mais adiante. Se você adicionar esse código ao aplicativo e executá-lo agora, verá que o comportamento não é o que você desejava. Continue lendo para ver uma explicação completa do que está acontecendo. Há muitas coisas acontecendo aqui, então, vamos falar sobre elas.
Primeiro, o FirestoreCounterManager implementa a interface do ICounterManager, por isso ele é um candidato qualificado para uso em widgets de produção. (Eventualmente, ele receberá o byDependenciesProvider.) O FirestoreCounterManager também mantém uma instância do FirebaseFirestore, que é a conexão ao vivo com o banco de dados de produção. Além disso, o FirestoreCounterManager chama _watchCollection() durante sua inicialização para configurar uma conexão com os dados específicos nos quais você está interessado, e é neste ponto que as coisas ficam interessantes.
O método _watchCollection() é muito útil e merece um exame mais cuidadoso.
Na Parte 1, _watchCollection() chama _firestore.collection('clicks').snapshots(), que retorna um stream de atualizações sempre que há uma mudança nos dados de hora da coleção.
Na Parte 2, _watchCollection() registra imediatamente um listener para esse stream utilizando .listen(). A callback transmitida para listen() recebe um novo objeto QuerySnapshot em cada mudança dos dados. Esse objeto de atualização é chamado de instantâneo porque reflete o estado correto do banco de dados em um determinado momento, mas pode ser substituído por um novo instantâneo a qualquer ponto.
Na Parte 3, a callback entra em curto-circuito se a coleção estiver vazia.
Na Parte 4, a callback faz o loop para os documentos do instantâneo e retorna uma lista de valores nulos e DateTime combinados.
Na Parte 5, a callback descarta todos os valores nulos. Esses valores vêm do bug que será corrigido em breve, mas esse tipo de codificação defensiva é sempre uma boa ideia quando estamos lidando com dados do Cloud Firestore.
Na Parte 6, a callback lida com o fato de que map() retorna um iterador, não uma lista. A chamada de .toList() em um iterador o força a processar toda a coleção, que é aquilo que desejamos fazer.
E, por fim, na Parte 7, a callback atualiza o objeto state.
Para usar a nova classe, abra o dependencies.dart e substitua o conteúdo do arquivo pelo seguinte:
import 'package:firebasecounter/counter_manager.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class DependenciesProvider extends StatelessWidget { final Widget child; DependenciesProvider({@required this.child}); @override Widget build(BuildContext context) { return MultiProvider( providers: [ Provider<ICounterManager>( create: (context) => FirestoreCounterManager()), ], child: child, ); } }Diagnóstico do bug Se você executar esse código como está, poderá quase ver o comportamento desejado. Tudo parece correto, com a exceção de que a tela sempre é renderizada um clique atrás da realidade. O que está acontecendo?
O problema vem de uma incompatibilidade entre a implementação inicial do contador e a implementação atual, baseada em stream. O gerenciador onPressed do FloatingActionButton tem a seguinte aparência:
floatingActionButton: FloatingActionButton( onPressed: () => setState(() => widget.manager.increment()), ... ) Esse gerenciador chama increment() e invoca imediatamente setState(), que instrui o Flutter a renderizar novamente.
Isso funcionava bem durante a atualização síncrona de estados mantidos na memória do dispositivo. No entanto, a nova implementação baseada em stream inicia uma série etapas assíncronas. Isso significa que, da forma como está, o código chama setState() imediatamente, depois, apenas em um ponto desconhecido no futuro, o objeto manager atualiza seu atributo de estado. Em resumo, a chamada a setState() no gerenciador onPressed está ocorrendo cedo demais. E, o que é pior, uma vez que toda essa atividade ocorre dentro de uma callback, dentro do FirestoreCounterManager, sobre o qual os widgets não sabem nada, não há um Future pelo qual os widgets possam aguardar para solucionar o problema.
É quase como se o objeto manager precisasse ser capaz de dizer aos widgets quando redesenhar. 🤔
É aí que entra o ChangeNotifier .
Observação: caso você encontre obstáculos nesta seção, compare suas mudanças com este commit no repositório público. Elas incluem mudanças do Xcode e do build.gradle resultantes da adição do Firebase, mas você provavelmente pode se concentrar mais nas mudanças dos arquivos Dart. Como usar ChangeNotifier para renderizar novamente a árvore de widgets ChangeNotifier é uma classe que faz exatamente o que seu nome sugere: notifica os widgets quando ocorrem mudanças que exigem uma nova renderização.
A primeira etapa do processo é atualizar a interface do ICounterManager para estender ChangeNotifier. Para isso, abra firebasecounter/lib/counter_manager.dart e faça as seguintes mudanças na declaração ICounterManager:
// Add `extends ChangeNotifier` to your declaration abstract class ICounterManager extends ChangeNotifier { // Everything inside the class is the same. } Se você ainda não tiver importado o flutter/material.dart, abra firebasecounter/lib/counter_manager.dart e adicione-o ao início:
import 'package:flutter/material.dart'; Agora, você está pronto para atualizar as definições de CounterManager e FirestoreCounterManager. Para o CounterManager, substitua seu código pela seguinte implementação:
class CounterManager extends ChangeNotifier implements ICounterManager { AppState state = AppState(); /// Copies the state object with the timestamp of the most /// recent click and tells the stream to update. void increment() { state = state.copyWith(DateTime.now()); // Adding this line is how `ChangeNotifier` tells widgets to // re-render themselves. notifyListeners(); } } E, para o FirebaseCounterManager, aplique as seguintes mudanças:
Edite sua assinatura de acordo com o seguinte: class FirestoreCounterManager extends ChangeNotifier implements ICounterManager { ... } 2. Adicione a mesma linha notifyListeners(); ao final de _watchCollection(), da seguinte maneira:
void _watchCollection() { _firestore .collection('clicks') .snapshots() .listen((QuerySnapshot snapshot) { // Generation of `_clicks` omitted for clarity, but do not // change that code. state = AppState(_clicks); // The only change necessary is to add this line! notifyListeners(); }); } Agora, você já configurou metade das mudanças necessárias para que as classes do ICounterManager instruam os widgets a renderizar novamente sempre que os dados mudarem. As classes Manager estão instruindo os widgets a renderizar novamente, mas, se você executar o aplicativo agora, verá que os widgets não estão ouvindo.
Para corrigir isso, abra o dependencies.dart e substitua a implementação de DependenciesProvider pelo seguinte:
class DependenciesProvider extends StatelessWidget { final Widget child; DependenciesProvider({@required this.child}); @override Widget build(BuildContext context) { return MultiProvider( providers: [ // `Provider` has been replaced by ChangeNotifierProvider ChangeNotifierProvider<ICounterManager>( create: (context) => FirestoreCounterManager(), ), ], child: child, ); } } Como última mudança, remova setState de _MyHomePageState para ignorar qualquer nova renderização desnecessária. Atualize FloatingActionButton desta forma:
floatingActionButton: FloatingActionButton( // Remove setState()! onPressed: widget.manager.increment, tooltip: 'Increment', child: Icon(Icons.add), ), Pronto! O ChangeNotifierProvider garante que os widgets sejam “listeners”, para que, quando notifyListeners() for chamado por uma classe do ICounterManager, os widgets recebam a mensagem para fazer a renderização novamente.
Neste ponto, você deve ser capaz de reiniciar a quente o aplicativo e ver tudo funcionando corretamente.
Observação: caso você encontre obstáculos nesta seção, compare suas mudanças com este commit no repositório público.
Correção dos testes Embora a última rodada de mudanças tenha implementado com êxito a funcionalidade desejada, infelizmente, ela também prejudicou os testes. Em seguida, você aplicará alguns ajustes adicionais para que tudo funcione novamente, e assim, terá terminado.
No widget_test.dart, o código transmite uma instância do CounterManager diretamente sem um ChangeNotifierListener auxiliar. A forma como isso era tratado na árvore de widgets era agrupando tudo no DependenciesProvider, mas essa classe tem conhecimento do Firestore, e o objetivo disto tudo é manter o Firestore de fora dos testes.
Uma solução é criar o TestDependenciesProvider, que pode conter as versões de teste de todas as dependências. Abra firebasecounter/lib/dependencies.dart e adicione a seguinte classe:
class TestDependenciesProvider extends StatelessWidget { final Widget child; TestDependenciesProvider({@required this.child}); @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider<ICounterManager>( create: (context) => CounterManager(), ), ], child: child, ); } } Essa classe é quase idêntica ao DependenciesProvider, mas o TestDependenciesProvider fornece uma instância de CounterManager() em vez de FirestoreCounterManager().
Agora, em test/widget_test.dart, atualize a inicialização do widget de teste para:
await tester.pumpWidget( TestDependenciesProvider( child: MaterialApp( home: Consumer<ICounterManager>( builder: (context, manager, _child) => MyHomePage( manager: manager, title: 'Flutter Test Home Page', ), ), ), ), ); Se ainda não tiver feito isso, adicione estes dois imports perto do início do test/widget_test.dart:
import 'package:firebasecounter/dependencies.dart'; import 'package:provider/provider.dart'; Execute os teste novamente e... pronto!
Observação: caso você encontre obstáculos nesta seção, compare suas mudanças com este commit no repositório público.
Conclusão Neste artigo, você remodelou o aplicativo contador clássico do Flutter para que ele mantenha toda a atividade no Cloud Firestore. Também evitou misturar a lógica de negócios com os widgets, o que facilita o teste do aplicativo.
As técnicas de gerenciamento de estados discutidas aqui são viáveis para muitos aplicativos, mas não são as únicas formas de se fazer isso, nem as melhores. A comunidade do Flutter está repleta de excelentes soluções de gerenciamento de estados que valem uma investigação. Estas são algumas a serem consideradas:
O Flutter Bloc é particularmente útil para quem tem experiência com o Redux . Esta série de tutoriais de vídeo do Flutter Firebase e DDD , da Reso Coder , fornece instruções passo a passo de todo o processo de uso do Flutter Bloc, do Cloud Firestore e de várias outras excelentes bibliotecas. Esta porta mais direta do Redux também é muito utilizada. O item mais recente da lista: o criador do Provider lançou um novo pacote, o Riverpod , que é o sucessor do Provider. Para obter mais informações sobre gerenciamento de estados, consulte os documentos sobre gerenciamento de estados no flutter.dev.
Seja qual for a solução de gerenciamento de estados escolhida, boa sorte e boa codificação!
68 comentários :
If you wish to play some good Sydney Pokies https://sydneypokies.com/ then follow my link!
spalling concrete
types of doors
gutter leak
fiberglass water tank
monolithic slab definition foundation
modified proctor test
terracotta cladding
foundation failure
metal roof insulation
vibro stone columns
Prestige Park Grove want to say how grateful I am that you shared your expertise with us because it has really improved our conversation.
I appreciate your thoughtful analysis – it's akin to the meticulous planning behind Provident Ecopolitan's design.
Whoever puts in the time and effort to write a stunning article like this has done an outstanding job. I truly appreciate all of the students' efforts.
You must teach the Prestige Pine Forest assistance. They might only require a little help.
I have faced many complicated problems in restructuring my project website
Prestige RainTree Park
only this page has helped me out to clear all my doubts & errors , Thanks
daftar harga seragam sekolah ada di cak cloth
terima kasih info yang sangat menarik lookerstudio
Very good info sir. Thank you for article technet
good job for create nice article for all, amazing msdn
Good job vimeo
thanks information qq
I'm looking to incorporate Cloud Firestore into my Commercial Cleaning WordPress website. What's the process for this integration?
mtcgame bigo live : This term refers to the services or products offered by MTCGame for the Bigo Live application. Bigo Live is a live streaming and social interaction app, and MTCGame may offer virtual currency or credits for in-app purchases on Bigo Live.
Prestige Southern Star residential township project is being built by the Prestige Group.
Thanks for sharing this quality information.
Check and visit this site to know more about Excel Corporate Training in Chennai and Power bi Corporate Training in Chennai
Experience the epitome of modern living at Prestige Southern Star, a Prestige Group masterpiece on Begur Road, presenting a diverse range of upscale residences.
Unleash your entrepreneurial spirit and launch your dream business without the inventory burden. Imagine tapping into a global market while sipping Karak under the Burj Khalifa’s shadow. No hefty investments; focus on branding and marketing your unique product selection catered to Dubai’s diverse consumers. You hold the key from the latest gadget to the most niche finds.
Partner with reliable suppliers, craft your online haven and master the art of digital attraction. Dubai’s booming e-commerce scene, tech-savvy population, and supportive community await. Embrace the future of retail by starting dropshipping in UAE today!
By leveraging Flutter's built-in testing features along with Cloud Firestore's support for unit and integration testing, developers can ensure the reliability and performance of their applications through comprehensive test suites. This synergy between Flutter and Cloud Firestore empowers developers to create feature-rich mobile apps that are not only visually appealing but also robust, scalable, and thoroughly tested, delivering a superior user experience to their audience.
Divorce Lawyers Fairfax VA
gud job developer
Century Regalia, which offers a unique range of spacious 3 and 4 BHK flats, aims to revolutionize the art of premium living with its beautifully planned residences and world-class amenities. Each apartment is a work of modern architecture, meticulously designed to optimize space and natural light, creating an air of elegance and sophistication.
Century Regalia
You consistently provide interesting and educational content. Every time you post, I eagerly await them. Continue the fantastic work. Prestige Southern Star
This article is a masterclass in engaging writing, seamlessly blending rich detail with profound insight to create a truly compelling read.KAMPUS88
Your article is a brilliant blend of insightful analysis and captivating narrative, making it BORNEO338
Your article is a masterful blend of thorough research and eloquent writing, making it both informative and engaging for readers. GASPOL77
Your article is a brilliant masterpiece that effortlessly blends insightful analysis with captivating prose. I found it incredibly enlightening. Join me in exploring more great content at my site too! Situs Slot Terbaru LASKAR268
BORNEO338 Rekomendasi situs slot terpercaya.
Situs slot online tergacor hari ini BORNEO338
nice blog
Borneo338
Situs slot mudah scatter AMPERASLOT
This article is exceptionally well-researched and informative. LINK GACOR GASPOL77
Your article is incredibly well-researched and thorough. LINK GACOR AMPERASLOT
The article presents a compelling argument supported by solid evidence. LINK SLOT TERPERCAYA
Website SLot Online Terbaik
Borneo338
This article provides a comprehensive analysis of the subject matter. LINK SLOT TERGACOR
This article is a masterpiece of insight and clarity. BORNEO338
You have a remarkable talent for writing. BORNEO338
Your article is incredibly insightful. AMPERASLOT
The best blog for slot broo
Borneo338
https://calisafauba.com/
https://getabusinessinsurance.com/
https://jokyodesigns.com/
https://luxuryvacationpack.com/
https://mamashealinghands.com/
https://openrealestatearea.com/
Choose a job that you like and you won't have to work all your life. LASKAR268
Your article is incredibly insightful. KAMPUS88
This article is incredibly well-researched and thorough. LASKAR268 GACOR
This piece is a masterpiece. LASKAR 268
Thank you for creating such an interesting and useful article. I hope it helps many people. Don't forget to check out my website for more exciting information! LASKAR268
I appreciate your effort in writing this informative article. It will surely benefit many. Please also visit my website for more intriguing insights! LASKAR268
Situs slot terbaik KAMPUS88
You are incredibly talented! Don't forget to visit my site for interesting info, many promotions, and other benefits. laskar268
This article is a masterpiece of insightful analysis and engaging writingGASPOL77
This article is an outstanding piece of writing. KAMPUS88
good blog my brother, don't forget to also visit this website
LASKAR268
This article is incredibly well-researched. GASPOL77
This article is incredibly insightful! Thank you for sharing your knowledge. Check out my website for more interesting news and information. jawara338
Your writing is absolutely brilliant and engaging! Don't forget to check out my website for more interesting information. gaspol77
This article truly opened my eyes. JAWARA338
nice blog
slot gacor hari ini
This article is exceptionally well-written and insightful. LASKAR 268
This article is exceptionally well-researched and insightful. WIJAYA 99
I appreciate your advice. SEO Hooks Team Values Your Contribution.
This article is truly enlightening. AMPERA SLOT
Your article is truly insightful and well-written. LASKAR 268
Your article is incredibly insightful and well-researched. GASPOL 77
This article is incredibly informative. AMPERASLOT
Your article is exceptionally well-written. BORNEO338
Dürbün, optik lensler aracılığı ile uzak mesafedeki nesneleri yakından görmemize yardımcı olan optik bir cihazdır. İki merceğin bir araya getirilmesiyle oluşan bu cihazlar, uzaktaki görüntüleri büyüterek net bir görüş alanı sunar. Farklı büyütme oranları ve objektif çapları ile kullanım alanlarında değişme yaşanabilir.
Your article is truly enlightening. KAMPUS 88
Postar um comentário