Flutter UI Tutorial – How to Build a Chat App with Stories Using the Flutter SDK

freeCodeCampfreeCodeCamp
11 min read

By Krissanawat

Chat applications have become one of the easiest ways to communicate over the internet. As such, many applications incorporate chat features in them so that users can interact and engage in social communications.

These applications have made the world smaller with their powerful end to end audio/video and text-based communication. On top of that, other features such as sharing stories, sending attachments, and more have made these apps even more engaging and useful.

Most apps also have a stories feature. It lets users share their experiences in brief and the story disappears after a certain amount of time. These features get users hooked to that app and make them want to share their thoughts, ideas, memories, and experiences.

Facebook Messenger is one of the most widely-used chat applications. It is just behind WhatsApp in overall usage across the world.

In this tutorial, we are going to replicate the Messenger UI using the Flutter mobile application development framework. We'll also explore widget-based UI development using Flutter coding. This will get us familiar with the Flutter ecosystem as well as best practices for writing Flutter code.

Here, we are going to implement the main conversation home screen of the messenger app which will contain a top app bar, a search bar, a stories section, and a conversation list section.

Through this process, we will see how Flutter makes the UI development easier and we'll get a messenger clone chat application out of it.

So, let's get started!

Create a new Flutter project

First, we need to create a new Flutter project. To do that, make sure that the Flutter SDK and other Flutter app development-related requirements are properly installed.

If everything is properly set up, then to create a project we can simply run the following command in the desired local directory:

flutter create messengerUI

After the project has been set up, we can navigate inside the project directory and execute the following command in the terminal to run the project in either an available emulator or an actual device:

flutter run

After a successful build, we will get the following result in the emulator screen:

Image

Now, we need to replace the default template with our own project structure template.

First, we need to create a folder called ./screens inside the ./lib folder. Then, inside the ./lib/screens folder, we need to create a new file called conversations.dart.

Inside conversation.dart, we are going to implement a simple Stateful widget class returning a Scaffold widget with a basic App bar and an empty Container body. The code for conversations.dart is shown in the code snippet below:

import 'package:flutter/material.dart';

class Conversations extends StatefulWidget {
  @override
  _ConversationsState createState() => _ConversationsState();
}

class _ConversationsState extends State<Conversations> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Chat"),
      ),
      body: Container(),
    );
  }
}

Now, we need to replace the default template in the main.dart file and call the Conversations screen in the home option of MaterialApp widget as shown in the code snippet below:

import 'package:flutter/material.dart';
import 'package:messangerUI/screens/conversations.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Messenger Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Conversations(),
    );
  }
}

We get the result as shown in the image below:

Image

How to Add the AppBar

Now, we are going to customize the App bar at the top. Since the app bar needs to be scrollable, we are not going to use the appBar option provided by Scaffold widget. We are simply going to use the ListView widget in the body option of Scaffold and keep all other widgets as children of the ListView widget.

The overall implementation of the custom app bar is provided in the code snippet below:

Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
          child: Container(
        padding: EdgeInsets.only(left: 20, right: 20, top: 15),
        child: ListView(
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                Container(
                  width: 40,
                  height: 40,
                  decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      image: DecorationImage(
                          image: NetworkImage(
                              "<https://randomuser.me/api/portraits/men/11.jpg>"),
                          fit: BoxFit.cover)),
                ),
                Text(
                  "Chats",
                  style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
                ),
                Icon(Icons.edit)
              ],
            ),
          ],
        ),
      )),
    );
  }

For the app bar UI, we have used a Row widget inside the ListView widget. Inside the Row widget, we have placed a Container widget with NetworkImage as a child, a Text widget, and an Icon widget.

Hence, we will get the result as shown in the image below:

Image

Now, we are going to add a search input field just below the app bar. It will contain an InputField widget decorated with styles and a search icon.

Since we need a text controller for the InputField widget we need to initialize it first as shown in the code snippet below:

TextEditingController _searchController = new TextEditingController();

Now, we are going to implement the UI for the search bar just below the Row widget which is inside the parent ListView widget. We'll use the SizedBox widget to give a tiny separation between the two sections.

The overall implementation of search bar using a TextField widget inside a Container widget with decoration is shown in the code snippet below:

body: SafeArea(
          child: Container(
        padding: EdgeInsets.only(left: 20, right: 20, top: 15),
        child: ListView(
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                Container(
                  width: 40,
                  height: 40,
                  decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      image: DecorationImage(
                          image: NetworkImage(
                              "<https://randomuser.me/api/portraits/men/11.jpg>"),
                          fit: BoxFit.cover)),
                ),
                Text(
                  "Chats",
                  style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
                ),
                Icon(Icons.edit)
              ],
            ),
            SizedBox(
              height: 15,
            ),
            Container(
              width: double.infinity,
              height: 40,
              decoration: BoxDecoration(
                  color: Color(0xFFe9eaec),
                  borderRadius: BorderRadius.circular(15)),
              child: TextField(
                cursorColor: Color(0xFF000000),
                controller: _searchController,
                decoration: InputDecoration(
                    prefixIcon: Icon(
                      Icons.search,
                      color: Color(0xFF000000).withOpacity(0.5),
                    ),
                    hintText: "Search",
                    border: InputBorder.none),
              ),
            ),
          ],
        ),
      )),
    );

Hence, we will get the search bar as shown in the image below:

Image

How to Implement the Stories Section

Now it is time to implement the stories section. Stories are popular feature in every social app these days. We are going to implement it just below the search bar.

This section will contain an image of the user with their name at the bottom. The users who have stories will have a blue circular ring around their image whereas others don't.

But first, we need to prepare a list of mock users to show them in the stories section.

For that, we are going to initialize a List called storyList. We'll keep some objects containing the user's information such as name, imageUrl, isOnline (to check if the user is online), and hasStory (to check it the user has a story).

The mock list data is provided in the code snippet below:

List storyList = [
    {
      "name": "Novac",
      "imageUrl": "<https://randomuser.me/api/portraits/men/31.jpg>",
      "isOnline": true,
      "hasStory": true,
    },
    {
      "name": "Derick",
      "imageUrl": "<https://randomuser.me/api/portraits/men/81.jpg>",
      "isOnline": false,
      "hasStory": false,
    },
    {
      "name": "Mevis",
      "imageUrl": "<https://randomuser.me/api/portraits/women/49.jpg>",
      "isOnline": true,
      "hasStory": false,
    },
    {
      "name": "Emannual",
      "imageUrl": "<https://randomuser.me/api/portraits/men/35.jpg>",
      "isOnline": true,
      "hasStory": true,
    },
    {
      "name": "Gracy",
      "imageUrl": "<https://randomuser.me/api/portraits/women/56.jpg>",
      "isOnline": false,
      "hasStory": false,
    },
    {
      "name": "Robert",
      "imageUrl": "<https://randomuser.me/api/portraits/men/36.jpg>",
      "isOnline": false,
      "hasStory": true,
    }
  ];

Now, we are going to implement a separate function that returns the overall UI for the Stories section. The implementation is provided in the code snippet below:

_stories() {
    return SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: Row(
        children: <Widget>[
          Padding(
            padding: EdgeInsets.only(right: 20),
            child: Column(
              children: <Widget>[
                Container(
                  width: 60,
                  height: 60,
                  decoration: BoxDecoration(
                      shape: BoxShape.circle, color: Color(0xFFe9eaec)),
                  child: Center(
                    child: Icon(
                      Icons.add,
                      size: 33,
                    ),
                  ),
                ),
                SizedBox(
                  height: 10,
                ),
                SizedBox(
                  width: 75,
                  child: Align(
                      child: Text(
                    'Your Story',
                    overflow: TextOverflow.ellipsis,
                  )),
                )
              ],
            ),
          ),
          Row(
              children: List.generate(storyList.length, (index) {
            return Padding(
              padding: const EdgeInsets.only(right: 20),
              child: Column(
                children: <Widget>[
                  Container(
                    width: 60,
                    height: 60,
                    child: Stack(
                      children: <Widget>[
                        storyList[index]['hasStory']
                            ? Container(
                                decoration: BoxDecoration(
                                    shape: BoxShape.circle,
                                    border: Border.all(
                                        color: Colors.blueAccent, width: 3)),
                                child: Padding(
                                  padding: const EdgeInsets.all(3.0),
                                  child: Container(
                                    width: 75,
                                    height: 75,
                                    decoration: BoxDecoration(
                                        shape: BoxShape.circle,
                                        image: DecorationImage(
                                            image: NetworkImage(
                                                storyList[index]['imageUrl']),
                                            fit: BoxFit.cover)),
                                  ),
                                ),
                              )
                            : Container(
                                width: 70,
                                height: 70,
                                decoration: BoxDecoration(
                                    shape: BoxShape.circle,
                                    image: DecorationImage(
                                        image: NetworkImage(
                                            storyList[index]['imageUrl']),
                                        fit: BoxFit.cover)),
                              ),
                        storyList[index]['isOnline']
                            ? Positioned(
                                top: 38,
                                left: 42,
                                child: Container(
                                  width: 20,
                                  height: 20,
                                  decoration: BoxDecoration(
                                      color: Color(0xFF66BB6A),
                                      shape: BoxShape.circle,
                                      border: Border.all(
                                          color: Color(0xFFFFFFFF), width: 3)),
                                ),
                              )
                            : Container()
                      ],
                    ),
                  ),
                  SizedBox(
                    height: 10,
                  ),
                  SizedBox(
                    width: 75,
                    child: Align(
                        child: Text(
                      storyList[index]['name'],
                      overflow: TextOverflow.ellipsis,
                    )),
                  )
                ],
              ),
            );
          }))
        ],
      ),
    );
  }

Here, we returned a SingleChildScrollView as a parent widget with an option for horizontal scrolling.

Then, we used the List.generate widget inside the Row widget to iterate through our storyList array. For each item in the list, a template inside the List.generate is returned. The conditional rendering is used for online users and those users who have stories.

Now, we need to call the function inside the ListView children just below the InputField making a separation using the SizedBox widget as shown in the code snippet below:

body: SafeArea(
          child: Container(
        padding: EdgeInsets.only(left: 20, right: 20, top: 15),
        child: ListView(
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                Container(
                  width: 40,
                  height: 40,
                  decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      image: DecorationImage(
                          image: NetworkImage(
                              "<https://randomuser.me/api/portraits/men/11.jpg>"),
                          fit: BoxFit.cover)),
                ),
                Text(
                  "Chats",
                  style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
                ),
                Icon(Icons.edit)
              ],
            ),
            SizedBox(
              height: 15,
            ),
            Container(
              width: double.infinity,
              height: 40,
              decoration: BoxDecoration(
                  color: Color(0xFFe9eaec),
                  borderRadius: BorderRadius.circular(15)),
              child: TextField(
                cursorColor: Color(0xFF000000),
                controller: _searchController,
                decoration: InputDecoration(
                    prefixIcon: Icon(
                      Icons.search,
                      color: Color(0xFF000000).withOpacity(0.5),
                    ),
                    hintText: "Search",
                    border: InputBorder.none),
              ),
            ),
            SizedBox(
              height: 20,
            ),
            _stories(),
          ],
        ),
      )),
    );

We will get the result as shown in the demo below:

Image

As you can see, the stories section is scrollable horizontally.

How to Create the Conversation List Section

Now, we are going to create a list of conversations just below the Stories section. It will contain an image of the user, their name, message, and time.

For the conversation list as well we are going to create some mock data. The list is similar to storiesList but has two extra pieces of information for message and time. The mock data list conversationList is shown in the code snippet below:

List conversationList = [
    {
      "name": "Novac",
      "imageUrl": "<https://randomuser.me/api/portraits/men/31.jpg>",
      "isOnline": true,
      "hasStory": true,
      "message": "Where are you?",
      "time": "5:00 pm"
    },
    {
      "name": "Derick",
      "imageUrl": "<https://randomuser.me/api/portraits/men/81.jpg>",
      "isOnline": false,
      "hasStory": false,
      "message": "It's good!!",
      "time": "7:00 am"
    },
    {
      "name": "Mevis",
      "imageUrl": "<https://randomuser.me/api/portraits/women/49.jpg>",
      "isOnline": true,
      "hasStory": false,
      "message": "I love You too!",
      "time": "6:50 am"
    },
    {
      "name": "Emannual",
      "imageUrl": "<https://randomuser.me/api/portraits/men/35.jpg>",
      "isOnline": true,
      "hasStory": true,
      "message": "Got to go!! Bye!!",
      "time": "yesterday"
    },
    {
      "name": "Gracy",
      "imageUrl": "<https://randomuser.me/api/portraits/women/56.jpg>",
      "isOnline": false,
      "hasStory": false,
      "message": "Sorry, I forgot!",
      "time": "2nd Feb"
    },
    {
      "name": "Robert",
      "imageUrl": "<https://randomuser.me/api/portraits/men/36.jpg>",
      "isOnline": false,
      "hasStory": true,
      "message": "No, I won't go!",
      "time": "28th Jan"
    },
    {
      "name": "Lucy",
      "imageUrl": "<https://randomuser.me/api/portraits/women/56.jpg>",
      "isOnline": false,
      "hasStory": false,
      "message": "Hahahahahaha",
      "time": "25th Jan"
    },
    {
      "name": "Emma",
      "imageUrl": "<https://randomuser.me/api/portraits/women/56.jpg>",
      "isOnline": false,
      "hasStory": false,
      "message": "Been a while!",
      "time": "15th Jan"
    }
  ];

Similar to the stories section, we are going to build the conversation list section as a separate function.

As a parent widget, we have returned the Column widget. The child of the Column widget contains the List.generate widget that iterates through the conversationList array and provides the UI for each item in the conversation list.

We use conditional rendering for online users and those users who have stories. The overall implementation of the function is provided in the code snippet below:

_conversations(BuildContext context) {
    return Column(
      children: List.generate(conversationList.length, (index) {
        return InkWell(
          child: Padding(
            padding: const EdgeInsets.only(bottom: 20),
            child: Row(
              children: <Widget>[
                Container(
                  width: 60,
                  height: 60,
                  child: Stack(
                    children: <Widget>[
                      conversationList[index]['hasStory'] ? 
                      Container(
                        decoration: BoxDecoration(
                            shape: BoxShape.circle,
                            border:
                                Border.all(color: Colors.blueAccent, width: 3)),
                        child: Padding(
                          padding: const EdgeInsets.all(3.0),
                          child: Container(
                            width: 75,
                            height: 75,
                            decoration: BoxDecoration(
                                shape: BoxShape.circle,
                                image: DecorationImage(
                                    image: NetworkImage(
                                        conversationList[index]['imageUrl']),
                                    fit: BoxFit.cover)),
                          ),
                        ),
                      )
                      : Container(
                        width: 70,
                        height: 70,
                        decoration: BoxDecoration(
                            shape: BoxShape.circle,
                            image: DecorationImage(
                                image: NetworkImage(
                                    conversationList[index]['imageUrl']),
                                fit: BoxFit.cover)),
                      ),
                      conversationList[index]['isOnline']
                          ? Positioned(
                              top: 38,
                              left: 42,
                              child: Container(
                                width: 20,
                                height: 20,
                                decoration: BoxDecoration(
                                    color: Color(0xFF66BB6A),
                                    shape: BoxShape.circle,
                                    border: Border.all(color: Color(0xFFFFFFFF), width: 3)),
                              ),
                            )
                          : Container()
                    ],
                  ),
                ),
                SizedBox(
                  width: 20,
                ),
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(
                      conversationList[index]['name'],
                      style:
                          TextStyle(fontSize: 17, fontWeight: FontWeight.w500),
                    ),
                    SizedBox(
                      height: 5,
                    ),
                    SizedBox(
                      width: MediaQuery.of(context).size.width - 135,
                      child: Text(
                        conversationList[index]['message'] +
                            " - " +
                            conversationList[index]['time'],
                        style: TextStyle(
                            fontSize: 15, color: Color(0xFF000000).withOpacity(0.7)),
                        overflow: TextOverflow.ellipsis,
                      ),
                    )
                  ],
                )
              ],
            ),
          ),
        );
      }),
    );
  }

Now we need to call the _converstions() function in the in ListView of Scaffold just below the Stories function as shown in the code snippet below:

body: SafeArea(
          child: Container(
        padding: EdgeInsets.only(left: 20, right: 20, top: 15),
        child: ListView(
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: <Widget>[
                Container(
                  width: 40,
                  height: 40,
                  decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      image: DecorationImage(
                          image: NetworkImage(
                              "<https://randomuser.me/api/portraits/men/11.jpg>"),
                          fit: BoxFit.cover)),
                ),
                Text(
                  "Chats",
                  style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
                ),
                Icon(Icons.edit)
              ],
            ),
            SizedBox(
              height: 15,
            ),
            Container(
              width: double.infinity,
              height: 40,
              decoration: BoxDecoration(
                  color: Color(0xFFe9eaec),
                  borderRadius: BorderRadius.circular(15)),
              child: TextField(
                cursorColor: Color(0xFF000000),
                controller: _searchController,
                decoration: InputDecoration(
                    prefixIcon: Icon(
                      Icons.search,
                      color: Color(0xFF000000).withOpacity(0.5),
                    ),
                    hintText: "Search",
                    border: InputBorder.none),
              ),
            ),
            SizedBox(
              height: 20,
            ),
            _stories(),
            SizedBox(
              height: 20,
            ),
            **_conversations(context)**
          ],
        ),
      )),
    );
  }

We will get the result as shown in the demo below:

Image

As you can see, the overall screen is scrollable vertically with Conversation List view and the Stories section is scrollable horizontally.

Finally, we have successfully created the home screen of a messenger app using Flutter.

Conclusion

The main objective of this tutorial was to show you how to build a UI like Facebook messenger using the Flutter ecosystem.

If you look closely at the code, you'll see that most of the implementation was pretty easy because of the flexibility and structured layout build that Flutter provides.

With only a few widgets, we can place every component in the UI in the correct position. Along with this beautiful UI creation, you can also learn basic coding patterns for Flutter development.

Separating large sections of code into separate functions helps simplify and clean up our code. This demonstrated a best coding practice for UI development in Flutter.

The tutorial also highlights how some widgets in Flutter make things easier for us, such as horizontal scrolling and placement icons and images with styles. You can definitely take this information and use it to build your own chat application in the future.

Moreover, you can also get inspiration from state of the art Flutter chat app templates out there that provide beautiful UIs as well as powerful features. And in case you want to check out chat application templates built using other mobile application development frameworks, you peruse these React Native Chat app templates as well.

0
Subscribe to my newsletter

Read articles from freeCodeCamp directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

freeCodeCamp
freeCodeCamp

Learn to code. Build projects. Earn certifications—All for free.