Build a Samsung Messages Clone UI
Build a native Android messaging app UI with Flutter and Dart.
Introduction
30 Second Summary
Messaging apps are something you use every single day. Have you ever wondered how they're actually built?
In this project, you will build a clone of Samsung Messages' interface using Flutter. You'll create a conversation list screen and a chat detail screen that run on an Android emulator, all from your Windows machine.
What You'll Build
A native Android messaging app UI that mirrors Samsung Messages' clean design, featuring a scrollable conversation list and interactive chat bubbles.
By the end of this project, you'll have:
- A complete Flutter development environment configured on Windows with an Android emulator running your app.
- A conversation list screen showing contacts with avatars, message previews, and timestamps.
- A chat detail screen with styled message bubbles for sent and received messages, plus a working send button.
- Secret Mission: Add a search bar to filter conversations and a floating action button to start new messages.
Are there any prerequisites?
No prior mobile development experience is needed. This guide walks you through installing every tool from scratch, including Flutter, Android Studio, and Git.
Set Up Your Flutter Development Environment
Before you can build a messaging app, you need the right tools on your machine. Flutter is Google's toolkit for building native mobile apps from a single codebase, and it needs a few supporting tools to work.
In this step, you will install Git, the Flutter SDK, and Android Studio so that you have a complete development environment ready to go.
In this step, get ready to:
- Install Git for Windows, the Flutter SDK, and verify your setup.
- Configure Android Studio with the Android SDK and required tools.
- Create an Android emulator, scaffold your Flutter project, and run it.
Install Git, Flutter SDK, and verify your setup
Flutter's installer requires Git to download and manage its source code. You will install Git for Windows first, then use VS Code to install the Flutter SDK.
- Download Git for Windows by visiting this page in your browser:
- https://git-scm.com/download/win
- Run the downloaded installer.
- Accept all the default options by clicking Next through each screen, then click Install.
- Click Finish when the installation completes.
What is Git?
Git is a version control system that tracks changes to your code. Flutter uses it behind the scenes to download and update itself.
- Verify Git installed correctly by opening a terminal in VS Code. Press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) to open the Command Palette.
- Type Terminal: Create New Terminal and press Enter.
- Check your Git installation by running this command:
git --version
✔️ I see a version number
Git is ready to go. Now you can install the Flutter SDK.
ⓧ Command not found
The installer may not have added Git to your system PATH. Close and reopen VS Code completely, then try the command again.
If it still fails, restart your computer and try once more. Windows sometimes needs a restart to register new PATH entries.
- Now install the Flutter SDK using VS Code's built-in installer. Press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) to open the Command Palette.
- Type flutter and select Flutter: New Project from the dropdown.
- VS Code will prompt you that the Flutter SDK is not found. Click Download SDK.
- Choose a folder to store the SDK (your home folder or C:\src works well).
- Click Clone Flutter. This downloads the Flutter SDK, which may take a few minutes.
- When the download finishes, click Add SDK to PATH.
What just happened?
VS Code downloaded the Flutter SDK (version 3.44) and added it to your system PATH. The PATH tells your terminal where to find the flutter command.
- Close and reopen your terminal in VS Code so the PATH update takes effect.
- Verify Flutter is installed by running this command:
flutter doctor -v
✔️ I see Flutter version info
Flutter is installed and responding. You will likely see some warnings about missing Android tooling. That is expected and you will fix it in the next substep.
ⓧ flutter: command not recognized
Your terminal cannot find the flutter command. Try closing VS Code entirely and reopening it.
If that does not work, go back to the Command Palette and repeat the Flutter: New Project step. Make sure you click Add SDK to PATH when prompted.
Still seeing errors?
Make sure Git for Windows was installed before you tried installing Flutter. Flutter needs Git to download its packages.
help me troubleshoot Flutter SDK installation issues on Windows
Install Android Studio and configure the SDK
Flutter builds Android apps using the Android SDK, which ships with Android Studio. You will use Android Studio primarily for its SDK and emulator tools, while keeping VS Code as your code editor.
- Download Android Studio by visiting this page in your browser:
- https://developer.android.com/studio
- Run the downloaded installer and follow the setup wizard. Accept all default options.
- When Android Studio opens for the first time, it will run an initial setup wizard that downloads essential SDK components. Follow the prompts and accept the defaults.
- Once the welcome screen appears, click More Actions and then SDK Manager.
Why install Android Studio if you code in VS Code?
Android Studio bundles the Android SDK, emulator, and build tools that Flutter needs to compile and run your app on an Android device. You will not write code in Android Studio. It simply provides the tools Flutter depends on.
- In the SDK Manager, click the SDK Platforms tab.
- Check the box next to the latest Android platform (API Level 36).
- Click the SDK Tools tab.
- Check the boxes next to these four tools:
- Android SDK Build-Tools.
- Android SDK Command-line Tools.
- Android Emulator.
- Android SDK Platform-Tools.
- Click Apply, then OK to download and install everything. This may take several minutes.
What are these SDK tools?
Build-Tools compile your Dart code into an Android app. Platform-Tools include adb, which communicates with your emulator. Command-line Tools let Flutter manage packages. The Emulator runs a virtual Android phone on your computer.
- Now accept the Android licenses so Flutter can use these tools. Switch back to your VS Code terminal and run this command:
flutter doctor --android-licenses
- The command will present several license agreements. Type y and press Enter for each one to accept them all.
What does this command do?
Android SDK components have license agreements from Google. Running flutter doctor --android-licenses accepts them all at once so Flutter can use the SDK without issues.
- Run flutter doctor -v again to confirm everything is green:
flutter doctor -v
You should see checkmarks next to Flutter, Android toolchain, and Android Studio. Some items like Chrome or VS Code may still show warnings, which is fine.
Seeing Android toolchain issues?
Make sure you installed all four SDK Tools listed above and that the licenses were accepted. Try running flutter doctor --android-licenses again.
help me fix Android toolchain issues in flutter doctor
Create an emulator, scaffold your project, and run it
With your tools installed, you can now create a virtual Android phone to test your app on. Then you will scaffold a new Flutter project and launch it on the emulator.
- Open Android Studio. Press the Windows key, type Android Studio, and press Enter.
- From the welcome screen, click More Actions and then Virtual Device Manager.
- Click Create Virtual Device.
- Select Phone as the category, then choose Pixel 6 (or any Pixel device) and click Next.
- On the System Image screen, select a recommended system image for API Level 36. If you see a Download link next to it, click it and wait for the download to complete.
- After selecting the system image, click Next.
- Leave the default AVD name and settings, then click Finish.
- Back in the Device Manager list, click the Start button (play triangle) next to your new emulator. The emulator may take a minute or two to boot for the first time.
What is an emulator?
An Android emulator is a virtual Android phone that runs on your computer. It lets you test your app without needing a physical device. Each emulator mimics a specific phone model and Android version.
Now you will create your Flutter project and run it on the emulator.
- Switch back to your VS Code terminal.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your Flutter project by running this command:
flutter create samsung_messages --empty
What does this command do?
The flutter create command scaffolds a brand-new Flutter project in a folder called samsung_messages. The --empty flag creates a minimal starting point instead of the default counter-app boilerplate.
- Move into the project folder by running this command:
cd samsung_messages
- Open this project in VS Code by running this command:
code .
- Confirm the project files exist by running:
ls lib/
You should see main.dart listed in the output. This is the entry point of your Flutter app.
- Now run your app on the emulator. Make sure your Android emulator is still running, then execute this command in the VS Code terminal:
flutter run
- Flutter will compile the app and deploy it to your emulator. This first build takes a few minutes. When it finishes, you should see a white screen with the text Hello, World! in the center of your emulator.
App not appearing on the emulator?
Make sure the emulator is fully booted (you see the Android home screen) before running flutter run.
If you see a message saying no devices found, run flutter devices to check if the emulator is detected. If not, restart the emulator from Android Studio's Device Manager.
help me connect my Android emulator to Flutter
- Stop the running app by pressing q in the terminal.
✔️ Awesome, I've got everything!
Great. Double check that lib/main.dart in your project matches the code below.
ⓧ I'd like to double check the full code
Your lib/main.dart should look exactly like this:
import 'package:flutter/material.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Text('Hello, World!'),
),
),
);
}
}
Your development environment is ready and your first Flutter app is running. Next up, you will replace this Hello World screen with a Samsung Messages-style conversation list.
Build the Conversation List Screen
Your Flutter environment is ready and the emulator is showing the default "Hello, World!" app. Now it's time to build what users actually see when they open a messaging app.
Every messaging app starts with a conversation list. In this step, you'll create the data that powers it, build the scrollable list UI, and apply a clean Samsung Messages-inspired theme.
In this step, get ready to:
- Create a data model class for conversations with hardcoded sample data.
- Build the conversation list UI with avatars, message previews, and timestamps.
- Style the app with a Samsung Messages-inspired theme.
Create the conversation data model
Before building any UI, you need to define the shape of your data. A data model is a class that describes what information each conversation holds.
- In VS Code's Explorer sidebar (the file tree on the left), right-click the lib folder.
- Select New Folder and name it models.
- Right-click the new models folder and select New File.
- Name the file conversation.dart and press Enter.
The file opens in the editor. This is where you'll define what a conversation looks like in your app.
- Define the Conversation class by pasting this code into conversation.dart:
import 'package:flutter/material.dart';
// Defines the shape of a single conversation
class Conversation {
final String contactName;
final String lastMessage;
final String timestamp;
final Color avatarColor;
const Conversation({
required this.contactName,
required this.lastMessage,
required this.timestamp,
required this.avatarColor,
});
}
What does this code do?
This class acts like a blueprint. Every conversation in your app will have four pieces of information: a contact name, the last message they sent, a timestamp, and a color for their avatar circle.
The required keyword means you must provide all four fields whenever you create a new Conversation. The final keyword means once set, these values cannot change.
Now you need some sample conversations to display. In a real app, this data would come from a database. For now, hardcoded data lets you focus purely on the UI.
- Add the sample conversation data below the class definition (after the closing } of the Conversation class):
// Hardcoded sample data for the conversation list
final List<Conversation> sampleConversations = [
Conversation(
contactName: 'Mom',
lastMessage: 'Are you coming for dinner tonight?',
timestamp: '2:30 PM',
avatarColor: Colors.purple,
),
Conversation(
contactName: 'Alex Kim',
lastMessage: 'Got it, see you tomorrow!',
timestamp: '1:15 PM',
avatarColor: Colors.blue,
),
Conversation(
contactName: 'Work Group',
lastMessage: 'Meeting moved to 3pm',
timestamp: '11:42 AM',
avatarColor: Colors.green,
),
Conversation(
contactName: 'Sarah Chen',
lastMessage: 'Thanks for sending that over',
timestamp: 'Yesterday',
avatarColor: Colors.orange,
),
Conversation(
contactName: 'Pizza Palace',
lastMessage: 'Your order is on the way!',
timestamp: 'Yesterday',
avatarColor: Colors.red,
),
Conversation(
contactName: 'Dad',
lastMessage: 'Call me when you get a chance',
timestamp: 'Monday',
avatarColor: Colors.teal,
),
];
What does this code do?
This creates a list of 6 Conversation objects that your app will display. Each one uses the blueprint you defined above, filling in a contact name, message, time, and color.
The Colors.purple, Colors.blue, etc. values come from Flutter's built-in Material colors. These will tint each contact's avatar circle.
- Save conversation.dart with Ctrl+S.
- Confirm you see no red underlines in the file. If everything is correct, Dart accepts your model without errors.
Seeing red underlines?
- Check that import 'package:flutter/material.dart'; is the very first line of the file. Without it, Dart doesn't know about Color or Colors.
- Make sure the file is saved inside lib/models/ and not directly in lib/.
Help me fix red underlines in my conversation.dart file.
✔️ Awesome, I've got everything!
Great. Make sure you've saved your file before moving on.
ⓧ I'd like to double check the full code
import 'package:flutter/material.dart';
class Conversation {
final String contactName;
final String lastMessage;
final String timestamp;
final Color avatarColor;
const Conversation({
required this.contactName,
required this.lastMessage,
required this.timestamp,
required this.avatarColor,
});
}
final List<Conversation> sampleConversations = [
Conversation(
contactName: 'Mom',
lastMessage: 'Are you coming for dinner tonight?',
timestamp: '2:30 PM',
avatarColor: Colors.purple,
),
Conversation(
contactName: 'Alex Kim',
lastMessage: 'Got it, see you tomorrow!',
timestamp: '1:15 PM',
avatarColor: Colors.blue,
),
Conversation(
contactName: 'Work Group',
lastMessage: 'Meeting moved to 3pm',
timestamp: '11:42 AM',
avatarColor: Colors.green,
),
Conversation(
contactName: 'Sarah Chen',
lastMessage: 'Thanks for sending that over',
timestamp: 'Yesterday',
avatarColor: Colors.orange,
),
Conversation(
contactName: 'Pizza Palace',
lastMessage: 'Your order is on the way!',
timestamp: 'Yesterday',
avatarColor: Colors.red,
),
Conversation(
contactName: 'Dad',
lastMessage: 'Call me when you get a chance',
timestamp: 'Monday',
avatarColor: Colors.teal,
),
];
Build the conversation list UI
With your data model ready, you can build the screen that displays it. Flutter UIs are built with widgets. Each widget is a building block that describes a piece of the interface.
- In the Explorer sidebar, right-click the lib folder and select New Folder.
- Name it screens.
- Right-click the screens folder and select New File.
- Name the file conversation_list_screen.dart and press Enter.
- Build the conversation list screen by pasting this code into conversation_list_screen.dart:
import 'package:flutter/material.dart';
import '../models/conversation.dart';
// The main screen that shows all conversations
class ConversationListScreen extends StatelessWidget {
const ConversationListScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
// Top bar with the app title
appBar: AppBar(
title: const Text(
'Messages',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22),
),
),
// Scrollable list of conversations
body: ListView.builder(
itemCount: sampleConversations.length,
itemBuilder: (context, index) {
final conversation = sampleConversations[index];
return ListTile(
// Colored circle with the contact's first initial
leading: CircleAvatar(
backgroundColor: conversation.avatarColor,
child: Text(
conversation.contactName[0],
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
// Contact name
title: Text(
conversation.contactName,
style: const TextStyle(fontWeight: FontWeight.w600),
),
// Last message preview (truncated if too long)
subtitle: Text(
conversation.lastMessage,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.grey[600]),
),
// Timestamp on the right side
trailing: Text(
conversation.timestamp,
style: TextStyle(color: Colors.grey[500], fontSize: 12),
),
);
},
),
);
}
}
What does this code do?
- Scaffold provides the basic page structure (app bar + body area).
- AppBar creates the top bar with "Messages" as the title in bold.
- ListView.builder creates a scrollable list that only builds items as they appear on screen. This is efficient for long lists.
- ListTile is a ready-made widget with positions for a leading icon, title, subtitle, and trailing content.
- CircleAvatar draws a colored circle with the contact's first letter inside it.
What is a StatelessWidget?
A StatelessWidget is a widget that never changes after it's built. The conversation list doesn't need to update itself (no user interaction yet), so stateless is the right choice here.
Later, when you add a send button that updates the screen, you'll use a StatefulWidget instead.
- Save conversation_list_screen.dart with Ctrl+S.
- Confirm there are no red underlines in the editor. You won't see changes on the emulator yet because main.dart still points to the old "Hello, World!" screen.
Seeing an error on the import line?
- Make sure your conversation.dart file is inside lib/models/ (not lib/screens/ or directly in lib/).
- Check the import path uses ../models/conversation.dart with two dots at the start. The .. means "go up one folder" from screens to lib.
Help me fix import errors in my conversation_list_screen.dart file.
✔️ Awesome, I've got everything!
Great. Make sure you've saved your file before moving on.
ⓧ I'd like to double check the full code
import 'package:flutter/material.dart';
import '../models/conversation.dart';
class ConversationListScreen extends StatelessWidget {
const ConversationListScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'Messages',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22),
),
),
body: ListView.builder(
itemCount: sampleConversations.length,
itemBuilder: (context, index) {
final conversation = sampleConversations[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: conversation.avatarColor,
child: Text(
conversation.contactName[0],
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
title: Text(
conversation.contactName,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
conversation.lastMessage,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.grey[600]),
),
trailing: Text(
conversation.timestamp,
style: TextStyle(color: Colors.grey[500], fontSize: 12),
),
);
},
),
);
}
}
Style the app with a Samsung Messages theme
Your screen widget is ready, but main.dart still shows the old "Hello, World!" app. You need to replace it with a themed MaterialApp that points to your new ConversationListScreen.
- Switch back to main.dart by clicking its tab at the top of the editor.
- Select all the code with Ctrl+A and delete it.
- Replace the entire file with this themed app code:
import 'package:flutter/material.dart';
import 'screens/conversation_list_screen.dart';
void main() {
runApp(const SamsungMessagesApp());
}
// Root widget that configures the app theme and starting screen
class SamsungMessagesApp extends StatelessWidget {
const SamsungMessagesApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Messages',
debugShowCheckedModeBanner: false,
// Samsung Messages-inspired theme
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1A73E8),
brightness: Brightness.light,
),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
),
scaffoldBackgroundColor: Colors.white,
),
// Start on the conversation list screen
home: const ConversationListScreen(),
);
}
}
What does this code do?
- ColorScheme.fromSeed generates a full color palette from one seed color (0xFF1A73E8 is a Google-style blue). Flutter creates matching shades, tints, and surface colors automatically.
- AppBarTheme sets a white background with black text and no shadow (elevation: 0) for every app bar in the app.
- debugShowCheckedModeBanner: false removes the red "DEBUG" banner from the top-right corner of the emulator.
- home: const ConversationListScreen() tells the app which screen to show when it launches.
What is Material Design?
Material Design is Google's design system that Flutter uses by default. It provides ready-made widgets (buttons, cards, app bars) that follow consistent visual and interaction patterns. Samsung Messages uses a clean, minimal take on Material principles.
- Save main.dart with Ctrl+S.
- Switch to your emulator. The app should hot-reload automatically. If it doesn't, press r in the terminal where flutter run is active, or press R for a full restart.
- You should see a white screen with "Messages" in bold at the top and a list of 6 conversations below. Each conversation shows a colored circle with the contact's initial, the contact name, a message preview, and a timestamp on the right.
Emulator still showing Hello World?
- Press R (capital R) in your terminal to do a full hot restart. Some changes to main.dart require a restart instead of a reload.
- Check that your import path in main.dart says import 'screens/conversation_list_screen.dart'; (no .. prefix, since main.dart is already in lib/).
- If the terminal shows a compile error, read the error message carefully. It usually names the file and line number where the problem is.
My emulator isn't showing my conversation list after saving main.dart.
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved before moving on.
ⓧ I'd like to double check the full code
import 'package:flutter/material.dart';
import 'screens/conversation_list_screen.dart';
void main() {
runApp(const SamsungMessagesApp());
}
class SamsungMessagesApp extends StatelessWidget {
const SamsungMessagesApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Messages',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1A73E8),
brightness: Brightness.light,
),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
),
scaffoldBackgroundColor: Colors.white,
),
home: const ConversationListScreen(),
);
}
}
Your conversation list looks great. Next up, you'll build the chat detail screen where users read and send messages.
Build the Chat Detail Screen
Your conversation list looks great. Now it is time to build the screen that opens when someone taps a conversation.
The chat detail screen is where actual messages live. It displays message bubbles aligned by sender, timestamps, and a text input bar at the bottom for composing new messages.
In this step, get ready to:
- Create a Message data model with hardcoded sample conversations.
- Build the chat screen layout with styled message bubbles for sent and received messages.
- Add a message input bar with a text field and send button.
Create the Message data model
Just like the conversation list needed a data model, the chat screen needs a structure for individual messages. Each message has text content, a sender name, a timestamp, and a flag indicating whether you sent it.
- In the VS Code Explorer sidebar, right-click the lib/models folder and select New File.
- Name the file message.dart.
- Define the Message class by pasting this code:
class Message {
final String text;
final String sender;
final String timestamp;
final bool isMe;
const Message({
required this.text,
required this.sender,
required this.timestamp,
required this.isMe,
});
}
What does this code do?
- The Message class defines the shape of a single chat message. Each field is final because message data does not change after creation.
- The isMe boolean is the key field. It tells the UI whether to render the bubble on the right (sent) or left (received) side of the screen.
- The const constructor means Dart can optimize memory when creating message instances with compile-time constant values.
You will not see a change yet. The sample data in the next chunk uses this class.
- Below the class, add the sample message data by pasting this code:
final Map<String, List<Message>> sampleMessages = {
'Mom': [
Message(
text: 'Hey, how was your day?',
sender: 'Mom',
timestamp: '2:00 PM',
isMe: false,
),
Message(
text: 'Pretty good! Just finished work.',
sender: 'Me',
timestamp: '2:15 PM',
isMe: true,
),
Message(
text: 'Are you coming for dinner tonight?',
sender: 'Mom',
timestamp: '2:30 PM',
isMe: false,
),
],
'Alex Kim': [
Message(
text: 'Hey, want to grab coffee tomorrow?',
sender: 'Me',
timestamp: '12:50 PM',
isMe: true,
),
Message(
text: 'Got it, see you tomorrow!',
sender: 'Alex Kim',
timestamp: '1:15 PM',
isMe: false,
),
],
'Work Group': [
Message(
text: 'Can everyone check the new schedule?',
sender: 'Manager',
timestamp: '11:30 AM',
isMe: false,
),
Message(
text: 'Meeting moved to 3pm',
sender: 'Manager',
timestamp: '11:42 AM',
isMe: false,
),
],
'Sarah Chen': [
Message(
text: 'Here is the document you asked for',
sender: 'Me',
timestamp: '3:00 PM',
isMe: true,
),
Message(
text: 'Thanks for sending that over',
sender: 'Sarah Chen',
timestamp: '3:10 PM',
isMe: false,
),
],
'Pizza Palace': [
Message(
text: 'Your order is on the way!',
sender: 'Pizza Palace',
timestamp: '6:45 PM',
isMe: false,
),
],
'Dad': [
Message(
text: 'Call me when you get a chance',
sender: 'Dad',
timestamp: '9:00 AM',
isMe: false,
),
],
};
Why a Map instead of a List?
The sampleMessages map uses contact names as keys and lists of Message objects as values. When the learner taps a conversation, you already know the contact name. A Map lets you fetch that contact's messages instantly with sampleMessages['Mom'] instead of searching through a flat list.
Each conversation has a realistic mix of sent and received messages so the chat screen looks authentic when you test it.
- Save message.dart.
Seeing red underlines in VS Code?
- Make sure the sampleMessages map is placed below the closing brace of the Message class, not inside it.
- Check that every Message( has a matching closing ) and every [ has a matching ]. Each entry also needs a trailing comma.
Help me fix red underlines in my message.dart file.
✔️ Awesome, I've got everything!
Great. Double check you have saved message.dart.
ⓧ I'd like to double check the full code
class Message {
final String text;
final String sender;
final String timestamp;
final bool isMe;
const Message({
required this.text,
required this.sender,
required this.timestamp,
required this.isMe,
});
}
final Map<String, List<Message>> sampleMessages = {
'Mom': [
Message(
text: 'Hey, how was your day?',
sender: 'Mom',
timestamp: '2:00 PM',
isMe: false,
),
Message(
text: 'Pretty good! Just finished work.',
sender: 'Me',
timestamp: '2:15 PM',
isMe: true,
),
Message(
text: 'Are you coming for dinner tonight?',
sender: 'Mom',
timestamp: '2:30 PM',
isMe: false,
),
],
'Alex Kim': [
Message(
text: 'Hey, want to grab coffee tomorrow?',
sender: 'Me',
timestamp: '12:50 PM',
isMe: true,
),
Message(
text: 'Got it, see you tomorrow!',
sender: 'Alex Kim',
timestamp: '1:15 PM',
isMe: false,
),
],
'Work Group': [
Message(
text: 'Can everyone check the new schedule?',
sender: 'Manager',
timestamp: '11:30 AM',
isMe: false,
),
Message(
text: 'Meeting moved to 3pm',
sender: 'Manager',
timestamp: '11:42 AM',
isMe: false,
),
],
'Sarah Chen': [
Message(
text: 'Here is the document you asked for',
sender: 'Me',
timestamp: '3:00 PM',
isMe: true,
),
Message(
text: 'Thanks for sending that over',
sender: 'Sarah Chen',
timestamp: '3:10 PM',
isMe: false,
),
],
'Pizza Palace': [
Message(
text: 'Your order is on the way!',
sender: 'Pizza Palace',
timestamp: '6:45 PM',
isMe: false,
),
],
'Dad': [
Message(
text: 'Call me when you get a chance',
sender: 'Dad',
timestamp: '9:00 AM',
isMe: false,
),
],
};
Build the chat screen layout with message bubbles
The chat screen is a StatefulWidget because it needs to track messages that change over time. When the user sends a new message, the screen rebuilds to show it.
- In the Explorer sidebar, right-click the lib/screens folder and select New File.
- Name the file chat_screen.dart.
- Add the widget class and state setup by pasting this code:
import 'package:flutter/material.dart';
import '../models/message.dart';
class ChatScreen extends StatefulWidget {
// Constructor parameters passed from the conversation list
final String contactName;
final Color avatarColor;
final List<Message> messages;
const ChatScreen({
super.key,
required this.contactName,
required this.avatarColor,
required this.messages,
});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
// Mutable copy of messages so we can add new ones
late List<Message> _messages;
final TextEditingController _controller = TextEditingController();
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
// Copy the original messages so additions don't modify source data
_messages = List.from(widget.messages);
}
void _sendMessage() {
if (_controller.text.trim().isEmpty) return;
setState(() {
_messages.add(
Message(
text: _controller.text.trim(),
sender: 'Me',
timestamp: 'Now',
isMe: true,
),
);
});
_controller.clear();
// Auto-scroll to the newest message after a brief delay
Future.delayed(const Duration(milliseconds: 100), () {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
});
}
@override
void dispose() {
_controller.dispose();
_scrollController.dispose();
super.dispose();
}
What does this code do?
- ChatScreen extends StatefulWidget and accepts three required parameters: the contact name, avatar color, and list of messages to display.
- _messages is a mutable copy of the initial messages. Copying with List.from() lets you add new messages without modifying the original data.
- _sendMessage() uses setState() to add a new message and trigger a UI rebuild. It also auto-scrolls the list to the bottom so the newest message stays visible.
- dispose() cleans up the text and scroll controllers to prevent memory leaks when the screen is closed.
You will not see a change yet. The next chunk adds the build() method that renders the screen.
- Below the dispose() method, add the build() method and the message bubble helper by pasting this code:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
children: [
// Small avatar in the app bar
CircleAvatar(
radius: 18,
backgroundColor: widget.avatarColor,
child: Text(
widget.contactName[0],
style: const TextStyle(color: Colors.white, fontSize: 14),
),
),
const SizedBox(width: 12),
Text(widget.contactName),
],
),
),
body: Column(
children: [
// Message list takes all available vertical space
Expanded(
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(16),
itemCount: _messages.length,
itemBuilder: (context, index) {
final message = _messages[index];
return _buildMessageBubble(message);
},
),
),
// Input bar pinned to the bottom
_buildInputBar(),
],
),
);
}
Widget _buildMessageBubble(Message message) {
return Align(
// Sent messages go right, received messages go left
alignment: message.isMe ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
constraints: BoxConstraints(
// Cap bubble width at 70% of screen
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
decoration: BoxDecoration(
color: message.isMe
? Theme.of(context).colorScheme.primary
: Colors.grey[200],
// Different corner radius creates the "tail" effect
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16),
topRight: const Radius.circular(16),
bottomLeft: Radius.circular(message.isMe ? 16 : 4),
bottomRight: Radius.circular(message.isMe ? 4 : 16),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
message.text,
style: TextStyle(
color: message.isMe ? Colors.white : Colors.black87,
fontSize: 15,
),
),
const SizedBox(height: 4),
Text(
message.timestamp,
style: TextStyle(
color: message.isMe ? Colors.white70 : Colors.grey[500],
fontSize: 11,
),
),
],
),
),
);
}
How do message bubbles work?
- Align pushes sent messages to the right and received messages to the left, mimicking how real messaging apps distinguish between sides of the conversation.
- BorderRadius.only creates the chat bubble "tail" effect. Sent bubbles have a small bottom-right corner (4px) and received bubbles have a small bottom-left corner (4px).
- Each bubble is capped at 70% of the screen width using BoxConstraints so long messages wrap instead of stretching edge to edge.
- Sent bubbles use the app's primary blue color. Received bubbles use a light grey background.
You will not see a change yet. The next chunk adds _buildInputBar() which completes the file.
Add the message input bar
Every messaging app needs a way to compose new messages. The input bar sits at the bottom of the screen with a rounded text field and a send button.
- Below the _buildMessageBubble() method, add the input bar and close the class by pasting this code:
Widget _buildInputBar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
// Top border separates the input from messages above
border: Border(top: BorderSide(color: Colors.grey[200]!)),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(
hintText: 'Type a message...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey[100],
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
),
onSubmitted: (_) => _sendMessage(),
),
),
const SizedBox(width: 8),
IconButton(
onPressed: _sendMessage,
icon: Icon(
Icons.send,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
);
}
}
What does this code do?
- The Container has a top border to visually separate the input area from the message list above it.
- A Row places the TextField and IconButton side by side. The Expanded widget makes the text field fill all remaining horizontal space.
- The TextField uses BorderRadius.circular(24) for the rounded pill shape you see in messaging apps. The onSubmitted callback triggers _sendMessage() when the user presses Enter on the keyboard.
- Save chat_screen.dart.
- To preview the chat screen, you need to temporarily point the app at it. In lib/main.dart, add this import below the existing imports:
import 'screens/chat_screen.dart';
- Find this line in main.dart:
home: const ConversationListScreen(),
- Replace it with this temporary code to point directly to the chat screen:
home: ChatScreen(
contactName: 'Mom',
avatarColor: Colors.purple,
messages: sampleMessages['Mom'] ?? [],
),
- Save main.dart and wait for hot reload to update the emulator.
You should see the chat screen with a purple "M" avatar in the app bar, three message bubbles (two grey on the left, one blue on the right), and a rounded input field at the bottom.
Don't see the chat screen?
- Make sure you saved both chat_screen.dart and main.dart. Hot reload only picks up saved changes.
- If you see a red error screen, check for missing commas or brackets in chat_screen.dart. Each widget's closing parenthesis must match its opening one.
- If hot reload does not update, try pressing R (capital R) in the terminal to perform a full hot restart.
Help me fix an error on my Flutter chat screen.
- Once you have confirmed the chat screen works, revert main.dart back to the original. Remove the import 'screens/chat_screen.dart'; line you added.
- Change the home property back to:
home: const ConversationListScreen(),
- Save main.dart. The emulator should return to showing the conversation list.
✔️ Awesome, I've got everything!
Great. Make sure both files are saved and main.dart points back to ConversationListScreen.
ⓧ I'd like to double check the full code
import 'package:flutter/material.dart';
import '../models/message.dart';
class ChatScreen extends StatefulWidget {
final String contactName;
final Color avatarColor;
final List<Message> messages;
const ChatScreen({
super.key,
required this.contactName,
required this.avatarColor,
required this.messages,
});
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
late List<Message> _messages;
final TextEditingController _controller = TextEditingController();
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_messages = List.from(widget.messages);
}
void _sendMessage() {
if (_controller.text.trim().isEmpty) return;
setState(() {
_messages.add(
Message(
text: _controller.text.trim(),
sender: 'Me',
timestamp: 'Now',
isMe: true,
),
);
});
_controller.clear();
Future.delayed(const Duration(milliseconds: 100), () {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
});
}
@override
void dispose() {
_controller.dispose();
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
children: [
CircleAvatar(
radius: 18,
backgroundColor: widget.avatarColor,
child: Text(
widget.contactName[0],
style: const TextStyle(color: Colors.white, fontSize: 14),
),
),
const SizedBox(width: 12),
Text(widget.contactName),
],
),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(16),
itemCount: _messages.length,
itemBuilder: (context, index) {
final message = _messages[index];
return _buildMessageBubble(message);
},
),
),
_buildInputBar(),
],
),
);
}
Widget _buildMessageBubble(Message message) {
return Align(
alignment: message.isMe ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.7,
),
decoration: BoxDecoration(
color: message.isMe
? Theme.of(context).colorScheme.primary
: Colors.grey[200],
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16),
topRight: const Radius.circular(16),
bottomLeft: Radius.circular(message.isMe ? 16 : 4),
bottomRight: Radius.circular(message.isMe ? 4 : 16),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
message.text,
style: TextStyle(
color: message.isMe ? Colors.white : Colors.black87,
fontSize: 15,
),
),
const SizedBox(height: 4),
Text(
message.timestamp,
style: TextStyle(
color: message.isMe ? Colors.white70 : Colors.grey[500],
fontSize: 11,
),
),
],
),
),
);
}
Widget _buildInputBar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.grey[200]!)),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(
hintText: 'Type a message...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey[100],
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
),
onSubmitted: (_) => _sendMessage(),
),
),
const SizedBox(width: 8),
IconButton(
onPressed: _sendMessage,
icon: Icon(
Icons.send,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
);
}
}
Your chat screen is fully built with message bubbles and an input bar. Next up, you will connect the two screens so tapping a conversation actually opens this chat view.
Connect Screens with Navigation
You've built a conversation list screen and a fully functional chat screen with message bubbles and an input bar. Right now, tapping a conversation does nothing because the two screens aren't connected.
In this step, you'll wire up Flutter's navigation system so tapping a conversation opens the chat screen for that contact. You'll also test the send button that's already built into your chat screen.
In this step, get ready to:
- Wire up navigation from the conversation list to the chat screen.
- Verify the send button works using setState to update the UI.
- Test the complete app flow from conversation list to sending messages.
Add navigation from the conversation list to the chat screen
Flutter uses a navigation stack to move between screens. When you "push" a new screen onto the stack, it slides in on top of the current one. Flutter also automatically adds a back arrow so the user can return.
- In VS Code, open lib/screens/conversation_list_screen.dart.
- Find the trailing property inside the ListTile widget. It ends with this line:
trailing: Text(
conversation.timestamp,
style: TextStyle(color: Colors.grey[500], fontSize: 12),
),
- Add the following onTap property directly below the trailing property (still inside the ListTile):
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(
contactName: conversation.contactName,
avatarColor: conversation.avatarColor,
messages: sampleMessages[conversation.contactName] ?? [],
),
),
);
},
What does this code do?
Navigator.push adds a new screen on top of the current one. MaterialPageRoute defines the transition animation (a platform-appropriate slide or zoom).
Inside the route's builder, you create a ChatScreen and pass it three pieces of data: the contact's name, their avatar color, and their messages from sampleMessages. The ?? [] ensures an empty list is passed if no messages exist for that contact.
- Save conversation_list_screen.dart.
- In the emulator, tap any conversation in the list. You should see the chat screen slide in with that contact's name in the app bar and their messages displayed as bubbles.
Tapping a conversation does nothing?
Make sure the onTap property is inside the ListTile parentheses, not outside them. It should be a sibling of leading, title, subtitle, and trailing.
Also confirm you saved the file. Flutter's hot reload only picks up saved changes.
Help me debug why tapping a ListTile isn't navigating to ChatScreen
Why pass data through the constructor?
Flutter screens are just widgets, and widgets receive their configuration through constructor parameters. By passing contactName, avatarColor, and messages directly, the chat screen has everything it needs to render without accessing global state.
Understand how the send button works
Now that you can navigate to the chat screen, the send button you built in the previous step is reachable. The chat screen is a StatefulWidget, which means it can update its own UI in response to user actions.
- In VS Code, switch to lib/screens/chat_screen.dart and find the _sendMessage() method:
void _sendMessage() {
if (_controller.text.trim().isEmpty) return;
setState(() {
_messages.add(
Message(
text: _controller.text.trim(),
sender: 'Me',
timestamp: 'Now',
isMe: true,
),
);
});
_controller.clear();
Future.delayed(const Duration(milliseconds: 100), () {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
});
}
What does setState do?
setState tells Flutter that the widget's data changed and the UI needs to rebuild. Without it, adding a message to the list would update the data but the screen would stay the same.
After adding the message, _controller.clear() empties the text field. Then animateTo smoothly scrolls the list to the bottom so the new message is visible.
StatelessWidget vs StatefulWidget
Your conversation list screen is a StatelessWidget because its content never changes after it's built. The chat screen is a StatefulWidget because it needs to add new messages to the list when the user taps send.
The rule is simple: if a widget's UI can change after it's first displayed, make it stateful.
Test the complete app flow
Your app now has both screens connected. Time to test the entire experience from start to finish.
- In the emulator, confirm you see the conversation list with all six contacts.
- Tap the Mom conversation.
- Verify you see three messages: two from Mom (grey bubbles on the left) and one from you (blue bubble on the right).
- Tap the text field at the bottom and type any message.
- Tap the blue send icon.
- Confirm your new message appears as a blue bubble on the right with the timestamp "Now".
- Tap the back arrow in the top-left corner to return to the conversation list.
Send button not adding messages?
Make sure you're tapping the send icon (the arrow button), not just pressing Enter on the keyboard. Both should work, but try the icon first to confirm.
If the message still doesn't appear, check that chat_screen.dart uses StatefulWidget (not StatelessWidget) and that _sendMessage() calls setState.
Help me debug why my sent messages aren't appearing in the chat
✔️ Awesome, I've got everything!
Great. Double check you've saved conversation_list_screen.dart and confirmed navigation works in the emulator.
ⓧ I'd like to double check the full code
Here is the full conversation_list_screen.dart with the navigation code added:
import 'package:flutter/material.dart';
import '../models/conversation.dart';
import '../models/message.dart';
import 'chat_screen.dart';
class ConversationListScreen extends StatelessWidget {
const ConversationListScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'Messages',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22),
),
),
body: ListView.builder(
itemCount: sampleConversations.length,
itemBuilder: (context, index) {
final conversation = sampleConversations[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: conversation.avatarColor,
child: Text(
conversation.contactName[0],
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
title: Text(
conversation.contactName,
style: const TextStyle(fontWeight: FontWeight.w600),
),
subtitle: Text(
conversation.lastMessage,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.grey[600]),
),
trailing: Text(
conversation.timestamp,
style: TextStyle(color: Colors.grey[500], fontSize: 12),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(
contactName: conversation.contactName,
avatarColor: conversation.avatarColor,
messages: sampleMessages[conversation.contactName] ?? [],
),
),
);
},
);
},
),
);
}
}
Your Samsung Messages clone is fully functional. You can browse conversations, open a chat, and send new messages. Next up, you'll take on the Secret Mission and add search filtering plus a new message button to level up your app.
Secret mission
Add Search and a New Message Button
Your messaging app looks great, but real messaging apps let you search through conversations and start new ones. Can you add a search bar that filters conversations in real time, plus a floating action button to compose a new message?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing costs or cloud bills to worry about. Decide whether to keep your resources installed, pause them to come back later, or delete them entirely.
Resources you used:
- samsung_messages Flutter project folder on your Desktop.
- Android emulator (running process).
- Android Studio and Android SDK (several GB of disk space).
- Flutter SDK (installed via VS Code).
- Git for Windows.
Keep everything running
No action needed. Choose this if you plan to continue building with Flutter or want to tackle the follow-on project (adding Firebase for real-time messaging).
- Your samsung_messages project stays on your Desktop, ready for future edits.
- Android Studio, the Android SDK, Flutter SDK, and Git remain installed and configured.
- You can close the emulator and VS Code whenever you like. They will relaunch normally next time.
Pause - I'll come back to this later
Shut down running processes to free up memory, but keep all tools installed so you can pick up where you left off.
- Close the Android emulator by clicking the X button on the emulator window.
- Close Android Studio if it is open.
- Close VS Code.
- Your project files and all installed tools remain on disk. When you return, open VS Code, relaunch the emulator from Android Studio Device Manager, and run flutter run again.
Delete - I don't want to use this again
Remove all project resources and tools to reclaim disk space. You can always reinstall later if you change your mind.
- Close the Android emulator and VS Code before proceeding.
- Delete the project folder by opening a terminal and running:
cd %USERPROFILE%\Desktop
rmdir /s /q samsung_messages
- Uninstall Android Studio by pressing the Windows key, searching for Add or remove programs, finding Android Studio in the list, and clicking Uninstall.
- After the uninstaller finishes, delete leftover Android SDK and config files by running:
rmdir /s /q %USERPROFILE%\.android
rmdir /s /q %LOCALAPPDATA%\Android
- Delete the Flutter SDK by removing the folder where you installed it (the folder you chose during the VS Code setup). For example, if you installed it to C:\flutter:
rmdir /s /q C:\flutter
- Remove Flutter from your PATH. Press the Windows key, search for Edit the system environment variables, click Environment Variables, select the Path variable under User variables, click Edit, find the entry pointing to your Flutter bin folder, and click Delete.
- Uninstall Git for Windows by pressing the Windows key, searching for Add or remove programs, finding Git in the list, and clicking Uninstall.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully functional messaging app UI from scratch using Flutter and Dart.
You've learned how to:
- Built a Samsung Messages-style conversation list with avatars, message previews, and timestamps using ListView.builder and ListTile.
- Created a chat detail screen with styled message bubbles aligned differently for sent and received messages using Container and BoxDecoration.
- Implemented navigation between screens with data passing and made the send button functional using StatefulWidget and setState.
- Secret Mission: Added real-time search filtering and a FloatingActionButton for composing new messages.
Ready to quiz yourself?