Introdução ao Firebase para Flutter

1. Antes de começar

Neste codelab, você vai aprender alguns dos conceitos básicos do Firebase para criar apps do Flutter para dispositivos móveis Android e iOS.

Pré-requisitos

Conteúdo

  • Como criar um app de chat para RSVP de um evento e livro de visitas em Android, iOS, Web e macOS com o Flutter.
  • Como autenticar usuários com o Firebase Authentication e sincronizar dados com o Firestore.

Tela inicial do app no Android

Tela inicial do app no iOS

Pré-requisitos

Qualquer um dos seguintes dispositivos:

  • Um dispositivo físico Android ou iOS conectado ao seu computador e configurado para o modo de desenvolvedor.
  • O simulador para iOS (requer ferramentas do Xcode).
  • O Android Emulator, que requer configuração no Android Studio.

Você também precisa de:

  • no navegador da sua escolha, como o Google Chrome;
  • Um ambiente de desenvolvimento integrado ou editor de texto da sua escolha configurado com os plug-ins Dart e Flutter, como o Android Studio ou o Visual Studio Code.
  • A versão mais recente do stable do Flutter (link em inglês) ou do beta, se você gosta de morar fora do ar.
  • Uma Conta do Google para a criação e o gerenciamento do seu projeto do Firebase.
  • A CLI Firebase fez login na sua Conta do Google.

2. Acessar o exemplo de código

Faça o download da versão inicial do seu projeto no GitHub:

  1. Na linha de comando, clone o repositório do GitHub (link em inglês) no diretório flutter-codelabs:
git clone https://github.com/flutter/codelabs.git flutter-codelabs

O diretório flutter-codelabs contém o código de uma coleção de codelabs. O código deste codelab está no diretório flutter-codelabs/firebase-get-to-know-flutter. O diretório contém uma série de snapshots que mostram como o projeto vai ficar ao final de cada etapa. Por exemplo, você está na segunda etapa.

  1. Encontre os arquivos correspondentes da segunda etapa:
cd flutter-codelabs/firebase-get-to-know-flutter/step_02

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

Importar o app inicial

  • Abra ou importe o diretório flutter-codelabs/firebase-get-to-know-flutter/step_02 no seu ambiente de desenvolvimento integrado preferido. Esse diretório contém o código inicial do codelab, que consiste em um app de encontro do Flutter ainda não funcional.

Encontrar os arquivos que precisam de ajustes

O código neste app é distribuído por vários diretórios. Essa divisão de funcionalidade facilita o trabalho porque agrupa o código por funcionalidade.

  • Localize os seguintes arquivos:
    • lib/main.dart: esse arquivo contém o ponto de entrada principal e o widget do app.
    • lib/home_page.dart: esse arquivo contém o widget da página inicial.
    • lib/src/widgets.dart: esse arquivo contém vários widgets para ajudar a padronizar o estilo do app. Eles compõem a tela do app inicial.
    • lib/src/authentication.dart: esse arquivo contém uma implementação parcial do Authentication com um conjunto de widgets para criar uma experiência do usuário com login para a autenticação baseada em e-mail do Firebase. Esses widgets para o fluxo de autenticação ainda não são usados no app inicial, mas serão adicionados em breve.

Você adiciona outros arquivos conforme necessário para criar o restante do app.

Revise o arquivo lib/main.dart

Este app usa o pacote google_fonts para tornar Roboto a fonte padrão em todo o app. Você pode acessar fonts.google.com e usar as fontes encontradas em diferentes partes do app.

Use os widgets auxiliares do arquivo lib/src/widgets.dart na forma de Header, Paragraph e IconAndDetail. Esses widgets eliminam o código duplicado para reduzir a confusão no layout da página descrito em HomePage. Isso também permite uma aparência consistente.

O app fica assim no Android, no iOS, na Web e no macOS:

Tela inicial do app no Android

Tela inicial do app no iOS

Tela inicial do app na Web

Tela inicial do app no macOS

3. Criar e configurar um projeto do Firebase

A exibição de informações de eventos é ótima para os seus convidados, mas não é muito útil para ninguém por si só. Adicione algumas funcionalidades dinâmicas ao app. Para isso, conecte o Firebase ao app. Para começar a usar o Firebase, crie e configure um projeto.

criar um projeto do Firebase

  1. Faça login no Firebase.
  2. No console, clique em Adicionar projeto ou Criar um projeto.
  3. No campo Nome do projeto, digite Firebase-Flutter-Codelab e clique em Continuar.

4395e4e67c08043a.png

  1. Clique nas opções de criação do projeto. Se solicitado, aceite os termos do Firebase, mas pule a configuração do Google Analytics, já que ele não será usado neste app.

b7138cde5f2c7b61.png

Para saber mais sobre os projetos do Firebase, consulte Noções básicas sobre os projetos do Firebase.

O app usa os seguintes produtos do Firebase, que estão disponíveis para apps da Web:

  • Autenticação:permite que os usuários façam login no seu app.
  • Firestore: salva dados estruturados na nuvem e recebe notificações instantâneas quando os dados são alterados.
  • Regras de segurança do Firebase:protege seu banco de dados.

Alguns desses produtos precisam de configuração especial ou você precisa ativá-los no Console do Firebase.

Ativar a autenticação de login por e-mail

  1. No painel Visão geral do projeto do Console do Firebase, expanda o menu Build.
  2. Clique em Autenticação > Primeiros passos > Método de login > E-mail/senha > Ativar > Salvar.

58e3e3e23c2f16a4.png

Ativar o Firestore

O app da Web usa o Firestore para salvar mensagens de chat e receber novas mensagens.

Ative o Firestore:

  • No menu Build, clique em Firestore Database > Create database.

99e8429832d23fa3.png

  1. Selecione Iniciar no modo de teste e leia a exoneração de responsabilidade sobre as regras de segurança. O modo de teste garante que você possa gravar livremente no banco de dados durante o desenvolvimento.

6be00e26c72ea032.png

  1. Clique em Next e selecione o local do seu banco de dados. Você pode usar o padrão. Não será possível mudar o local depois.

278656eefcfb0216.png

  1. Selecione Ativar.

4. Configurar o Firebase

Para usar o Firebase com o Flutter, é necessário concluir as seguintes tarefas para configurar o projeto Flutter para usar as bibliotecas FlutterFire corretamente:

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

No diretório de nível superior do app do Flutter, há subdiretórios android, ios, macos e web, que armazenam os arquivos de configuração específicos da plataforma para iOS e Android, respectivamente.

Configurar dependências

É necessário adicionar as bibliotecas FlutterFire para os dois produtos do Firebase usados no app: Authentication e Firestore.

  • Na linha de comando, adicione as seguintes dependências:
$ flutter pub add firebase_core

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

$ flutter pub add firebase_auth

O pacote firebase_auth permite a integração com o Authentication.

$ flutter pub add cloud_firestore

O pacote cloud_firestore permite acesso ao armazenamento de dados do Firestore.

$ flutter pub add provider

O pacote firebase_ui_auth (link em inglês) fornece um conjunto de widgets e utilitários para aumentar a velocidade do desenvolvedor com fluxos de autenticação.

$ flutter pub add firebase_ui_auth

Você adicionou os pacotes necessários, mas também precisa configurar os projetos do executor do iOS, Android, macOS e Web para usar corretamente o Firebase. Você também usa o pacote provider, que permite a separação da lógica de negócios da lógica de exibição.

Instalar a CLI do FlutterFire

A CLI do FlutterFire depende da CLI do Firebase subjacente.

  1. Instale a CLI do Firebase na sua máquina, caso ainda não tenha feito isso.
  2. Instale a CLI do FlutterFire:
$ dart pub global activate flutterfire_cli

Depois de instalado, o comando flutterfire vai ficar disponível globalmente.

Configurar seus apps

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

Na raiz do seu app, execute o comando configure:

$ flutterfire configure

O comando de configuração orienta você nos seguintes processos:

  1. Selecione um projeto do Firebase com base no arquivo .firebaserc ou no Console do Firebase.
  2. determinar as plataformas para configuração, como Android, iOS, macOS e Web;
  3. Identifique os apps do Firebase de onde extrair a configuração. Por padrão, a CLI tenta corresponder automaticamente os apps do Firebase com base na configuração do projeto atual.
  4. Gere um arquivo firebase_options.dart no projeto.

Configurar o macOS

O Flutter no macOS cria apps totalmente em sandbox. Como este app se integra à rede para se comunicar com os servidores do Firebase, você precisa configurá-lo com privilégios de cliente de rede.

macos/Runner/DebugProfile.entitlements (link em inglês)

<?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 (link em inglês)

<?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>

Para saber mais, consulte Suporte para o Flutter em computadores.

5. Adicionar a funcionalidade de RSVP

Agora que você adicionou o Firebase ao aplicativo, crie um botão RSVP para registrar pessoas usando o Authentication. Há pacotes FirebaseUI Auth pré-criados para Android nativo, iOS e Web, mas é necessário criar esse recurso para o Flutter.

O projeto que você recuperou anteriormente incluía um conjunto de widgets que implementa a interface do usuário na maior parte do fluxo de autenticação. Você implementa a lógica de negócios para integrar o Authentication ao app.

Adicionar lógica de negócios com o pacote Provider

Use o pacote provider (link em inglês) para disponibilizar um objeto de estado centralizado do app em toda a árvore de widgets do Flutter do app:

  1. Crie um novo arquivo chamado app_state.dart com o seguinte conteúdo:

lib/app_state.dart (link em inglês)

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

import 'firebase_options.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();
    });
  }
}

As instruções import introduzem o Firebase Core e o Auth, extraem o pacote provider que disponibiliza o objeto de estado do app em toda a árvore de widgets e incluem os widgets de autenticação do pacote firebase_ui_auth.

O objeto de estado do aplicativo ApplicationState tem uma responsabilidade principal nesta etapa, que é alertar a árvore de widgets de que houve uma atualização para um estado autenticado.

Você só vai usar um provedor para comunicar o estado do status de login de um usuário ao app. Para permitir que um usuário faça login, use as interfaces fornecidas pelo pacote firebase_ui_auth, que são uma ótima maneira de inicializar rapidamente as telas de login nos seus apps.

Integrar o fluxo de autenticação

  1. Modifique as importações na parte de cima do arquivo lib/main.dart:

lib/main.dart

import 'package:firebase_ui_auth/firebase_ui_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';               // new
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';                 // new

import 'app_state.dart';                                 // new
import 'home_page.dart';
  1. Conecte o estado do app à inicialização e adicione o fluxo de autenticação a HomePage:

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 pela instanciação do objeto de estado do app com o widget ChangeNotifierProvider. Use essa classe provider específica porque o objeto de estado do app estende a classe ChangeNotifier, o que permite que o pacote provider saiba quando reexibir widgets dependentes.

  1. Atualize seu app para processar a navegação em diferentes telas fornecidas pela FirebaseUI criando uma configuração de GoRouter:

lib/main.dart

// Add GoRouter configuration outside the App class
final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
      routes: [
        GoRoute(
          path: 'sign-in',
          builder: (context, state) {
            return SignInScreen(
              actions: [
                ForgotPasswordAction(((context, email) {
                  final uri = Uri(
                    path: '/sign-in/forgot-password',
                    queryParameters: <String, String?>{
                      'email': email,
                    },
                  );
                  context.push(uri.toString());
                })),
                AuthStateChangeAction(((context, state) {
                  final user = switch (state) {
                    SignedIn state => state.user,
                    UserCreated state => state.credential.user,
                    _ => null
                  };
                  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);
                  }
                  context.pushReplacement('/');
                })),
              ],
            );
          },
          routes: [
            GoRoute(
              path: 'forgot-password',
              builder: (context, state) {
                final arguments = state.uri.queryParameters;
                return ForgotPasswordScreen(
                  email: arguments['email'],
                  headerMaxExtent: 200,
                );
              },
            ),
          ],
        ),
        GoRoute(
          path: 'profile',
          builder: (context, state) {
            return ProfileScreen(
              providers: const [],
              actions: [
                SignedOutAction((context) {
                  context.pushReplacement('/');
                }),
              ],
            );
          },
        ),
      ],
    ),
  ],
);
// end of GoRouter configuration

// Change MaterialApp to MaterialApp.router and add the routerConfig
class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      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,
        useMaterial3: true,
      ),
      routerConfig: _router, // new
    );
  }
}

Cada tela tem um tipo diferente de ação associado com base no novo estado do fluxo de autenticação. Depois da maioria das mudanças de estado na autenticação, você pode redirecionar para uma tela preferencial, seja a tela inicial ou outra, como o perfil.

  1. No método de build da classe HomePage, integre o estado do app ao widget AuthFunc:

lib/home_page.dart (link em inglês)

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

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

class HomePage extends StatelessWidget {
  const HomePage({super.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'),
          // 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 une a um widget Consumer. O widget Consumer é a maneira mais comum de usar o pacote provider para recriar parte da árvore quando o estado do app mudar. O widget AuthFunc são os widgets complementares testados.

Testar o fluxo de autenticação

cdf2d25e436bd48d.png

  1. No app, toque no botão RSVP para iniciar o SignInScreen.

2a2cd6d69d172369.png

  1. Insira um endereço de e-mail. Se você já se registrou, o sistema vai pedir para você digitar uma senha. Caso contrário, o sistema vai pedir que você preencha o formulário de registro.

e5e65065dba36b54.png

  1. Insira uma senha com menos de seis caracteres para verificar o fluxo de tratamento de erros. Se você estiver registrado, verá a senha de.
  2. Insira senhas incorretas para verificar o fluxo de tratamento de erros.
  3. Digite a senha correta. Você vê a experiência de login, que permite que o usuário saia.

4ed811a25b0cf816.png

6. Gravar mensagens no Firestore

É ótimo saber que os usuários estão chegando, mas você precisa oferecer a eles outras coisas para fazer no app. E se eles pudessem deixar mensagens em um livro de visitas? Eles podem contar por que estão animados em participar ou quem espera conhecer.

Para armazenar as mensagens de chat que os usuários escrevem no app, use o Firestore.

Modelo de dados

O Firestore é um banco de dados NoSQL. Os dados armazenados nele são divididos em coleções, documentos, campos e subcoleções. Você armazena cada mensagem do chat como um documento em uma coleção guestbook, que é uma coleção de nível superior.

7c20dc8424bb1d84.png

Adicione mensagens ao Firestore

Nesta seção, você vai adicionar a funcionalidade para que os usuários gravem mensagens no banco de dados. Primeiro, adicione um campo de formulário e um botão de envio e, em seguida, adicione o código que conecta esses elementos ao banco de dados.

  1. Crie um novo arquivo chamado guest_book.dart, adicione um widget com estado GuestBook para construir os elementos da interface de um campo de mensagem e um botão "Enviar":

lib/guest_book.dart (link em inglês)

import 'dart:async';

import 'package:flutter/material.dart';

import 'src/widgets.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ê instancia um formulário para validar se a mensagem realmente tem conteúdo e mostrar ao usuário uma mensagem de erro se não houver nenhuma. Para validar um formulário, acesse o estado atrás dele com um GlobalKey. Para mais informações sobre chaves e como usá-las, consulte Quando usar chaves.

Observe também a forma como os widgets estão dispostos. Você tem um Row com um TextFormField e um StyledButton, que contém um Row. Observe também que o TextFormField é unido em um widget Expanded, o que força o TextFormField a preencher qualquer espaço extra na linha. Para entender melhor por que isso é necessário, consulte Noções básicas sobre restrições.

Agora que você tem um widget que permite ao usuário inserir um texto para adicionar ao livro de visitas, você precisa mostrá-lo na tela.

  1. Edite o corpo de HomePage para adicionar as duas linhas a seguir ao final dos filhos da 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 mostrar o widget, eles não são suficientes para fazer nada útil. Você atualizará esse código em breve para torná-lo funcional.

Visualização do app

A tela inicial do app no Android com integração de chat

A tela inicial do app no iOS com integração de chat

Tela inicial do app na Web com integração de chat

Tela inicial do app no macOS com integração de chat

Quando o usuário clica em ENVIAR, o snippet de código a seguir é acionado. Ele adiciona o conteúdo do campo de entrada da mensagem à coleção 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 na coleção guestbook.

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

  • No arquivo lib/app_state.dart, adicione o método addMessageToGuestBook. Você vai conectar esse recurso à interface do usuário na próxima etapa.

lib/app_state.dart (link em inglês)

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

import 'firebase_options.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.
}

Conectar interface e banco de dados

Você tem uma interface em que o usuário pode inserir o texto que quer adicionar ao livro de visitas e tem o código para adicionar a entrada ao Firestore. Agora você só precisa conectar os dois.

  • No arquivo lib/home_page.dart, faça a seguinte mudança no widget HomePage:

lib/home_page.dart (link em inglês)

import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'app_state.dart';
import 'guest_book.dart';                         // new
import 'src/authentication.dart';
import 'src/widgets.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 adicionadas no início desta etapa pela implementação completa. Use novamente Consumer<ApplicationState> para disponibilizar o estado do app para a parte da árvore renderizada. Isso permite reagir a alguém que insere uma mensagem na IU e publicá-la no banco de dados. Na próxima seção, você vai testar se as mensagens adicionadas foram publicadas no banco de dados.

Teste o envio de mensagens

  1. Se necessário, faça login no app.
  2. Digite uma mensagem, como Hey there!, e clique em ENVIAR.

Essa ação grava a mensagem no seu banco de dados do Firestore. No entanto, você não vê a mensagem no seu app do Flutter porque ainda precisa implementar a recuperação dos dados, o que será feito na próxima etapa. No entanto, no painel Database do console do Firebase, é possível ver a mensagem adicionada na coleção guestbook. Se você enviar mais mensagens, adicionará mais documentos à sua coleção guestbook. Por exemplo, confira o snippet de código a seguir:

713870af0b3b63c.png

7. Leia mensagens

Os convidados podem escrever mensagens no banco de dados, mas ainda não conseguem vê-las no app. Hora de corrigir isso.

Sincronizar mensagens

Para exibir mensagens, você precisa adicionar listeners que são acionados quando os dados são alterados e criar um elemento de IU que mostre novas mensagens. Você adiciona o código ao estado do app que detecta mensagens recém-adicionadas.

  1. Crie um novo arquivo guest_book_message.dart e adicione a classe a seguir para expor uma visualização estruturada dos dados armazenados no Firestore.

lib/guest_book_message.dart (link em inglês)

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

  final String name;
  final String message;
}
  1. No arquivo lib/app_state.dart, adicione estas importações:

lib/app_state.dart (link em inglês)

import 'dart:async';                                     // new

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'
    hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';
import 'guest_book_message.dart';                        // new
  1. Na seção de ApplicationState em que você define o estado e os getters, adicione as seguintes linhas:

lib/app_state.dart (link em inglês)

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

  // Add from here...
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // ...to here.
  1. Na seção de inicialização do ApplicationState, adicione as seguintes linhas para se inscrever em uma consulta na coleção de documentos quando um usuário fizer login e cancelar a inscrição quando ele sair:

lib/app_state.dart (link em inglês)

  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();
    });
  }

Essa seção é importante porque é nela que você constrói uma consulta sobre a coleção guestbook e processa a inscrição e o cancelamento da inscrição. Você ouve o stream, em que reconstrua um cache local das mensagens na coleção guestbook e também armazena uma referência a essa assinatura para que possa cancelar a inscrição mais tarde. Há muita coisa acontecendo aqui, então você precisa explorá-la em um depurador para inspecionar o que acontece para conseguir um modelo mental mais claro. Para mais informações, consulte Receber atualizações em tempo real com o Firestore.

  1. No arquivo lib/guest_book.dart, adicione a seguinte importação:
import 'guest_book_message.dart';
  1. No widget GuestBook, adicione uma lista de mensagens como parte da configuração para conectar esse estado de mudança à interface do usuário:

lib/guest_book.dart (link em inglês)

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();
}
  1. Em _GuestBookState, modifique o método build da seguinte maneira para expor essa configuração:

lib/guest_book.dart (link em inglês)

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ê una o conteúdo anterior do método build() com um widget Column e, em seguida, adiciona uma collection for na cauda dos filhos do Column para gerar um novo Paragraph para cada mensagem na lista.

  1. Atualize o corpo de HomePage para construir GuestBook corretamente com o novo parâmetro messages:

lib/home_page.dart (link em inglês)

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

Testar a sincronização de mensagens

O Firestore sincroniza dados de forma automática e instantânea com os clientes inscritos no banco de dados.

Testar a sincronização de mensagens:

  1. No app, encontre as mensagens que você criou anteriormente no banco de dados.
  2. Escrever novas mensagens. Eles aparecem instantaneamente.
  3. Abra seu espaço de trabalho em várias janelas ou guias. As mensagens são sincronizadas em tempo real entre as janelas e guias.
  4. Opcional: no menu Banco de dados do console do Firebase, exclua, modifique ou adicione novas mensagens manualmente. Todas as mudanças vão aparecer na interface.

Parabéns! Você lê os documentos do Firestore no seu app.

Visualização do app

A tela inicial do app no Android com integração de chat

A tela inicial do app no iOS com integração de chat

Tela inicial do app na Web com integração de chat

Tela inicial do app no macOS com integração de chat

8. Configure regras básicas de segurança

Você configurou inicialmente o Firestore para usar o modo de teste, o que significa que seu banco de dados está aberto para leituras e gravações. No entanto, use o modo de teste apenas durante os estágios iniciais de desenvolvimento. A prática recomendada é configurar regras de segurança para o banco de dados ao desenvolver o app. A segurança é essencial para a estrutura e o comportamento do app.

As regras de segurança do Firebase permitem controlar o acesso a documentos e coleções no seu banco de dados. A sintaxe de regras flexíveis permite que você crie regras que correspondem desde todas as gravações em todo o banco de dados até operações em um documento específico.

Configure regras básicas de segurança:

  1. No menu Desenvolver do Console do Firebase, clique em Banco de dados > Regras. Você verá as seguintes regras de segurança padrão e um aviso sobre a possibilidade de elas serem públicas:

7767a2d2e64e7275.png

  1. Identifique as coleções em que o app grava dados:

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

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

Como você usou o UID de autenticação como um campo em cada documento de livro de visitas, é possível obter o UID de autenticação e verificar se qualquer pessoa que tentar gravar no documento tem um UID de autenticação correspondente.

  1. Adicione as regras de leitura e gravação ao seu conjunto de regras:
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, apenas os usuários conectados podem ler as mensagens no livro de visitas, mas somente o autor de uma mensagem pode editá-la.

  1. Adicione a 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 você aprendeu

Registrar o status de RSVP de um convidado

No momento, o app só permite que as pessoas conversem quando estiverem interessadas no evento. Além disso, a única maneira de saber se alguém está chegando é quando alguém diz isso no chat.

Nesta etapa, você se organiza e informa quantas pessoas vão participar. Você adiciona alguns recursos ao estado do app. A primeira é a possibilidade de um usuário conectado indicar se vai participar. O segundo é um contador de quantas pessoas vão participar.

  1. No arquivo lib/app_state.dart, adicione as seguintes linhas à seção de acessadores do ApplicationState para que o código da interface possa interagir com esse estado:

lib/app_state.dart (link em inglês)

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});
  }
}
  1. Atualize o método init() do ApplicationState desta maneira:

lib/app_state.dart (link em inglês)

  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) {
        _loggedIn = true;
        _emailVerified = user.emailVerified;
        _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 {
        _loggedIn = false;
        _emailVerified = false;
        _guestBookMessages = [];
        _guestBookSubscription?.cancel();
        _attendingSubscription?.cancel(); // new
      }
      notifyListeners();
    });
  }

Esse código adiciona uma consulta sempre inscrita para determinar o número de participantes e uma segunda consulta que fica ativa somente enquanto um usuário está conectado para determinar se ele participará.

  1. Adicione a seguinte enumeração na parte superior do arquivo lib/app_state.dart.

lib/app_state.dart (link em inglês)

enum Attending { yes, no, unknown }
  1. Crie um novo arquivo yes_no_selection.dart e defina um novo widget que funciona como botões de opção:

lib/yes_no_selection.dart (link em inglês)

import 'package:flutter/material.dart';

import 'app_state.dart';
import 'src/widgets.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: [
              FilledButton(
                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),
              FilledButton(
                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'),
              ),
            ],
          ),
        );
    }
  }
}

Ela começa em um estado indeterminado com Sim ou Não selecionados. Quando o usuário seleciona se quer participar, você mostra essa opção destacada com um botão preenchido, e a outra é ocultada com uma renderização simples.

  1. Atualize o método build() de HomePage para aproveitar YesNoSelection, permita que um usuário conectado indique se vai participar e mostre o número de participantes do evento:

lib/home_page.dart (link em inglês)

Consumer<ApplicationState>(
  builder: (context, appState, _) => Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // Add from here...
      switch (appState.attendees) {
        1 => const Paragraph('1 person going'),
        >= 2 => Paragraph('${appState.attendees} people going'),
        _ => 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á configurou algumas regras, os dados adicionados com os botões serão rejeitados. É necessário atualizar as regras para permitir adições à coleção attendees.

  1. Na coleção attendees, capture o UID de autenticação usado como o nome do documento e verifique se o uid do remetente é igual ao documento que ele está escrevendo:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

Isso permite que todos leiam a lista de participantes, porque não há dados particulares lá, mas somente o criador pode atualizá-la.

  1. Adicione a 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;

    }
  }
}
  1. Opcional: no app, clique nos botões para ver os resultados no painel do Firestore no Console do Firebase.

Visualização do app

Tela inicial do app no Android

Tela inicial do app no iOS

Tela inicial do app na Web

Tela inicial do app no macOS

10. Parabéns!

Você usou o Firebase para criar um app da Web interativo em tempo real.

Saiba mais