Chat com Flutter e Firebase | Parte 4 — Persistindo dados da conversa no firebase

Douglas Possas
4 min readJun 4, 2023

--

Fala pessoal! Na parte 3 criamos uma interface simples e funcional e agora na parte 4 vamos persistir as mensagens do chat.

Vamos iniciar alterando nossa classe de serviço para que ela seja capaz de ler e escrever os dados do chat. Iremos remover o método writeTestMessage criado para validar o funcionamento da mesma.

Nosso método de escrita deve receber como argumento o nome da collection que desejamos popular e os dados que devem ser escritos.

O nome da collection a gente já deixou em uma propriedade estática — collectoinName — da nossa model Message.

Os dados que desejamos escrever podem ser obtidos através do método toJson() da nossa model Message.

Nosso método deve ficar assim:

  /// Write data to a collection
void write({required String collectionName, required Map<String, dynamic> data}) {
firestore
.collection(collectionName)
.add(data);
}

Nosso método de leitura deve receber como argumento o nome da collection, o campo de ordenação que desejamos e a ordem esperada.

Nosso método deve ficar assim:

  /// Read data from a collection sorting by property
Stream<QuerySnapshot> readSortedData(
{required collectionName,
required propertyToOrder,
bool descending = true}) {

return firestore
.collection(collectionName)
.orderBy(propertyToOrder, descending: descending)
.snapshots();
}

Perceba que o retorno do nosso método de leitura é do tipo Stream<QuerySnapshot>, isso permite com que a cada nova informação inserida na collection, o cliente (dispositivo) que estiver escutando tal Stream sofra a alteração em tempo de execução sem a necessidade de uso de longpooling (veremos como tratar isso em nossa página mais a frente).

Com nossa classe de serviço alterada, iremos alterar nosso widget ChatPage, onde usaremos o retorno do método de leitura para mostrar as mensagens e também trataremos o método _handleSubmit para usar o método de escrita da nossa classe de serviço.

Iniciaremos removendo a lista de mensagens criadas como dados de teste. Ao apagar a lista, teremos alguns erros pois a variável não existirá mais. Nosso trabalho agora é mudar esse código com problema para usar nossa classe de serviço. Verifique os comentários para identificar onde alteramos nossa classe.

Nossa página deve ficar assim:

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutterfirebasechat/services/firebase_service.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';
/// Instance of FirebaseService
final _firebaseService =
FirebaseService(firestore: FirebaseFirestore.instance);

@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,
// Change child to StreamBuider
child: StreamBuilder<QuerySnapshot>(
stream: _firebaseService.readSortedData(
collectionName: Message.collectionName,
propertyToOrder: 'timestamp'),
builder: (context, snapshot) {
if (snapshot.hasData) {
final messageList = snapshot.data!.docs;

// If there are no message
if (messageList.isEmpty) {
return const Center(
child: Text('Nenhuma mensagem ainda...'),
);
} else {
// If there are messages
return ListView.builder(
itemCount: messageList.length,
reverse: true,
controller: _scrollController,
itemBuilder: (_, index) {
final message =
Message.fromDocument(messageList[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,
),
),
),
],
);
},
);
}
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
),
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 {
_firebaseService.write(
collectionName: Message.collectionName,
data: Message(
author: currentUser(context),
message: _messageController.text,
timestamp: DateTime.now(),
).toJson());

_messageController.clear();
}

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

Perceba ainda que foi necessário alterarmos o factory da classe Message para ela fazer o parse de um QuerySnapshot e não mais de um Map<String, dynamic>. Nossa model ficou assim:

import 'package:cloud_firestore/cloud_firestore.dart';

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.fromDocument(DocumentSnapshot ds) {
String author = ds.get('author');
String message = ds.get('message');
DateTime timestamp = DateTime.fromMillisecondsSinceEpoch(
(ds.get('timestamp') as Timestamp).millisecondsSinceEpoch);

return Message(author: author, message: message, timestamp: timestamp);
}

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

bool isMe(nickname) => author == nickname;
}
Chat integrado ao firebase lendo dados através de um StreamBuilder

Ainda não chegamos no cenário ideal. Temos alguns pontos de melhoria que iremos resolver no próximo artigo. Veja que ao informar um terceiro nome, a conversa parece ter mais de 2 pessoas.

Três pessoas em um chat P2P :/

Isso deve-se ao fato de tratarmos cada usuário único através da String que representa o nome, quando deveríamos ter um identificador único e também alguma forma de categorizar a conversa em P2P ou Grupo.

Dados da conversa persistidos em nossa collection

No próximo artigo iremos refatorar a forma de salvar as informações para que tenhamos um chat realmente P2P. Nos vemos lá!

Compartilhe e deixe sua sugestão de melhoria.

--

--

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!