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á alguns dos fundamentos do Firebase para criar aplicativos móveis do Flutter para Android e iOS.

Pré-requisitos

O que você aprenderá

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

A tela inicial do aplicativo no Android

A tela inicial do aplicativo no iOS

O que você precisará

Qualquer um dos seguintes dispositivos:

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

Você também precisa do seguinte:

  • Um navegador de sua escolha, como o Google Chrome.
  • Um IDE ou editor de texto de sua escolha configurado com os plug-ins Dart e Flutter, como Android Studio ou Visual Studio Code .
  • A versão stable mais recente do Flutter ou beta , se você gosta de viver no limite.
  • Uma Conta do Google para a criação e gerenciamento do seu projeto Firebase.
  • A CLI do Firebase fez login na sua Conta do Google.

2. Obtenha o código de amostra

Baixe a versão inicial do seu projeto no GitHub:

  1. Na linha de comando, clone o repositório GitHub 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 para 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 instantâneos que mostram como seu projeto deve ficar no final de cada etapa. Por exemplo, você está na segunda etapa.

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

Se você quiser avançar ou ver como algo deve ficar depois de uma etapa, procure no diretório com o nome da etapa na qual 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 do Flutter ainda não funcional.

Localize os arquivos que precisam de trabalho

O código neste aplicativo está distribuído em 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 : Este arquivo contém o ponto de entrada principal e o widget do aplicativo.
    • lib/home_page.dart : Este arquivo contém o widget da página inicial.
    • lib/src/widgets.dart : Este arquivo contém vários widgets para ajudar a padronizar o estilo do aplicativo. Eles compõem a tela do aplicativo inicial.
    • lib/src/authentication.dart : este arquivo contém uma implementação parcial de autenticação com um conjunto de widgets para criar uma experiência de usuário de login 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 adicionará em breve.

Você adiciona arquivos adicionais conforme necessário para criar o restante do aplicativo.

Revise o arquivo lib/main.dart

Este aplicativo aproveita o pacote google_fonts para tornar o Roboto a fonte padrão em todo o aplicativo. Você pode explorar fonts.google.com e usar as fontes que descobrir lá em diferentes partes do app.

Você usa 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.

Esta é a aparência do seu aplicativo no Android, iOS, Web e macOS:

A tela inicial do aplicativo no Android

A tela inicial do aplicativo no iOS

A tela inicial do aplicativo na web

A tela inicial do aplicativo no macOS

3. Crie e configure um projeto Firebase

A exibição de informações do evento é ótima para seus convidados, mas não é muito útil para ninguém por conta própria. Você precisa adicionar algumas funcionalidades dinâmicas ao aplicativo. Para fazer isso, você precisa conectar o Firebase ao seu aplicativo. Para começar a usar o Firebase, você precisa criar e configurar um projeto do Firebase.

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 , insira 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 porque você não o usará para este aplicativo.

b7138cde5f2c7b61.png

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

O aplicativo usa os seguintes produtos Firebase, que estão disponíveis para aplicativos da web:

  • Autenticação: permite que os usuários façam login no seu aplicativo.
  • 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 habilitá-los no Firebase console.

Habilitar autenticação de entrada de e-mail

  1. No painel de visão geral do projeto do Firebase console, expanda o menu Build .
  2. Clique em Autenticação > Introdução > Método de login > E-mail/Senha > Ativar > Salvar .

58e3e3e23c2f16a4.png

Ativar Firestore

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

Ativar Firestore:

  • No menu Construir , clique em Cloud Firestore > Criar banco de dados.

99e8429832d23fa3.png

  1. Selecione Iniciar no modo de teste e 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.

6be00e26c72ea032.png

  1. Clique em Avançar e selecione o local para seu banco de dados. Você pode usar o padrão. Não é possível alterar o local posteriormente.

278656eefcfb0216.png

  1. Clique em Habilitar .

4. Configure o Firebase

Para usar Firebase com Flutter, você precisa concluir as seguintes tarefas para configurar o projeto Flutter para usar as bibliotecas FlutterFire corretamente:

  1. Adicione as dependências do FlutterFire ao seu projeto.
  2. Cadastre a plataforma desejada no projeto Firebase.
  3. 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 os subdiretórios android , ios , macos e web , que 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ê usa neste 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 Firebase Flutter.

$ flutter pub add firebase_auth

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

$ 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 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 iOS, Android, macOS e executor da Web para usar o Firebase adequadamente. Você também usa o pacote provider que permite a separação da lógica de negócios da lógica de exibição.

Instale a CLI do FlutterFire

A CLI do FlutterFire depende da CLI do Firebase subjacente.

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

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

Configurar seus aplicativos

A CLI extrai informações do seu projeto 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 guia você pelos seguintes processos:

  1. Selecione um projeto do Firebase com base no arquivo .firebaserc ou no Console do Firebase.
  2. Determine plataformas para configuração, como Android, iOS, macOS e web.
  3. Identifique os aplicativos do Firebase dos quais extrair a configuração. Por padrão, a CLI tenta 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 macOS

O Flutter no macOS cria aplicativos totalmente em área restrita. Como este aplicativo se integra à rede para se comunicar com os servidores Firebase, você precisa 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>

Para obter mais informações, consulte Suporte de área de trabalho para Flutter .

5. Adicione a funcionalidade RSVP

Agora que você adicionou o Firebase ao aplicativo, pode criar um botão RSVP que registre as pessoas com autenticação . Para Android nativo, iOS nativo e Web, existem pacotes FirebaseUI Auth pré-criados, mas você precisa 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 para a maior parte do fluxo de autenticação. Você implementa a lógica de negócios para integrar a Autenticação ao aplicativo.

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

Use o pacote do provider para disponibilizar um objeto de estado de aplicativo centralizado em toda a árvore de widgets do Flutter do aplicativo:

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

lib/app_state.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';

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 de import introduzem Firebase Core e Auth, extraem o pacote do provider que disponibiliza o objeto de estado do aplicativo em toda a árvore de widgets e incluem os widgets de autenticação do pacote 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.

Você só usa um provedor para comunicar o estado do status de login de um usuário ao aplicativo. Para permitir que um usuário faça login, use as UIs fornecidas pelo pacote firebase_ui_auth , que é uma ótima maneira de inicializar rapidamente as telas de login em seus aplicativos.

Integre o fluxo de autenticação

  1. Modifique as importações na parte superior 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 aplicativo com a inicialização do aplicativo e adicione o fluxo de autenticação à 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 aplicativo com o widget ChangeNotifierProvider . Você usa essa classe de provider específica porque o objeto de estado do aplicativo estende a classe ChangeNotifier , que permite que o pacote do provider saiba quando exibir novamente os widgets dependentes.

  1. Atualize seu aplicativo para lidar com a navegação para diferentes telas que o FirebaseUI fornece para você, criando uma configuração 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) {
                  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);
                    }
                    context.pushReplacement('/');
                  }
                })),
              ],
            );
          },
          routes: [
            GoRoute(
              path: 'forgot-password',
              builder: (context, state) {
                final arguments = state.queryParams;
                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 associada a ela com base no novo estado do fluxo de autenticação. Após a maioria das alterações de estado na autenticação, você pode redirecionar de volta para uma tela preferida, seja a tela inicial ou uma tela diferente, como o perfil.

  1. No método de construção da classe HomePage , integre o estado do aplicativo com o widget AuthFunc :

lib/home_page.dart

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 envolve em um widget Consumer . O widget Consumidor é a maneira usual de usar o pacote do provider para reconstruir parte da árvore quando o estado do aplicativo é alterado. O widget AuthFunc são os widgets complementares que você testa.

Teste o fluxo de autenticação

cdf2d25e436bd48d.png

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

2a2cd6d69d172369.png

  1. Digite um endereço de e-mail. Se você já estiver registrado, o sistema solicitará que você digite uma senha. Caso contrário, o sistema solicitará 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 oferece ao usuário a capacidade de fazer logout.

4ed811a25b0cf816.png

6. Escreva mensagens no Firestore

É ótimo saber que os usuários estão chegando, mas você precisa dar aos convidados outra coisa para fazer no aplicativo. E se eles pudessem deixar mensagens em um livro de visitas? Eles podem compartilhar por que estão ansiosos para vir ou quem esperam conhecer.

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

Modelo de dados

O 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ê armazena cada mensagem do bate-papo como um documento em uma coleção de gustbook , que é uma coleção de nível superior.

7c20dc8424bb1d84.png

Adicionar mensagens ao Firestore

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

  1. Crie um novo arquivo chamado guest_book.dart , adicione um widget Stateful GuestBook para construir os elementos da interface do usuário de um campo de mensagem e um botão enviar:

lib/guest_book.dart

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 poder validar se a mensagem realmente contém conteúdo e mostrar ao usuário uma mensagem de erro, caso não haja nenhuma. Para validar um formulário, você acessa o estado do formulário por trás do formulário com uma GlobalKey . Para obter mais informações sobre chaves e como usá-las, consulte Quando usar chaves .

Observe também a forma como os widgets são dispostos, você tem uma Row com um TextFormField e um StyledButton , que contém uma Row . Observe também que o TextFormField é agrupado em um widget Expanded , 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 algum texto para adicionar ao livro de visitas, você precisa colocá-lo na tela.

  1. Edite o corpo de HomePage para adicionar as duas linhas a seguir no final dos filhos de 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ê atualiza esse código em breve para torná-lo funcional.

Visualização do aplicativo

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

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

A tela inicial do app na web com integração de chat

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

Quando um usuário clica em SEND , ele aciona o seguinte trecho de código. Ele adiciona o conteúdo do campo de entrada de mensagem à coleção de livro 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 na coleção do guestbook de visitas.

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

  • No arquivo lib/app_state.dart , inclua o método addMessageToGuestBook . Você conecta esse recurso com a interface do usuário na próxima etapa.

lib/app_state.dart

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.
}

Conecte a interface do usuário e o banco de dados

Você tem uma IU onde o usuário pode inserir o texto que deseja adicionar ao livro de visitas e você tem o código para adicionar a entrada ao Firestore. Agora tudo que você precisa fazer é conectar os dois.

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

lib/home_page.dart

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 que incluiu no início desta etapa pela implementação completa. Você novamente usa Consumer<ApplicationState> para disponibilizar o estado do aplicativo para a parte da árvore que você renderiza. Isso permite que você reaja a alguém que insere uma mensagem na interface do usuário e a publica no banco de dados. Na próxima seção, você testa se as mensagens adicionadas são publicadas no banco de dados.

Teste o envio de mensagens

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

Esta ação grava a mensagem em seu banco de dados do Firestore. No entanto, você não vê a mensagem em seu aplicativo Flutter real porque ainda precisa implementar a recuperação dos dados, o que será feito na próxima etapa. No entanto, no painel do banco de dados do Firebase console, você pode ver sua mensagem adicionada na coleção do guestbook de visitas. Se você enviar mais mensagens, adicionará mais documentos à sua coleção de livro de guestbook . Por exemplo, veja o trecho de código a seguir:

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. Hora de consertar isso!

Sincronizar mensagens

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

  1. Crie um novo arquivo guest_book_message.dart , adicione a classe a seguir para expor uma visualização estruturada dos dados que você armazena no Firestore.

lib/guest_book_message.dart

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

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

lib/app_state.dart

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 do ApplicationState onde você define o estado e os getters, adicione as seguintes linhas:

lib/app_state.dart

  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 de ApplicationState , adicione as seguintes linhas para assinar uma consulta sobre a coleção de documentos quando um usuário fizer login e cancelar a assinatura quando ele fizer logout:

lib/app_state.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 porque é onde você constrói uma consulta sobre a coleção de guestbook de visitas e lida com a assinatura e o cancelamento da assinatura nessa coleção. Você ouve o fluxo, onde 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 possa cancelá-la posteriormente. Há muita coisa acontecendo aqui, então você deve explorá-lo em um depurador para inspecionar o que acontece para obter um modelo mental mais claro. Para obter mais informações, consulte Obtenha 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

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 de build da seguinte maneira para expor esta configuração:

lib/guest_book.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 build() com um widget Column e, em seguida, adiciona uma coleção no final dos filhos de Column para gerar um novo Paragraph para cada mensagem na lista de mensagens.

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

lib/home_page.dart

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
        ),
      ],
    ],
  ),
),

Sincronização de mensagem de teste

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

Sincronização de mensagem de teste:

  1. No aplicativo, encontre as mensagens que você criou anteriormente no banco de dados.
  2. Escreva 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 nas janelas e guias.
  4. Opcional: no menu Banco de dados do console do Firebase, exclua, modifique ou adicione novas mensagens manualmente. Todas as alterações aparecem na interface do usuário.

Parabéns! Você lê os documentos do Firestore em seu aplicativo!

Visualização do aplicativo

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

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

A tela inicial do app na web com integração de chat

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

8. Configure regras básicas de segurança

Inicialmente, você configurou 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, 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 ao desenvolver seu aplicativo. A segurança é parte integrante da estrutura e do comportamento do seu aplicativo.

As regras de segurança do Firebase permitem que você controle o acesso a documentos e coleções em seu banco de dados. A sintaxe flexível das regras permite criar 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.

Configure as regras básicas de segurança:

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

7767a2d2e64e7275.png

  1. Identifique as coleções nas quais o aplicativo grava dados:

Em match /databases/{database}/documents , identifique a coleção que deseja 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 do livro de visitas, você pode 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 usuários conectados podem ler mensagens no livro de visitas, mas apenas o autor de uma mensagem pode editá-la.

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

Registrar o status RSVP de um participante

No momento, seu aplicativo só permite que as pessoas conversem quando estão interessadas no evento. Além disso, a única maneira de saber se alguém está vindo é quando eles dizem isso no chat.

Nessa etapa, você se organiza e avisa quantas pessoas estão chegando. Você adiciona alguns recursos ao estado do aplicativo. A primeira é a capacidade de um usuário conectado indicar se está participando. O segundo é um contador de quantas pessoas estão participando.

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

lib/app_state.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});
  }
}
  1. Atualize o método init() do ApplicationState da seguinte forma:

lib/app_state.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();
    });
  }

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

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

lib/app_state.dart

enum Attending { yes, no, unknown }
  1. Crie um novo arquivo yes_no_selection.dart , defina um novo widget que atue como botões de rádio:

lib/yes_no_selection.dart

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: [
              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'),
              ),
            ],
          ),
        );
    }
  }
}

Ele inicia em um estado indeterminado sem Sim nem Não selecionado. Depois que o usuário seleciona se está participando, você mostra essa opção destacada com um botão preenchido e a outra opção recua com uma renderização plana.

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

lib/home_page.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

Você já configurou algumas regras, então os dados que você adicionar com os botões serão rejeitados. Você precisa atualizar as regras para permitir adições à coleção de attendees .

  1. Na coleção de attendees , pegue o UID de autenticação que você usou como o nome do documento e verifique se o uid do remetente é o mesmo do documento que 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 privados lá, mas apenas o criador pode atualizá-los.

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

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

Visualização do aplicativo

A tela inicial do aplicativo no Android

A tela inicial do aplicativo no iOS

A tela inicial do aplicativo na web

A tela inicial do aplicativo no macOS

10. Parabéns!

Você usou o Firebase para criar um aplicativo da web interativo e em tempo real.

Saber mais

,

1. Antes de começar

Neste codelab, você aprenderá alguns dos fundamentos do Firebase para criar aplicativos móveis do Flutter para Android e iOS.

Pré-requisitos

O que você aprenderá

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

A tela inicial do aplicativo no Android

A tela inicial do aplicativo no iOS

O que você precisará

Qualquer um dos seguintes dispositivos:

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

Você também precisa do seguinte:

  • Um navegador de sua escolha, como o Google Chrome.
  • Um IDE ou editor de texto de sua escolha configurado com os plug-ins Dart e Flutter, como Android Studio ou Visual Studio Code .
  • A versão stable mais recente do Flutter ou beta , se você gosta de viver no limite.
  • Uma Conta do Google para a criação e gerenciamento do seu projeto Firebase.
  • A CLI do Firebase fez login na sua Conta do Google.

2. Obtenha o código de amostra

Baixe a versão inicial do seu projeto no GitHub:

  1. Na linha de comando, clone o repositório GitHub 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 para 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 instantâneos que mostram como seu projeto deve ficar no final de cada etapa. Por exemplo, você está na segunda etapa.

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

Se você quiser avançar ou ver como algo deve ficar depois de uma etapa, procure no diretório com o nome da etapa na qual 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 do Flutter ainda não funcional.

Localize os arquivos que precisam de trabalho

O código neste aplicativo está distribuído em 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 : Este arquivo contém o ponto de entrada principal e o widget do aplicativo.
    • lib/home_page.dart : Este arquivo contém o widget da página inicial.
    • lib/src/widgets.dart : Este arquivo contém vários widgets para ajudar a padronizar o estilo do aplicativo. Eles compõem a tela do aplicativo inicial.
    • lib/src/authentication.dart : este arquivo contém uma implementação parcial de autenticação com um conjunto de widgets para criar uma experiência de usuário de login 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 adicionará em breve.

Você adiciona arquivos adicionais conforme necessário para criar o restante do aplicativo.

Revise o arquivo lib/main.dart

Este aplicativo aproveita o pacote google_fonts para tornar o Roboto a fonte padrão em todo o aplicativo. Você pode explorar fonts.google.com e usar as fontes que descobrir lá em diferentes partes do app.

Você usa 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.

Esta é a aparência do seu aplicativo no Android, iOS, Web e macOS:

A tela inicial do aplicativo no Android

A tela inicial do aplicativo no iOS

A tela inicial do aplicativo na web

A tela inicial do aplicativo no macOS

3. Crie e configure um projeto Firebase

A exibição de informações do evento é ótima para seus convidados, mas não é muito útil para ninguém por conta própria. Você precisa adicionar algumas funcionalidades dinâmicas ao aplicativo. Para fazer isso, você precisa conectar o Firebase ao seu aplicativo. Para começar a usar o Firebase, você precisa criar e configurar um projeto do Firebase.

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 , insira 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 porque você não o usará para este aplicativo.

b7138cde5f2c7b61.png

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

O aplicativo usa os seguintes produtos Firebase, que estão disponíveis para aplicativos da web:

  • Autenticação: permite que os usuários façam login no seu aplicativo.
  • 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 habilitá-los no Firebase console.

Habilitar autenticação de entrada de e-mail

  1. No painel de visão geral do projeto do Firebase console, expanda o menu Build .
  2. Clique em Autenticação > Introdução > Método de login > E-mail/Senha > Ativar > Salvar .

58e3e3e23c2f16a4.png

Ativar Firestore

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

Ativar Firestore:

  • No menu Construir , clique em Cloud Firestore > Criar banco de dados.

99e8429832d23fa3.png

  1. Selecione Iniciar no modo de teste e 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.

6be00e26c72ea032.png

  1. Clique em Avançar e selecione o local para seu banco de dados. Você pode usar o padrão. Não é possível alterar o local posteriormente.

278656eefcfb0216.png

  1. Clique em Habilitar .

4. Configure o Firebase

Para usar Firebase com Flutter, você precisa concluir as seguintes tarefas para configurar o projeto Flutter para usar as bibliotecas FlutterFire corretamente:

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

In the top-level directory of your Flutter app, there are android , ios , macos and web subdirectories, which hold the platform-specific configuration files for iOS and Android, respectively.

Configure dependencies

You need to add the FlutterFire libraries for the two Firebase products that you use in this app: Authentication and Firestore.

  • From the command line, add the following depencies:
$ flutter pub add firebase_core

The firebase_core package is the common code required for all Firebase Flutter plugins.

$ flutter pub add firebase_auth

The firebase_auth package enables integration with Authentication.

$ flutter pub add cloud_firestore

The cloud_firestore package enables access to Firestore data storage.

$ flutter pub add provider

The firebase_ui_auth package provides a set of widgets and utilities to increase developer velocity with authentication flows.

$ flutter pub add firebase_ui_auth

You added the required packages, but you also need to configure the iOS, Android, macOS, and Web runner projects to appropriately use Firebase. You also use the provider package that enables separation of business logic from display logic.

Install the FlutterFire CLI

The FlutterFire CLI depends on the underlying Firebase CLI.

  1. If you haven't done so already, install the Firebase CLI on your machine.
  2. Install the FlutterFire CLI:
$ dart pub global activate flutterfire_cli

Once installed, the flutterfire command is globally available.

Configure your apps

The CLI extracts information from your Firebase project and selected project apps to generate all the configuration for a specific platform.

In the root of your app, run the configure command:

$ flutterfire configure

The configuration command guides you through the following processes:

  1. Select a Firebase project based on the .firebaserc file or from the Firebase Console.
  2. Determine platforms for configuration, such as Android, iOS, macOS, and web.
  3. Identify the Firebase apps from which to extract configuration. By default, the CLI attempts to automatically match Firebase apps based on your current project configuration.
  4. Generate a firebase_options.dart file in your project.

Configure macOS

Flutter on macOS builds fully sandboxed apps. As this app integrates with the network to communicate with the Firebase servers, you need to configure your app with network client privileges.

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>

For more information, see Desktop support for Flutter .

5. Add RSVP functionality

Now that you added Firebase to the app, you can create an RSVP button that registers people with Authentication . For Android native, iOS native, and Web, there are prebuilt FirebaseUI Auth packages, but you need to build this capability for Flutter.

The project that you retrieved earlier included a set of widgets that implements the user interface for most of the authentication flow. You implement the business logic to integrate Authentication with the app.

Add business logic with the Provider package

Use the provider package to make a centralized app state object available throughout the app's tree of Flutter widgets:

  1. Create a new file named app_state.dart with the following content:

lib/app_state.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';

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

The import statements introduce Firebase Core and Auth, pull in the provider package that makes app state object available throughout the widget tree, and include the authentication widgets from the firebase_ui_auth package.

This ApplicationState application state object has one main responsibility for this step, which is to alert the widget tree that there was an update to an authenticated state.

You only use a provider to communicate the state of a user's login status to the app. To let a user log in, you use the UIs provided by the firebase_ui_auth package, which is a great way to quickly bootstrap login screens in your apps.

Integrate the authentication flow

  1. Modify the imports at the top of the lib/main.dart file:

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. Connect the app state with the app initialization and then add the authentication flow to HomePage :

lib/main.dart

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

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

The modification to the main() function makes the provider package responsible for the instantiation of the app state object with the ChangeNotifierProvider widget. You use this specific provider class because the app state object extends the ChangeNotifier class, which lets the provider package know when to redisplay dependent widgets.

  1. Update your app to handle navigation to different screens that FirebaseUI provides for you, by creating a GoRouter configuration:

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) {
                  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);
                    }
                    context.pushReplacement('/');
                  }
                })),
              ],
            );
          },
          routes: [
            GoRoute(
              path: 'forgot-password',
              builder: (context, state) {
                final arguments = state.queryParams;
                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
    );
  }
}

Each screen has a different type of action associated with it based on the new state of the authentication flow. After most state changes in authentication, you can reroute back to a preferred screen, whether it's the home screen or a different screen, such as profile.

  1. In the HomePage class's build method, integrate the app state with the AuthFunc widget:

lib/home_page.dart

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!',
          ),
        ],
      ),
    );
  }
}

You instantiate the AuthFunc widget and wrap it in a Consumer widget. The Consumer widget is the usual way that the provider package can be used to rebuild part of the tree when the app state changes. The AuthFunc widget is the supplementary widgets that you test.

Test the authentication flow

cdf2d25e436bd48d.png

  1. In the app, tap the RSVP button to initiate the SignInScreen .

2a2cd6d69d172369.png

  1. Enter an email address. If you're already registered, the system prompts you to enter a password. Otherwise, the system prompts you to complete the registration form.

e5e65065dba36b54.png

  1. Enter a password that's less than six characters to check the error-handling flow. If you're registered, you see the password for instead.
  2. Enter incorrect passwords to check the error-handling flow.
  3. Enter the correct password. You see the logged-in experience, which offers the user the ability to log out.

4ed811a25b0cf816.png

6. Write messages to Firestore

It's great to know that users are coming, but you need to give the guests something else to do in the app. What if they could leave messages in a guestbook? They can share why they're excited to come or who they hope to meet.

To store the chat messages that users write in the app, you use Firestore .

Data model

Firestore is a NoSQL database, and data stored in the database is split into collections, documents, fields, and subcollections. You store each message of the chat as a document in a gustbook collection, which is a top-level collection.

7c20dc8424bb1d84.png

Add messages to Firestore

In this section, you add the functionality for users to write messages to the database. First, you add a form field and send button, and then you add the code that connects these elements with the database.

  1. Create a new file named guest_book.dart , add a GuestBook stateful widget to construct the UI elements of a message field and a send button:

lib/guest_book.dart

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'),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

There are a couple of points of interest here. First, you instantiate a form so that you can validate that the message actually contains content and show the user an error message if there isn't any. To validate a form, you access the form state behind the form with a GlobalKey . For more information about Keys and how to use them, see When to Use Keys .

Also note the way that the widgets are laid out, you have a Row with a TextFormField and a StyledButton , which contains a Row . Also note the TextFormField is wrapped in an Expanded widget, which forces the TextFormField to fill any extra space in the row. To better understand why this is required, see Understanding constraints .

Now that you have a widget that enables the user to enter some text to add to the Guest Book, you need to get it on the screen.

  1. Edit the body of HomePage to add the following two lines at the end of the ListView 's children:
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)),

While this is enough to display the widget, it isn't sufficient to do anything useful. You update this code shortly to make it functional.

App preview

The home screen of the app on Android with chat integration

The home screen of the app on iOS with chat integration

The home screen of the app on web with chat integration

The home screen of the app on macOS with chat integration

When a user clicks SEND , it triggers the following code snippet. It adds the contents of the message input field to the guestbook collection of the database. Specifically, the addMessageToGuestBook method adds the message content to a new document with an automatically generated ID in the guestbook collection.

Note that FirebaseAuth.instance.currentUser.uid is a reference to the autogenerated unique ID that Authentication gives for all logged-in users.

  • In the lib/app_state.dart file, add the addMessageToGuestBook method. You connect this capability with the user interface in the next step.

lib/app_state.dart

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.
}

Connect UI and database

You have a UI where the user can enter the text they want to add to the Guest Book and you have the code to add the entry to Firestore. Now all you need to do is connect the two.

  • In the lib/home_page.dart file, make the following change to the HomePage widget:

lib/home_page.dart

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.
        ],
      ),
    );
  }
}

You replaced the two lines that you added at the start of this step with the full implementation. You again use Consumer<ApplicationState> to make the app state available to the part of the tree that you render. This lets you react to someone who enters a message in the UI and publish it in the database. In the next section, you test whether the added messages are published in the database.

Test sending messages

  1. If necessary, sign in to the app.
  2. Enter a message, such as Hey there! , and then click SEND .

This action writes the message to your Firestore database. However, you don't see the message in your actual Flutter app because you still need to implement retrieval of the data, which you do in the next step. However, in the Firebase console's Database dashboard , you can see your added message in the guestbook collection. If you send more messages, you add more documents to your guestbook collection. For example, see the following code snippet:

713870af0b3b63c.png

7. Read messages

It's lovely that guests can write messages to the database, but they can't see them in the app yet. Time to fix that!

Synchronize messages

To display messages, you need to add listeners that trigger when data changes and then create a UI element that shows new messages. You add code to the app state that listens for newly added messages from the app.

  1. Create a new file guest_book_message.dart , add the following class to expose a structured view of the data that you store in Firestore.

lib/guest_book_message.dart

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

  final String name;
  final String message;
}
  1. In the lib/app_state.dart file, add the following imports:

lib/app_state.dart

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. In section of ApplicationState where you define state and getters, add the following lines:

lib/app_state.dart

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

  // Add from here...
  StreamSubscription<QuerySnapshot>? _guestBookSubscription;
  List<GuestBookMessage> _guestBookMessages = [];
  List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
  // ...to here.
  1. In the initialization section of ApplicationState , add the following lines to subscribe to a query over the document collection when a user logs in and unsubscribe when they log out:

lib/app_state.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();
    });
  }

This section is important because it's where you construct a query over the guestbook collection, and handle subscribing and unsubscribing to this collection. You listen to the stream, where you reconstruct a local cache of the messages in the guestbook collection and also store a reference to this subscription so that you can unsubscribe from it later. There's a lot going on here, so you should explore it in a debugger to inspect what happens to get a clearer mental model. For more information, see Get realtime updates with Firestore .

  1. In the lib/guest_book.dart file, add the following import:
import 'guest_book_message.dart';
  1. In the GuestBook widget, add a list of messages as part of the configuration to connect this changing state to the user interface:

lib/guest_book.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();
}
  1. In _GuestBookState , modify the build method as follows to expose this configuration:

lib/guest_book.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.
    );
  }
}

You wrap the previous content of the build() method with a Column widget and then you add a collection for at the tail of the Column 's children to generate a new Paragraph for each message in the list of messages.

  1. Update the body of HomePage to correctly construct GuestBook with the new messages parameter:

lib/home_page.dart

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
        ),
      ],
    ],
  ),
),

Test message synchronization

Firestore automatically and instantly synchronizes data with clients subscribed to the database.

Test message synchronization:

  1. In the app, find the messages that you created earlier in the database.
  2. Write new messages. They appear instantly.
  3. Open your workspace in multiple windows or tabs. The messages sync in real time across the windows and tabs.
  4. Optional: In the Firebase console's Database menu, manually delete, modify, or add new messages. All changes appear in the UI.

Congratulations! You read Firestore documents in your app!

App preview

The home screen of the app on Android with chat integration

The home screen of the app on iOS with chat integration

The home screen of the app on web with chat integration

The home screen of the app on macOS with chat integration

8. Set up basic security rules

You initially set up Firestore to use test mode, which means that your database is open for reads and writes. However, you should only use test mode during early stages of development. As a best practice, you should set up security rules for your database as you develop your app. Security is integral to your app's structure and behavior.

Firebase Security Rules let you control access to documents and collections in your database. The flexible rules syntax lets you create rules that match anything from all writes to the entire database to operations on a specific document.

Set up basic security rules:

  1. In the Firebase console's Develop menu, click Database > Rules . You should see the following default security rules and a warning about the rules being public:

7767a2d2e64e7275.png

  1. Identify the collections to which the app writes data:

In match /databases/{database}/documents , identify the collection that you want to secure:

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

Because you used the Authentication UID as a field in each guestbook document, you can get the Authentication UID and verify that anyone attempting to write to the document has a matching Authentication UID.

  1. Add the read and write rules to your rule set:
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;
    }
  }
}

Now, only signed-in users can read messages in the guest book, but only a message's author can edit a message.

  1. Add data validation to ensure that all the expected fields are present in the document:
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. Bonus step: Practice what you've learned

Record an attendee's RSVP status

Right now, your app only allows people to chat when they're interested in the event. Also, the only way that you know whether someone's coming is when they say so in the chat.

In this step, you get organized and let people know how many people are coming. You add a couple of capabilities to the app state. The first is the ability for a logged-in user to nominate whether they're attending. The second is a counter of how many people are attending.

  1. In the lib/app_state.dart file, add the following lines to the accessors section of the ApplicationState so that the UI code can interact with this state:

lib/app_state.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});
  }
}
  1. Update the ApplicationState 's init() method as follows:

lib/app_state.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();
    });
  }

This code adds an always-subscribed query to determine the number of attendees and a second query that's only active while a user is logged in to determine whether the user is attending.

  1. Add the following enumeration at the top of the lib/app_state.dart file.

lib/app_state.dart

enum Attending { yes, no, unknown }
  1. Create a new file yes_no_selection.dart , define a new widget that acts like radio buttons:

lib/yes_no_selection.dart

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: [
              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'),
              ),
            ],
          ),
        );
    }
  }
}

It starts in an indeterminate state with neither Yes nor No selected. Once the user selects whether they're attending, you show that option highlighted with a filled button and the other option recedes with a flat rendering.

  1. Update HomePage 's build() method to take advantage of YesNoSelection , enable a logged-in user to nominate whether they're attending, and display the number of attendees for the event:

lib/home_page.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,
        ),
      ],
    ],
  ),
),

Add rules

You already set up some rules, so the data that you add with the buttons will be rejected. You need to update the rules to allow additions to the attendees collection.

  1. In the attendees collection, grab the Authentication UID that you used as the document name and verify that the submitter's uid is the same as the document they're writing:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ... //
    match /attendees/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

This lets everyone read the attendees list because there's no private data there, but only the creator can update it.

  1. Add data validation to ensure that all the expected fields are present in the document:
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. Optional: In the app, click buttons to see the results in the Firestore dashboard in the Firebase console.

App preview

The home screen of the app on Android

The home screen of the app on iOS

The home screen of the app on web

The home screen of the app on macOS

10. Congratulations!

You used Firebase to build an interactive, real-time web app!

Learn more