Modern OTT App Profile UI in Android Using Jetpack Compose
OTT apps (Netflix-style apps) usually have one screen that users open again and again—the Profile screen. It’s where users switch accounts, open downloads, manage watchlist, change settings, and access preferences like data saver or notifications.
In this post, we’ll build a clean, modern OTT-style Profile UI using Jetpack Compose, and I’ll explain the code line-by-line in a practical way. By the end, you’ll understand how to structure Compose UI like a real production screen: top bar, profile avatars, action shortcuts, settings list, and logout action.
If you’re building an OTT UI clone, a streaming app, or even a media player app, this screen layout is a great reusable pattern.
What we are building
This profile screen includes:
- A top bar with a back button and “Profile” title
- A row of profile avatars (two profiles + “Add profile” button)
- Quick action cards (Downloads, Watchlist, Settings)
- A settings-style menu list (Data saver, Watch history, Notifications, etc.)
- A Logout button at the bottom
This UI is built using only Compose layouts like Column, Row, Card, and clickable elements.
Requirements (Before you start)
Make sure you already have:
- Jetpack Compose enabled in your project
- Material 3 dependencies (since we use CardDefaults, HorizontalDivider, etc.)
- Drawable icons like:
- back_arrow_icon, right_chevron_icon
- download_icon, star_icon, setting_icon
- data_saver_icon, history_icon, notification_icon, bookmark_icon, accessibility_icon
- avatar_1, avatar_2
- Breaking UI into small composables (reusability)
- Clear spacing using Spacer
- Consistent padding, shapes, and colors
If you don’t have icons, you can temporarily replace them with Material Icons, but using drawables gives that OTT-app custom feel.
High-level architecture of the screen
Your UI is structured like this:
1.Root Column (full screen, background color, padding)2.Top bar Row
3.Profiles Row (Avatar composables + Add profile composable)
4.Action buttons Row (each button placed inside its own Card)
5.Menu section inside one big Card containing multiple MenuItems separated by dividers
6.Logout action as a TextButton
This structure is simple but powerful. In Compose, a clean layout is mostly about:
Full ProfileScreen breakdown (Detailed Explanation)
1) Root layout: Column container
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFE8EDF2))
.padding(16.dp)
) { ... }This Column is the main vertical container for the entire screen.
- fillMaxSize() makes the screen occupy the full available space.
- background(Color(0xFFE8EDF2)) sets a soft light-gray/blue background, commonly used in modern UI to separate white cards from the base background.
- padding(16.dp) gives breathing space around the content.
This background + card combination is a popular OTT style: the page is slightly tinted, while sections like action buttons and menus sit inside clean white cards.
2) Top bar: Back button + title
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBackToLogin) { ... }
Text(text = "Profile", ...)
}
This row creates a simple top bar.
- fillMaxWidth() makes the row stretch across the screen.
- verticalAlignment = Alignment.CenterVertically ensures the icon and the text align nicely.
Back button logic
IconButton(onClick = onBackToLogin) {
Icon(
painter = painterResource(id = R.drawable.back_arrow_icon),
contentDescription = "Back",
tint = Color.Black,
modifier = Modifier.size(15.dp)
)
}
- IconButton provides a built-in clickable area with proper touch size.
- onBackToLogin is a callback passed to the screen, so the screen doesn’t directly control navigation. This is a good practice: UI should expose events, and the caller (navigation layer) should handle routing.
- The icon is loaded using painterResource(...) because your icon is in drawable.
Title
Text(
text = "Profile",
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = Color.Black
)
Simple and readable. Semi-bold helps the title stand out without looking heavy.
3) Spacing using Spacer
You use:Spacer(modifier = Modifier.height(20.dp))Spacing is important for “premium” UI. OTT apps tend to have consistent whitespace between sections. Spacer is the Compose-native way to do this without hacks.
Profile avatars row (OTT profile selection style)
4) The Row layout
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
horizontalArrangement = Arrangement.SpaceBetween
) { ... }- padding(horizontal = 24.dp) pushes avatars inward, making the row look centered and balanced.
- Arrangement.SpaceBetween spreads the children evenly across available width, with space between them.
- ProfileAvatar(name="Ine", imageRes=avatar_1)
- ProfileAvatar(name="Timo", imageRes=avatar_2)
- AddProfileButton()
ProfileAvatar composable (detailed)
@Composable
fun ProfileAvatar(
name: String,
imageRes: Int,
onClick: () -> Unit
)
This composable is reusable. You can add more profiles by calling it again with different name/icon.Layout: Column
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.clickable(onClick = onClick)
) { ... }
- Column stacks avatar image and name vertically.
- horizontalAlignment = Alignment.CenterHorizontally centers both image and text.
- clickable makes the whole profile tappable (better UX than only making the image clickable).
Avatar circle image
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(Color(0xFFD1E3F5))
) {
Image(
painter = painterResource(id = imageRes),
contentDescription = name,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
Key ideas here:
- Box is used because you want a container that can hold an image and apply shape/background.
- size(80.dp) defines the avatar size.
- clip(CircleShape) makes it perfectly round.
- background(...) adds a soft placeholder background behind the image.
- ContentScale.Crop ensures the image fills the circle without distortion (some parts may be cropped, but it looks modern and clean).
Text(
text = name,
fontSize = 13.sp,
color = Color.Black
)Add profile button (dashed circle design)
This is one of the most “premium” UI parts: dashed circular border.Layout
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.clickable(onClick = onClick)
) { ... }Same pattern: icon + label.
Dashed circular border using drawBehind
Box(
modifier = Modifier
.size(80.dp)
.drawBehind {
val stroke = Stroke(
width = 4f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(12f, 8f), 0f)
)
drawCircle(
color = Color.Gray.copy(alpha = 0.4f),
style = stroke
)
},
contentAlignment = Alignment.Center
) { ... }This is a very useful technique in Compose:
- drawBehind { ... } lets you draw custom shapes behind the composable content.
- Stroke(width = 4f) draws outline thickness.
- PathEffect.dashPathEffect(floatArrayOf(12f, 8f), 0f) creates a dashed pattern.
- 12f is the dash length
- 8f is the gap length
- drawCircle(... style = stroke) draws only the border, not a filled circle.
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add profile",
tint = Color.Gray.copy(alpha = 0.6f),
modifier = Modifier.size(30.dp)
)Finally the label:
Text(text = "Add profile", fontSize = 13.sp)Action shortcuts row (Downloads / Watchlist / Settings)
This section is designed as 3 separate cards with equal width. That’s a smart UI decision: cards look tappable and neatly separated.
Row container
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) { ... }You give a small horizontal padding so cards don’t touch screen edges.
Each action button inside Card
Example:Card(
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
ActionButton(...)
}Important details:
- weight(1f) ensures each card takes equal space in the row.
- RoundedCornerShape(20.dp) creates a modern “pill card” look.
- White container color makes it pop on tinted background.
- Small elevation 2.dp adds subtle depth (looks premium without being too heavy).
Spacer(modifier = Modifier.width(12.dp))That spacing prevents them from looking like a single connected component.
ActionButton composable (reusable)
@Composable
fun ActionButton(
icon: Painter,
label: String,
onClick: () -> Unit
)You pass a Painter so you can use drawable resources (perfect for custom OTT icons).
Layout
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(vertical = 18.dp, horizontal = 8.dp)
) { ... }This ensures:
- Full card width is clickable
- Content is centered
- Padding gives enough tap comfort and visual balance
Icon(... size(24.dp))
Text(text = label, fontSize = 11.sp, textAlign = TextAlign.Center)The label font is intentionally small (11.sp) because these are shortcuts, not primary headings.
Menu section (Settings list style inside a Card)
You wrap menu items inside one main card:Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(vertical = 4.dp)) { ... }
}This gives a cohesive “settings panel” look—again a common OTT pattern.
Each MenuItem
MenuItem(
icon = painterResource(id = R.drawable.data_saver_icon),
text = "Data saver",
onClick = { }
)Between items you add a divider:
HorizontalDivider(
color = Color(0xFFE8EDF2),
thickness = 1.dp,
modifier = Modifier.padding(horizontal = 16.dp)
)The divider:
- Matches the screen background color (nice visual consistency)
- Uses horizontal padding so it doesn’t touch the rounded edges
MenuItem composable (detailed)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically
) { ... }This layout is the standard settings row style:
- Left icon
- Text in the middle
- Right chevron on the end (navigation hint)
Left icon
Icon(
painter = icon,
contentDescription = text,
modifier = Modifier.size(22.dp),
tint = Color.Black
)Text with weight
Text(
text = text,
fontSize = 15.sp,
modifier = Modifier.weight(1f)
)weight(1f) is crucial: it forces the text to take remaining space, pushing the chevron to the right edge consistently.
Right chevron icon
Icon(
painter = painterResource(id = R.drawable.right_chevron_icon),
contentDescription = "Navigate",
tint = Color.Gray.copy(alpha = 1f),
modifier = Modifier.size(15.dp)
)This communicates “tap to open next screen”.
Logout button
TextButton(
onClick = { },
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Logout",
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = Color.Black
)
}- TextButton gives a simple low-emphasis action.
- Full width makes it easy to tap.
- You can later style it as red if needed (many apps do), but black also works in minimalist designs.
In real apps, you’d connect onClick to your auth logic and clear session tokens.
UI improvement tips (Optional but useful for production)
If you want to make this UI even more like a real OTT app:- Use LazyColumn instead of Column if content may exceed screen height (small devices).
- Add ripple effect using Modifier.clickable(...) with proper interactionSource and indication if needed.
- Add dark theme support by using Material colors instead of hard-coded Color.Black/Color.White.
- Replace Spacer(height = ...) values with a consistent spacing system (like 8,12,16,24)
- Add profile selection state (highlight the currently selected avatar with border).
Full Code :
@Composable
fun ProfileScreen(onBackToLogin: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFE8EDF2))
.padding(16.dp)
) {
// Top Bar
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBackToLogin) {
Icon(
painter = painterResource(id = R.drawable.back_arrow_icon),
contentDescription = "Back",
tint = Color.Black,
modifier = Modifier.size(15.dp)
)
}
Text(
text = "Profile",
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = Color.Black
)
}
Spacer(modifier = Modifier.height(20.dp))
// Profile Avatars Row
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
// Profile 1
ProfileAvatar(
name = "Ine",
imageRes = R.drawable.avatar_1,
onClick = { }
)
// Profile 2
ProfileAvatar(
name = "Timo",
imageRes = R.drawable.avatar_2,
onClick = { }
)
// Add Profile
AddProfileButton(onClick = {})
}
Spacer(modifier = Modifier.height(28.dp))
// Action Buttons Row
// Action Buttons Row with Separate Cards
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// Downloads Card
Card(
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
ActionButton(
icon = painterResource(id = R.drawable.download_icon),
label = "Downloads",
onClick = { }
)
}
Spacer(modifier = Modifier.width(12.dp))
// Watchlist Card
Card(
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
ActionButton(
icon = painterResource(id = R.drawable.star_icon),
label = "Watchlist",
onClick = { }
)
}
Spacer(modifier = Modifier.width(12.dp))
// Settings Card
Card(
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
ActionButton(
icon = painterResource(id = R.drawable.setting_icon),
label = "Settings",
onClick = { }
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// Menu Items
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(vertical = 4.dp)) {
MenuItem(
icon = painterResource(id = R.drawable.data_saver_icon),
text = "Data saver",
onClick = { }
)
HorizontalDivider(
color = Color(0xFFE8EDF2),
thickness = 1.dp,
modifier = Modifier.padding(horizontal = 16.dp)
)
MenuItem(
icon = painterResource(id = R.drawable.history_icon),
text = "Watch history",
onClick = { }
)
HorizontalDivider(
color = Color(0xFFE8EDF2),
thickness = 1.dp,
modifier = Modifier.padding(horizontal = 16.dp)
)
MenuItem(
icon = painterResource(id = R.drawable.notification_icon),
text = "Notifications",
onClick = { }
)
HorizontalDivider(
color = Color(0xFFE8EDF2),
thickness = 1.dp,
modifier = Modifier.padding(horizontal = 16.dp)
)
MenuItem(
icon = painterResource(id = R.drawable.bookmark_icon),
text = "Manage bookmarks",
onClick = { }
)
HorizontalDivider(
color = Color(0xFFE8EDF2),
thickness = 1.dp,
modifier = Modifier.padding(horizontal = 16.dp)
)
MenuItem(
icon = painterResource(id = R.drawable.accessibility_icon),
text = "Accessibility",
onClick = { }
)
}
}
Spacer(modifier = Modifier.height(24.dp))
// Logout Button
TextButton(
onClick = { },
modifier = Modifier
.fillMaxWidth()
) {
Text(
text = "Logout",
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = Color.Black
)
}
}
}
@Composable
fun ProfileAvatar(
name: String,
imageRes: Int,
onClick: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.clickable(onClick = onClick)
) {
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(Color(0xFFD1E3F5))
) {
Image(
painter = painterResource(id = imageRes),
contentDescription = name,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
Spacer(modifier = Modifier.height(6.dp))
Text(
text = name,
fontSize = 13.sp,
color = Color.Black,
fontWeight = FontWeight.Normal
)
}
}
@Composable
fun AddProfileButton(onClick: () -> Unit) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.clickable(onClick = onClick)
) {
Box(
modifier = Modifier
.size(80.dp)
.drawBehind {
val stroke = Stroke(
width = 4f,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(12f, 8f), 0f)
)
drawCircle(
color = Color.Gray.copy(alpha = 0.4f),
style = stroke
)
},
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add profile",
tint = Color.Gray.copy(alpha = 0.6f),
modifier = Modifier.size(30.dp)
)
}
Spacer(modifier = Modifier.height(6.dp))
Text(
text = "Add profile",
fontSize = 13.sp,
color = Color.Black,
fontWeight = FontWeight.Normal
)
}
}
@Composable
fun ActionButton(
icon: Painter,
label: String,
onClick: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(vertical = 18.dp, horizontal = 8.dp)
) {
Icon(
painter = icon,
contentDescription = label,
modifier = Modifier.size(24.dp),
tint = Color.Black
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = label,
fontSize = 11.sp,
color = Color.Black,
textAlign = TextAlign.Center
)
}
}
@Composable
fun MenuItem(
icon: Painter,
text: String,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = icon,
contentDescription = text,
modifier = Modifier.size(22.dp),
tint = Color.Black
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = text,
fontSize = 15.sp,
color = Color.Black,
modifier = Modifier.weight(1f)
)
Icon(
painter = painterResource(id = R.drawable.right_chevron_icon),
contentDescription = "Navigate",
tint = Color.Gray.copy(alpha = 1f),
modifier = Modifier.size(15.dp)
)
}
}FAQ :
1) How do I create a modern OTT Profile UI in Jetpack Compose?
Build the screen using a root Column, then add sections like a top bar (Row), profile avatar row, shortcut action cards, a settings/menu card list, and a logout button. This structure matches OTT apps because each section feels separate and easy to scan.
2) Why does the screen use a light background and white cards?
A slightly tinted background helps white cards stand out clearly, which makes the UI look modern and “premium.” It also improves readability because important items (actions and menu) stay inside clean containers.
3) How do I create a circular profile avatar in Jetpack Compose?
Use a Box with a fixed size, then apply clip(CircleShape) so the content becomes circular. Place an Image inside with ContentScale.Crop to fill the circle nicely without stretching.
4) How does the “Add profile” dashed circle work in Compose?
It uses Modifier.drawBehind to draw a custom dashed circle. Inside drawBehind, a Stroke is created with PathEffect.dashPathEffect(...), and then drawCircle(...) draws the dashed border.
5) Why are Downloads/Watchlist/Settings inside separate cards?
Each action is wrapped in its own Card to create a clear tap target and visual separation. Using Modifier.weight(1f) makes all cards equal width, and a small elevation adds depth without making the UI heavy.
6) How do I handle click actions (downloads, watchlist, menu items) properly?
The best approach is to pass callbacks (lambdas) into the UI composables, like onDownloadsClick, onWatchlistClick, etc. This keeps your UI reusable and lets your navigation layer (like NavController) decide what happens.
7) How do I build a settings menu list like an OTT app?
Create one large Card, then place multiple clickable MenuItem rows inside it. Add HorizontalDivider between rows to visually separate items, similar to a settings screen layout.
8) Why is Modifier.weight(1f) used on the menu text?
weight(1f) forces the menu text to take the remaining space, so the right chevron icon always stays aligned at the far right. This keeps the layout consistent even if menu titles have different lengths.
9) How do I make this screen scrollable on small phones?
If content can exceed the screen height, switch the root layout from Column to LazyColumn. That way, the UI can scroll smoothly and also performs better for longer lists.
10) How can I add dark theme support to this UI?
Avoid hardcoding colors like Color.Black and Color.White. Instead, use MaterialTheme.colorScheme values (like surface, background, onSurface) so the UI automatically adapts to dark mode.

0 Comments