Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, desktop, and embedded devices from a single codebase.
I’ve been using Flutter since its very early release days. Having an experience with Native Android Development, I could clearly see the perks of Flutter’s declarative UI approach with its cross-platform compatibility being the cherry on top.
In this series of Flutter UI designs, I’ll try to show you how with Flutter, it becomes very easy to replicate some of the renowned apps we use on our daily basis. What better way to start the series off than with WhatsApp. Probably the most used Social Media Application in the world.
Enough of the talking. Time to get our hands dirty now.
We’ll start off with the very simple app as a skeleton which has an AppBar and Text saying ‘Hello World’ in the centre of the body.
Here’s how it looks in code:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: const Color(0xff128C7E),
title: const Text('WhatsApp'),
),
body: const Center(child: Text('Hello World'),
),
),
);
}
}
I know, I know it doesn’t look like the Original WhatsApp but we’ll get there together. Step-by-step. Let’s start with the AppBar. What’s missing?
- Search button and three-Dot Menu parallel to WhatsApp text.
- Tabs to open Camera, Chats, Status, Calls.
For The search button and the three-dot Menu Button, AppBar widget has a property called actions that takes in a list of Widgets. We can use an Icon widget to get a search button.
For the three-dot Menu, There’s a widget called PopupMenuButton in Flutter with which we can get as many Popup items as we need in the form of a list. Let’s see how that would look and dive into the code:
Let's see the code now. Shall we?
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
actions: [
// Widget for the search button
InkWell(child: const Icon(Icons.search), onTap:(){
// Implement Search functionality here
}),
// Widget for implementing the three-dot menu
PopupMenuButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
itemBuilder: (context) {
return [
// In this case, we need 5 popupmenuItems one for each option.
const PopupMenuItem(child: Text('New Group')),
const PopupMenuItem(child: Text('New Broadcast')),
const PopupMenuItem(child: Text('Linked Devices')),
const PopupMenuItem(child: Text('Starred Messages')),
const PopupMenuItem(child: Text('Settings')),
];
},
),
],
backgroundColor: const Color(0xff128C7E),
title: const Text('WhatsApp'),
),
body: const Center(child: Text('Hello World'))
),
);
}
}
Now let’s get to the Tabs Implementation, For that we’ll get the help of AppBar’s bottom property and provider it with TabBar widget which takes in list of tabs. For every Tab selected, We can show a different view to the User using the TabBarView widget which we can use in the body but we’ll get there later on in this article.
Here’s the code for it:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
Widget openPopUp() {
return PopupMenuButton(
itemBuilder: (context) {
return List.generate(
3,
(index) => const PopupMenuItem(
child: Text('Setting'),
));
},
);
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
actions: [
// Widget for the search
const Icon(Icons.search),
// Widget for implementing the three-dot menu
PopupMenuButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
itemBuilder: (context) {
return [
// In this case, we need 5 popupmenuItems one for each option.
const PopupMenuItem(child: Text('New Group')),
const PopupMenuItem(child: Text('New Broadcast')),
const PopupMenuItem(child: Text('Linked Devices')),
const PopupMenuItem(child: Text('Starred Messages')),
const PopupMenuItem(child: Text('Settings')),
];
},
),
],
backgroundColor: const Color(0xff128C7E),
title: const Text('WhatsApp'),
bottom: const TabBar(
indicatorSize: TabBarIndicatorSize.tab,
indicatorColor: Colors.white,
tabs: [
Tab(
iconMargin: EdgeInsets.all(100),
child: Icon(
Icons.camera_alt_rounded,
)),
Tab(
child: Text('CHATS', style: TextStyle(color: Colors.white)),
),
Tab(
child: Text('STATUS', style: TextStyle(color: Colors.white)),
),
Tab(
child: Text('CALLS', style: TextStyle(color: Colors.white)),
),
],
labelColor: Colors.white,
),
),
body: const Center(child: Text('Hello World'))
);
}
}
I think we’re pretty much done with the AppBar. Since the first tab Camera requires a functionality and that’s beyond the scope of this article, we’ll ignore that and create a Single Tab Views for Chats, Status, Calls tab.
Chats Tab:
Lets Make the Chats Tab View First. For that we need to break down the layout. In WhatsApp Chat Screen, We have a profile picture, Username, last message,status of the message and the timestamp. Let’s first see how to UI would look and then dive into the code implementation.
Here’s a look at its code implementation:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
Widget openPopUp() {
return PopupMenuButton(
itemBuilder: (context) {
return List.generate(
3,
(index) => const PopupMenuItem(
child: Text('Setting'),
));
},
);
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
actions: [
// Widget for the search
const Icon(Icons.search),
// Widget for implementing the three-dot menu
PopupMenuButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
itemBuilder: (context) {
return [
// In this case, we need 5 popupmenuItems one for each option.
const PopupMenuItem(child: Text('New Group')),
const PopupMenuItem(child: Text('New Broadcast')),
const PopupMenuItem(child: Text('Linked Devices')),
const PopupMenuItem(child: Text('Starred Messages')),
const PopupMenuItem(child: Text('Settings')),
];
},
),
],
backgroundColor: const Color(0xff128C7E),
title: const Text('WhatsApp'),
bottom: const TabBar(
indicatorSize: TabBarIndicatorSize.tab,
indicatorColor: Colors.white,
tabs: [
Tab(
iconMargin: EdgeInsets.all(100),
child: Icon(
Icons.camera_alt_rounded,
)),
Tab(
child: Text('CHATS', style: TextStyle(color: Colors.white)),
),
Tab(
child: Text('STATUS', style: TextStyle(color: Colors.white)),
),
Tab(
child: Text('CALLS', style: TextStyle(color: Colors.white)),
),
],
labelColor: Colors.white,
),
),
// ! THE DESIGNED BODY
body: const TabBarView(
children: [
Center(child: Text('This feature is coming soon')),
ChatsTab(),
Center(child: Text('Status feature is coming soon')),
Center(child: Text('Call feature is coming soon')),
],
),
),
);
}
}
class ChatsTab extends StatelessWidget {
const ChatsTab({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
children: const [
SingleChatWidget(
chatTitle: "Arya Stark",
chatMessage: 'I wish GoT had better ending',
seenStatusColor: Colors.blue,
imageUrl:
'https://static-koimoi.akamaized.net/wp-content/new-galleries/2020/09/maisie-williams-aka-arya-stark-of-game-of-thrones-someone-told-me-in-season-three-that-i-was-going-to-kill-the-night-king001.jpg'),
SingleChatWidget(
chatTitle: "Robb Stark",
chatMessage: 'Did you check Maisie\'s latest post?',
seenStatusColor: Colors.grey,
imageUrl:
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRDXCC-UB67rk0HtbmrDvVsIGvnPfTAMc_tSg&usqp=CAU'),
SingleChatWidget(
chatTitle: "Jaqen H'ghar",
chatMessage: 'Valar Morghulis',
seenStatusColor: Colors.grey,
imageUrl:
'https://static3.srcdn.com/wordpress/wp-content/uploads/2017/06/Jaqen-Hghar-Game-of-Thrones.jpg'),
SingleChatWidget(
chatTitle: "Sansa Stark",
chatMessage: 'The North Remembers',
seenStatusColor: Colors.blue,
imageUrl:
'https://i.insider.com/5ce420e193a15232821d3084?width=700'),
SingleChatWidget(
chatTitle: "Jon Snow",
chatMessage: 'Stick em\' with the pointy end',
seenStatusColor: Colors.grey,
imageUrl:
'https://i.insider.com/5cb3c8e96afbee373d4f2b62?width=700'),
SingleChatWidget(
chatTitle: "Arya Stark",
chatMessage: 'I wish GoT had better ending',
seenStatusColor: Colors.blue,
imageUrl:
'https://static-koimoi.akamaized.net/wp-content/new-galleries/2020/09/maisie-williams-aka-arya-stark-of-game-of-thrones-someone-told-me-in-season-three-that-i-was-going-to-kill-the-night-king001.jpg'),
SingleChatWidget(
chatTitle: "Robb Stark",
chatMessage: 'Did you check Maisie\'s latest post?',
seenStatusColor: Colors.blue,
imageUrl:
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRDXCC-UB67rk0HtbmrDvVsIGvnPfTAMc_tSg&usqp=CAU'),
SingleChatWidget(
chatTitle: "Jon Snow",
chatMessage: 'Stick em\' with the pointy end',
seenStatusColor: Colors.blue,
imageUrl:
'https://i.insider.com/5cb3c8e96afbee373d4f2b62?width=700'),
],
),
),
);
}
}
In the code, We’re using a ListView. Since the number of chat items is constant a simple ListView would do just fine otherwise if the items were dynamic, ListView.builder would have been our choice. The SingleChatWidget used in the ListView defines how a single item would look like. Here’s its implementation in code:
// Widget to define how a single chat widget would look like
class SingleChatWidget extends StatelessWidget {
final String? chatMessage;
final String? chatTitle;
final Color? seenStatusColor;
final String? imageUrl;
const SingleChatWidget({
Key? key,
this.chatMessage,
this.chatTitle,
this.seenStatusColor,
this.imageUrl,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(imageUrl!),
),
Expanded(
child: ListTile(
title: Text('$chatTitle',
style: const TextStyle(fontWeight: FontWeight.w600)),
subtitle: Row(children: [
Icon(
seenStatusColor == Colors.blue ? Icons.done_all : Icons.done,
size: 15,
color: seenStatusColor,
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 6.0),
child: Text(
'$chatMessage',
style: const TextStyle(overflow: TextOverflow.ellipsis),
),
),
),
]),
trailing: Column(
children: const [
Padding(
padding: EdgeInsets.only(top: 8.0),
child: Text(
'Yesterday',
),
),
],
),
),
),
],
);
}
}
Status Tab:
Let’s now move to the status tab. The status tab on WhatsApp has firsly a row displaying the user’s photo if the user has no status update and a button to add one. Below it are the viewed stories from the contacts and after it are the muted status updates of contacts.
Confused already? Let’s make it easier by taking a look at the UI and then understanding its implementation in code.
Let’s decode the status screen now:
import 'package:flutter/material.dart';
import 'package:whatsapp_ui/widgets/single_status_widget.dart';
class StatusTab extends StatelessWidget {
const StatusTab({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Stack(
children: const [
CircleAvatar(
backgroundColor: Color(0xff128C7E),
foregroundColor: Color(0xff128C7E),
radius: 30,
backgroundImage: AssetImage('lib/assets/images/Me.jpg'),
),
Positioned(
top: 40,
left: 40,
child: CircleAvatar(
radius: 10,
child: Icon(Icons.add, size: 20),
),
),
],
),
const Expanded(
child: ListTile(
title: Text('My Status'),
subtitle: Padding(
padding: EdgeInsets.only(top: 2.0),
child: Text('Tap to add status update'),
),
),
),
],
),
const Padding(
padding: EdgeInsets.only(top: 8.0, bottom: 8.0),
child: Text('Viewed updates',
style: TextStyle(fontWeight: FontWeight.w400)),
),
Row(
children: [
Stack(
children: const [
CircleAvatar(
backgroundColor: Colors.grey,
radius: 30,
child: CircleAvatar(
radius: 28,
backgroundImage:
AssetImage('lib/assets/images/Mountains.jpg'),
),
),
],
),
const Expanded(
child: ListTile(
title: Text('Arya Stark'),
subtitle: Padding(
padding: EdgeInsets.only(top: 2.0),
child: Text('7 minutes ago'),
),
),
),
],
),
// Since the ExpansionTile has top and bottom borders by default and we don't want that so we
//use Theme to override its dividerColor property
Theme(
data: ThemeData().copyWith(dividerColor: Colors.transparent),
child: const ExpansionTile(
textColor: Colors.black,
tilePadding: EdgeInsets.all(0.0),
title: Text('Muted updates',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
)),
children: [
SingleStatusItem(
statusTitle: 'Cersei Lannister',
statusTime: '56 minutes ago',
statusImage: 'lib/assets/images/Ansu.jpg',
),
SingleStatusItem(
statusTitle: 'Lyanna Mormont',
statusTime: '2 minutes ago',
statusImage: 'lib/assets/images/Ubuntu.png',
),
SingleStatusItem(
statusTitle: 'Daenerys Targaryen',
statusTime: '12 minutes ago',
statusImage: 'lib/assets/images/Mountains.jpg',
),
],
),
)
],
),
),
);
}
}
As for the SingleStatusItem widget, here’s how its implemented:
import 'package:flutter/material.dart';
class SingleStatusItem extends StatelessWidget {
final String? statusTitle;
final String? statusTime;
final String? statusImage;
const SingleStatusItem({
Key? key,
this.statusTitle,
this.statusTime,
this.statusImage,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
Stack(
children: [
CircleAvatar(
backgroundColor: Colors.grey,
radius: 30,
child: CircleAvatar(
radius: 28,
backgroundImage: AssetImage('$statusImage'),
),
),
],
),
Expanded(
child: ListTile(
title: Text('$statusTitle'),
subtitle: Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Text("$statusTime"),
),
),
),
],
);
}
}
Calls Tab:
Last but not the least, the calls tab.If we take a closer look at the calls screen, its pretty much the same as the chat screen with some minor differences such as in the place of last sent or received text, there’s status of the call and in place of the timestamp, there’s a button for an audio or a video call. Let’s see how it looks after implementation and then go through the code.
Since the UI is very much alike the chat screen, so is the code for the calls screen. Let’s take a look at it.
import 'package:flutter/material.dart';
import 'package:whatsapp_ui/widgets/single_call_widget.dart';
import 'package:whatsapp_ui/widgets/single_chat_widget.dart';
class CallTab extends StatelessWidget {
const CallTab({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
children: const [
SingleCallWidget(
callStatus: 'Outgoing',
callType: 'Audio',
chatMessage: 'Today, 12:28 PM',
chatTitle: 'Arya Stark',
imageUrl:
'https://static-koimoi.akamaized.net/wp-content/new-galleries/2020/09/maisie-williams-aka-arya-stark-of-game-of-thrones-someone-told-me-in-season-three-that-i-was-going-to-kill-the-night-king001.jpg',
),
SingleCallWidget(
callStatus: 'Incoming',
callType: 'Video',
chatMessage: 'Today, 01:11 AM',
chatTitle: 'Cersei Lannister',
imageUrl:
'https://pyxis.nymag.com/v1/imgs/8a2/6a6/8ddcaf4454e34b72484c5393560af17366-09-cersei-lannister.rsquare.w700.jpg',
),
SingleCallWidget(
callStatus: 'Incoming',
callType: 'Video',
chatMessage: 'Today, 5:28 AM',
chatTitle: 'Red Woman',
imageUrl:
'https://upload.wikimedia.org/wikipedia/en/8/80/Melisandre-Carice_van_Houten.jpg',
),
SingleCallWidget(
callStatus: 'Outgoing',
callType: 'Audio',
chatMessage: 'Today, 12:28 PM',
chatTitle: 'The Mountain',
imageUrl: 'lib/assets/images/Mountains.jpg',
),
],
),
),
);
}
}
As for the SingleCallWidget Implementation, here’s the code for it:
// Widget to define how a single call widget would look like
import 'package:flutter/material.dart';
class SingleCallWidget extends StatelessWidget {
final String? chatMessage;
final String? chatTitle;
final String? callStatus;
final String? imageUrl;
final String? callType;
const SingleCallWidget({
Key? key,
this.chatMessage,
this.chatTitle,
this.callStatus,
this.imageUrl, this.callType,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(imageUrl!),
),
Expanded(
child: ListTile(
title: Text('$chatTitle',
style: const TextStyle(fontWeight: FontWeight.w600)),
subtitle: Row(children: [
Icon(
callStatus == 'Incoming'
? (Icons.call_received_sharp)
: Icons.call_made_sharp,
size: 15,
color: callStatus == 'Incoming' ? Colors.teal : Colors.red,
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 6.0),
child: Text(
'$chatMessage',
style: const TextStyle(overflow: TextOverflow.ellipsis),
),
),
),
]),
trailing: Column(
children: [
Padding(
padding:const EdgeInsets.only(top: 8.0),
child: Icon(callType == 'Audio' ? Icons.call :Icons.videocam,color: Colors.teal),
),
],
)),
),
],
);
}
}
One thing that I find missing is the Floating action button at the bottom. We can add that by using the floatingActionButton property on the Scaffold and then we can either make our own floatingActionButton or use a built-in Flutter FloatingActionButton(FAB) widget as follows:
import 'package:flutter/material.dart';
import 'screens/call_screen.dart';
import 'screens/chat_screen.dart';
import 'screens/status_screen.dart';
import 'widgets/single_chat_widget.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.teal,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
Widget openPopUp() {
return PopupMenuButton(
itemBuilder: (context) {
return List.generate(
3,
(index) => const PopupMenuItem(
child: Text('Setting'),
));
},
);
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
child: Scaffold(
floatingActionButton: Container(
width: 50,
height: 50,
child: FloatingActionButton(
child: const Icon(Icons.chat),
onPressed: () {},
),
),
appBar: AppBar(
actions: [
// Widget for the search
const Icon(Icons.search),
// Widget for implementing the three-dot menu
PopupMenuButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
itemBuilder: (context) {
return [
// In this case, we need 5 popupmenuItems one for each option.
const PopupMenuItem(child: Text('New Group')),
const PopupMenuItem(child: Text('New Broadcast')),
const PopupMenuItem(child: Text('Linked Devices')),
const PopupMenuItem(child: Text('Starred Messages')),
const PopupMenuItem(child: Text('Settings')),
];
},
),
],
backgroundColor: const Color(0xff128C7E),
title: const Text('WhatsApp'),
bottom: const TabBar(
indicatorSize: TabBarIndicatorSize.tab,
indicatorColor: Colors.white,
tabs: [
Tab(
iconMargin: EdgeInsets.all(100),
child: Icon(
Icons.camera_alt_rounded,
)),
Tab(
child: Text('CHATS', style: TextStyle(color: Colors.white)),
),
Tab(
child: Text('STATUS', style: TextStyle(color: Colors.white)),
),
Tab(
child: Text('CALLS', style: TextStyle(color: Colors.white)),
),
],
labelColor: Colors.white,
),
),
body: const TabBarView(
children: [
Center(child: Text('This feature is coming soon')),
ChatsTab(),
StatusTab(),
CallTab(),
],
),
),
);
}
}
With that, I think we’re pretty much done for this article. Thanks for reading this article! ❤️ You can find all the code on my github @WhatsApp Clone Repo
That’s all for now Folks! These were some of the changes that Flutter 2.5 brings. Thanks for reading this article ❤️
Clap 👏 If this article helped you.
Feel free to post any queries or corrections you think are required ✔
Do leave a feedback so I can improve on my content. Thankyou! 😃
If you’re interested, here are some of my other articles:
Top comments (1)
What about a chat itself? Did you implement that by any chance?