Pressione enter para ver os resultados ou esc para cancelar.

Criando um Chat Completo Usando Flutter e Firebase – Parte 1

Está cada vez mais comum acompanharmos o surgimento de novos aplicativos de interação social sendo criados por aí, e por isso, decidi criar um chat usando Flutter e Firebase.
Pensei que seria bem legal e desafiador escrever uma aplicação de conversa em tempo real e depois transformar em artigo para que mais pessoas pudessem acompanhar o desenvolvimento de algo desse tipo.
Esse artigo será dividido em 3 partes: chat, envio de mídia e confirmação de leitura.


Primeiro, vamos escolher o nome do nosso aplicativo. O nome do meu será “MyChat”, por ser um nome curto e objetivo.
Vamos criar o projeto Flutter, para isso digite o comando:
flutter create mychat

 

Depois que o nosso projeto for gerado, vamos acessar o diretório para podermos instalar algumas dependências do Firebase.
cd mychat

Adicionando as dependências e configurando o Firebase

Agora iremos adicionar nas dependências do projeto os pacotes firebase_core e cloud_firestore, pacotes importantes para podermos configurar e nos comunicar com o Firebase.
flutter pub add firebase_core
flutter pub add cloud_firestore

 

Também vamos adicionar a dependência que vai nos ajudar no momento de trabalhar com datas:
flutter pub add intl

 

Agora, precisamos instalar o Firebase CLI para preparar o nosso app com as configurações do nosso projeto no Firebase.
dart pub global activate flutterfire_cli

 

Antes de seguirmos, precisamos fazer a autenticação no Firebase CLI que acabamos de instalar, digite o comando:
firebase login

 

Agora que estamos autenticados no Firebase CLI, precisamos gerar o arquivo de configuração do Firebase para que a integração seja feita de fato. Para isto, precisamos digitar o seguinte comando:
flutterfire configure

Atenção: Se você estiver recebendo uma mensagem de erro com a mensagem “uses-sdk:minSdkVersion 16 cannot be smaller than version 19 declared in library [:cloud_firestore]”, vá ao arquivo android/app/build.gradle e mude a versão do minSdkVersion de 16 para 21.
Note que será apresentado alguns passos para preenchermos com informações que ficarão registradas no nosso arquivo de configuração firebase_options.dart que será gerado no final.
Preencheremos da seguinte forma:
Select a Firebase project to configure your Flutter application with: Escolha a opção <create a new project>
Enter a project id for your new Firebase project: Dê um nome único ao seu projeto no Firebase, o meu ficará my-chat-[id_aleatório].
Which platforms should your configuration support: Nosso tutorial será feito no Android, então só selecionarei Android.

No final será perguntado se você deseja atualizar os arquivos build.gradle, é só pressionar Enter para aceitar e finalizar.

Um arquivo firebase_options.dart foi criado na raiz da estrutura do projeto, nele fica reunido todas as informações necessárias para se conectar com o nosso projeto no FIrebase.

Agora precisamos ir no arquivo lib/main.dart e inserir o código que inicializa o Firebase na aplicação. Acrescente o seguinte bloco de código no método main(). Não esqueça de colocar o async no método main() e de importar o pacote firebase_core e o arquivo firebase_options que se encontra na pasta /lib.

import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

void main() async {
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform,
    );
    runApp(const MyApp());
}

Acabamos de colocar o método que inicializa o Firebase na nossa aplicação, agora precisamos criar a model da mensagem.

Model

No diretório /lib, crie uma pasta chamada models e dentro dela crie um arquivo chamado message_model.dart. A classe da nossa model terá três propriedades, são elas: author, message e timestamp. O código ficará assim:

import 'package:cloud_firestore/cloud_firestore.dart';
 
class MessageModel {
 String author;
 String message;
 Timestamp timestamp;
 
 MessageModel(
     {required this.author, required this.message, required this.timestamp});
 
 Map<String, dynamic> toJson() {
   return {
     'author': author,
     'message': message,
     'timestamp': timestamp,
   };
 }
 
 factory MessageModel.fromDocument(DocumentSnapshot documentSnapshot) {
   String author = documentSnapshot.get('author');
   String message = documentSnapshot.get('message');
   Timestamp timestamp = documentSnapshot.get('timestamp');
 
   return MessageModel(author: author, message: message, timestamp: timestamp);
 }
}

Provider

Depois da nossa model criada, vamos escrever o código do nosso provider. O provider ficará responsável pelas requisições feitas no Firestore, é nele que criaremos os métodos que envia as mensagens e que recupera as mensagens enviadas.
No diretório /lib, crie uma pasta chamada providers e dentro dela crie um arquivo chamado chat_provider.dart.
O código ficará da seguinte forma:

import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/message_model.dart';
 
class ChatProvider {
 final FirebaseFirestore firebaseFirestore;
 
 ChatProvider({required this.firebaseFirestore});
 
 Stream<QuerySnapshot> getMessageList() {
   return firebaseFirestore
       .collection('messages')
       .orderBy('timestamp', descending: true)
       .snapshots();
 }
 
 void sendMessage(String message, String author) {
   MessageModel chatMessages = MessageModel(
     author: author,
     timestamp: Timestamp.now(),
     message: message,
   );
 
   firebaseFirestore.collection('messages').add(chatMessages.toJson());
 }
}

 

Tela da “sala de entrada”

No diretório lib, crie uma pasta chamada “screens” e nela crie um arquivo “enter_room_screen.dart”, ficando da seguinte forma: lib/screens/enter_room_screen.dart. Nessa tela o usuário poderá inserir um nickname no input que criamos, logo abaixo terá um botão para ele entrar na sala do chat.
O código ficará da seguinte forma:

import 'package:flutter/material.dart';
 
class EnterRoomScreen extends StatelessWidget {
 EnterRoomScreen({Key? key}) : super(key: key);
 
 final TextEditingController _nicknameEditingController =
     TextEditingController();
 
 @override
 Widget build(BuildContext context) {
   return GestureDetector(
     onTap: () => FocusScope.of(context).unfocus(),
     child: Scaffold(
       backgroundColor: const Color(0XFF23272a),
       body: Center(
         child: Padding(
           padding: const EdgeInsets.all(12.0),
           child: Column(
             mainAxisAlignment: MainAxisAlignment.center,
             crossAxisAlignment: CrossAxisAlignment.start,
             children: <Widget>[
               const Text(
                 'Seu nickname é...',
                 style: TextStyle(fontSize: 20, color: Colors.white),
               ),
               const SizedBox(height: 12),
               _nicknameInput(),
               const SizedBox(height: 12),
               _enterButton(context),
             ],
           ),
         ),
       ),
     ),
   );
 }
 
 Widget _nicknameInput() {
   return TextFormField(
     textInputAction: TextInputAction.done,
     controller: _nicknameEditingController,
     onChanged: (value) {},
     cursorColor: const Color(0xff9b84ec),
     style: const TextStyle(color: Colors.white),
     decoration: const InputDecoration(
       floatingLabelBehavior: FloatingLabelBehavior.never,
       contentPadding: EdgeInsets.all(20.0),
       filled: true,
       fillColor: Color(0xff2f3136),
       labelText: 'Nickname',
       suffixText: 'Nickname',
       hintStyle: TextStyle(color: Colors.white54),
       labelStyle: TextStyle(color: Colors.white54),
       enabledBorder: OutlineInputBorder(
         borderSide: BorderSide(color: Colors.black26),
       ),
       focusedBorder: OutlineInputBorder(
         borderSide: BorderSide(color: Color(0xff9b84ec), width: 1),
         borderRadius: BorderRadius.all(Radius.circular(8.0)),
       ),
       border: OutlineInputBorder(
         borderSide: BorderSide(color: Colors.red, width: 5),
         borderRadius: BorderRadius.all(Radius.circular(8.0)),
         gapPadding: 8.0,
       ),
     ),
   );
 }
 
 Widget _enterButton(context) {
   return Row(
     children: [
       Expanded(
         child: ElevatedButton(
           onPressed: () {
             Navigator.pushNamed(context, '/chat',
                 arguments: _nicknameEditingController.text);
             _nicknameEditingController.clear();
           },
           child: const Text('Entrar'),
           style: ElevatedButton.styleFrom(
             textStyle: const TextStyle(
               fontSize: 14,
               fontWeight: FontWeight.w500,
             ),
             primary: const Color(0xff9b84ec),
           ),
         ),
       ),
     ],
   );
 }
}

No código acima montamos uma tela que terá um título, um campo de texto para o usuário inserir o nickname e um campo para entrar na sala.
Agora vou explicar o que cada parte do código faz:
_nicknameEditingController: Controller que salvará o valor do campo nickname
_nicknameInput: Cria um widget com um campo de texto
_enterButton: Botão para entrar no chat, onde seu evento principal é redirecionar o usuário para a rota “/chat” e enviar o valor do nickname como argumento. Logo após o usuário ser redirecionado, o campo nickname é limpo usando o método clear() do nicknameEditingController.
Você deve estar se perguntando, “mas que rota é essa?“…bom, mais pra frente eu explico.

Tela da conversa

Criaremos agora a tela do chat, onde o usuário poderá visualizar todas as mensagens, digitar mensagens e enviar.
No diretório lib/screens, crie um arquivo chamado chat_screen.dart.
Na nossa classe iremos criar uma instância do Firestore:

final ChatProvider = ChatProvider(firebaseFirestore: FirebaseFirestore.instance);

Depois precisamos criar um controlador de input para armazenar a mensagem que o usuário pretende enviar:

final TextEditingController messageEditingController = TextEditingController();

Agora, temos que criar a variável onde será armazenada as mensagens, assim que tivermos um retorno da nossa requisição no Firebase:

 List<QueryDocumentSnapshot> messageList = [];

 

Até o momento nossa classe se parecerá com isso:

import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
 
import '../providers/chat_provider.dart';
 
class ChatScreen extends StatefulWidget {
const ChatScreen({Key? key}) : super(key: key);
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
final ChatProvider chatProvider =
    ChatProvider(firebaseFirestore: FirebaseFirestore.instance);
final TextEditingController messageEditingController =
    TextEditingController();
List<QueryDocumentSnapshot> messageList = [];
final ScrollController scrollController = ScrollController();
@override
Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: const Color(0XFF36393f),
    appBar: AppBar(
      centerTitle: true,
      backgroundColor: const Color(0XFF23272a),
      elevation: 1,
      title: const Text('My.chat', style: TextStyle(fontSize: 16)),
    ),
    body: Stack(
      children: const <Widget>[
      ],
    ),
  );
}
}

Agora precisamos criar dois métodos, um que será responsável pelo envio das mensagens e outro para pegarmos o nickname do usuário.
sendMessage: Responsável por registrar as mensagens no Firestore,
onde receberá o texto da mensagem como seu único argumento. Além de criar novos registros no Firebase, a função fica responsável por limpar nosso input de mensagem e também de descer o scroll para a base de uma forma suavizada.
Perceba que antes de enviar a mensagem, nós verificamos se o campo da mensagem está vazio, se estiver, o método de enviar não é chamado. Também, usamos a função trim() ao enviar a mensagem, para que espaços em brancos antes e depois da mensagem sejam removidos.
currentUser: Nos retorna o nickname que foi passado como argumento para a rota.
Nosso método sendMessage ficará assim:

void sendMessage(String message) {
   if (message.isNotEmpty) {
     messageEditingController.clear();
     chatProvider.sendMessage(message.trim(), currentUser(context));
     scrollController.animateTo(0,
         duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
   }
 }

E o nosso método de pegar o nickname do usuário ficará desse jeito:

currentUser(context) => ModalRoute.of(context)?.settings.arguments as String;

Depois dessas adições, nosso código ficará assim:

import 'package:cloud_firestore/cloud_firestore.dart';
import '../providers/chat_provider.dart';

class ChatScreen extends StatefulWidget {
  const ChatScreen({Key? key}) : super(key: key);

  @override
    State<ChatScreen> createState() => _ChatScreenState();
  }

  class _ChatScreenState extends State<ChatScreen> {
    final ChatProvider chatProvider = ChatProvider(firebaseFirestore: FirebaseFirestore.instance);
    final TextEditingController messageEditingController = TextEditingController();
    List<QueryDocumentSnapshot> messageList = [];
    final ScrollController scrollController = ScrollController();

    @override
    Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0XFF36393f),
      appBar: AppBar(
        centerTitle: true,
        backgroundColor: const Color(0XFF23272a),
        elevation: 1,
        title: const Text(‘My.chat’, style: TextStyle(fontSize: 16)),
      ),
      body: Stack(
        children: const <Widget>[
        ],
      ),
    );
  }
}

Nada será renderizado na nossa tela, ainda. Precisamos criar nossos widgets.
Veja na imagem abaixo quais widgets iremos criar:

Começaremos criando nosso widget do timestamp, como o nome já diz será um widget para exibir a data e hora que a mensagem foi enviada. No diretório lib, crie um diretório chamado widgets, dentro deste diretório crie um arquivo com o nome message_timestamp.dart. Nosso código ficará assim:

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
 
class MessageTimestampWidget extends StatelessWidget {
 const MessageTimestampWidget({
   Key? key,
   required this.timestamp,
 }) : super(key: key);
 
 final Timestamp timestamp;
 
 @override
 Widget build(BuildContext context) {
   const datePattern = 'dd MMM yyyy, HH:mm';
   final timestampFormatted =
       DateFormat(datePattern).format(timestamp.toDate());
 
   return Container(
     margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
     child: Text(
       timestampFormatted,
       style: const TextStyle(
           color: Colors.grey, fontSize: 12, fontStyle: FontStyle.italic),
     ),
   );
 }
}

Perceba que nosso widget recebe um argumento com o timestamp que irá ser exibido logo após formatarmos usando o DateFormat do Intl. Na linha anterior a esta, criamos uma constante chamada datePattern para determinarmos como queremos que o timestamp seja formatado.
Agora, criaremos nosso widget do message bubble. No diretório lib/widgets, crie um arquivo chamado message_bubble.dart.
Nosso widget recebe dois argumentos: chatMessage e isMe.
chatMessage: um objeto da mensagem com as propriedades author, timestamp e message.
isMe: um boolean para determinar se a mensagem está sendo enviada ou recebida. Isto vai nos ajudar a diferenciar o próprio widget, estilizando e posicionando corretamente.
O código ficará assim:

import 'package:flutter/material.dart';
 
import '../models/message_model.dart';
import 'message_timestamp.dart';
 
class MessageBubbleWidget extends StatelessWidget {
 const MessageBubbleWidget({
   Key? key,
   required this.chatMessage,
   required this.isMe,
 }) : super(key: key);
 
 final MessageModel chatMessage;
 final bool isMe;
 
 @override
 Widget build(BuildContext context) {
   return Column(
     crossAxisAlignment:
         isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
     children: [
       Row(
         mainAxisAlignment:
             isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
         children: [
           Container(
             padding: const EdgeInsets.all(10),
             margin: isMe
                 ? const EdgeInsets.only(right: 10)
                 : const EdgeInsets.only(left: 10),
             width: 200,
             decoration: BoxDecoration(
               color: isMe ? Colors.green : Colors.black12,
               borderRadius: BorderRadius.circular(10),
             ),
             child: Column(
               crossAxisAlignment: CrossAxisAlignment.start,
               children: [
                 Text(
                   chatMessage.author,
                   style: const TextStyle(fontSize: 13, color: Colors.white),
                 ),
                 const SizedBox(height: 5),
                 Text(
                   chatMessage.message,
                   style: const TextStyle(fontSize: 16, color: Colors.white),
                 ),
               ],
             ),
           ),
         ],
       ),
       MessageTimestampWidget(timestamp: chatMessage.timestamp),
     ],
   );
 }
}

Precisamos criar agora o input onde o usuário digitará a mensagem a ser enviada, no diretório lib/widgets, iremos criar o arquivo chamado input_message.dart.
Note que estaremos recebendo por argumento o controller do input e o método responsável pelo envio da mensagem.

import 'package:flutter/material.dart';
 
class InputMessageWidget extends StatelessWidget {
 const InputMessageWidget({
   Key? key,
   required this.messageEditingController,
   required this.handleSubmit,
 }) : super(key: key);
 
 final TextEditingController messageEditingController;
 final Function(String message) handleSubmit;
 
 @override
 Widget build(BuildContext context) {
   return Container(
     padding: const EdgeInsets.all(8.0),
     child: SizedBox(
       width: double.infinity,
       height: 60,
       child: Row(
         children: [
           Flexible(
               child: TextField(
             keyboardType: TextInputType.text,
             textCapitalization: TextCapitalization.sentences,
             controller: messageEditingController,
             cursorColor: const Color(0xff9b84ec),
             style: const TextStyle(color: Colors.white),
             decoration: const InputDecoration(
               floatingLabelBehavior: FloatingLabelBehavior.never,
               contentPadding: EdgeInsets.all(20.0),
               filled: true,
               fillColor: Color(0xff2f3136),
               labelText: 'Message',
               hintStyle: TextStyle(color: Colors.white),
               labelStyle: TextStyle(color: Colors.white),
               enabledBorder: OutlineInputBorder(
                 borderSide: BorderSide(color: Colors.black26),
                 borderRadius: BorderRadius.all(Radius.circular(60.0)),
               ),
               focusedBorder: OutlineInputBorder(
                 borderSide: BorderSide(color: Color(0xff9b84ec), width: 1),
                 borderRadius: BorderRadius.all(Radius.circular(60.0)),
               ),
               border: OutlineInputBorder(
                 borderSide: BorderSide(color: Colors.green, width: 5),
                 borderRadius: BorderRadius.all(Radius.circular(60.0)),
                 gapPadding: 8.0,
               ),
             ),
           )),
           Container(
             margin: const EdgeInsets.only(left: 4),
             decoration: BoxDecoration(
               color: Colors.green,
               borderRadius: BorderRadius.circular(30),
             ),
             child: IconButton(
               onPressed: () => handleSubmit(messageEditingController.text),
               icon: const Icon(Icons.send_rounded),
               color: Colors.white,
             ),
           ),
         ],
       ),
     ),
   );
 }
}

Com os nossos widgets já escritos, vamos voltar no arquivo chat_screen.dart, terminar de montar a tela e importar os novos widgets.
Para que possamos atualizar as mensagens em tempo real, vamos usar o StreamBuilder, explicarei um pouco mais sobre…
StreamBuilder é um widget que converte um fluxo de objetos em widgets. De forma simples, ele escuta o stream e se constrói em cada novo evento emitido. Os eventos emitidos determinam se o fluxo de dados conseguiu ou não se conectar, se está em espera, se terminou ou se iniciou mas não terminou.
Nosso body vai ficar da seguinte forma:

Stack(
 children: <Widget>[
   Column(
     children: <Widget>[
       Flexible(
         child: StreamBuilder<QuerySnapshot>(
           stream: chatProvider.getMessageList(),
           builder: (BuildContext context,
               AsyncSnapshot<QuerySnapshot> snapshot) {
             if (snapshot.hasData) {
               messageList = snapshot.data!.docs;
 
               if (messageList.isNotEmpty) {
                 return ListView.builder(
                     padding: const EdgeInsets.all(10),
                     itemCount: messageList.length,
                     reverse: true,
                     controller: scrollController,
                     itemBuilder: (context, index) =>
                         _buildItem(index, messageList[index]));
               } else {
                 return const Center(
                   child: Text('Sem mensagens',
                       style: TextStyle(
                         color: Colors.white,
                         fontSize: 20,
                       )),
                 );
               }
             } else {
               return const Center(
                 child: CircularProgressIndicator(
                   color: Colors.blue,
                 ),
               );
             }
           },
         ),
       ),
       InputMessageWidget(
         messageEditingController: messageEditingController,
         handleSubmit: sendMessage,
       ),
     ],
   )
 ],
),

 

Estamos usando o widget Stack para que possamos empilhar nosso input e o restante das mensagens. Também, fazemos verificações para saber se as mensagens estão carregando e se tem ou não mensagens para que assim o estado da tela mude. Se houver mensagens, obviamente vai mostrá-las, se ainda estiver carregando então mostre um loading e se não possuir mensagem alguma, mostre um aviso que está sem mensagem.
O resultado do chat_screen.dart, será:

import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
 
import '../models/message_model.dart';
import '../providers/chat_provider.dart';
import '../widgets/input_message.dart';
import '../widgets/message_bubble.dart';
 
class ChatScreen extends StatefulWidget {
 const ChatScreen({Key? key}) : super(key: key);
 
 @override
 State<ChatScreen> createState() => _ChatScreenState();
}
 
class _ChatScreenState extends State<ChatScreen> {
 final ChatProvider chatProvider =
     ChatProvider(firebaseFirestore: FirebaseFirestore.instance);
 
 final TextEditingController messageEditingController =
     TextEditingController();
 
 List<QueryDocumentSnapshot> messageList = [];
 
 final ScrollController scrollController = ScrollController();
 
 currentUser(context) => ModalRoute.of(context)?.settings.arguments as String;
 
 void sendMessage(String message) {
   if (message.isNotEmpty) {
     messageEditingController.clear();
     chatProvider.sendMessage(message.trim(), currentUser(context));
     scrollController.animateTo(0,
         duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
   }
 }
 
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     backgroundColor: const Color(0XFF36393f),
     appBar: AppBar(
       centerTitle: true,
       backgroundColor: const Color(0XFF23272a),
       elevation: 1,
       title: const Text('My.chat', style: TextStyle(fontSize: 16)),
     ),
     body: Stack(
       children: <Widget>[
         Column(
           children: <Widget>[
             Flexible(
               child: StreamBuilder<QuerySnapshot>(
                 stream: chatProvider.getMessageList(),
                 builder: (BuildContext context,
                     AsyncSnapshot<QuerySnapshot> snapshot) {
                   if (snapshot.hasData) {
                     messageList = snapshot.data!.docs;
 
                     if (messageList.isNotEmpty) {
                       return ListView.builder(
                           padding: const EdgeInsets.all(10),
                           itemCount: messageList.length,
                           reverse: true,
                           controller: scrollController,
                           itemBuilder: (context, index) =>
                               _buildItem(index, messageList[index]));
                     } else {
                       return const Center(
                         child: Text('Sem mensagens...',
                             style: TextStyle(
                               color: Colors.white,
                               fontSize: 20,
                             )),
                       );
                     }
                   } else {
                     return const Center(
                       child: CircularProgressIndicator(
                         color: Colors.blue,
                       ),
                     );
                   }
                 },
               ),
             ),
             InputMessageWidget(
               messageEditingController: messageEditingController,
               handleSubmit: sendMessage,
             ),
           ],
         )
       ],
     ),
   );
 }
 
 _buildItem(int index, DocumentSnapshot? documentSnapshot) {
   if (documentSnapshot != null) {
     final chatMessage = MessageModel.fromDocument(documentSnapshot);
     final isMe = chatMessage.author == currentUser(context);
 
     return MessageBubbleWidget(chatMessage: chatMessage, isMe: isMe);
   }
 }
}

 

Para finalizarmos, precisamos criar as rotas das nossas páginas no arquivo main.dart.
No widget MaterialApp, vamos informar nossas duas rotas no argumento routes e também vamos informar nossa rota inicial em initialRoute, que será “/”.
Nossa classe MyApp ficará assim:

class MyApp extends StatelessWidget {
 const MyApp({Key? key}) : super(key: key);
 
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     debugShowCheckedModeBanner: false,
     title: 'My.chat',
     theme: ThemeData(
       primarySwatch: Colors.blue,
     ),
     initialRoute: '/',
     routes: {
       '/': (context) => EnterRoomScreen(),
       '/chat': (context) => const ChatScreen(),
     },
   );
 }
}

 

O resultado do arquivo main.dart, será:

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
 
import 'firebase_options.dart';
 
import 'screens/chat_screen.dart';
import 'screens/enter_room_screen.dart';
 
void main() async {
 WidgetsFlutterBinding.ensureInitialized();
 await Firebase.initializeApp(
   options: DefaultFirebaseOptions.currentPlatform,
 );
 runApp(const MyApp());
}
 
class MyApp extends StatelessWidget {
 const MyApp({Key? key}) : super(key: key);
 
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     debugShowCheckedModeBanner: false,
     title: 'My.chat',
     theme: ThemeData(
       primarySwatch: Colors.blue,
     ),
     initialRoute: '/',
     routes: {
       '/': (context) => EnterRoomScreen(),
       '/chat': (context) => const ChatScreen(),
     },
   );
 }
}

 

Rode o projeto e veja o resultado, nosso app parecerá com isto:

Criando um chat com Flutter e FirebaseCriando um chat com Flutter e FirebaseCriando um chat com Flutter e Firebase


Concluindo a criação de um chat usando Flutter e Firebase

O Firebase é uma ótima ferramenta quando precisamos fazer algo rápido e fácil, sem que para isso seja necessário criar todo um projeto backend que levaria algumas horas ou quem sabe dias.
Quando unimos o Flutter e Firebase, conseguimos construir boas ferramentas de forma eficiente e gratuita (até um certo ponto). À medida que sua aplicação cresce, o Firebase começa a cobrar pelas requisições, então recomendo dar uma olhadinha nos preços caso queira começar um projeto grande com ele.
Se você não sabe o que é um widget e como funciona, recomendo dar uma olhadinha neste outro artigo aqui no blog da Taller.

Ciclo de Vida dos Widgets no Flutter