Chat com Flutter e Firebase | Parte 4 — Persistindo dados da conversa no firebase
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;
}
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.
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.
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.