1. Antes de começar
Neste codelab, você vai aprender alguns conceitos básicos do Firebase para criar apps Flutter para dispositivos móveis Android e iOS.
Pré-requisitos
- Familiaridade com o Flutter
- O SDK do Flutter
- Um editor de texto de sua escolha
O que você vai aprender
- Como criar um app de chat de confirmação de presença em eventos e livro de visitas no Android, iOS, Web e macOS com o Flutter.
- Como autenticar usuários com o Firebase Authentication e sincronizar dados com o Firestore.
Pré-requisitos
Qualquer um dos seguintes dispositivos:
- Um dispositivo físico Android ou iOS conectado ao computador e configurado para o modo de desenvolvedor.
- O simulador para iOS, que exige as ferramentas do Xcode.
- O Android Emulator, que requer configuração no Android Studio.
Você também precisa de:
- Um navegador da sua escolha, como o Google Chrome.
- Um ambiente de desenvolvimento integrado ou editor de texto de sua preferência configurado com os plug-ins Dart e Flutter, como o Android Studio ou o Visual Studio Code (links em inglês).
- A versão mais recente do
stable
do Flutter oubeta
se você gosta de viver no limite. - Uma Conta do Google para a criação e o gerenciamento do seu projeto do Firebase.
- A CLI
Firebase
conectada à sua Conta do Google.
2. Acessar o exemplo de código
Faça o download da versão inicial do seu projeto no GitHub:
- Na linha de comando, clone o repositório do GitHub (link em inglês) no diretório
flutter-codelabs
:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
O diretório flutter-codelabs
contém o código de uma coleção de codelabs. O código deste codelab está no diretório flutter-codelabs/firebase-get-to-know-flutter
. O diretório contém uma série de snapshots que mostram como o projeto deve ficar ao final de cada etapa. Por exemplo, você está na segunda etapa.
- Encontre os arquivos correspondentes para a segunda etapa:
cd flutter-codelabs/firebase-get-to-know-flutter/step_02
Se quiser avançar ou ver como algo deve ficar após uma etapa, procure no diretório com o nome da etapa em que você tem interesse.
Importar o app inicial
- Abra ou importe o diretório
flutter-codelabs/firebase-get-to-know-flutter/step_02
no ambiente de desenvolvimento integrado de sua preferência. Esse diretório contém o código inicial do codelab, que consiste em um app de encontro do Flutter ainda não funcional.
Localizar os arquivos que precisam de trabalho
O código deste app está distribuído em vários diretórios. Essa divisão de funcionalidades facilita o trabalho porque agrupa o código por funcionalidade.
- Localize os seguintes arquivos:
lib/main.dart
: esse arquivo contém o ponto de entrada principal e o widget do app.lib/home_page.dart
: esse arquivo contém o widget da página inicial.lib/src/widgets.dart
: esse arquivo contém alguns widgets para ajudar a padronizar o estilo do app. Eles compõem a tela do app inicial.lib/src/authentication.dart
: esse arquivo contém uma implementação parcial da autenticação com um conjunto de widgets para criar uma experiência de login para a autenticação baseada em e-mail do Firebase. Esses widgets para o fluxo de autenticação ainda não são usados no app inicial, mas você os adicionará em breve.
Adicione outros arquivos conforme necessário para criar o restante do app.
Revise o arquivo lib/main.dart
Esse app usa o pacote google_fonts
para definir a Roboto como a fonte padrão em todo o app. Acesse fonts.google.com e use as fontes que você encontrar 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 poluição visual no layout da página descrito em HomePage
. Isso também permite uma aparência consistente.
Veja como seu app aparece no Android, iOS, na Web e no macOS:
3. Criar e configurar um projeto do Firebase
A exibição de informações sobre o evento é ótima para os convidados, mas não é muito útil para ninguém sozinha. É necessário adicionar alguma funcionalidade dinâmica ao app. Para isso, conecte o Firebase ao app. Para começar a usar o Firebase, crie e configure um projeto do Firebase.
Criar um projeto do Firebase
- Faça login no console do Firebase usando sua Conta do Google.
- Clique no botão para criar um projeto e insira um nome (por exemplo,
Firebase-Flutter-Codelab
).
- Clique em Continuar.
- Se solicitado, leia e aceite os Termos do Firebase e clique em Continuar.
- (Opcional) Ative a assistência de IA no console do Firebase (chamada de "Gemini no Firebase").
- Neste codelab, você não precisa do Google Analytics. Portanto, desative a opção do Google Analytics.
- Clique em Criar projeto, aguarde o provisionamento e clique em Continuar.
Para saber mais sobre os projetos do Firebase, consulte Noções básicas sobre projetos do Firebase.
Configurar produtos do Firebase
O app usa os seguintes produtos do Firebase, que estão disponíveis para apps da Web:
- Autenticação:permite que os usuários façam login no seu app.
- Firestore:salva dados estruturados na nuvem e recebe notificações instantâneas quando os dados são alterados.
- Regras de segurança do Firebase:protegem seu banco de dados.
Alguns desses produtos precisam de configuração especial ou precisam ser ativados no console do Firebase.
Ativar a autenticação de login por e-mail
- No painel Visão geral do projeto do console do Firebase, abra o menu Build.
- Clique em Autenticação > Começar > Método de login > E-mail/senha > Ativar > Salvar.
Configurar o Firestore
O app da Web usa o Firestore para salvar e receber mensagens de chat.
Veja como configurar o Firestore no seu projeto do Firebase:
- No painel à esquerda do console do Firebase, expanda Build e selecione Banco de dados do Firestore.
- Clique em Criar banco de dados.
- Deixe o ID do banco de dados definido como
(default)
. - Selecione um local para o banco de dados e clique em Próxima.
No caso de apps reais, escolha um local próximo aos usuários. - Clique em Iniciar no modo de teste. Leia o aviso sobre as regras de segurança.
Mais adiante neste codelab, você vai adicionar regras de segurança para proteger seus dados. Não distribua ou exponha um aplicativo publicamente sem adicionar regras de segurança ao seu banco de dados. - Clique em Criar.
4. configurar o Firebase
Para usar o Firebase com o Flutter, conclua as seguintes tarefas para configurar o projeto do Flutter e usar as bibliotecas FlutterFire
corretamente:
- Adicione as dependências
FlutterFire
ao seu projeto. - Registre a plataforma desejada no projeto do Firebase.
- Faça o download do arquivo de configuração específico da plataforma e adicione-o ao código.
No diretório de nível superior do seu app Flutter, há 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 do Firebase usados 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 a autenticação.
$ flutter pub add cloud_firestore
O pacote cloud_firestore
permite o acesso ao armazenamento de dados do Firestore.
$ flutter pub add provider
O pacote firebase_ui_auth
(link em inglês) oferece 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 de execução do iOS, Android, macOS e Web para usar o Firebase de maneira adequada. Você também usa o pacote provider
, que permite a separação da lógica de negócios da lógica de exibição.
Instalar a CLI do FlutterFire
A CLI do FlutterFire depende da CLI do Firebase.
- Se ainda não tiver feito isso, instale a CLI do Firebase na sua máquina.
- Instale a CLI do FlutterFire:
$ dart pub global activate flutterfire_cli
Depois de instalado, o comando flutterfire
fica disponível globalmente.
Configurar seus apps
A CLI extrai informações do seu projeto do Firebase e dos apps selecionados para gerar toda a configuração de uma plataforma específica.
Na raiz do app, execute o comando configure
:
$ flutterfire configure
O comando de configuração orienta você pelos seguintes processos:
- Selecione um projeto do Firebase com base no arquivo
.firebaserc
ou no console do Firebase. - Determine as plataformas para configuração, como Android, iOS, macOS e Web.
- Identifique os apps do Firebase de que você quer extrair a configuração. Por padrão, a CLI tenta corresponder automaticamente os apps do Firebase com base na configuração atual do projeto.
- Gere um arquivo
firebase_options.dart
no seu projeto.
Configurar o macOS
O Flutter no macOS cria apps totalmente isolados em sandbox. Como esse app se integra à rede para se comunicar com os servidores do Firebase, é necessário configurar o app com privilégios de cliente de rede.
macos/Runner/DebugProfile.entitlements (link em inglês)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<!-- Add the following two lines -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
macos/Runner/Release.entitlements (link em inglês)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Add the following two lines -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
Para mais informações, consulte Suporte para o Flutter em computadores.
5. Adicionar funcionalidade de confirmação de presença
Agora que você adicionou o Firebase ao app, é possível criar um botão RSVP que registra pessoas com a Autenticação. Para Android nativo, iOS nativo e Web, há pacotes FirebaseUI Auth
pré-criados, mas é necessário criar essa capacidade para o Flutter.
O projeto que você recuperou antes 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 app.
Adicionar lógica de negócios com o pacote Provider
Use o pacote provider
para disponibilizar um objeto de estado do app centralizado em toda a árvore de widgets do Flutter:
- 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 import
introduzem o Firebase Core e o Auth, extraem o pacote provider
que disponibiliza o objeto de estado do app em toda a árvore de widgets e incluem os widgets de autenticação do pacote firebase_ui_auth
.
Esse objeto de estado do aplicativo ApplicationState
tem uma principal responsabilidade nesta etapa: alertar a árvore de widgets de que houve uma atualização em um estado autenticado.
Você só usa um provedor para comunicar o estado do status de login de um usuário ao app. Para permitir que um usuário faça login, use as UIs fornecidas pelo pacote firebase_ui_auth
, que é uma ótima maneira de inicializar rapidamente telas de login nos seus apps.
Integrar o fluxo de autenticação
- Modifique as importações na parte de cima do arquivo
lib/main.dart
:
lib/main.dart
import 'package:firebase_ui_auth/firebase_ui_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; // new
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart'; // new
import 'app_state.dart'; // new
import 'home_page.dart';
- Conecte o estado do app à inicialização dele e adicione o fluxo de autenticação a
HomePage
:
lib/main.dart
void main() {
// Modify from here...
WidgetsFlutterBinding.ensureInitialized();
runApp(ChangeNotifierProvider(
create: (context) => ApplicationState(),
builder: ((context, child) => const App()),
));
// ...to here.
}
A modificação na função main()
torna o pacote do provedor responsável pela instanciação do objeto de estado do app com o widget ChangeNotifierProvider
. Você usa essa classe provider
específica porque o objeto de estado do app estende a classe ChangeNotifier
, o que permite que o pacote provider
saiba quando mostrar novamente os widgets dependentes.
- Atualize o app para processar a navegação em diferentes telas fornecidas pelo FirebaseUI 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) {
final user = switch (state) {
SignedIn state => state.user,
UserCreated state => state.credential.user,
_ => null
};
if (user == null) {
return;
}
if (state is UserCreated) {
user.updateDisplayName(user.email!.split('@')[0]);
}
if (!user.emailVerified) {
user.sendEmailVerification();
const snackBar = SnackBar(
content: Text(
'Please check your email to verify your email address'));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
context.pushReplacement('/');
})),
],
);
},
routes: [
GoRoute(
path: 'forgot-password',
builder: (context, state) {
final arguments = state.uri.queryParameters;
return ForgotPasswordScreen(
email: arguments['email'],
headerMaxExtent: 200,
);
},
),
],
),
GoRoute(
path: 'profile',
builder: (context, state) {
return ProfileScreen(
providers: const [],
actions: [
SignedOutAction((context) {
context.pushReplacement('/');
}),
],
);
},
),
],
),
],
);
// end of GoRouter configuration
// Change MaterialApp to MaterialApp.router and add the routerConfig
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Firebase Meetup',
theme: ThemeData(
buttonTheme: Theme.of(context).buttonTheme.copyWith(
highlightColor: Colors.deepPurple,
),
primarySwatch: Colors.deepPurple,
textTheme: GoogleFonts.robotoTextTheme(
Theme.of(context).textTheme,
),
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
),
routerConfig: _router, // new
);
}
}
Cada tela tem um tipo diferente de ação associada a ela com base no novo estado do fluxo de autenticação. Depois da maioria das mudanças de estado na autenticação, é possível redirecionar de volta para uma tela preferida, seja a inicial ou outra, como a de perfil.
- No método de build da classe
HomePage
, integre o estado do app ao widgetAuthFunc
:
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 Consumer é a maneira comum de usar o pacote provider
para reconstruir parte da árvore quando o estado do app muda. O widget AuthFunc
é o widget complementar que você testa.
Testar o fluxo de autenticação
- No app, toque no botão Confirmar presença para iniciar o
SignInScreen
.
- Insira um endereço de e-mail. Se você já estiver registrado, o sistema vai pedir uma senha. Caso contrário, o sistema vai pedir que você preencha o formulário de inscrição.
- Insira uma senha com menos de seis caracteres para verificar o fluxo de tratamento de erros. Se você estiver registrado, a senha será mostrada.
- Insira senhas incorretas para verificar o fluxo de tratamento de erros.
- Insira a senha correta. Você vê a experiência de login, que oferece ao usuário a capacidade de sair.
6. Gravar mensagens no Firestore
É ótimo saber que os usuários estão chegando, mas você precisa oferecer algo mais para os convidados fazerem no app. E se eles pudessem deixar mensagens em um livro de visitas? Eles podem compartilhar por que estão animados para ir ou quem esperam encontrar.
Para armazenar as mensagens de chat que os usuários escrevem no app, use o Firestore.
Modelo de dados
O Firestore é um banco de dados NoSQL, e os dados armazenados nele são divididos em coleções, documentos, campos e subcoleções. Cada mensagem do chat é armazenada como um documento em uma coleção guestbook
, que é uma coleção de nível superior.
Adicione mensagens ao Firestore
Nesta seção, você vai adicionar a funcionalidade para que os usuários escrevam mensagens no banco de dados. Primeiro, adicione um campo de formulário e um botão de envio. Depois, adicione o código que conecta esses elementos ao banco de dados.
- Crie um arquivo chamado
guest_book.dart
e adicione um widget com estadoGuestBook
para construir os elementos da interface de um campo de mensagem e um botão de envio:
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ê cria uma instância de um formulário para validar se a mensagem realmente contém conteúdo e mostrar uma mensagem de erro ao usuário se não houver nada. Para validar um formulário, acesse o estado dele com um GlobalKey
. Para mais informações sobre chaves e como usá-las, consulte Quando usar chaves.
Observe também a maneira como os widgets são organizados. Você tem um Row
com um TextFormField
e um StyledButton
, que contém um Row
. Observe também que o TextFormField
está incluído 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 um texto para adicionar ao livro de visitas, é necessário mostrá-lo na tela.
- Edite o corpo de
HomePage
para adicionar as duas linhas a seguir ao final dos filhos deListView
:
const Header("What we'll be doing"),
const Paragraph(
'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),
Embora isso seja suficiente para mostrar o widget, não é suficiente para fazer algo útil. Você vai atualizar esse código em breve para torná-lo funcional.
Visualização do app
Quando um usuário clica em ENVIAR, o snippet de código a seguir é acionado. Ele adiciona o conteúdo do campo de entrada de mensagem à coleção guestbook
do banco de dados. Especificamente, o método addMessageToGuestBook
adiciona o conteúdo da mensagem a um novo documento com um ID gerado automaticamente na coleção guestbook
.
FirebaseAuth.instance.currentUser.uid
é uma referência ao ID exclusivo gerado automaticamente que a autenticação fornece para todos os usuários conectados.
- No arquivo
lib/app_state.dart
, adicione o métodoaddMessageToGuestBook
. Você vai conectar essa capacidade à 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.
}
Conectar a interface e o banco de dados
Você tem uma interface em que o usuário pode inserir o texto que quer adicionar ao livro de visitas e o código para adicionar a entrada ao Firestore. Agora, basta conectar os dois.
- No arquivo
lib/home_page.dart
, faça a seguinte mudança no widgetHomePage
:
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 adicionadas no início desta etapa pela implementação completa. Você usa Consumer<ApplicationState>
novamente para disponibilizar o estado do app à parte da árvore que você renderiza. Isso permite que você reaja a alguém que insere uma mensagem na interface e a publique no banco de dados. Na próxima seção, você vai testar se as mensagens adicionadas são publicadas no banco de dados.
Teste o envio de mensagens
- Se necessário, faça login no app.
- Digite uma mensagem, como
Hey there!
, e clique em ENVIAR.
Essa ação grava a mensagem no banco de dados do Firestore. No entanto, você não vai ver a mensagem no seu app Flutter real porque ainda é necessário implementar a recuperação dos dados, o que será feito na próxima etapa. No entanto, no painel Banco de dados do Console do Firebase, você pode ver a mensagem adicionada na coleção guestbook
. Se você enviar mais mensagens, vai adicionar mais documentos à sua coleção guestbook
. Por exemplo, confira o snippet de código a seguir:
7. Leia mensagens
É ótimo que os convidados possam escrever mensagens no banco de dados, mas elas ainda não aparecem no app. É hora de corrigir isso!
Sincronizar mensagens
Para mostrar mensagens, adicione listeners que são acionados quando os dados mudam e crie um elemento da interface que mostre novas mensagens. Você adiciona um código ao estado do app que fica aguardando mensagens recém-adicionadas do app.
- Crie um arquivo
guest_book_message.dart
e adicione a seguinte classe para expor uma visualização estruturada dos dados armazenados no Firestore.
lib/guest_book_message.dart (link em inglês)
class GuestBookMessage {
GuestBookMessage({required this.name, required this.message});
final String name;
final String message;
}
- 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
- Na seção de
ApplicationState
em que 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.
- Na seção de inicialização de
ApplicationState
, adicione as seguintes linhas para se inscrever em uma consulta na coleção de documentos quando um usuário fizer login e cancelar a inscrição quando ele sair:
lib/app_state.dart
Future<void> init() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform);
FirebaseUIAuth.configureProviders([
EmailAuthProvider(),
]);
FirebaseAuth.instance.userChanges().listen((user) {
if (user != null) {
_loggedIn = true;
_guestBookSubscription = FirebaseFirestore.instance
.collection('guestbook')
.orderBy('timestamp', descending: true)
.snapshots()
.listen((snapshot) {
_guestBookMessages = [];
for (final document in snapshot.docs) {
_guestBookMessages.add(
GuestBookMessage(
name: document.data()['name'] as String,
message: document.data()['text'] as String,
),
);
}
notifyListeners();
});
} else {
_loggedIn = false;
_guestBookMessages = [];
_guestBookSubscription?.cancel();
}
notifyListeners();
});
}
Essa seção é importante porque é nela que você cria uma consulta na coleção guestbook
e processa a inscrição e o cancelamento da inscrição nessa coleção. Você ouve o stream, em que reconstrói um cache local das mensagens na coleção guestbook
e também armazena uma referência a essa assinatura para que você possa cancelar a inscrição mais tarde. Há muita coisa acontecendo aqui. Por isso, explore em um depurador para inspecionar o que acontece e ter um modelo mental mais claro. Para mais informações, consulte Receber atualizações em tempo real com o Firestore.
- No arquivo
lib/guest_book.dart
, adicione a seguinte importação:
import 'guest_book_message.dart';
- 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();
}
- Em
_GuestBookState
, modifique o métodobuild
da seguinte maneira para expor essa 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 adiciona uma coleção for no final dos filhos do Column
para gerar um novo Paragraph
para cada mensagem na lista.
- Atualize o corpo de
HomePage
para construir corretamenteGuestBook
com o novo parâmetromessages
:
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
),
],
],
),
),
Testar a sincronização de mensagens
O Firestore sincroniza dados de forma automática e instantânea com os clientes inscritos no banco de dados.
Teste a sincronização de mensagens:
- No app, encontre as mensagens que você criou anteriormente no banco de dados.
- Escrever novas mensagens. Elas aparecem instantaneamente.
- Abra o espaço de trabalho em várias janelas ou guias. As mensagens são sincronizadas em tempo real nas janelas e guias.
- Opcional: no menu Banco de dados do Console do Firebase, exclua, modifique ou adicione novas mensagens manualmente. Todas as mudanças aparecem na interface.
Parabéns! Você leu documentos do Firestore no seu app!
Visualização do app
8. Configurar 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, use o modo de teste apenas nos estágios iniciais do desenvolvimento. Como prática recomendada, configure regras de segurança para seu banco de dados à medida que desenvolve o app. A segurança é parte integrante da estrutura e do comportamento do app.
Com as regras de segurança do Firebase, é possível controlar o acesso a documentos e coleções no banco de dados. Com a sintaxe de regras flexíveis, é possível criar regras que correspondam a qualquer tipo de operação, desde todas as gravações no banco de dados inteiro até operações em um documento específico.
Configure regras básicas de segurança:
- No menu Desenvolver do Console do Firebase, clique em Database > Regras. As seguintes regras de segurança padrão e um aviso sobre elas serem públicas vão aparecer:
- Identifique as coleções em que o app grava dados:
Em match /databases/{database}/documents
, identifique a coleção que você quer proteger:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /guestbook/{entry} {
// You'll add rules here in the next step.
}
}
Como você usou o UID de autenticação como um campo em cada documento do livro de visitas, é possível receber o UID de autenticação e verificar se qualquer pessoa que tenta gravar no documento tem um UID de autenticação correspondente.
- Adicione as regras de leitura e gravação ao 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 somente o autor de uma mensagem pode editá-la.
- Adicione a validação de dados para garantir que todos os campos esperados estejam presentes no documento:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /guestbook/{entry} {
allow read: if request.auth.uid != null;
allow write:
if request.auth.uid == request.resource.data.userId
&& "name" in request.resource.data
&& "text" in request.resource.data
&& "timestamp" in request.resource.data;
}
}
}
9. Etapa bônus: pratique o que você aprendeu
Registrar o status de confirmação de presença de um participante
No momento, seu app só permite que as pessoas conversem quando têm interesse no evento. Além disso, a única maneira de saber se alguém está chegando é quando a pessoa avisa no chat.
Nesta etapa, você se organiza e informa às pessoas quantas pessoas vão comparecer. Você adiciona algumas funcionalidades ao estado do app. A primeira é a capacidade de um usuário conectado indicar se vai participar. O segundo é um contador de quantas pessoas estão participando.
- No arquivo
lib/app_state.dart
, adicione as seguintes linhas à seção de acessadores doApplicationState
para que o código da interface 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});
}
}
- Atualize o método
init()
doApplicationState
desta 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) {
_loggedIn = true;
_emailVerified = user.emailVerified;
_guestBookSubscription = FirebaseFirestore.instance
.collection('guestbook')
.orderBy('timestamp', descending: true)
.snapshots()
.listen((snapshot) {
_guestBookMessages = [];
for (final document in snapshot.docs) {
_guestBookMessages.add(
GuestBookMessage(
name: document.data()['name'] as String,
message: document.data()['text'] as String,
),
);
}
notifyListeners();
});
// Add from here...
_attendingSubscription = FirebaseFirestore.instance
.collection('attendees')
.doc(user.uid)
.snapshots()
.listen((snapshot) {
if (snapshot.data() != null) {
if (snapshot.data()!['attending'] as bool) {
_attending = Attending.yes;
} else {
_attending = Attending.no;
}
} else {
_attending = Attending.unknown;
}
notifyListeners();
});
// ...to here.
} else {
_loggedIn = false;
_emailVerified = false;
_guestBookMessages = [];
_guestBookSubscription?.cancel();
_attendingSubscription?.cancel(); // new
}
notifyListeners();
});
}
Esse código adiciona uma consulta sempre inscrita para determinar o número de participantes e uma segunda consulta que só fica ativa enquanto um usuário está conectado para determinar se ele está participando.
- Adicione a seguinte enumeração na parte de cima do arquivo
lib/app_state.dart
.
lib/app_state.dart
enum Attending { yes, no, unknown }
- Crie um novo arquivo
yes_no_selection.dart
e defina um novo widget que funcione como botões de opção:
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: [
FilledButton(
onPressed: () => onSelection(Attending.yes),
child: const Text('YES'),
),
const SizedBox(width: 8),
TextButton(
onPressed: () => onSelection(Attending.no),
child: const Text('NO'),
),
],
),
);
case Attending.no:
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
TextButton(
onPressed: () => onSelection(Attending.yes),
child: const Text('YES'),
),
const SizedBox(width: 8),
FilledButton(
onPressed: () => onSelection(Attending.no),
child: const Text('NO'),
),
],
),
);
default:
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
StyledButton(
onPressed: () => onSelection(Attending.yes),
child: const Text('YES'),
),
const SizedBox(width: 8),
StyledButton(
onPressed: () => onSelection(Attending.no),
child: const Text('NO'),
),
],
),
);
}
}
}
Ele começa em um estado indeterminado, sem Sim nem Não selecionado. Depois que o usuário selecionar se vai participar, destaque essa opção com um botão preenchido e deixe a outra opção com uma renderização simples.
- Atualize o método
build()
deHomePage
para aproveitarYesNoSelection
, permitir que um usuário conectado indique se vai participar e mostrar o número de participantes do evento:
lib/home_page.dart
Consumer<ApplicationState>(
builder: (context, appState, _) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Add from here...
switch (appState.attendees) {
1 => const Paragraph('1 person going'),
>= 2 => Paragraph('${appState.attendees} people going'),
_ => const Paragraph('No one going'),
},
// ...to here.
if (appState.loggedIn) ...[
// Add from here...
YesNoSelection(
state: appState.attending,
onSelection: (attending) => appState.attending = attending,
),
// ...to here.
const Header('Discussion'),
GuestBook(
addMessage: (message) =>
appState.addMessageToGuestBook(message),
messages: appState.guestBookMessages,
),
],
],
),
),
Adicionar regras
Você já configurou algumas regras, então os dados adicionados com os botões serão rejeitados. É necessário atualizar as regras para permitir adições à coleção attendees
.
- Na coleção
attendees
, pegue o UID de autenticação que você usou como nome do documento e verifique se ouid
do remetente é o mesmo do documento que ele está escrevendo:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ... //
match /attendees/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId;
}
}
}
Isso permite que todos leiam a lista de participantes porque não há dados particulares nela, mas apenas o criador pode atualizar.
- Adicione a validação de dados para garantir que todos os campos esperados estejam presentes no documento:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ... //
match /attendees/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId
&& "attending" in request.resource.data;
}
}
}
- Opcional: no app, clique nos botões para ver os resultados no painel do Firestore no Console do Firebase.
Visualização do app
10. Parabéns!
Você usou o Firebase para criar um app da Web interativo em tempo real.