Skip to main content

Overview

Trackmart includes a built-in messaging system that enables real-time communication between buyers and drivers. Built on Firebase Firestore, the chat system supports text messages, images, and stickers for enhanced communication during the delivery process.

Chat Architecture

The messaging system uses Firestore for real-time synchronization:

Firestore

Cloud-based real-time message storage and sync

Firebase Storage

Image upload and hosting for shared media

Cached Network Images

Efficient image loading with caching

Message Types

The chat system supports three message types:
Standard text communication
void onSendMessage(String content, int type) {
  // type: 0 = text, 1 = image, 2 = sticker
  if (content.trim() != '') {
    textEditingController.clear();
    
    var documentReference = firestore
        .collection('messages')
        .document(groupChatId)
        .collection(groupChatId)
        .document(DateTime.now().millisecondsSinceEpoch.toString());
    
    firestore.runTransaction((transaction) async {
      await transaction.set(
        documentReference,
        {
          'idFrom': id,
          'idTo': peerId,
          'timestamp': DateTime.now().millisecondsSinceEpoch.toString(),
          'content': content,
          'type': type
        },
      );
    });
  }
}

Chat ID Generation

Each conversation has a unique group chat ID:
readLocal() async {
  prefs = await SharedPreferences.getInstance();
  id = prefs.getString('id') ?? '';
  if (id.hashCode <= peerId.hashCode) {
    groupChatId = '$id-$peerId';
  } else {
    groupChatId = '$peerId-$id';
  }
  
  firestore
      .collection('buyers')
      .document(id)
      .updateData({'chattingWith': peerId});
  
  setState(() {});
}
The group chat ID is generated consistently regardless of which user initiates the chat by comparing hash codes.

Message Display

Messages are displayed in a ListView with different layouts for sent vs received:

Sent Messages (Right-aligned)

if (document['idFrom'] == id) {
  return Row(
    children: <Widget>[
      document['type'] == 0
          ? Container(
              child: Text(
                document['content'],
                style: TextStyle(color: primaryColor),
              ),
              padding: EdgeInsets.fromLTRB(15.0, 10.0, 15.0, 10.0),
              width: 200.0,
              decoration: BoxDecoration(
                  color: greyColor2,
                  borderRadius: BorderRadius.circular(8.0)),
              margin: EdgeInsets.only(
                  bottom: isLastMessageRight(index) ? 20.0 : 10.0,
                  right: 10.0),
            )
          : // Image or sticker handling...
    ],
    mainAxisAlignment: MainAxisAlignment.end,
  );
}

Received Messages (Left-aligned)

else {
  return Container(
    child: Column(
      children: <Widget>[
        Row(
          children: <Widget>[
            isLastMessageLeft(index)
                ? peerAvatar == null
                    ? CircleAvatar(
                        child: Text(peerName[0]),
                        radius: 17.5,
                      )
                    : Material(
                        child: CachedNetworkImage(
                          imageUrl: peerAvatar,
                          width: 35.0,
                          height: 35.0,
                          fit: BoxFit.cover,
                        ),
                        borderRadius: BorderRadius.all(
                          Radius.circular(18.0),
                        ),
                        clipBehavior: Clip.hardEdge,
                      )
                : Container(width: 35.0),
            document['type'] == 0
                ? Container(
                    child: Text(
                      document['content'],
                      style: TextStyle(color: Colors.white),
                    ),
                    padding: EdgeInsets.fromLTRB(15.0, 10.0, 15.0, 10.0),
                    width: 200.0,
                    decoration: BoxDecoration(
                        color: primaryColor,
                        borderRadius: BorderRadius.circular(8.0)),
                    margin: EdgeInsets.only(left: 10.0),
                  )
                : // Image or sticker handling...
          ],
        ),
        isLastMessageLeft(index)
            ? Container(
                child: Text(
                  DateFormat('dd MMM kk:mm').format(
                      DateTime.fromMillisecondsSinceEpoch(
                          int.parse(document['timestamp']))),
                  style: TextStyle(
                      color: greyColor,
                      fontSize: 12.0,
                      fontStyle: FontStyle.italic),
                ),
                margin: EdgeInsets.only(left: 50.0, top: 5.0, bottom: 5.0),
              )
            : Container()
      ],
      crossAxisAlignment: CrossAxisAlignment.start,
    ),
    margin: EdgeInsets.only(bottom: 10.0),
  );
}
Avatars and timestamps are only shown for the last message in a consecutive series from the same sender to keep the UI clean.

Real-Time Message Stream

Widget buildListMessage() {
  return Flexible(
    child: groupChatId == ''
        ? Center(
            child: CircularProgressIndicator(
                valueColor: AlwaysStoppedAnimation<Color>(themeColor)))
        : StreamBuilder(
            stream: firestore
                .collection('messages')
                .document(groupChatId)
                .collection(groupChatId)
                .orderBy('timestamp', descending: true)
                .limit(20)
                .snapshots(),
            builder: (context, snapshot) {
              if (!snapshot.hasData) {
                return Center(
                    child: CircularProgressIndicator(
                        valueColor:
                            AlwaysStoppedAnimation<Color>(themeColor)));
              } else {
                listMessage = snapshot.data.documents;
                return ListView.builder(
                  padding: EdgeInsets.all(10.0),
                  itemBuilder: (context, index) =>
                      buildItem(index, snapshot.data.documents[index]),
                  itemCount: snapshot.data.documents.length,
                  reverse: true,
                  controller: listScrollController,
                );
              }
            },
          ),
  );
}
  • orderBy(‘timestamp’, descending: true): Latest messages first
  • limit(20): Load 20 most recent messages initially
  • reverse: true: Display with oldest at top, newest at bottom
  • Auto-scroll: Automatically scrolls to newest message on send

Chat Input Interface

The input area includes multiple interaction options:
Widget buildInput() {
  return Container(
    child: Row(
      children: <Widget>[
        // Button send image
        Material(
          child: new Container(
            margin: new EdgeInsets.symmetric(horizontal: 1.0),
            child: new IconButton(
              icon: new Icon(Icons.image),
              onPressed: getImage,
              color: primaryColor,
            ),
          ),
          color: Colors.white,
        ),
        Material(
          child: new Container(
            margin: new EdgeInsets.symmetric(horizontal: 1.0),
            child: new IconButton(
              icon: new Icon(Icons.face),
              onPressed: getSticker,
              color: primaryColor,
            ),
          ),
          color: Colors.white,
        ),
        
        // Edit text
        Flexible(
          child: Container(
            child: TextField(
              style: TextStyle(color: primaryColor, fontSize: 15.0),
              controller: textEditingController,
              decoration: InputDecoration.collapsed(
                hintText: 'Type your message...',
                hintStyle: TextStyle(color: greyColor),
              ),
              focusNode: focusNode,
            ),
          ),
        ),
        
        // Button send message
        Material(
          child: new Container(
            margin: new EdgeInsets.symmetric(horizontal: 8.0),
            child: new IconButton(
              icon: new Icon(Icons.send),
              onPressed: () => onSendMessage(textEditingController.text, 0),
              color: primaryColor,
            ),
          ),
          color: Colors.white,
        ),
      ],
    ),
    width: double.infinity,
    height: 50.0,
    decoration: new BoxDecoration(
        border:
            new Border(top: new BorderSide(color: greyColor2, width: 0.5)),
        color: Colors.white),
  );
}

Image Button

Opens gallery picker

Sticker Button

Toggles sticker panel

Send Button

Submits text message

Sticker Panel

A toggleable panel displays available stickers:
Widget buildSticker() {
  return Container(
    child: Column(
      children: <Widget>[
        Row(
          children: <Widget>[
            FlatButton(
              onPressed: () => onSendMessage('mimi1', 2),
              child: new Image.asset(
                'assets/images/mimi1.gif',
                width: 50.0,
                height: 50.0,
                fit: BoxFit.cover,
              ),
            ),
            // More stickers...
          ],
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        ),
        // Additional rows...
      ],
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    ),
    decoration: new BoxDecoration(
        border:
            new Border(top: new BorderSide(color: greyColor2, width: 0.5)),
        color: Colors.white),
    padding: EdgeInsets.all(5.0),
    height: 180.0,
  );
}
The sticker panel automatically hides when the keyboard appears to maximize screen space.

Focus Management

void onFocusChange() {
  if (focusNode.hasFocus) {
    // Hide sticker when keyboard appear
    setState(() {
      isShowSticker = false;
    });
  }
}

void getSticker() {
  // Hide keyboard when sticker appear
  focusNode.unfocus();
  setState(() {
    isShowSticker = !isShowSticker;
  });
}

Chat Status Updates

The app tracks who the user is currently chatting with:
firestore
    .collection('buyers')
    .document(id)
    .updateData({'chattingWith': peerId});
Future<bool> onBackPress() {
  if (isShowSticker) {
    setState(() {
      isShowSticker = false;
    });
  } else {
    firestore
        .collection('buyers')
        .document(id)
        .updateData({'chattingWith': null});
    Navigator.pop(context);
  }
  
  return Future.value(false);
}
This status can be used to show online indicators, typing indicators, or to route notifications appropriately.

Driver Contact List

Buyers can access chats from the contact list:
Widget buildItem(BuildContext context, DocumentSnapshot document) {
  return document['displayName']
          .toLowerCase()
          .contains(_searchText.toLowerCase())
      ? Container(
          child: FlatButton(
            child: Row(
              children: <Widget>[
                Material(
                  child: document['photoUrl'] != null
                      ? CachedNetworkImage(
                          imageUrl: document['photoUrl'],
                          width: 50.0,
                          height: 50.0,
                          fit: BoxFit.cover,
                        )
                      : Icon(
                          Icons.account_circle,
                          size: 50.0,
                        ),
                  borderRadius: BorderRadius.all(Radius.circular(25.0)),
                  clipBehavior: Clip.hardEdge,
                ),
                Flexible(
                  child: Container(
                    child: Column(
                      children: <Widget>[
                        Container(
                          child: Text(
                            '${document['displayName']}',
                            style: TextStyle(
                                color: Theme.of(context).primaryColor),
                          ),
                          alignment: Alignment.centerLeft,
                          margin: EdgeInsets.fromLTRB(10.0, 0.0, 0.0, 5.0),
                        ),
                      ],
                    ),
                    margin: EdgeInsets.only(left: 5.0),
                  ),
                ),
              ],
            ),
            onPressed: () {
              Navigator.push(
                  context,
                  MaterialPageRoute(
                      builder: (context) => Chat(
                            peerName: document['displayName'],
                            store: firestore,
                            peerId: document.documentID,
                            peerAvatar: document['photoUrl'],
                          )));
            },
          ),
        )
      : Container();
}

Image Handling

Images are displayed with loading states and error handling:
CachedNetworkImage(
  placeholder: (context, url) => Container(
    child: CircularProgressIndicator(
      valueColor:
          AlwaysStoppedAnimation<Color>(themeColor),
    ),
    width: 200.0,
    height: 200.0,
    padding: EdgeInsets.all(70.0),
    decoration: BoxDecoration(
      color: greyColor2,
      borderRadius: BorderRadius.all(
        Radius.circular(8.0),
      ),
    ),
  ),
  errorWidget: (context, url, error) => Material(
    child: Image.asset(
      'assets/images/img_not_available.jpeg',
      width: 200.0,
      height: 200.0,
      fit: BoxFit.cover,
    ),
  ),
  imageUrl: document['content'],
  width: 200.0,
  height: 200.0,
  fit: BoxFit.cover,
)
Ensure Firebase Storage security rules allow authenticated users to upload images to prevent unauthorized access.

Message Validation

void onSendMessage(String content, int type) {
  if (content.trim() != '') {
    textEditingController.clear();
    // Send message...
    listScrollController.animateTo(0.0,
        duration: Duration(milliseconds: 300), curve: Curves.easeOut);
  } else {
    Fluttertoast.showToast(msg: 'Nothing to send');
  }
}

Performance Optimizations

1

Message Limit

Load only 20 most recent messages initially to reduce data transfer
2

Image Caching

Use CachedNetworkImage to cache downloaded images locally
3

Lazy Loading

ListView.builder creates widgets on-demand as user scrolls
4

Firestore Indexes

Ensure composite index on timestamp for efficient queries
Chats are accessible from the Chats tab:
TabBar(
  controller: _tabController,
  tabs: [
    Tab(text: ('Chats')),      // Chat list
    Tab(text: ('Request')),
    Tab(text: ('Orders')),
  ],
)

Next Steps

Order Management

Create and manage product orders

Delivery Tracking

Track deliveries in real-time

Build docs developers (and LLMs) love