Conheça o Firebase para Flutter

Mantenha tudo organizado com as coleções Salve e categorize o conteúdo com base nas suas preferências.

1. Antes de começar

Neste codelab, você aprenderá algumas noções básicas do Firebase para criar aplicativos móveis Flutter para Android e iOS.

Pré-requisitos

Este codelab pressupõe que você esteja familiarizado com o Flutter e tenha instalado o Flutter SDK e um editor .

O que você vai criar

Neste codelab, você criará um RSVP de evento e um aplicativo de bate-papo do livro de visitas no Android, iOS, Web e macOS usando o Flutter. Você autenticará os usuários com o Firebase Authentication e sincronizará os dados usando o Cloud Firestore.

O que você precisará

Você pode executar este codelab usando qualquer um dos seguintes dispositivos:

Além dos itens acima, você também precisará de:

  • Um navegador de sua escolha, como o Chrome.
  • Um IDE ou editor de texto de sua escolha, como Android Studio ou VS Code configurado com os plug-ins Dart e Flutter.
  • A versão stable mais recente do Flutter (ou beta , se você gosta de viver no limite).
  • Uma conta do Google, como uma conta do Gmail, para criar e gerenciar seu projeto do Firebase.
  • A ferramenta de linha de comando do firebase , conectada à sua conta do Gmail.
  • O código de amostra do codelab. Veja a próxima etapa para saber como obter o código.

2. Obtenha o código de amostra

Vamos começar baixando a versão inicial do nosso projeto do GitHub.

Clone o repositório do GitHub na linha de comando:

git clone https://github.com/flutter/codelabs.git flutter-codelabs

Como alternativa, se você tiver a ferramenta cli do GitHub instalada:

gh repo clone flutter/codelabs flutter-codelabs

O código de amostra deve ser clonado no diretório flutter-codelabs , que contém o código para uma coleção de codelabs. O código deste codelab está em flutter-codelabs/firebase-get-to-know-flutter .

A estrutura de diretórios em flutter-codelabs/firebase-get-to-know-flutter é uma série de instantâneos de onde você deve estar no final de cada etapa nomeada. Esta é a Etapa 2, portanto, localizar os arquivos correspondentes é tão fácil quanto:

cd flutter-codelabs/firebase-get-to-know-flutter/step_02

Se você quiser avançar ou ver como algo deve ficar após uma etapa, procure no diretório com o nome da etapa em que você está interessado.

Importar o aplicativo inicial

Abra ou importe o flutter-codelabs/firebase-get-to-know-flutter/step_02 em seu IDE preferido. Este diretório contém o código inicial para o codelab, que consiste em um aplicativo de encontro Flutter ainda não funcional.

Localize os arquivos para trabalhar

O código neste aplicativo está espalhado por vários diretórios. Essa divisão de funcionalidade foi projetada para facilitar o trabalho, agrupando o código por funcionalidade.

Localize os seguintes arquivos no projeto:

  • lib/main.dart : Este arquivo contém o ponto de entrada principal e o widget do aplicativo.
  • lib/src/widgets.dart : Este arquivo contém vários widgets para ajudar a padronizar o estilo do aplicativo. Estes são usados ​​para compor a tela do app inicial.
  • lib/src/authentication.dart : este arquivo contém uma implementação parcial do FirebaseUI Auth com um conjunto de widgets para criar uma experiência de login do usuário para autenticação baseada em e-mail do Firebase. Esses widgets para o fluxo de autenticação ainda não são usados ​​no aplicativo inicial, mas você os conectará em breve.

Você adicionará arquivos adicionais conforme necessário para construir o restante do aplicativo.

Revisando o arquivo lib/main.dart

Este aplicativo aproveita o pacote google_fonts para nos permitir tornar Roboto a fonte padrão em todo o aplicativo. Um exercício para o leitor motivado é explorar fonts.google.com e usar as fontes que você encontra lá em diferentes partes do aplicativo.

Você está utilizando os widgets auxiliares de lib/src/widgets.dart na forma de Header , Paragraph e IconAndDetail . Esses widgets reduzem a confusão no layout da página descrito na HomePage , eliminando o código duplicado. Isso tem o benefício adicional de permitir uma aparência consistente.

Aqui está a aparência do seu aplicativo no Android, iOS, Web e macOS:

Visualização do aplicativo

3. Crie e configure um projeto do Firebase

Exibir as informações do evento é ótimo para seus convidados, mas apenas mostrar os eventos não é muito útil para ninguém. Vamos adicionar algumas funcionalidades dinâmicas a este aplicativo. Para isso, você precisará conectar o Firebase ao seu aplicativo. Para começar a usar o Firebase, você precisará criar e configurar um projeto do Firebase.

Criar um projeto do Firebase

  1. Faça login no Firebase .
  2. No console do Firebase, clique em Add Project (ou Create a project ) e nomeie seu projeto do Firebase Firebase-Flutter-Codelab .

4395e4e67c08043a.png

  1. Clique nas opções de criação do projeto. Aceite os termos do Firebase, se solicitado. Ignore a configuração do Google Analytics, pois você não usará o Analytics para este aplicativo.

b7138cde5f2c7b61.png

Para saber mais sobre os projetos do Firebase, consulte Entender os projetos do Firebase .

O aplicativo que você está criando usa vários produtos do Firebase disponíveis para aplicativos da Web:

  • Firebase Authentication para permitir que seus usuários façam login no seu aplicativo.
  • Cloud Firestore para salvar dados estruturados na nuvem e receber notificações instantâneas quando os dados forem alterados.
  • Regras de segurança do Firebase para proteger seu banco de dados.

Alguns desses produtos precisam de configuração especial ou precisam ser ativados usando o Firebase console.

Ativar login por e-mail para o Firebase Authentication

Para permitir que os usuários façam login no aplicativo da Web, você usará o método de login por e- mail/senha para este codelab:

  1. No console do Firebase, expanda o menu Build no painel esquerdo.
  2. Clique em Autenticação e, em seguida, clique no botão Começar e, em seguida, na guia Método de entrada (ou clique aqui para ir diretamente à guia Método de entrada).
  3. Clique em Email/Senha na lista de provedores de login , defina a opção Ativar para a posição ativado e clique em Salvar . 58e3e3e23c2f16a4.png

Ativar o Cloud Firestore

O aplicativo da Web usa o Cloud Firestore para salvar mensagens de bate-papo e receber novas mensagens de bate-papo.

Ative o Cloud Firestore:

  1. Na seção Build do console do Firebase, clique em Cloud Firestore .
  2. Clique em Criar banco de dados. 99e8429832d23fa3.png
  1. Selecione a opção Iniciar no modo de teste . Leia o aviso sobre as regras de segurança. O modo de teste garante que você possa gravar livremente no banco de dados durante o desenvolvimento. Clique em Avançar . 6be00e26c72ea032.png
  1. Selecione o local para seu banco de dados (você pode usar apenas o padrão). Observe que esse local não pode ser alterado posteriormente. 278656eefcfb0216.png
  2. Clique em Ativar .

4. Configuração do Firebase

Para usar o Firebase com Flutter, você precisa seguir um processo para configurar o projeto Flutter para utilizar as bibliotecas FlutterFire corretamente:

  • Adicione as dependências do FlutterFire ao seu projeto
  • Registre a plataforma desejada no projeto Firebase
  • Baixe o arquivo de configuração específico da plataforma e adicione-o ao código.

No diretório de nível superior do seu aplicativo Flutter, existem subdiretórios chamados android , ios , macos e web . Esses diretórios contêm os arquivos de configuração específicos da plataforma para iOS e Android, respectivamente.

Configurar dependências

Você precisa adicionar as bibliotecas FlutterFire para os dois produtos Firebase que você está utilizando neste aplicativo - Firebase Auth e Cloud Firestore. Execute os três comandos a seguir para adicionar as dependências.

$ flutter pub add firebase_core

O firebase_core é o código comum necessário para todos os plug-ins do Firebase Flutter.

$ flutter pub add firebase_auth

O firebase_auth permite a integração com o recurso de autenticação do Firebase.

$ flutter pub add cloud_firestore

O cloud_firestore permite o acesso ao armazenamento de dados do Cloud Firestore.

$ flutter pub add provider

O pacote firebase_ui_auth fornece um conjunto de widgets e utilitários especificamente para aumentar a velocidade do desenvolvedor com fluxos de autenticação.

$ flutter pub add firebase_ui_auth

Embora tenha adicionado os pacotes necessários, você também precisa configurar os projetos iOS, Android, macOS e Web runner para utilizar o Firebase de forma adequada. Você também está usando o pacote do provider que permitirá a separação da lógica de negócios da lógica de exibição.

Instalando flutterfire

A CLI do FlutterFire depende da CLI do Firebase subjacente. Se você ainda não fez isso, verifique se a Firebase CLI está instalada em sua máquina.

Em seguida, instale a CLI do FlutterFire executando o seguinte comando:

$ dart pub global activate flutterfire_cli

Uma vez instalado, o comando flutterfire estará disponível globalmente.

Configurando seus aplicativos

A CLI extrai informações do seu projeto do Firebase e aplicativos de projeto selecionados para gerar toda a configuração para uma plataforma específica.

Na raiz do seu aplicativo, execute o comando configure:

$ flutterfire configure

O comando de configuração o guiará por vários processos:

  1. Seleção de um projeto do Firebase (com base no arquivo .firebaserc ou no Firebase Console).
  2. Pergunta para quais plataformas (por exemplo, Android, iOS, macOS e web) você deseja configurar.
  3. Identifique quais aplicativos do Firebase para as plataformas escolhidas devem ser usados ​​para extrair a configuração. Por padrão, a CLI tentará corresponder automaticamente aos aplicativos do Firebase com base na configuração atual do seu projeto.
  4. Gere um arquivo firebase_options.dart em seu projeto.

Configurar o macOS

O Flutter no macOS cria aplicativos totalmente em área restrita. Como este aplicativo está se integrando usando a rede para se comunicar com os servidores Firebase, você precisará configurar seu aplicativo com privilégios de cliente de rede.

macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
  <!-- Add the following two lines -->
	<key>com.apple.security.network.client</key>
	<true/>
</dict>
</plist>

Consulte Direitos e o App Sandbox para obter mais detalhes.

5. Adicionar login do usuário (RSVP)

Agora que você adicionou o Firebase ao aplicativo, pode configurar um botão RSVP que registre pessoas usando o Firebase Authentication . Para Android nativo, iOS nativo e Web, há pacotes FirebaseUI Auth pré-criados, mas para Flutter você precisará criar esse recurso.

O projeto que você recuperou na Etapa 2 incluiu um conjunto de widgets que implementa a interface do usuário para a maior parte do fluxo de autenticação. Você implementará a lógica de negócios para integrar o Firebase Authentication ao aplicativo.

Lógica de Negócios com Provedor

Você usará o pacote do provider para disponibilizar um objeto de estado do aplicativo centralizado em toda a árvore de widgets Flutter do aplicativo. Para começar, modifique as importações na parte superior de lib/main.dart :

lib/main.dart

import 'dart:async';                                     // new
import 'package:firebase_auth/firebase_auth.dart'        // new
    hide EmailAuthProvider, PhoneAuthProvider;           // new
import 'package:firebase_core/firebase_core.dart';       // new
import 'package:firebase_ui_auth/firebase_ui_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';                 // new

import 'firebase_options.dart';                          // new
import 'src/authentication.dart';                        // new
import 'src/widgets.dart';

As linhas import apresentam o Firebase Core e o Auth, extraem o pacote do provider que você está usando para disponibilizar o objeto de estado do aplicativo por meio da árvore de widgets e incluem os widgets de autenticação de firebase_ui_auth .

Este objeto de estado do aplicativo, ApplicationState , tem uma responsabilidade principal para esta etapa que é alertar a árvore de widgets de que houve uma atualização para um estado autenticado. Adicione a seguinte classe ao final de lib/main.dart :

lib/main.dart

class ApplicationState extends ChangeNotifier {
  ApplicationState() {
    init();
  }

  bool _loggedIn = false;
  bool get loggedIn => _loggedIn;

  Future<void> init() async {
    await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform);

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
      } else {
        _loggedIn = false;
      }
      notifyListeners();
    });
  }
}

Estamos usando um provedor aqui para comunicar ao aplicativo o estado do status de login de um usuário, nada mais. Para fazer o login de um usuário, usaremos as UIs fornecidas pelo firebase_ui_auth , que é uma ótima maneira de inicializar rapidamente as telas de login para seus aplicativos.

Integrando o fluxo de autenticação

Agora que você tem o início do estado do aplicativo, é hora de conectar o estado do aplicativo à inicialização do aplicativo e adicionar o fluxo de autenticação à HomePage . Atualize o ponto de entrada principal para integrar o estado do aplicativo por meio do pacote do provider :

lib/main.dart

void main() {
  // Modify from here
  WidgetsFlutterBinding.ensureInitialized();

  runApp(ChangeNotifierProvider(
    create: (context) => ApplicationState(),
    builder: ((context, child) => const App()),
  ));
  // to here.
}

A modificação na função main torna o pacote do provedor responsável por instanciar o objeto de estado do aplicativo usando o widget ChangeNotifierProvider . Você está usando essa classe de provedor específica porque o objeto de estado do aplicativo estende ChangeNotifier e isso permite que o pacote do provider saiba quando exibir novamente os widgets dependentes.

Como estamos usando o FirebaseUI para Flutter, atualizaremos nosso aplicativo para lidar com a navegação em diferentes telas que o FirebaseUI fornece para nós. Para fazer isso, adicionamos uma propriedade initialRoute e adicionamos nossas telas preferidas para as quais podemos rotear na propriedade routes . As alterações devem ficar assim:

lib/main.dart

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //Start adding here
      initialRoute: '/home',
      routes: {
        '/home': (context) {
          return const HomePage();
        },
        '/sign-in': ((context) {
          return SignInScreen(
            actions: [
              ForgotPasswordAction(((context, email) {
                Navigator.of(context)
                    .pushNamed('/forgot-password', arguments: {'email': email});
              })),
              AuthStateChangeAction(((context, state) {
                if (state is SignedIn || state is UserCreated) {
                  var user = (state is SignedIn)
                      ? state.user
                      : (state as UserCreated).credential.user;
                  if (user == null) {
                    return;
                  }
                  if (state is UserCreated) {
                    user.updateDisplayName(user.email!.split('@')[0]);
                  }
                  if (!user.emailVerified) {
                    user.sendEmailVerification();
                    const snackBar = SnackBar(
                        content: Text(
                            'Please check your email to verify your email address'));
                    ScaffoldMessenger.of(context).showSnackBar(snackBar);
                  }
                  Navigator.of(context).pushReplacementNamed('/home');
                }
              })),
            ],
          );
        }),
        '/forgot-password': ((context) {
          final arguments = ModalRoute.of(context)?.settings.arguments
              as Map<String, dynamic>?;

          return ForgotPasswordScreen(
            email: arguments?['email'] as String,
            headerMaxExtent: 200,
          );
        }),
        '/profile': ((context) {
          return ProfileScreen(
            providers: [],
            actions: [
              SignedOutAction(
                ((context) {
                  Navigator.of(context).pushReplacementNamed('/home');
                }),
              ),
            ],
          );
        })
      },
      // end adding here
      title: 'Firebase Meetup',
      theme: ThemeData(
        buttonTheme: Theme.of(context).buttonTheme.copyWith(
              highlightColor: Colors.deepPurple,
            ),
        primarySwatch: Colors.deepPurple,
        textTheme: GoogleFonts.robotoTextTheme(
          Theme.of(context).textTheme,
        ),
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
    );
  }
}

Cada tela tem um tipo diferente de ação associado a ela com base no novo estado do fluxo de autenticação. Após a maioria das mudanças de estado na autenticação, podemos redirecionar de volta para uma tela preferencial, seja a tela inicial ou uma tela diferente, como perfil. Por fim, integre o estado do aplicativo com AuthFunc atualizando o método de construção do HomePage :

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer<ApplicationState>(
        builder: (context, appState, child) => Scaffold(
              appBar: AppBar(
                title: const Text('Firebase Meetup'),
              ),
              body: ListView(
                children: <Widget>[
                  Image.asset('assets/codelab.png'),
                  const SizedBox(height: 8),
                  const IconAndDetail(Icons.calendar_today, 'October 30'),
                  const IconAndDetail(Icons.location_city, 'San Francisco'),
                  // Add from here
                  Consumer<ApplicationState>(
                    builder: (context, appState, _) => AuthFunc(
                        loggedIn: appState.loggedIn,
                        signOut: () {
                          FirebaseAuth.instance.signOut();
                        }),
                  ),
                  // to here
                  const Divider(
                    height: 8,
                    thickness: 1,
                    indent: 8,
                    endIndent: 8,
                    color: Colors.grey,
                  ),
                  const Header("What we'll be doing"),
                  const Paragraph(
                    'Join us for a day full of Firebase Workshops and Pizza!',
                  ),
                ],
              ),
            ));
  }
}

Você instancia o widget AuthFunc e o envolve em um widget Consumer . O widget Consumidor da maneira usual que o pacote do provider pode ser usado para reconstruir parte da árvore quando o estado do aplicativo muda. O widget AuthFunc são os widgets complementares que você testará agora.

Testando o fluxo de autenticação

cdf2d25e436bd48d.png

Aqui está o início do fluxo de autenticação, onde o usuário pode tocar no botão RSVP, para iniciar o SignInScreen .

2a2cd6d69d172369.png

Ao inserir o e-mail, o sistema confirma se o usuário já está cadastrado, neste caso é solicitada uma senha ao usuário, ou caso o usuário não esteja cadastrado, então ele passa pelo formulário de cadastro.

e5e65065dba36b54.png

Certifique-se de tentar inserir uma senha curta (menos de seis caracteres) para verificar o fluxo de tratamento de erros. Se o usuário estiver registrado, ele verá a senha.

Nesta página, certifique-se de inserir senhas incorretas para verificar o tratamento de erros nesta página. Finalmente, quando o usuário estiver logado, você verá a experiência de login que oferece ao usuário a capacidade de sair novamente.

4ed811a25b0cf816.png

E com isso, você implementou um fluxo de autenticação. Parabéns!

6. Gravar mensagens no Cloud Firestore

Saber que os usuários estão chegando é ótimo, mas vamos dar aos convidados algo mais para fazer no aplicativo. E se eles pudessem deixar mensagens em um livro de visitas? Eles podem compartilhar por que estão animados para vir ou quem esperam conhecer.

Para armazenar as mensagens de bate-papo que os usuários escrevem no aplicativo, você usará o Cloud Firestore .

Modelo de dados

O Cloud Firestore é um banco de dados NoSQL e os dados armazenados no banco de dados são divididos em coleções, documentos, campos e subcoleções. Você armazenará cada mensagem do bate-papo como um documento em uma coleção de nível superior chamada guestbook .

7c20dc8424bb1d84.png

Adicionar mensagens ao Firestore

Nesta seção, você adicionará a funcionalidade para que os usuários escrevam novas mensagens no banco de dados. Primeiro, você adiciona os elementos da interface do usuário (campo de formulário e botão de envio) e, em seguida, adiciona o código que conecta esses elementos ao banco de dados.

Primeiro, adicione importações para o pacote cloud_firestore e dart:async .

lib/main.dart

import 'dart:async';                                    // new

import 'package:cloud_firestore/cloud_firestore.dart';  // new
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';

import 'firebase_options.dart';
import 'src/authentication.dart';
import 'src/widgets.dart';

Para construir os elementos de UI de um campo de mensagem e um botão de envio, inclua um novo widget com estado GuestBook na parte inferior de lib/main.dart .

lib/main.dart

class GuestBook extends StatefulWidget {
  const GuestBook({required this.addMessage, super.key});
  final FutureOr<void> Function(String message) addMessage;

  @override
  State<GuestBook> createState() => _GuestBookState();
}

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Form(
        key: _formKey,
        child: Row(
          children: [
            Expanded(
              child: TextFormField(
                controller: _controller,
                decoration: const InputDecoration(
                  hintText: 'Leave a message',
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Enter your message to continue';
                  }
                  return null;
                },
              ),
            ),
            const SizedBox(width: 8),
            StyledButton(
              onPressed: () async {
                if (_formKey.currentState!.validate()) {
                  await widget.addMessage(_controller.text);
                  _controller.clear();
                }
              },
              child: Row(
                children: const [
                  Icon(Icons.send),
                  SizedBox(width: 4),
                  Text('SEND'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Há alguns pontos de interesse aqui. Primeiro, você está instanciando um Form para poder validar se a mensagem realmente tem algum conteúdo e mostrar ao usuário uma mensagem de erro se não houver. A forma de validar um formulário envolve acessar o estado do formulário por trás do formulário, e para isso você usa uma GlobalKey . Para obter mais informações sobre as chaves e como usá-las, consulte o episódio Flutter Widgets 101 "Quando usar as chaves" .

Observe também a maneira como os widgets são dispostos, você tem um Row , com um TextFormField e um StyledButton , que contém um Row . Observe também que o TextFormField é encapsulado em um widget Expanded , isso força o TextFormField a ocupar qualquer espaço extra na linha. Para entender melhor por que isso é necessário, leia Entendendo as restrições .

Agora que você tem um widget que permite ao usuário inserir algum texto para adicionar ao Livro de visitas, você precisa colocá-lo na tela. Para fazer isso, edite o corpo da HomePage para adicionar as duas linhas a seguir na parte inferior dos filhos do ListView :

const Header("What we'll be doing"),
const Paragraph(
  'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),

Embora isso seja suficiente para exibir o Widget, não é suficiente para fazer algo útil. Você atualizará este código em breve para torná-lo funcional.

Visualização do aplicativo

Um usuário clicando no botão ENVIAR acionará o snippet de código abaixo. Ele adiciona o conteúdo do campo de entrada de mensagem à coleção de livros de guestbook do banco de dados. Especificamente, o método addMessageToGuestBook adiciona o conteúdo da mensagem a um novo documento (com um ID gerado automaticamente) à coleção do guestbook de visitas.

Observe que FirebaseAuth.instance.currentUser.uid é uma referência ao ID exclusivo gerado automaticamente que o Firebase Authentication fornece a todos os usuários conectados.

Faça outra alteração no arquivo lib/main.dart . Adicione o método addMessageToGuestBook . Você conectará a interface do usuário e esse recurso na próxima etapa.

lib/main.dart

class ApplicationState extends ChangeNotifier {

  // Current content of ApplicationState elided ...

  // Add from here
  Future<DocumentReference> addMessageToGuestBook(String message) {
    if (!_loggedIn) {
      throw Exception('Must be logged in');
    }

    return FirebaseFirestore.instance
        .collection('guestbook')
        .add(<String, dynamic>{
      'text': message,
      'timestamp': DateTime.now().millisecondsSinceEpoch,
      'name': FirebaseAuth.instance.currentUser!.displayName,
      'userId': FirebaseAuth.instance.currentUser!.uid,
    });
  }
  // To here
}

Conectando a interface do usuário ao banco de dados

Você tem uma IU em que o usuário pode inserir o texto que deseja adicionar ao Livro de visitas e tem o código para adicionar a entrada ao Cloud Firestore. Agora tudo que você precisa fazer é conectar os dois juntos. Em lib/main.dart faça a seguinte alteração no widget HomePage .

lib/main.dart

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firebase Meetup'),
      ),
      body: ListView(
        children: <Widget>[
          Image.asset('assets/codelab.png'),
          const SizedBox(height: 8),
          const IconAndDetail(Icons.calendar_today, 'October 30'),
          const IconAndDetail(Icons.location_city, 'San Francisco'),
          Consumer<ApplicationState>(
            builder: (context, appState, _) => AuthFunc(
                loggedIn: appState.loggedIn,
                signOut: () {
                  FirebaseAuth.instance.signOut();
                }),
          ),
          const Divider(
            height: 8,
            thickness: 1,
            indent: 8,
            endIndent: 8,
            color: Colors.grey,
          ),
          const Header("What we'll be doing"),
          const Paragraph(
            'Join us for a day full of Firebase Workshops and Pizza!',
          ),
          // Modify from here
          Consumer<ApplicationState>(
            builder: (context, appState, _) => Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                if (appState.loggedIn) ...[
                  const Header('Discussion'),
                  GuestBook(
                    addMessage: (message) =>
                        appState.addMessageToGuestBook(message),
                  ),
                ],
              ],
            ),
          ),
          // To here.
        ],
      ),
    );
  }
}

Você substituiu as duas linhas que adicionou no início desta etapa pela implementação completa. Você está novamente usando Consumer<ApplicationState> para disponibilizar o estado do aplicativo para a parte da árvore que está renderizando. Isso permite que você reaja a alguém inserindo uma mensagem na interface do usuário e a publique no banco de dados. Na próxima seção você testará se as mensagens adicionadas são publicadas no banco de dados.

Teste o envio de mensagens

  1. Verifique se você está conectado ao aplicativo.
  2. Digite uma mensagem como "E aí!" e clique em ENVIAR .

Esta ação grava a mensagem no banco de dados do Cloud Firestore. No entanto, você ainda não verá a mensagem em seu aplicativo Flutter real porque ainda precisa implementar a recuperação dos dados. Você fará isso na próxima etapa.

Mas você pode ver a mensagem recém-adicionada no console do Firebase.

No console do Firebase, no painel do banco de dados , você verá a coleção de livros de guestbook com sua mensagem recém-adicionada. Se você continuar enviando mensagens, sua coleção de livros de visitas conterá muitos documentos, como este:

Console do Firebase

713870af0b3b63c.png

7. Leia as mensagens

É ótimo que os convidados possam escrever mensagens no banco de dados, mas ainda não podem vê-las no aplicativo. Vamos consertar isso!

Sincronizar mensagens

Para exibir mensagens, você precisará adicionar ouvintes que são acionados quando os dados são alterados e, em seguida, criar um elemento de interface do usuário que mostra novas mensagens. Você adicionará código ao estado do aplicativo que escuta as mensagens recém-adicionadas do aplicativo.

Logo acima do widget GuestBook , a seguinte classe de valor. Esta classe expõe uma visualização estruturada dos dados que você está armazenando no Cloud Firestore.

lib/main.dart

class GuestBookMessage {
  GuestBookMessage({required this.name, required this.message});
  final String name;
  final String message;
}

Na seção de ApplicationState onde você define state e getters, adicione as seguintes novas linhas:

lib/main.dart

  bool _loggedIn = false;
  bool get loggedIn => _loggedIn;

  // Add from here
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // to here.

E, finalmente, na seção de inicialização de ApplicationState , adicione o seguinte para assinar uma consulta sobre a coleção de documentos quando um usuário fizer login e cancelar a assinatura quando fizer logout.

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform);

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);
    
    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loggedIn = true;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
      } else {
        _loggedIn = false;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
      }
      notifyListeners();
    });
  }

Esta seção é importante, pois é aqui que você constrói uma consulta sobre a coleção de livros de guestbook e lida com a assinatura e o cancelamento da assinatura dessa coleção. Você ouve o fluxo, onde você reconstrói um cache local das mensagens na coleção do guestbook de visitas e também armazena uma referência a essa assinatura para que você possa cancelar a assinatura posteriormente. Há muita coisa acontecendo aqui, e vale a pena gastar algum tempo em um depurador inspecionando o que acontece quando se obtém um modelo mental mais claro.

Para obter mais informações, consulte a documentação do Cloud Firestore .

No widget GuestBook , você precisa conectar esse estado de mudança à interface do usuário. Você modifica o widget adicionando uma lista de mensagens como parte de sua configuração.

lib/main.dart

class GuestBook extends StatefulWidget {
  // Modify the following line
  const GuestBook({super.key, required this.addMessage, required this.messages,});
  final FutureOr<void> Function(String message) addMessage;
  final List<GuestBookMessage> messages; // new

  @override
  _GuestBookState createState() => _GuestBookState();
}

Em seguida, expomos essa nova configuração em _GuestBookState modificando o método de build da seguinte maneira.

lib/main.dart

class _GuestBookState extends State<GuestBook> {
  final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
  final _controller = TextEditingController();

  @override
  // Modify from here
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // to here.
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Form(
            key: _formKey,
            child: Row(
              children: [
                Expanded(
                  child: TextFormField(
                    controller: _controller,
                    decoration: const InputDecoration(
                      hintText: 'Leave a message',
                    ),
                    validator: (value) {
                      if (value == null || value.isEmpty) {
                        return 'Enter your message to continue';
                      }
                      return null;
                    },
                  ),
                ),
                const SizedBox(width: 8),
                StyledButton(
                  onPressed: () async {
                    if (_formKey.currentState!.validate()) {
                      await widget.addMessage(_controller.text);
                      _controller.clear();
                    }
                  },
                  child: Row(
                    children: const [
                      Icon(Icons.send),
                      SizedBox(width: 4),
                      Text('SEND'),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        // Modify from here
        const SizedBox(height: 8),
        for (var message in widget.messages)
          Paragraph('${message.name}: ${message.message}'),
        const SizedBox(height: 8),
      ],
      // to here.
    );
  }
}

Você envolve o conteúdo anterior do método de construção com um widget Column e, em seguida, no final dos filhos da Column , você adiciona uma coleção para gerar um novo Paragraph para cada mensagem na lista de mensagens.

Finalmente, agora você precisa atualizar o corpo da HomePage para construir corretamente o GuestBook com o novo parâmetro de messages .

lib/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      if (appState.loginState == ApplicationLoginState.loggedIn) ...[
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages, // new
        ),
      ],
    ],
  ),
),

Testar mensagens de sincronização

O Cloud Firestore sincroniza automaticamente e instantaneamente os dados com os clientes inscritos no banco de dados.

  1. As mensagens que você criou anteriormente no banco de dados devem ser exibidas no aplicativo. Sinta-se à vontade para escrever novas mensagens; eles devem aparecer instantaneamente.
  2. Se você abrir seu espaço de trabalho em várias janelas ou guias, as mensagens serão sincronizadas em tempo real entre as guias.
  3. (Opcional) Você pode tentar excluir, modificar ou adicionar novas mensagens manualmente diretamente na seção Banco de dados do console do Firebase; todas as alterações devem aparecer na interface do usuário.

Parabéns! Você está lendo documentos do Cloud Firestore em seu aplicativo!

Revisão do aplicativo

8. Configure regras básicas de segurança

Você configurou inicialmente o Cloud Firestore para usar o modo de teste, o que significa que seu banco de dados está aberto para leituras e gravações. No entanto, você só deve usar o modo de teste durante os estágios iniciais de desenvolvimento. Como prática recomendada, você deve configurar regras de segurança para seu banco de dados à medida que desenvolve seu aplicativo. A segurança deve ser parte integrante da estrutura e do comportamento do seu aplicativo.

As Regras de Segurança permitem controlar o acesso a documentos e coleções em seu banco de dados. A sintaxe de regras flexíveis permite que você crie regras que correspondam a qualquer coisa, desde todas as gravações em todo o banco de dados até operações em um documento específico.

Você pode escrever regras de segurança para o Cloud Firestore no console do Firebase:

  1. Na seção Desenvolver do console do Firebase, clique em Banco de dados e selecione a guia Regras (ou clique aqui para ir diretamente para a guia Regras ).
  2. Você deve ver as seguintes regras de segurança padrão, juntamente com um aviso sobre as regras serem públicas.

7767a2d2e64e7275.png

Identificar coleções

Primeiro, identifique as coleções nas quais o aplicativo grava dados.

Em match /databases/{database}/documents , identifique a coleção que você deseja proteger:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
     // You'll add rules here in the next step.
  }
}

Adicionar regras de segurança

Como você usou o UID de autenticação como um campo em cada documento do livro de visitas, você pode obter o UID de autenticação e verificar se qualquer pessoa tentando gravar no documento tem um UID de autenticação correspondente.

Adicione as regras de leitura e gravação ao seu conjunto de regras, conforme mostrado abaixo:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
        if request.auth.uid == request.resource.data.userId;
    }
  }
}

Agora, para o livro de visitas, apenas usuários conectados podem ler mensagens (qualquer mensagem!), mas apenas o autor de uma mensagem pode editar uma mensagem.

Adicionar regras de validação

Adicione validação de dados para garantir que todos os campos esperados estejam presentes no documento:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /guestbook/{entry} {
      allow read: if request.auth.uid != null;
      allow write:
      if request.auth.uid == request.resource.data.userId
          && "name" in request.resource.data
          && "text" in request.resource.data
          && "timestamp" in request.resource.data;
    }
  }
}

9. Etapa bônus: pratique o que aprendeu

Gravar o status de RSVP de um participante

No momento, seu aplicativo apenas permite que as pessoas comecem a conversar se estiverem interessadas no evento. Além disso, a única maneira de saber se alguém está vindo é postando no chat. Vamos nos organizar e deixar as pessoas saberem quantas pessoas estão vindo.

Você adicionará alguns novos recursos ao estado do aplicativo. A primeira é a capacidade de um usuário logado indicar se está participando ou não. A segunda capacidade é um contador de quantas pessoas estão realmente participando.

Em lib/main.dart , adicione o seguinte à seção accessors para permitir que o código da interface do usuário interaja com esse estado:

lib/main.dart

int _attendees = 0;
int get attendees => _attendees;

Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
  final userDoc = FirebaseFirestore.instance
      .collection('attendees')
      .doc(FirebaseAuth.instance.currentUser!.uid);
  if (attending == Attending.yes) {
    userDoc.set(<String, dynamic>{'attending': true});
  } else {
    userDoc.set(<String, dynamic>{'attending': false});
  }
}

Atualize o método init do ApplicationState da seguinte forma:

lib/main.dart

  Future<void> init() async {
    await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform);

    FirebaseUIAuth.configureProviders([
      EmailAuthProvider(),
    ]);

    // Add from here
    FirebaseFirestore.instance
        .collection('attendees')
        .where('attending', isEqualTo: true)
        .snapshots()
        .listen((snapshot) {
      _attendees = snapshot.docs.length;
      notifyListeners();
    });
    // To here

    FirebaseAuth.instance.userChanges().listen((user) {
      if (user != null) {
        _loginState = ApplicationLoginState.loggedIn;
        _guestBookSubscription = FirebaseFirestore.instance
            .collection('guestbook')
            .orderBy('timestamp', descending: true)
            .snapshots()
            .listen((snapshot) {
          _guestBookMessages = [];
          for (final document in snapshot.docs) {
            _guestBookMessages.add(
              GuestBookMessage(
                name: document.data()['name'] as String,
                message: document.data()['text'] as String,
              ),
            );
          }
          notifyListeners();
        });
        // Add from here
        _attendingSubscription = FirebaseFirestore.instance
            .collection('attendees')
            .doc(user.uid)
            .snapshots()
            .listen((snapshot) {
          if (snapshot.data() != null) {
            if (snapshot.data()!['attending'] as bool) {
              _attending = Attending.yes;
            } else {
              _attending = Attending.no;
            }
          } else {
            _attending = Attending.unknown;
          }
          notifyListeners();
        });
        // to here
      } else {
        _loginState = ApplicationLoginState.loggedOut;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

O acima adiciona uma consulta sempre assinada para descobrir o número de participantes e uma segunda consulta que está ativa apenas enquanto um usuário está conectado para descobrir se o usuário está participando. Em seguida, adicione a seguinte enumeração após a declaração GuestBookMessage :

lib/main.dart

enum Attending { yes, no, unknown }

Agora você vai definir um novo widget que funciona como botões de rádio antigos. Ele começa em um estado indeterminado, com nem sim nem não selecionado, mas uma vez que o usuário seleciona se está participando ou não, você mostra essa opção destacada com um botão preenchido e a outra opção retrocedendo com uma renderização plana.

lib/main.dart

class YesNoSelection extends StatelessWidget {
  const YesNoSelection(
      {super.key, required this.state, required this.onSelection});
  final Attending state;
  final void Function(Attending selection) onSelection;

  @override
  Widget build(BuildContext context) {
    switch (state) {
      case Attending.yes:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              TextButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      case Attending.no:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              TextButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                style: ElevatedButton.styleFrom(elevation: 0),
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
      default:
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: Row(
            children: [
              StyledButton(
                onPressed: () => onSelection(Attending.yes),
                child: const Text('YES'),
              ),
              const SizedBox(width: 8),
              StyledButton(
                onPressed: () => onSelection(Attending.no),
                child: const Text('NO'),
              ),
            ],
          ),
        );
    }
  }
}

Em seguida, você precisa atualizar o método de compilação do HomePage para aproveitar YesNoSelection , permitindo que um usuário conectado indique se estiver participando. Você também exibirá o número de participantes para este evento.

lib/main.dart

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here
      if (appState.attendees >= 2)
        Paragraph('${appState.attendees} people going')
      else if (appState.attendees == 1)
        const Paragraph('1 person going')
      else
        const Paragraph('No one going'),
      // To here.
      if (appState.loggedIn) ...[
        // Add from here
        YesNoSelection(
          state: appState.attending,
          onSelection: (attending) => appState.attending = attending,
        ),
        // To here.
        const Header('Discussion'),
        GuestBook(
          addMessage: (message) =>
              appState.addMessageToGuestBook(message),
          messages: appState.guestBookMessages,
        ),
      ],
    ],
  ),
),

Adicionar regras

Como você já tem algumas regras configuradas, os novos dados que você está adicionando com os botões serão rejeitados. Você precisará atualizar as regras para permitir a adição à coleção de attendees .

Para a coleção de attendees , como você usou o UID de autenticação como o nome do documento, você pode pegá-lo e verificar se o uid do remetente é o mesmo do documento que ele está escrevendo. Você permitirá que todos leiam a lista de participantes (já que não há dados privados lá), mas apenas o criador deve poder atualizá-la.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

Adicionar regras de validação

Adicione validação de dados para garantir que todos os campos esperados estejam presentes no documento:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId
          && "attending" in request.resource.data;

    }
  }
}

(Opcional) Agora você pode visualizar os resultados de clicar nos botões. Acesse o painel do Cloud Firestore no console do Firebase.

Visualização do aplicativo

10. Parabéns!

Você usou o Firebase para criar um aplicativo da Web interativo em tempo real!

O que cobrimos

  • Autenticação do Firebase
  • Cloud Firestore
  • Regras de segurança do Firebase

Próximos passos

Saber mais

Como foi?

Adoraríamos seu feedback! Por favor, preencha um formulário (muito) curto aqui .