Trang Profile
Hướng dẫn xây dựng màn hình Profile với header, avatar, và settings trong Compose Multiplatform.
Profile Screen
@Composable
fun ProfileScreen(
user: User,
onEditProfile: () -> Unit,
onSettings: () -> Unit,
onLogout: () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("Profile") },
actions = {
IconButton(onClick = onSettings) {
Icon(Icons.Default.Settings, "Settings")
}
}
)
}
) { padding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
// Profile Header
item {
ProfileHeader(
user = user,
onEditProfile = onEditProfile
)
}
// Stats
item {
ProfileStats(
posts = 42,
followers = 1234,
following = 567
)
}
// Menu items
item {
ProfileMenuSection(onLogout = onLogout)
}
}
}
}Profile Header
@Composable
fun ProfileHeader(
user: User,
onEditProfile: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Avatar
Box(contentAlignment = Alignment.BottomEnd) {
Box(
modifier = Modifier
.size(100.dp)
.background(
MaterialTheme.colorScheme.primary,
CircleShape
),
contentAlignment = Alignment.Center
) {
if (user.avatarUrl != null) {
// AsyncImage(user.avatarUrl)
Text(
user.name.first().uppercase(),
style = MaterialTheme.typography.headlineLarge,
color = Color.White
)
} else {
Text(
user.name.first().uppercase(),
style = MaterialTheme.typography.headlineLarge,
color = Color.White
)
}
}
// Edit avatar button
Box(
modifier = Modifier
.size(32.dp)
.background(
MaterialTheme.colorScheme.surface,
CircleShape
)
.border(2.dp, MaterialTheme.colorScheme.surface, CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.CameraAlt,
contentDescription = "Change avatar",
modifier = Modifier.size(16.dp)
)
}
}
Spacer(Modifier.height(16.dp))
// Name
Text(
user.name,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
// Email
Text(
user.email,
style = MaterialTheme.typography.bodyMedium,
color = Color.Gray
)
Spacer(Modifier.height(8.dp))
// Bio
user.bio?.let { bio ->
Text(
bio,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 32.dp)
)
}
Spacer(Modifier.height(16.dp))
// Edit button
OutlinedButton(
onClick = onEditProfile,
shape = RoundedCornerShape(20.dp)
) {
Icon(Icons.Default.Edit, contentDescription = null, Modifier.size(16.dp))
Spacer(Modifier.width(8.dp))
Text("Edit Profile")
}
}
}Profile Stats
@Composable
fun ProfileStats(
posts: Int,
followers: Int,
following: Int
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
StatItem(value = posts, label = "Posts")
StatItem(value = followers, label = "Followers")
StatItem(value = following, label = "Following")
}
}
@Composable
private fun StatItem(
value: Int,
label: String
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = formatNumber(value),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
}
private fun formatNumber(num: Int): String {
return when {
num >= 1_000_000 -> "${num / 1_000_000}M"
num >= 1_000 -> "${num / 1_000}K"
else -> num.toString()
}
}Profile Menu Section
@Composable
fun ProfileMenuSection(
onLogout: () -> Unit
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
"Settings",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp)
)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
) {
Column {
ProfileMenuItem(
icon = Icons.Default.Person,
title = "Account",
subtitle = "Manage your account",
onClick = { }
)
HorizontalDivider()
ProfileMenuItem(
icon = Icons.Default.Notifications,
title = "Notifications",
subtitle = "Notification preferences",
onClick = { }
)
HorizontalDivider()
ProfileMenuItem(
icon = Icons.Default.Lock,
title = "Privacy",
subtitle = "Privacy settings",
onClick = { }
)
HorizontalDivider()
ProfileMenuItem(
icon = Icons.Default.Help,
title = "Help & Support",
subtitle = "Get help or report issues",
onClick = { }
)
}
}
Spacer(Modifier.height(16.dp))
// Logout button
OutlinedButton(
onClick = onLogout,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error
),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.error),
shape = RoundedCornerShape(12.dp)
) {
Icon(Icons.Default.Logout, contentDescription = null)
Spacer(Modifier.width(8.dp))
Text("Logout")
}
Spacer(Modifier.height(16.dp))
// App version
Text(
"Version 1.0.0",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
}
@Composable
fun ProfileMenuItem(
icon: ImageVector,
title: String,
subtitle: String,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Spacer(Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(title, fontWeight = FontWeight.Medium)
Text(
subtitle,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
)
}
Icon(
Icons.Default.ChevronRight,
contentDescription = null,
tint = Color.Gray
)
}
}Edit Profile Screen
@Composable
fun EditProfileScreen(
user: User,
onSave: (User) -> Unit,
onBack: () -> Unit
) {
var name by remember { mutableStateOf(user.name) }
var email by remember { mutableStateOf(user.email) }
var bio by remember { mutableStateOf(user.bio ?: "") }
var phone by remember { mutableStateOf(user.phone ?: "") }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Edit Profile") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.Default.Close, "Close")
}
},
actions = {
TextButton(onClick = {
onSave(user.copy(name = name, email = email, bio = bio, phone = phone))
}) {
Text("Save")
}
}
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
// Avatar
Box(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(16.dp)
) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Gray, CircleShape)
.clickable { /* Change photo */ },
contentAlignment = Alignment.Center
) {
Text(name.firstOrNull()?.uppercase() ?: "?",
style = MaterialTheme.typography.headlineLarge,
color = Color.White)
}
Icon(
Icons.Default.CameraAlt,
contentDescription = "Change photo",
modifier = Modifier
.align(Alignment.BottomEnd)
.background(MaterialTheme.colorScheme.primary, CircleShape)
.padding(8.dp)
.size(16.dp),
tint = Color.White
)
}
Spacer(Modifier.height(24.dp))
// Fields
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
)
Spacer(Modifier.height(16.dp))
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email)
)
Spacer(Modifier.height(16.dp))
OutlinedTextField(
value = phone,
onValueChange = { phone = it },
label = { Text("Phone") },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone)
)
Spacer(Modifier.height(16.dp))
OutlinedTextField(
value = bio,
onValueChange = { bio = it },
label = { Text("Bio") },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
minLines = 3,
maxLines = 5
)
}
}
}📝 Tóm tắt
| Component | Mục đích |
|---|---|
| ProfileHeader | Avatar, name, bio |
| ProfileStats | Số liệu thống kê |
| ProfileMenuItem | Menu items dạng list |
| EditProfile | Form chỉnh sửa |
Best Practices
- Avatar với fallback - Hiển thị chữ cái đầu nếu không có ảnh
- Grouped settings - Nhóm settings liên quan
- Clear CTAs - Edit, Logout buttons rõ ràng
- Version info - Hiện version ở cuối
Tiếp theo
Học về Animation trong Compose Multiplatform.
Last updated on