Skip to Content

Accessibility trong Jetpack Compose

Accessibility giúp ứng dụng của bạn có thể sử dụng được bởi người khuyết tật. Compose làm cho việc implement accessibility trở nên dễ dàng hơn.

1. Semantics là gì?

Compose sử dụng Semantics tree để cung cấp thông tin cho accessibility services (như TalkBack).

// Compose tự động tạo semantics cho nhiều composables Text("Hello") // Text content tự động được đọc Button(onClick = { }) { Text("Submit") // Button với label "Submit" }

2. Content Descriptions

Cho Images

// ❌ SAI: Không có description Image( painter = painterResource(R.drawable.cat), contentDescription = null // Accessibility bỏ qua image này ) // ✅ ĐÚNG: Có description Image( painter = painterResource(R.drawable.cat), contentDescription = "A cute cat sitting on a couch" )

Cho Icons

// Decorative icon (không cần đọc) Icon(Icons.Default.Star, contentDescription = null) // Meaningful icon Icon(Icons.Default.Favorite, contentDescription = "Add to favorites")

3. semantics modifier

Thêm hoặc sửa semantics

@Composable fun CustomAccessibleButton() { Box( modifier = Modifier .size(100.dp) .background(Color.Blue) .clickable { } .semantics { role = Role.Button contentDescription = "Confirm purchase" } ) { // Custom content } }

Merge semantics

// Mặc định: mỗi Text được đọc riêng Row { Icon(Icons.Default.Star, null) Text("4.5") Text("(123 reviews)") } // TalkBack đọc: "Star", "4.5", "(123 reviews)" // Với mergeDescendants: đọc cùng lúc Row( modifier = Modifier.semantics(mergeDescendants = true) { contentDescription = "Rating 4.5 stars from 123 reviews" } ) { Icon(Icons.Default.Star, null) Text("4.5") Text("(123 reviews)") } // TalkBack đọc: "Rating 4.5 stars from 123 reviews"

4. State Descriptions

@Composable fun ExpandableCard(expanded: Boolean, onExpand: () -> Unit) { Card( modifier = Modifier .clickable { onExpand() } .semantics { stateDescription = if (expanded) "Expanded" else "Collapsed" } ) { // Content } }

Toggle states

@Composable fun FavoriteButton(isFavorite: Boolean, onToggle: () -> Unit) { IconButton( onClick = onToggle, modifier = Modifier.semantics { contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites" } ) { Icon( if (isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder, contentDescription = null // Description ở IconButton ) } }

5. Headings và Structure

@Composable fun AccessibleScreen() { Column { Text( "Settings", style = MaterialTheme.typography.headlineLarge, modifier = Modifier.semantics { heading() } ) Text( "Account", style = MaterialTheme.typography.titleMedium, modifier = Modifier.semantics { heading() } ) // Settings items Text( "Notifications", style = MaterialTheme.typography.titleMedium, modifier = Modifier.semantics { heading() } ) // Notification settings } }

6. Custom Actions

@Composable fun SwipeableItem( item: Item, onDelete: () -> Unit, onArchive: () -> Unit ) { Card( modifier = Modifier.semantics { customActions = listOf( CustomAccessibilityAction("Delete") { onDelete() true }, CustomAccessibilityAction("Archive") { onArchive() true } ) } ) { Text(item.title) } }

7. Touch Target Size

Đảm bảo touch targets đủ lớn (ít nhất 48dp):

@Composable fun SmallIcon() { // ❌ Icon nhỏ, khó click IconButton(onClick = { }) { Icon( Icons.Default.Close, contentDescription = "Close", modifier = Modifier.size(20.dp) ) } // ✅ IconButton tự động có touch target 48dp // hoặc dùng minimumTouchTargetSize Box( modifier = Modifier .size(24.dp) .minimumInteractiveComponentSize() .clickable { } ) { Icon(Icons.Default.Close, "Close") } }

8. Focus và Keyboard Navigation

@Composable fun FocusExample() { val focusRequester = remember { FocusRequester() } Column { TextField( value = "", onValueChange = { }, modifier = Modifier.focusRequester(focusRequester) ) Button(onClick = { focusRequester.requestFocus() }) { Text("Focus TextField") } } }

Focus order

@Composable fun CustomFocusOrder() { val (first, second, third) = remember { FocusRequester.createRefs() } Column { TextField( value = "", onValueChange = { }, modifier = Modifier .focusRequester(second) .focusProperties { next = third previous = first } ) } }

9. Live Regions (Announcements)

@Composable fun LiveAnnouncement(message: String?) { message?.let { Text( text = it, modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite // Đợi, không ngắt // hoặc LiveRegionMode.Assertive // Ngắt ngay } ) } }

10. Testing Accessibility

Kiểm tra với Compose Test

@Test fun testAccessibility() { composeTestRule.setContent { MyScreen() } // Kiểm tra content description composeTestRule .onNodeWithContentDescription("Submit button") .assertExists() // Kiểm tra heading composeTestRule .onNode(hasTestTag("title") and isHeading()) .assertExists() }

Accessibility Scanner

Sử dụng Accessibility Scanner app để tìm vấn đề accessibility trong ứng dụng.


📝 Tóm tắt

Semantics PropertyMục đích
contentDescriptionMô tả cho screen readers
stateDescriptionMô tả state (expanded, selected)
roleLoại element (Button, Checkbox)
heading()Đánh dấu heading
customActionsActions bổ sung
liveRegionAnnounce thay đổi
mergeDescendantsGộp children thành 1 unit

Checklist Accessibility

  • Tất cả images có contentDescription phù hợp
  • Interactive elements có touch target >= 48dp
  • Headings được đánh dấu đúng
  • Color contrast đủ cao
  • State changes được announce
  • Test với TalkBack
Last updated on