Real-time shared expense tracker app using Flutter and Firebase: Part 4— Creating chat UI.

Siddhesh Inamdar
5 min readDec 25, 2023

--

In the previous article, Part 3, I introduced firebase authentication and flutter UI for user auth interface. In this article we will see what follows after a user is authenticated.

The user info is provided to a stateful widget ChatPage.

class ChatPage extends StatefulWidget {
final User user;
const ChatPage({super.key, required this.user});

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

The page state will be a scaffold view with attributes appBar, body and floatingActionButton.

Designing chat UI body:

We will need these imports to create a chat UI.

import 'package:firebase_database/ui/firebase_animated_list.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_chat_bubble/chat_bubble.dart';

Flutter provides a package called Firebase Animated List, which listenes to a stream from Firebase Realtime database (will be discussed in later articles). The data received as a snapshot map and used to build chat bubbles. The sort attribute assembles the received data in descending order. Setting reverse to True will always put the updated list on the latest chat bubble. The itemBuilder is a list builder which should read a stream termed as ref. The itemBuilder receives a data snapshot from stream and returns it as chat bubble widget.

    FirebaseAnimatedList(
sort: (DataSnapshot a, DataSnapshot b) => b.key!.compareTo(a.key!),
reverse: true,
query: ref,
controller: scrollController,
itemBuilder: (BuildContext context, DataSnapshot snapshot,
Animation<double> animation, int index) {
if (snapshot.exists) {
//String pushid = snapshot.key as String;
Map chatjson = snapshot.value as Map;

Widget msg =
MessageChatBubble(user: widget.user, chatjson: chatjson);

// Widget msg = MessageCard(
// user: widget.user,
// chatjson: chatjson,
// );
return msg;
} else {
return MessageChatBubble(
user: widget.user,
chatjson: const {
"notfound": {"uid": "None", "amount": "None", "text": "None"}
},
);
}
},
));

Now we can go deeper into how the chat bubble looks like. Chat bubble is a stateless widget which alters its alignment (left/right) based on the User information provided. If the information is provided by authenticated user based on the uid created by firebase while user authentication, it shows as a sender chat bubble or else it shows as a receiver chat bubble. If the User information map object has given fields, the chat bubble widget is returned. The chat bubble widget has two types, sender widget and reciever widget which can be seen in the clipper argument. Ways to customize the chat bubble can also be done by changing arguments like backgroundColor, alignment and margin. In this example chat bubble provides amount number, items as a string and date in local time zone.

class MessageChatBubble extends StatelessWidget {
final User user;
final Map chatjson;
late String amount;
late String content;
late String time;
MessageChatBubble({super.key, required this.user, required this.chatjson}) {
try {
amount = chatjson['amount'];
content = chatjson['content'];
var formatter = DateFormat('H:m dd-MMM-yyyy');
var date =
DateTime.fromMillisecondsSinceEpoch(int.parse(chatjson['timestamp']));
time = formatter.format(date);
} on Exception catch (_) {
amount = 'ERROR';
content = 'ERROR';
time = 'ERROR';
}
}

@override
Widget build(BuildContext context) {
if (user.uid == chatjson['userId']) {
return ChatBubble(
clipper: ChatBubbleClipper1(type: BubbleType.sendBubble),
backGroundColor: const Color.fromARGB(255, 138, 102, 223),
alignment: Alignment.topRight,
margin: const EdgeInsets.only(top: 20),
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
// child: Text(
// "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
// style: TextStyle(color: Colors.white),
// ),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// Number icon
Container(
alignment: Alignment.center,
decoration: const BoxDecoration(
color: Color.fromARGB(255, 229, 229, 255),
borderRadius: BorderRadius.all(Radius.circular(20))),
child: Text(amount,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 20.0,
)),
),
// Text below the icon
Text(
content,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 18.0,
color: Color.fromARGB(255, 246, 246, 255),
),
),
Text(
time,
textAlign: TextAlign.right,
style: const TextStyle(
fontSize: 12.0,
fontStyle: FontStyle.italic,
color: Color.fromARGB(255, 246, 246, 255),
),
),
],
),
),
);
} else {
return ChatBubble(
clipper: ChatBubbleClipper1(type: BubbleType.receiverBubble),
backGroundColor: const Color.fromARGB(255, 112, 102, 255),
margin: const EdgeInsets.only(top: 20),
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
// child: Text(
// "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
// style: TextStyle(color: Colors.white),
// ),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// Number icon
Padding(
padding: const EdgeInsets.all(2.0),
child: Container(
alignment: Alignment.center,
decoration: const BoxDecoration(
color: Color.fromARGB(255, 229, 229, 255),
borderRadius: BorderRadius.all(Radius.circular(20))),
child: Text(amount,
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
)),
),
),
// Text below the icon
Text(
content,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 18.0,
color: Color.fromARGB(255, 247, 247, 255),
),
),
Text(
time,
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 12.0,
fontStyle: FontStyle.italic,
color: Color.fromARGB(255, 246, 246, 255),
),
),
],
),
),
);
}
}
}

The last thing in the chat UI is the floating action button which creates a popup form for entering a new expense. We create a new functions inside the chatpagestate class to enter the new expense to the firebase realtime database once submit button is pressed or go back to the chat Ui once back button is pressed.

floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add, size: 35.0, weight: 15.0),
onPressed: () async {
OpenNewChatForm();
}),

We need an AlertDialog, which has two textfields and two buttons, back and submit. We will also need two TextControllers to store validate inputs before we submit. When the Submit button is pressed the string input is converted into a Message class and is sent to the Realtime Database as a json object. We will see the process of sending data to a json object in future parts.

  void OpenNewChatForm() {
final formKey = GlobalKey<FormState>();
final amountController = TextEditingController();
final contentController = TextEditingController();
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
scrollable: true,
title: const Text('Add new expense'),
content: Padding(
padding: const EdgeInsets.all(8.0),
child: Form(
key: formKey,
child: Column(children: [
TextFormField(
controller: amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: "Amount",
icon: Icon(Icons.numbers),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Cant save with no amount';
}
}),
TextFormField(
controller: contentController,
decoration: const InputDecoration(
labelText: "items",
icon: Icon(Icons.numbers),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Where did you spend this on?';
}
})
]),
)),
actions: [
Row(
children: [
ElevatedButton(
child: const Text("Back"),
onPressed: () {
Navigator.of(context).pop();
// your code
},
),
ElevatedButton(
child: const Text("Submit"),
onPressed: () {
if (formKey.currentState!.validate()) {
var msg = MessageClass(
datetime: DateTime.now()
.millisecondsSinceEpoch
.toString(),
userId: widget.user.uid,
amount: amountController.text,
content: contentController.text);

FirebaseDatabaseClass.setValue(msg);
Navigator.of(context).pop();
}
},
),
],
),
],
);
});
}

In the next section, Part 5, we will see how to send the expense input to the Firebase Realtime Database.

--

--

Siddhesh Inamdar

Techie chemical engineer. ML professional @ ExxonMobil BTC