Skip to Content

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

ComponentMục đích
ProfileHeaderAvatar, name, bio
ProfileStatsSố liệu thống kê
ProfileMenuItemMenu items dạng list
EditProfileForm chỉnh sửa

Best Practices

  1. Avatar với fallback - Hiển thị chữ cái đầu nếu không có ảnh
  2. Grouped settings - Nhóm settings liên quan
  3. Clear CTAs - Edit, Logout buttons rõ ràng
  4. Version info - Hiện version ở cuối

Tiếp theo

Học về Animation trong Compose Multiplatform.

Last updated on