Chat com Flutter e Firebase | Parte 3 — Criando a interface gráfica

Douglas Possas
5 min readJun 4, 2023

--

No artigo anterior (acesse aqui a parte 2), vimos como configurar e conectar nossa aplicação ao Firebase e escrever um registro de testes ao acessar a tela principal da aplicação. Neste artigos criaremos a UI simplificada da aplicação.

O intuito do artigo não é focar em usabilidade, nem tema da aplicação. Então basicamente vamos criar uma tela de "login", onde iremos apenas inserir um nome para nosso usuário, e ao clicar no botão "Entrar" seremos redirecionado para a tela da conversa em si.

Então vamos criar 2 arquivos, um representando a tela de login e outro para a tela do chat. Adicionaremos uma propriedade estática em cada um deles representando o nome da rota para poder-mos navegar entre eles.

A tela de login será um StatelessWidget representada abaixo (criei o arquivo lib/views/auth/login_page.dart):

import 'package:flutter/material.dart';

class LoginPage extends StatelessWidget {
static const routeName = '/login';

const LoginPage({super.key});

@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

A tela de chat será um StatefulWidget representada abaixo (criei o arquivo lib/views/chat/chat_page.dart):

import 'package:flutter/material.dart';

class ChatPage extends StatefulWidget {
static const pageName = '/chat';

const ChatPage({super.key});

@override
State<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

Iremos alterar nosso arquivo main.dart para que a gente tenha as rotas mapeadas. Iremos refatorar a classe MainApp transformando ela em StatelessWidget, pois não usaremos mais o initState (já vimos que a escrita está funcionando). Nosso arquivo deve ficar da seguinte forma:

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

import 'core/utils/env.dart';
import 'views/auth/login_page.dart';
import 'views/chat/chat_page.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();

await Firebase.initializeApp(
options: FirebaseOptions(
apiKey: Env.firebaseApiKey,
appId: Env.firebaseAppId,
messagingSenderId: Env.firebaseMessagingSenderId,
projectId: Env.firebaseProjectId,
),
);

runApp(const MainApp());
}

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

@override
Widget build(BuildContext context) {
return MaterialApp(
// We create the route map
routes: {
LoginPage.routeName: (_) => const LoginPage(),
ChatPage.pageName: (_) => const ChatPage(),
},
// We define the initial route
initialRoute: LoginPage.routeName,
title: 'Flutter Firebase Chat',
);
}
}

Vamos editar a tela de login. Basicamente adicionaremos um Input para informar o nick do usuário que irá acessar o chat e um botão para navegação passando o nick como argumento. Não vou entrar em detalhes de cada widget usado, deixarei comentários no código onde for importante, as demais linhas serão perfumaria para deixar a tela minimamente apresentável.

import 'package:flutter/material.dart';

import '../chat/chat_page.dart';

class LoginPage extends StatelessWidget {
static const routeName = '/login';

LoginPage({super.key});

final _nicknameController = TextEditingController();

@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// The inputtext to inform the user's nickname
TextFormField(
controller: _nicknameController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Nickname',
),
),
const SizedBox(
height: 12,
),
Row(
children: [
Expanded(
// The button for navigating to the chat page
child: ElevatedButton(
onPressed: () {
Navigator.pushNamed(
context,
ChatPage.pageName,
arguments: _nicknameController.text,
);
_nicknameController.clear();
},
child: const Text('Entrar'),
),
)
],
),
],
),
),
);
}
}

Agora iremos criar a estrutura base para a exibir a conversa do chat. A tela irá conter apenas uma coluna onde inicialmente mostraremos um texto com a mensagem que não existe nenhuma mensagem a exibir. No fim da tela adicionaremos um campo para digitar a mensagem e também um botão para o envio da mensagem. Nossa página deve ficar assim:

import 'package:flutter/material.dart';

class ChatPage extends StatefulWidget {
static const pageName = '/chat';

const ChatPage({super.key});

@override
State<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
final _messageController = TextEditingController();

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Chat P2P")),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
children: [
const Flexible(
flex: 1,
child: Center(
child: Text('Nenhuma mensagem ainda...'),
),
),
SizedBox(
height: 60,
width: double.infinity,
child: Row(
children: [
Flexible(
child: TextField(
controller: _messageController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
),
Container(
padding: const EdgeInsets.only(left: 8),
child: IconButton(
icon: const Icon(Icons.send_rounded),
onPressed: _handleSubmit,
),
)
],
),
),
],
),
),
);
}

Future<void> _handleSubmit() async {}
}

Vamos agora criar a classe que representa a mensagem. Criaremos 2 métodos (toJson e fromJson) para fazer a conversão do objeto e comunicar futuramente com o Firebase. Também adicionaremos uma propriedade estática para armazenar o nome da Collection que será salva a informação no firebase. Nossa classe deve se parecer com o seguinte código:

class Message {
/// Firebase collection name
static const collectionName = 'messages';

String author;
String message;
DateTime timestamp;

Message({
required this.author,
required this.message,
required this.timestamp,
});

factory Message.fromJson(Map<String, dynamic> json) => Message(
author: json['ahthor'],
message: json['message'],
timestamp: json['timestamp'],
);

Map<String, dynamic> toJson() => {
'author': author,
'message': message,
'timestamp': timestamp,
};

bool isMe(nickname) => author == nickname;
}

Iremos agora trabalhar os componentes responsáveis por exibir a mensagem. Criaremos um único componente para fins de aprendizado, mas você pode componentizar da forma que achar melhor.

Irei criar uma lista de mensagens a ser exibida como mensagens vindas do Firebase (mock). Farei isso nesse momento apenas para focar na interface da conversa e mais a frente iremos remover, pois buscaremos as informações em nossa base de dados dinamicamente.

Nosso arquivo que representa a conversa, agora está pronto para exibir uma lista de mensagem e simular um chat. O arquivo deve ficar da seguinte forma:

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

import '../../models/message.dart';

class ChatPage extends StatefulWidget {
static const pageName = '/chat';

const ChatPage({super.key});

@override
State<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
final _messageController = TextEditingController();
final _scrollController = ScrollController();
final datePattern = 'dd MMM yyyy, HH:mm';

List<Message> messages = [
Message(
author: 'John Doe',
message: 'Hey dev!',
timestamp: DateTime.now()
..subtract(
const Duration(minutes: 5),
),
),
Message(
author: 'John Doe',
message: 'How are you?',
timestamp: DateTime.now()
..subtract(
const Duration(minutes: 4),
),
),
];

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Chat P2P")),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
children: [
Flexible(
flex: 1,
child: (messages.isEmpty)
? const Center(
child: Text('Nenhuma mensagem ainda...'),
)
: ListView.builder(
itemCount: messages.length,
reverse: true,
controller: _scrollController,
itemBuilder: (_, index) {
final message = messages[index];
return Column(
crossAxisAlignment: message.isMe(currentUser(context))
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
message.isMe(currentUser(context))
? MainAxisAlignment.end
: MainAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(10),
margin: message.isMe(currentUser(context))
? const EdgeInsets.only(right: 10)
: const EdgeInsets.only(left: 10),
width: 200,
decoration: BoxDecoration(
color: Colors.black12,
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
message.author,
),
const SizedBox(height: 5),
Text(
message.message,
),
],
),
),
],
),
Container(
margin: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
child: Text(
DateFormat(datePattern).format(
message.timestamp,
),
),
),
],
);
},
),
),
SizedBox(
height: 60,
width: double.infinity,
child: Row(
children: [
Flexible(
child: TextField(
controller: _messageController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
),
),
Container(
padding: const EdgeInsets.only(left: 8),
child: IconButton(
icon: const Icon(Icons.send_rounded),
onPressed: _handleSubmit,
),
)
],
),
),
],
),
),
);
}

Future<void> _handleSubmit() async {
messages.add(
Message(
author: currentUser(context),
message: _messageController.text,
timestamp: DateTime.now(),
),
);
_messageController.clear();
messages.sort((a, b) => b.timestamp.compareTo(a.timestamp));
setState(() {});
}

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

Nossa aplicação agora deve contar com duas telas, como segue:

Tela de login criada
Tela de conversa

Vimos nesse artigo como criar uma interface simples que será a utilizada posteriormente para integrarmos nosso chat ao Firebase. Esse será o assunto abordado na Parte 4 da nossa série de artigos.

Curta e compartilhe, comente e deixe suas sugestões de melhoria. Até a próxima!

--

--

Douglas Possas

Desenvolvedor desde 2005, amante de tecnologia desde os 5 (cinco) anos de idade quando ajudava a copiar um livro para compilar o Snake Game em um CP 400 color!