Building Accessible Android Apps: A Complete Implementation Guide

by | Oct 30, 2025 | Tutorials | 0 comments

Accessibility is not optional. Over one billion people worldwide live with some form of disability. Many more experience temporary or situational impairments—a broken arm, bright sunlight obscuring a screen, or holding a baby while trying to use a phone. Building accessible applications ensures everyone can use your software, expands your potential user base, and in many jurisdictions, fulfills legal requirements.

Android provides comprehensive accessibility support through services like TalkBack, Switch Access, and voice control. This guide covers implementing accessibility correctly, from semantic markup through testing with real assistive technologies.

Understanding Assistive Technologies

TalkBack is Android’s screen reader, used by people with visual impairments. It announces UI elements audibly and provides gesture-based navigation. Users explore screens by touch, hearing descriptions of elements under their finger, then double-tap to activate.

Switch Access enables device control through one or more switches—physical buttons, keyboard keys, or head movements detected by camera. Users scan through interactive elements and activate their chosen switch to select.

Voice Access allows hands-free device control through spoken commands. Users speak element labels or numbers displayed on screen to interact with applications.

Magnification helps users with low vision by enlarging portions of the screen. Font scaling increases text size system-wide. Color correction and inversion assist users with color vision deficiencies.

Semantic Content Descriptions

Every interactive element needs a content description that conveys its purpose. Screen readers announce this text to users who cannot see the visual presentation.

// ImageButton needs content description
<ImageButton
    android:id="@+id/addToCartButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/ic_cart_add"
    android:contentDescription="@string/add_to_cart" />

// Decorative images should be marked
<ImageView
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:src="@drawable/decorative_banner"
    android:importantForAccessibility="no" />

Content descriptions should convey meaning, not appearance. Describe what the element does, not what it looks like. Say “Add to cart” not “Shopping cart icon with plus sign.”

For Jetpack Compose, use the semantics modifier to provide accessibility information:

@Composable
fun ProductCard(
    product: Product,
    onAddToCart: () -> Unit
) {
    Card(
        modifier = Modifier.semantics {
            // Merge child semantics into single announcement
            contentDescription = "${product.name}, ${product.formattedPrice}"
        }
    ) {
        // Product content
        
        IconButton(
            onClick = onAddToCart,
            modifier = Modifier.semantics {
                contentDescription = "Add ${product.name} to cart"
            }
        ) {
            Icon(Icons.Default.AddShoppingCart, contentDescription = null)
        }
    }
}

@Composable
fun RatingDisplay(rating: Float) {
    Row(
        modifier = Modifier.semantics {
            contentDescription = "Rating: $rating out of 5 stars"
        }
    ) {
        repeat(5) { index ->
            Icon(
                imageVector = if (index < rating) Icons.Filled.Star else Icons.Outlined.Star,
                contentDescription = null // Described at row level
            )
        }
    }
}

Focus Management and Navigation

Accessibility services navigate through focusable elements in a logical order. Ensure this order matches visual layout and user expectations.

The traversal order follows the view hierarchy by default. Use android:accessibilityTraversalBefore and android:accessibilityTraversalAfter to customize when default order is illogical.

Group related elements so screen readers announce them together rather than separately. This reduces verbosity and provides better context.

// Group product info into single announcement
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:focusable="true"
    android:contentDescription="@{@string/product_info_template(product.name, product.price)}">
    
    <TextView
        android:text="@{product.name}"
        android:importantForAccessibility="no" />
    
    <TextView
        android:text="@{product.formattedPrice}"
        android:importantForAccessibility="no" />
</LinearLayout>

In Compose, use Modifier.semantics with mergeDescendants to group related content:

Row(
    modifier = Modifier.semantics(mergeDescendants = true) {
        // Children's semantics merge into this node
    }
) {
    Text(product.name)
    Spacer(Modifier.width(8.dp))
    Text(product.formattedPrice)
}

Touch Target Sizes

Interactive elements need adequate touch targets for users with motor impairments. Android recommends minimum 48dp touch targets. Material Design specifies 48dp as the minimum and recommends 56dp for primary actions.

// Ensure minimum touch target
<ImageButton
    android:layout_width="48dp"
    android:layout_height="48dp"
    android:padding="12dp"
    android:src="@drawable/ic_close" />

In Compose, use Modifier.sizeIn to enforce minimums while allowing larger sizes:

IconButton(
    onClick = onClose,
    modifier = Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp)
) {
    Icon(Icons.Default.Close, contentDescription = "Close")
}

Color and Contrast

Never convey information through color alone. Users with color vision deficiencies cannot distinguish certain color combinations. Supplement color with text, icons, or patterns.

Maintain sufficient contrast between text and backgrounds. WCAG guidelines require 4.5:1 contrast ratio for normal text and 3:1 for large text. Android Studio's Layout Inspector can measure contrast ratios.

// Bad: Status indicated by color only
<View
    android:background="@{order.isPaid ? @color/green : @color/red}" />

// Good: Status indicated by text and color
<TextView
    android:text="@{order.isPaid ? @string/paid : @string/unpaid}"
    android:textColor="@{order.isPaid ? @color/green : @color/red}" />

Text Scaling

Users can increase system font size for readability. Applications must accommodate larger text without breaking layouts or clipping content.

Use sp units for text sizes to respect user font scaling preferences. Ensure layouts remain functional at maximum font scale (typically 2x default).

Test with large font settings enabled. Fixed-height containers may clip scaled text. Prefer wrap_content heights for text containers.

// Bad: Fixed height clips large text
<TextView
    android:layout_height="24dp"
    android:textSize="14sp" />

// Good: Height adapts to content
<TextView
    android:layout_height="wrap_content"
    android:minHeight="24dp"
    android:textSize="14sp" />

Custom Views and Controls

Custom views require explicit accessibility implementation. Extend AccessibilityNodeInfo to describe your custom view's role, state, and available actions.

class CustomRatingBar @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : View(context, attrs) {
    
    var rating: Float = 0f
        set(value) {
            field = value.coerceIn(0f, maxRating.toFloat())
            invalidate()
            // Announce rating change to accessibility services
            announceForAccessibility(context.getString(R.string.rating_changed, field))
        }
    
    private val maxRating = 5
    
    override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
        super.onInitializeAccessibilityNodeInfo(info)
        
        info.className = RatingBar::class.java.name
        info.contentDescription = context.getString(
            R.string.rating_description, 
            rating, 
            maxRating
        )
        
        // Indicate this is adjustable
        info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS)
        
        if (rating > 0) {
            info.addAction(AccessibilityNodeInfo.AccessibilityAction(
                AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD,
                context.getString(R.string.decrease_rating)
            ))
        }
        
        if (rating < maxRating) {
            info.addAction(AccessibilityNodeInfo.AccessibilityAction(
                AccessibilityNodeInfo.ACTION_SCROLL_FORWARD,
                context.getString(R.string.increase_rating)
            ))
        }
    }
    
    override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean {
        return when (action) {
            AccessibilityNodeInfo.ACTION_SCROLL_FORWARD -> {
                rating += 1
                true
            }
            AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD -> {
                rating -= 1
                true
            }
            else -> super.performAccessibilityAction(action, arguments)
        }
    }
}

Live Regions and Announcements

Dynamic content changes need announcement to screen reader users who cannot see updates visually. Live regions automatically announce content changes.

// Announce content changes
<TextView
    android:id="@+id/statusText"
    android:accessibilityLiveRegion="polite" />

// assertive interrupts current speech
// polite waits for current speech to complete

For important one-time announcements, use announceForAccessibility:

fun showError(message: String) {
    errorView.text = message
    errorView.announceForAccessibility(message)
}

Testing Accessibility

Automated testing catches many accessibility issues. Espresso's AccessibilityChecks integration flags common problems during instrumented tests.

@RunWith(AndroidJUnit4::class)
class ProductScreenAccessibilityTest {
    
    @Before
    fun setup() {
        AccessibilityChecks.enable()
            .setRunChecksFromRootView(true)
    }
    
    @Test
    fun productScreen_meetsAccessibilityGuidelines() {
        launchActivity<ProductActivity>()
        // Accessibility checks run automatically on interactions
        onView(withId(R.id.addToCart)).perform(click())
    }
}

Manual testing with TalkBack is essential. Automated tests cannot evaluate whether descriptions make sense or navigation flow is logical. Test every screen by enabling TalkBack and navigating without looking at the screen.

Conclusion

Accessibility implementation requires attention throughout development, not as an afterthought. Design with accessibility in mind from the beginning. Include accessibility testing in your regular QA process. Listen to feedback from users with disabilities.

The effort invested in accessibility benefits everyone. Larger touch targets help users in bumpy vehicles. Good contrast aids visibility in bright sunlight. Semantic structure improves voice control and future AI interfaces. Accessible applications are simply better applications.

At RyuPy, accessibility is a core requirement, not an optional enhancement. We believe technology should serve everyone, and we build our applications accordingly.

Written by RyuPy Team

Related Posts

0 Comments

Submit a Comment

Your email address will not be published. Required fields are marked *