<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Jetpack Compose - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Jetpack Compose - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sun, 11 Oct 2026 09:32:53 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/jetpack-compose/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build Accessible Android Apps with Jetpack Compose: A Comprehensive Guide ]]>
                </title>
                <description>
                    <![CDATA[ Making your mobile apps accessible makes sure that everyone, including people with visual, auditory, motor, or cognitive disabilities, can interact with and navigate the app effectively. Historically, ]]>
                </description>
                <link>https://www.freecodecamp.org/news/accessibility-in-jetpack-compose-comprehensive-tutorial/</link>
                <guid isPermaLink="false">6a96f33812e0a826683b9f0f</guid>
                
                    <category>
                        <![CDATA[ Android ]]>
                    </category>
                
                    <category>
                        <![CDATA[ android app development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Accessibility ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Jetpack Compose ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Kotlin ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Vamsi Vaddavalli ]]>
                </dc:creator>
                <pubDate>Tue, 01 Sep 2026 15:46:00 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/b89c199c-9ec0-48f2-b7ff-193c5b16a1d4.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Making your mobile apps accessible makes sure that everyone, including people with visual, auditory, motor, or cognitive disabilities, can interact with and navigate the app effectively.</p>
<p>Historically, accessibility has often been treated as an afterthought. Something to consider only if time permitted before a release.</p>
<p>Today, that perspective has fundamentally changed. With regulations like the European Accessibility Act (EAA) taking effect across European Union markets, building accessible software is increasingly a legal and business requirement.</p>
<p>More importantly, building accessible applications is just good engineering. Accessible apps improve usability for all users and help make sure that you're not excluding a substantial portion of your potential audience.</p>
<p>Jetpack Compose simplifies accessibility compared to the traditional Android View system. Because Compose is declarative and state-driven, it manages semantic metadata alongside your visual user interface.</p>
<p>In this comprehensive guide, you will learn how accessibility works in Jetpack Compose, how to use the Semantics tree, how to apply essential and advanced accessibility patterns, and how to thoroughly test your apps using automated tests, Google's Accessibility Scanner, Android Studio's Layout Inspector, and TalkBack.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along with this guide, you'll need:</p>
<ul>
<li><p>Basic familiarity with Kotlin and Jetpack Compose (such as Composables, Modifiers, and State).</p>
</li>
<li><p>Android Studio (Hedgehog, Iguana, Jellyfish, Koala, Ladybug, or newer).</p>
</li>
<li><p>An Android device or emulator running Android 9.0 (API 28) or higher with access to the Google Play Store (to install and run the current version of Google Accessibility Scanner).</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-why-accessibility-matters-in-modern-android-development">Why Accessibility Matters in Modern Android Development</a></p>
</li>
<li><p><a href="#heading-core-concepts-of-android-accessibility">Core Concepts of Android Accessibility</a></p>
</li>
<li><p><a href="#heading-semantics-in-jetpack-compose">Semantics in Jetpack Compose</a></p>
</li>
<li><p><a href="#heading-essential-accessibility-practices">Essential Accessibility Practices</a></p>
</li>
<li><p><a href="#heading-advanced-accessibility-techniques">Advanced Accessibility Techniques</a></p>
</li>
<li><p><a href="#heading-how-to-test-and-debug-accessibility">How to Test and Debug Accessibility</a></p>
</li>
<li><p><a href="#heading-real-world-accessible-ui-patterns">Real-World Accessible UI Patterns</a></p>
</li>
<li><p><a href="#heading-accessibility-audit-checklist">Accessibility Audit Checklist</a></p>
</li>
<li><p><a href="#heading-conclusion-and-next-steps">Conclusion and Next Steps</a></p>
</li>
<li><p><a href="#heading-essential-resources">Essential Resources</a></p>
</li>
</ul>
<h2 id="heading-why-accessibility-matters-in-modern-android-development">Why Accessibility Matters in Modern Android Development</h2>
<p>According to the World Health Organization (WHO), more than 1.3 billion people (representing roughly 16% of the global population) live with a significant disability. This includes people with permanent visual impairments, hearing loss, physical motor limitations, and cognitive or neurological differences.</p>
<p>In addition to permanent disabilities, users can often experience temporary or situational limitations. A person holding a baby with one hand has temporary motor constraints. Someone using their phone in harsh midday sunlight experiences situational visual impairment. And someone in a loud airport terminal experiences situational hearing limitations.</p>
<p>Designing an accessible app ensures that your interface remains resilient and usable across all of these scenarios and more.</p>
<p>Beyond empathy and inclusive design, accessibility is increasingly governed by international law.</p>
<p>In the European Union, the <strong>European Accessibility Act (EAA)</strong> mandates accessibility for a defined list of products and services, including smartphones and computers, e-commerce, consumer banking services, e-books, and passenger transport services. In practice, conformity is typically demonstrated against <strong>EN 301 549</strong>, the European accessibility standard for digital products and services.</p>
<p>In the United States, the <strong>Americans with Disabilities Act (ADA)</strong> applies to state and local governments (Title II, which, since 2024, has an explicit web and mobile app rule requiring compliance with WCAG 2.1 Level AA) and to public accommodations (Title III). Separately, <strong>Section 508</strong> of the Rehabilitation Act requires federal agencies' information and communications technology to be accessible.</p>
<p>Failing to meet these standards can result in legal penalties and brand damage. Conversely, prioritizing accessibility expands your total addressable market and boosts user retention.</p>
<p>Quality also affects distribution: Google's Android vitals documentation notes that a high user-perceived crash rate hurts your app's discoverability on Google Play.</p>
<h2 id="heading-core-concepts-of-android-accessibility">Core Concepts of Android Accessibility</h2>
<p>To design accessible applications in Jetpack Compose, you must understand how the underlying Android operating system communicates with assistive technologies.</p>
<h3 id="heading-understanding-android-accessibility-services">Understanding Android Accessibility Services</h3>
<p>Android includes several built-in accessibility services that run as background processes. These services intercept the app's user interface and translate it into alternative sensory feedback or alternative input mechanisms:</p>
<ul>
<li><p><strong>TalkBack (Screen Reader):</strong> TalkBack is Android's built-in screen reader designed for blind and low-vision users. It inspects your interface elements, converts visual content and semantic descriptions into synthesized speech and haptic vibrations, and enables non-visual navigation using linear swipe gestures.</p>
</li>
<li><p><strong>Switch Access:</strong> Designed for users with severe motor impairments who can't use a physical touch screen, Switch Access allows users to control their device using one or more physical switches (such as foot pedals, sip-and-puff devices, or single buttons) or a connected keyboard. It scans through focusable screen elements sequentially, allowing the user to select items when highlighted.</p>
</li>
<li><p><strong>Voice Access:</strong> Allows users to control their entire phone using spoken commands (such as "Tap Open," "Scroll down," or "Type hello"). It assigns numeric badges to interactive elements based on their accessibility labels so users can trigger actions by number.</p>
</li>
<li><p><strong>Select to Speak:</strong> Allows users to highlight specific paragraphs, buttons, or icons on the screen to hear them spoken aloud without turning on full TalkBack navigation.</p>
</li>
</ul>
<p>All of these accessibility services share one common requirement: they don't interact directly with visual pixels. Instead, they read the <strong>Semantics Tree</strong> generated by your application.</p>
<h3 id="heading-how-the-compose-semantics-tree-works">How the Compose Semantics Tree Works</h3>
<p>When you build a user interface in Jetpack Compose, Compose generates two separate but linked internal tree structures:</p>
<ol>
<li><p><strong>The Layout (UI) Tree:</strong> This tree contains the visual rendering nodes that measure, place, and draw pixels on the screen (such as Canvas, Box, Row, Column, Text, and Image).</p>
</li>
<li><p><strong>The Semantics Tree:</strong> This tree runs in parallel with the Layout tree. It contains metadata that describes the meaning, purpose, state, and interactive capabilities of each element.</p>
</li>
</ol>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Visual Layout Tree (UI &amp; Pixels)</th>
<th>Semantics Tree (Accessibility &amp; TalkBack)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Container</strong></td>
<td><code>Row(modifier = Modifier.clickable { ... })</code></td>
<td><strong>Single Merged Accessibility Node</strong></td>
</tr>
<tr>
<td><strong>Child 1</strong></td>
<td><code>Image(Icons.Default.Star)</code></td>
<td><em>Merged into parent description</em></td>
</tr>
<tr>
<td><strong>Child 2</strong></td>
<td><code>Text("4.5")</code></td>
<td><em>Merged into parent description</em></td>
</tr>
<tr>
<td><strong>Child 3</strong></td>
<td><code>Text("Rating")</code></td>
<td><em>Merged into parent description</em></td>
</tr>
<tr>
<td><strong>Result</strong></td>
<td>Renders separate visual pixels on screen</td>
<td><strong>TalkBack announces:</strong> <em>"Rating: 4.5 stars, button"</em></td>
</tr>
</tbody></table>
<p>Standard Material Compose components (such as <code>Button</code>, <code>Checkbox</code>, <code>Slider</code>, <code>Switch</code>, and <code>Text</code>) automatically populate appropriate semantic properties into the Semantics tree. For example, a <code>Button</code> composable automatically sets its role to <code>Role.Button</code> and attaches a click action.</p>
<p>But when you build custom layouts, canvas drawings, or non-standard interactive widgets, Compose can't automatically infer your design intent. In those cases, you must use Compose's semantic modifiers to enrich the Semantics tree manually.</p>
<h3 id="heading-the-wcag-21-pour-principles">The WCAG 2.1 POUR Principles</h3>
<p>The international benchmark for digital accessibility is the <strong>Web Content Accessibility Guidelines (WCAG) 2.1</strong>, published by the World Wide Web Consortium (W3C). These guidelines are organized around four core principles known by the acronym <strong>POUR</strong>:</p>
<ul>
<li><p><strong>Perceivable:</strong> Information and UI components must be presentable to users in ways they can perceive. Content can't be invisible to all of a user's senses. In Android apps, this means providing descriptive text alternatives for all visual media, supporting dynamic font scaling, and maintaining high color contrast.</p>
</li>
<li><p><strong>Operable:</strong> User interface components and navigation must be operable. Users must be able to perform all interactions regardless of whether they use a touchscreen, a hardware keyboard, voice commands, or a switch device. This requires adequate touch targets (Android's guideline is at least 48dp × 48dp) and logical navigation orders.</p>
</li>
<li><p><strong>Understandable:</strong> Users must be able to understand both the information and the interface's operation. This means writing clear labels, providing meaningful validation error messages, avoiding sudden unexpected layout shifts, and structuring forms predictably.</p>
</li>
<li><p><strong>Robust:</strong> Content must be sufficiently robust to be reliably interpreted by a wide variety of user agents and assistive technologies. In Compose, this means avoiding hacky workarounds, using standard semantic roles, and honoring system-level user preferences.</p>
</li>
</ul>
<h2 id="heading-semantics-in-jetpack-compose">Semantics in Jetpack Compose</h2>
<p>In this section, we'll examine how Compose exposes and manipulates semantic metadata.</p>
<h3 id="heading-what-are-semantics">What Are Semantics?</h3>
<p>Semantics in Jetpack Compose are key-value properties attached to layout nodes using the <code>Modifier.semantics</code> modifier. They convey information such as:</p>
<ul>
<li><p>What an element is called (<code>contentDescription</code>)</p>
</li>
<li><p>What kind of UI control it represents (<code>role = Role.Checkbox</code>, <code>Role.Button</code>, <code>Role.Tab</code>)</p>
</li>
<li><p>What state it currently holds (<code>stateDescription = "Checked"</code>, <code>progressBarRangeInfo</code>)</p>
</li>
<li><p>What actions the user can perform (<code>onClick</code>, <code>onLongClick</code>, <code>customActions</code>)</p>
</li>
</ul>
<h3 id="heading-basic-semantic-properties-and-content-descriptions">Basic Semantic Properties and Content Descriptions</h3>
<p>The most widely used semantic property is <code>contentDescription</code>. It provides a localized textual representation of non-textual UI elements, such as icons, photographs, and vector graphics.</p>
<p>Here's how you provide content descriptions for visual components:</p>
<pre><code class="language-kotlin">// A functional icon button that performs an action
IconButton(onClick = { /* Open camera */ }) {
    Icon(
        painter = painterResource(id = R.drawable.ic_camera),
        contentDescription = "Open camera"
    )
}

// A decorative background illustration
Image(
    painter = painterResource(id = R.drawable.decorative_pattern),
    contentDescription = null, // Informs TalkBack to skip this node completely
    modifier = Modifier.fillMaxWidth()
)
</code></pre>
<h4 id="heading-how-it-works-under-the-hood">How It Works Under the Hood</h4>
<p>When TalkBack navigates to the <code>Icon</code>, it reads the <code>contentDescription</code> aloud (something like "Open camera, button, double tap to activate"). TalkBack automatically appends the word "button" because <code>IconButton</code> exposes <code>Role.Button</code>. (Exact TalkBack phrasing varies by version and locale. The spoken examples throughout this guide are illustrative.)</p>
<p>For decorative elements (such as subtle background shapes, divider lines, or visual illustrations that accompany adjacent descriptive text), you should explicitly set <code>contentDescription = null</code>. Setting <code>contentDescription = null</code> instructs Compose to omit that element from the Semantics tree, preventing screen readers from stopping on meaningless visual noise.</p>
<h3 id="heading-custom-semantics-and-state-descriptions">Custom Semantics and State Descriptions</h3>
<p>When an element changes its state dynamically (such as toggling between playing and paused, or expanded and collapsed), screen reader users need to be notified of the current state before they interact with it.</p>
<p>You can set <code>stateDescription</code> and <code>role</code> inside the <code>Modifier.semantics</code> block:</p>
<pre><code class="language-kotlin">var isPlaying by remember { mutableStateOf(false) }

IconButton(
    onClick = { isPlaying = !isPlaying },
    modifier = Modifier.semantics {
        // Explicitly declare what state the media player is in
        stateDescription = if (isPlaying) "Playing audio" else "Audio paused"
        role = Role.Button
    }
) {
    Icon(
        imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
        contentDescription = if (isPlaying) "Pause" else "Play"
    )
}
</code></pre>
<h4 id="heading-how-it-works-under-the-hood">How It Works Under the Hood</h4>
<p>Without <code>stateDescription</code>, TalkBack only reads the action ("Play, button"). The user has to guess whether the audio is currently playing or stopped.</p>
<p>By adding <code>stateDescription</code>, TalkBack announces: <em>"Playing audio, Pause, button, double-tap to toggle."</em> This gives the user immediate confirmation of the current state followed by the action that activating the control will perform.</p>
<h3 id="heading-how-to-merge-semantics-with-mergedescendants">How to Merge Semantics with mergeDescendants</h3>
<p>In complex layouts, multiple individual composables often combine to represent a single logical entity. For instance, a user profile row might contain an avatar image, a username, an online badge, and a timestamp.</p>
<p>By default, TalkBack will treat every child <code>Text</code> and <code>Image</code> node as an independent stop, forcing the user to swipe four or five times just to move past a single list item.</p>
<p>You can use <code>Modifier.semantics(mergeDescendants = true)</code> to combine all descendant nodes into a single, cohesive accessibility node:</p>
<pre><code class="language-kotlin">// Bad: TalkBack focuses three separate times: "Star icon", "4.5", "Customer rating"
Row(modifier = Modifier.clickable { /* Navigate to reviews */ }) {
    Icon(
        imageVector = Icons.Default.Star,
        contentDescription = "Star icon"
    )
    Text(text = "4.5")
    Text(text = "Customer rating")
}

// Good: Merged into one single stop: "Customer rating: 4.5 out of 5 stars, button"
Row(
    modifier = Modifier
        .clickable(onClickLabel = "View all reviews") { /* Navigate to reviews */ }
        .semantics(mergeDescendants = true) {
            contentDescription = "Customer rating: 4.5 out of 5 stars"
        }
) {
    Icon(
        imageVector = Icons.Default.Star,
        contentDescription = null // Suppressed because parent provides full description
    )
    Text(text = "4.5")
    Text(text = "Customer rating")
}
</code></pre>
<h4 id="heading-how-it-works-under-the-hood">How It Works Under the Hood</h4>
<p>When <code>mergeDescendants = true</code> is set, Compose collapses the accessibility boundaries of all child composables. Instead of generating multiple stops in the Semantics tree, Compose exposes a single node to the accessibility framework. TalkBack focuses the entire <code>Row</code> as a single bounding box and reads the parent's <code>contentDescription</code>.</p>
<h3 id="heading-how-to-clear-semantics-with-clearandsetsemantics">How to Clear Semantics with clearAndSetSemantics</h3>
<p>In certain situations, a standard composable may include default semantic behaviors that interfere with your intended accessibility experience.</p>
<p>The <code>Modifier.clearAndSetSemantics</code> modifier clears all semantic properties that would otherwise be inherited from child composables or default implementations, allowing you to define a clean, custom semantic definition:</p>
<pre><code class="language-kotlin">// A composite badge displaying notification count
Box(
    modifier = Modifier.clearAndSetSemantics {
        contentDescription = "3 unread messages"
        role = Role.Button
    }
) {
    Icon(
        imageVector = Icons.Default.Email,
        contentDescription = "Email icon" // Cleared and ignored
    )
    Text(text = "3") // Cleared and ignored
}
</code></pre>
<h4 id="heading-how-it-works-under-the-hood">How It Works Under the Hood</h4>
<p>Unlike <code>Modifier.semantics</code>, which <em>adds</em> or <em>overrides</em> specific keys, <code>clearAndSetSemantics</code> discards all existing keys generated by the composable subtree. In the example above, the separate "Email icon" and "3" text nodes are completely erased from the Semantics tree and replaced with the single announcement: <em>"3 unread messages, button."</em></p>
<h2 id="heading-essential-accessibility-practices">Essential Accessibility Practices</h2>
<p>Now we'll cover the core accessibility practices you should implement in your applications, along with explanations of why each practice matters.</p>
<h3 id="heading-how-to-write-meaningful-content-descriptions">How to Write Meaningful Content Descriptions</h3>
<p>A content description should concisely explain the <strong>purpose</strong> or <strong>action</strong> of an element rather than its visual appearance.</p>
<h4 id="heading-what-to-do-recommended-practice">What to Do (Recommended Practice)</h4>
<pre><code class="language-kotlin">// Descriptive, action-oriented label
IconButton(onClick = { deleteDraft() }) {
    Icon(
        imageVector = Icons.Default.Delete,
        contentDescription = "Delete draft message"
    )
}

// Contextual weather information
Image(
    painter = painterResource(R.drawable.weather_sunny),
    contentDescription = "Current weather: Sunny, 75 degrees Fahrenheit"
)
</code></pre>
<p>The icon description tells the user exactly what will happen when they tap the button ("Delete draft message"). The weather image communicates the actual underlying data rather than describing the art style.</p>
<h4 id="heading-what-not-to-do-antipattern">What Not to Do (Antipattern)</h4>
<pre><code class="language-kotlin">// Avoid generic or redundant descriptions
IconButton(onClick = { deleteDraft() }) {
    Icon(
        imageVector = Icons.Default.Delete,
        contentDescription = "Trash can icon button" // Bad: includes visual style and element type
    )
}

// Avoid empty strings on interactive elements
IconButton(onClick = { openSettings() }) {
    Icon(
        imageVector = Icons.Default.Settings,
        contentDescription = "" // Bad: leaves the button with no accessible label
    )
}
</code></pre>
<p>Including words like "icon" or "button" is redundant because TalkBack already announces the component's role. Using an empty string (<code>""</code>) on an interactive element leaves TalkBack with nothing meaningful to announce (typically read out as an unlabeled button), giving screen reader users zero context about what the button does. Compose's accessibility checks treat an empty content description the same as a missing one.</p>
<h3 id="heading-how-to-ensure-minimum-touch-target-sizes">How to Ensure Minimum Touch Target Sizes</h3>
<p>Google's Material Design and Android accessibility guidelines require all interactive elements to have a minimum touch target size of at least <strong>48dp × 48dp</strong>, which corresponds to a physical size of about 9mm. This is squarely within the 7–10mm range Google recommends for touchscreen targets.</p>
<p>For comparison, WCAG itself sets lower web-oriented baselines: WCAG 2.1 Success Criterion 2.5.5 requires 44×44 CSS pixels at Level AAA, and WCAG 2.2 Success Criterion 2.5.8 requires 24×24 CSS pixels at Level AA. The 48dp rule is Android's stricter platform standard.</p>
<h4 id="heading-what-to-do-recommended-practice">What to Do (Recommended Practice)</h4>
<pre><code class="language-kotlin">// Method A: Using minimumInteractiveComponentSize()
Box(
    modifier = Modifier
        .minimumInteractiveComponentSize() // Expands touch area to 48dp x 48dp
        .clickable { /* Toggle bookmark */ }
) {
    Icon(
        imageVector = Icons.Default.Bookmark,
        contentDescription = "Bookmark article",
        modifier = Modifier.size(24.dp) // Visual size is 24dp, touch target is 48dp
    )
}

// Method B: Standard IconButton (built-in 48dp target)
IconButton(onClick = { /* Toggle bookmark */ }) {
    Icon(
        imageVector = Icons.Default.Bookmark,
        contentDescription = "Bookmark article"
    )
}
</code></pre>
<p><code>Modifier.minimumInteractiveComponentSize()</code> allows your visual design to remain compact (for instance, a 24dp icon) while ensuring that the invisible, clickable hit box expands to 48dp × 48dp. This prevents touch misses for users with motor tremors or limited fine motor control.</p>
<h4 id="heading-what-not-to-do-antipattern">What Not to Do (Antipattern)</h4>
<pre><code class="language-kotlin">// Bad: Tightly constrained 20dp clickable area
Icon(
    imageVector = Icons.Default.Close,
    contentDescription = "Close dialog",
    modifier = Modifier
        .size(20.dp)
        .clickable { dismissDialog() } // Touch target is strictly 20dp x 20dp!
)
</code></pre>
<p>A 20dp touch target is nearly impossible to tap reliably, especially on high-density displays or while walking. Google's Accessibility Scanner will flag it as a touch target issue.</p>
<h3 id="heading-how-to-maintain-accessible-color-contrast-ratios">How to Maintain Accessible Color Contrast Ratios</h3>
<p>Color contrast measures the difference in luminance between foreground text and its background. If the contrast ratio is too low, people with low vision, color blindness, or older eyes will likely not be able to read your text.</p>
<p>WCAG 2.1 defines the following minimum contrast ratios (Android's accessibility documentation maps WCAG's point sizes to <code>sp</code>):</p>
<ul>
<li><p><strong>Normal text (smaller than 18sp, or smaller than 14sp bold):</strong> Minimum <strong>4.5:1</strong></p>
</li>
<li><p><strong>Large text (18sp or larger, or 14sp bold or larger):</strong> Minimum <strong>3.0:1</strong></p>
</li>
<li><p><strong>UI components and graphical objects:</strong> Minimum <strong>3.0:1</strong></p>
</li>
</ul>
<h4 id="heading-what-to-do-recommended-practice">What to Do (Recommended Practice)</h4>
<pre><code class="language-kotlin">// Using Material 3 color tokens (paired roles maintain usable contrast)
Text(
    text = "Account Overview",
    color = MaterialTheme.colorScheme.onPrimaryContainer,
    modifier = Modifier.background(MaterialTheme.colorScheme.primaryContainer)
)

// Explicit high-contrast colors (for example, Black on White = 21:1 ratio)
Text(
    text = "Order Confirmed",
    color = Color(0xFF1B5E20), // Dark green
    modifier = Modifier.background(Color(0xFFE8F5E9)) // Light green tint
)
</code></pre>
<p>Material 3 color tokens (such as <code>onPrimaryContainer</code> paired with <code>primaryContainer</code>) are generated by the Material color system so that "on" colors maintain usable contrast against their paired container colors across both light and dark themes. You should still verify contrast for any custom brand colors you override.</p>
<h4 id="heading-what-not-to-do-antipattern">What Not to Do (Antipattern)</h4>
<pre><code class="language-kotlin">// Bad: Light gray on pure white (Contrast ratio ~ 1.6:1 - Fails WCAG)
Text(
    text = "Terms and Conditions apply",
    color = Color(0xFFB0B0B0),
    modifier = Modifier.background(Color.White)
)
</code></pre>
<p>Light gray text on a white background is one of the most common accessibility violations on the mobile web and in native apps. Sighted users in bright ambient light and users with low vision can't read this text.</p>
<h3 id="heading-how-to-provide-custom-clickable-labels">How to Provide Custom Clickable Labels</h3>
<p>When a user navigates to a clickable element using TalkBack, the screen reader appends default instructions such as <em>"Double-tap to activate."</em></p>
<p>You can customize this instruction using the <code>onClickLabel</code> parameter in <code>Modifier.clickable</code> to describe the precise outcome of the interaction.</p>
<h4 id="heading-what-to-do-recommended-practice">What to Do (Recommended Practice)</h4>
<pre><code class="language-kotlin">ListItem(
    headlineContent = { Text("Wi-Fi Networks") },
    supportingContent = { Text("Connected to Office_5G") },
    modifier = Modifier.clickable(
        onClickLabel = "Open Wi-Fi network settings"
    ) {
        navigateToWifiSettings()
    }
)
</code></pre>
<p>TalkBack will announce: <em>"Wi-Fi Networks, Connected to Office_5G, double-tap to open Wi-Fi network settings."</em> The user knows exactly what will happen before committing to the action.</p>
<h4 id="heading-what-not-to-do-antipattern">What Not to Do (Antipattern)</h4>
<pre><code class="language-kotlin">// Bad: Generic clickable element without context
Card(
    modifier = Modifier.clickable { openInvoiceDetails() }
) {
    Text("Invoice #4092")
}
</code></pre>
<p>TalkBack announces: <em>"Invoice #4092, double-tap to activate."</em> The user is left uncertain whether tapping will pay the invoice, download a PDF, or open an edit screen.</p>
<h3 id="heading-how-to-establish-heading-hierarchies">How to Establish Heading Hierarchies</h3>
<p>Visual readers scan large, bold text headings to understand a screen's layout and hierarchy. Screen reader users need the exact same structural overview.</p>
<p>By applying the <code>heading()</code> semantic modifier, you designate a text node as an accessibility heading. TalkBack users can change their navigation mode to "Headings" and quickly swipe up or down to jump between sections.</p>
<h4 id="heading-what-to-do-recommended-practice">What to Do (Recommended Practice)</h4>
<pre><code class="language-kotlin">Column(modifier = Modifier.padding(16.dp)) {
    // Screen title heading
    Text(
        text = "Security Settings",
        style = MaterialTheme.typography.headlineMedium,
        modifier = Modifier.semantics { heading() }
    )

    Spacer(modifier = Modifier.height(16.dp))

    // Section 1 heading
    Text(
        text = "Two-Factor Authentication",
        style = MaterialTheme.typography.titleMedium,
        modifier = Modifier.semantics { heading() }
    )
    
    // Section 1 content...

    Spacer(modifier = Modifier.height(16.dp))

    // Section 2 heading
    Text(
        text = "Connected Devices",
        style = MaterialTheme.typography.titleMedium,
        modifier = Modifier.semantics { heading() }
    )
    
    // Section 2 content...
}
</code></pre>
<p>TalkBack users can bypass dozens of individual switches and descriptions to jump directly to "Connected Devices" in seconds.</p>
<h4 id="heading-what-not-to-do-antipattern">What Not to Do (Antipattern)</h4>
<pre><code class="language-kotlin">// Bad: Visual heading without semantic markup
Text(
    text = "Two-Factor Authentication",
    style = MaterialTheme.typography.titleLarge // Visually large, but invisible as a heading to TalkBack
)
</code></pre>
<p>Without <code>semantics { heading() }</code>, TalkBack treats the text as regular body copy, preventing users from navigating by headings.</p>
<h3 id="heading-how-to-announce-dynamic-updates-with-live-regions">How to Announce Dynamic Updates with Live Regions</h3>
<p>When content on the screen updates asynchronously (such as a timer countdown, a file upload completion message, or a real-time validation banner), sighted users see the change immediately.</p>
<p>But screen reader users won't know that anything changed unless you mark the changing container as a <strong>live region</strong>.</p>
<p>Compose provides <code>liveRegion = LiveRegionMode.Polite</code> and <code>liveRegion = LiveRegionMode.Assertive</code>:</p>
<ul>
<li><p><code>LiveRegionMode.Polite</code><strong>:</strong> TalkBack waits until the current audio announcement is finished before reading the update. Use this for almost all status updates.</p>
</li>
<li><p><code>LiveRegionMode.Assertive</code><strong>:</strong> TalkBack announces the change immediately, ahead of other feedback. Reserve this strictly for time-sensitive, high-urgency alerts (such as emergency warnings).</p>
</li>
</ul>
<h4 id="heading-what-to-do-recommended-practice">What to Do (Recommended Practice)</h4>
<pre><code class="language-kotlin">var uploadStatus by remember { mutableStateOf("Ready to upload") }

Text(
    text = uploadStatus,
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Polite
    }
)

// Later, when upload finishes:
LaunchedEffect(Unit) {
    performUpload()
    uploadStatus = "Upload complete! 12 files saved."
}
</code></pre>
<p>As soon as <code>uploadStatus</code> changes, TalkBack automatically speaks: <em>"Upload complete! 12 files saved,"</em> keeping non-sighted users fully informed without requiring them to search the screen.</p>
<h3 id="heading-how-to-support-dynamic-text-scaling">How to Support Dynamic Text Scaling</h3>
<p>Users with low vision often increase their system font size in Android Settings (under <strong>Display</strong> or the accessibility settings, depending on the device). Android 14 and newer support <strong>non-linear font scaling up to 200%</strong>.</p>
<p>To respect this user preference, you must define all text sizes in <code>sp</code> <strong>(scale-independent pixels)</strong>, never in <code>dp</code> or raw pixels.</p>
<h4 id="heading-what-to-do-recommended-practice">What to Do (Recommended Practice)</h4>
<pre><code class="language-kotlin">// Good: Using Material Typography (uses sp automatically)
Text(
    text = "Dashboard Summary",
    style = MaterialTheme.typography.titleMedium
)

// Good: Explicit sp sizing
Text(
    text = "Custom Label",
    fontSize = 18.sp
)
</code></pre>
<p>When a user sets their font scale to 1.5×, an 18sp font cleanly renders at 27sp, ensuring readable text.</p>
<h4 id="heading-what-not-to-do-antipattern">What Not to Do (Antipattern)</h4>
<pre><code class="language-kotlin">// Bad: Fixed dp size converted to sp (does not scale with system settings)
Text(
    text = "Fixed Size Warning",
    fontSize = with(LocalDensity.current) { 16.dp.toSp() } // Will not scale!
)
</code></pre>
<p>Sizing text with fixed <code>dp</code> measurements prevents the text from enlarging when users increase their system font size, directly violating accessibility standards.</p>
<h2 id="heading-advanced-accessibility-techniques">Advanced Accessibility Techniques</h2>
<p>For complex, production-grade applications, you can apply advanced accessibility techniques to handle non-trivial user interactions.</p>
<h3 id="heading-how-to-control-traversal-and-focus-order">How to Control Traversal and Focus Order</h3>
<p>By default, accessibility services traverse screen elements based on their visual and structural coordinates (top-to-bottom, start-to-end). In multi-column layouts, financial ledgers, or custom grids, this default order can create confusing announcements.</p>
<p>You can customize the navigation sequence using <code>traversalIndex</code> and <code>isTraversalGroup</code>:</p>
<pre><code class="language-kotlin">Column(
    modifier = Modifier.semantics { isTraversalGroup = true }
) {
    Text(
        text = "Step 1: Account Info",
        modifier = Modifier.semantics { traversalIndex = 1f }
    )
    
    Text(
        text = "Step 3: Confirmation",
        modifier = Modifier.semantics { traversalIndex = 3f }
    )
    
    Text(
        text = "Step 2: Payment Details",
        modifier = Modifier.semantics { traversalIndex = 2f }
    )
}
</code></pre>
<p>How the code works under the hood:</p>
<ol>
<li><p>Setting <code>isTraversalGroup = true</code> creates a self-contained accessibility boundary, ensuring TalkBack reads all elements inside this container before moving to other screen elements.</p>
</li>
<li><p>The <code>traversalIndex</code> floating-point value establishes the exact relative order (lowest index first). TalkBack will read Step 1, then Step 2, and finally Step 3, regardless of their visual placement in the layout.</p>
</li>
</ol>
<h3 id="heading-how-to-create-custom-accessibility-actions">How to Create Custom Accessibility Actions</h3>
<p>Consider an e-commerce product card that contains "Quick View," "Add to Wishlist," and "Add to Cart" buttons. For a screen reader user, tabbing through three separate buttons on twenty consecutive cards is tedious.</p>
<p>With <code>customActions</code>, you can attach secondary actions directly to the parent card node. When the card gains focus, TalkBack tells the user that actions are available, and the user opens the TalkBack menu to pick one.</p>
<pre><code class="language-kotlin">var isBookmarked by remember { mutableStateOf(false) }

Card(
    modifier = Modifier
        .fillMaxWidth()
        .semantics {
            customActions = listOf(
                CustomAccessibilityAction(
                    label = if (isBookmarked) "Remove from bookmarks" else "Add to bookmarks",
                    action = {
                        isBookmarked = !isBookmarked
                        true // Return true to indicate the action was handled
                    }
                ),
                CustomAccessibilityAction(
                    label = "Share article link",
                    action = {
                        shareArticle()
                        true
                    }
                )
            )
        }
) {
    // Visual card content...
}
</code></pre>
<p>When a TalkBack user focuses on the card, TalkBack informs them that custom actions are available. The user opens the TalkBack menu (or uses accessibility gestures) to see a clean list containing "Add to bookmarks" and "Share article link." This keeps your visual design streamlined while providing direct, high-efficiency navigation for power users.</p>
<h3 id="heading-how-to-design-accessible-form-inputs-and-text-fields">How to Design Accessible Form Inputs and Text Fields</h3>
<p>Accessible form inputs require more than just a visual placeholder. They need explicit labels, clear input type hints, and logical keyboard navigation actions:</p>
<pre><code class="language-kotlin">var emailValue by remember { mutableStateOf("") }

OutlinedTextField(
    value = emailValue,
    onValueChange = { emailValue = it },
    label = { Text("Work Email") },
    placeholder = { Text("alex@example.com") },
    singleLine = true,
    keyboardOptions = KeyboardOptions(
        keyboardType = KeyboardType.Email,
        imeAction = ImeAction.Next
    ),
    keyboardActions = KeyboardActions(
        onNext = { /* Move focus to password field */ }
    ),
    modifier = Modifier
        .fillMaxWidth()
        .semantics {
            contentDescription = "Work email address input field"
        }
)
</code></pre>
<p>How it works under the hood:</p>
<ul>
<li><p>The <code>label</code> composable provides persistent context that remains visible even after the user types.</p>
</li>
<li><p><code>keyboardType = KeyboardType.Email</code> instructs Android to display an email-optimized keyboard (including <code>@</code> and <code>.com</code> shortcuts) and hints to accessibility services that standard email syntax is expected.</p>
</li>
<li><p><code>imeAction = ImeAction.Next</code> ensures hardware keyboard users and switch device users can smoothly advance through form fields using the keyboard Enter key.</p>
</li>
</ul>
<h3 id="heading-how-to-communicate-dynamic-error-states">How to Communicate Dynamic Error States</h3>
<p>When client-side validation fails, setting the visual border to red isn't enough. You must attach semantic error metadata so screen readers announce the validation failure immediately.</p>
<pre><code class="language-kotlin">var password by remember { mutableStateOf("") }
val isPasswordInvalid = password.isNotEmpty() &amp;&amp; password.length &lt; 8

OutlinedTextField(
    value = password,
    onValueChange = { password = it },
    label = { Text("Password") },
    isError = isPasswordInvalid,
    supportingText = {
        if (isPasswordInvalid) {
            Text(
                text = "Password must be at least 8 characters long",
                color = MaterialTheme.colorScheme.error
            )
        }
    },
    modifier = Modifier
        .fillMaxWidth()
        .semantics {
            if (isPasswordInvalid) {
                error("Password must be at least 8 characters long")
            }
        }
)
</code></pre>
<p>How it works under the hood:</p>
<p>The <code>isError = isPasswordInvalid</code> parameter handles the visual styling (red outline and error icons).</p>
<ul>
<li>The <code>error("...")</code> semantic property tells TalkBack to treat this element as invalid and announce the specific validation error message as soon as the user focuses the text field.</li>
</ul>
<h3 id="heading-how-to-manage-progress-indicators-and-asynchronous-loading">How to Manage Progress Indicators and Asynchronous Loading</h3>
<p>Loading states must clearly convey whether a task is indeterminate (in progress with unknown duration) or determinate (progressing toward 100%).</p>
<pre><code class="language-kotlin">// Indeterminate Progress (such as initial network fetch)
CircularProgressIndicator(
    modifier = Modifier.semantics {
        contentDescription = "Syncing messages, please wait"
    }
)

// Determinate Progress (such as file download)
val downloadProgress = 0.65f // 65%

LinearProgressIndicator(
    progress = { downloadProgress },
    modifier = Modifier.semantics {
        progressBarRangeInfo = ProgressBarRangeInfo(
            current = downloadProgress,
            range = 0f..1f
        )
        contentDescription = "Downloading update: ${(downloadProgress * 100).toInt()}% completed"
    }
)
</code></pre>
<p>For determinate progress indicators, <code>ProgressBarRangeInfo</code> tells assistive technologies the minimum, maximum, and current values. TalkBack interprets this information and provides periodic spoken and haptic updates as progress increases.</p>
<h3 id="heading-how-to-handle-expandable-and-collapsible-content">How to Handle Expandable and Collapsible Content</h3>
<p>Accordion menus and expandable FAQ cards must clearly indicate whether they're open or closed, and what action tapping them will trigger.</p>
<pre><code class="language-kotlin">var isExpanded by remember { mutableStateOf(false) }

Column(modifier = Modifier.fillMaxWidth()) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .fillMaxWidth()
            .clickable(
                onClickLabel = if (isExpanded) "Collapse section" else "Expand section"
            ) {
                isExpanded = !isExpanded
            }
            .semantics {
                stateDescription = if (isExpanded) "Expanded" else "Collapsed"
            }
            .padding(16.dp)
    ) {
        Text(
            text = "Frequently Asked Questions",
            style = MaterialTheme.typography.titleMedium,
            modifier = Modifier.weight(1f)
        )
        Icon(
            imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
            contentDescription = null // Decorative: parent Row communicates full state
        )
    }

    AnimatedVisibility(visible = isExpanded) {
        Text(
            text = "Our refund policy allows returns within 30 days of purchase...",
            modifier = Modifier.padding(16.dp)
        )
    }
}
</code></pre>
<p>The <code>stateDescription</code> announces whether the section is currently open or closed ("Expanded" or "Collapsed"). Simultaneously, <code>onClickLabel</code> clarifies the upcoming action ("Collapse section" or "Expand section"). The icon has <code>contentDescription = null</code> to avoid redundant announcements.</p>
<h3 id="heading-how-to-enhance-lazy-lists-and-large-collections">How to Enhance Lazy Lists and Large Collections</h3>
<p>When users navigate a long feed or list, they need context regarding where they are and how many items exist.</p>
<pre><code class="language-kotlin">val messages = remember { listOf("Order Shipped", "Delivery Delayed", "Payment Received") }

LazyColumn(
    modifier = Modifier.semantics {
        contentDescription = "Notifications list, ${messages.size} total alerts"
    }
) {
    itemsIndexed(messages) { index, messageText -&gt;
        Card(
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = 4.dp)
                .semantics {
                    contentDescription = "Notification ${index + 1} of ${messages.size}: $messageText"
                }
        ) {
            Text(
                text = messageText,
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}
</code></pre>
<p>TalkBack announces positional indexes ("Notification 1 of 3: Order Shipped"), allowing screen reader users to track their progress through lists without losing their place.</p>
<h2 id="heading-how-to-test-and-debug-accessibility">How to Test and Debug Accessibility</h2>
<p>Building accessible software requires a multi-layered testing strategy: automated regression checks, on-device diagnostic tools, semantics tree inspection, and manual verification with TalkBack.</p>
<table>
<thead>
<tr>
<th>Testing Level</th>
<th>Primary Tool</th>
<th>What It Validates</th>
<th>When to Run</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Manual Verification</strong></td>
<td>TalkBack Screen Reader</td>
<td>Real non-visual user experience and gesture navigation</td>
<td>Before major feature releases</td>
</tr>
<tr>
<td><strong>Visual Auditing</strong></td>
<td>Google Accessibility Scanner</td>
<td>Automated on-screen contrast and touch target violations</td>
<td>During feature QA testing</td>
</tr>
<tr>
<td><strong>Tree Inspection</strong></td>
<td>Android Studio Layout Inspector</td>
<td>Real-time semantics tree merging and node properties</td>
<td>During active development</td>
</tr>
<tr>
<td><strong>Automated Checks</strong></td>
<td>Compose UI Test (<code>ui-test-junit4</code>)</td>
<td>Regressions on touch targets and content descriptions</td>
<td>In continuous integration (CI)</td>
</tr>
</tbody></table>
<h3 id="heading-how-to-test-manually-with-talkback">How to Test Manually with TalkBack</h3>
<p>Nothing replaces manually navigating your application using TalkBack.</p>
<h4 id="heading-how-to-enable-and-use-talkback">How to Enable and Use TalkBack</h4>
<ol>
<li><p>Open your device's <strong>Settings</strong> app and navigate to <strong>Accessibility</strong> and then <strong>TalkBack</strong>.</p>
</li>
<li><p>Toggle the switch to <strong>On</strong> and accept the system permissions. (You can also use the volume key shortcut, if enabled: hold both volume keys for a few seconds to toggle TalkBack).</p>
</li>
<li><p>Core TalkBack gestures:</p>
<ul>
<li><p><strong>Swipe Right:</strong> Move accessibility focus to the next element.</p>
</li>
<li><p><strong>Swipe Left:</strong> Move accessibility focus to the previous element.</p>
</li>
<li><p><strong>Double Tap:</strong> Activate the currently focused element.</p>
</li>
<li><p><strong>Two-Finger Swipe:</strong> Scroll lists or pages.</p>
</li>
<li><p><strong>Three-Finger Tap (or swipe down then right in one motion):</strong> Open the TalkBack menu.</p>
</li>
</ul>
</li>
</ol>
<p>What to check during your TalkBack walkthrough:</p>
<ul>
<li><p>Can you complete critical user journeys (such as registration, login, searching, and checkout) with your eyes closed?</p>
</li>
<li><p>Are all buttons and interactive controls clearly announced with meaningful names?</p>
</li>
<li><p>Does focus move in a logical, expected reading order?</p>
</li>
<li><p>Are error messages and dynamic state changes announced automatically?</p>
</li>
</ul>
<h3 id="heading-how-to-write-automated-compose-accessibility-tests">How to Write Automated Compose Accessibility Tests</h3>
<p>You can integrate automated accessibility assertions into your JUnit instrumented tests to catch missing descriptions and undersized touch targets on continuous integration (CI) servers.</p>
<h4 id="heading-add-dependencies-to-buildgradlekts">Add Dependencies to <code>build.gradle.kts</code>:</h4>
<pre><code class="language-kotlin">androidTestImplementation("androidx.compose.ui:ui-test-junit4")
androidTestImplementation("androidx.compose.ui:ui-test-manifest")
</code></pre>
<h4 id="heading-writing-compose-accessibility-tests">Writing Compose Accessibility Tests:</h4>
<pre><code class="language-kotlin">@RunWith(AndroidJUnit4::class)
class AccessibilityTest {

    @get:Rule
    val composeTestRule = createAndroidComposeRule&lt;ComponentActivity&gt;()

    @Test
    fun testLoginButton_hasProperTouchTargetAndLabel() {
        composeTestRule.setContent {
            MaterialTheme {
                Button(
                    onClick = { /* Submit login */ },
                    modifier = Modifier.minimumInteractiveComponentSize()
                ) {
                    Text("Sign In")
                }
            }
        }

        // Verify the node exists with correct text and semantics
        composeTestRule
            .onNodeWithText("Sign In")
            .assertExists()
            .assertHasClickAction()
            .assertHeightIsAtLeast(48.dp)
            .assertWidthIsAtLeast(48.dp)
    }

    @Test
    fun testIconButton_containsContentDescription() {
        composeTestRule.setContent {
            MaterialTheme {
                IconButton(onClick = {}) {
                    Icon(
                        imageVector = Icons.Default.Favorite,
                        contentDescription = "Add to favorites"
                    )
                }
            }
        }

        // Verify content description is accurately exposed in semantics tree
        composeTestRule
            .onNode(hasContentDescription("Add to favorites"))
            .assertExists()
            .assertHasClickAction()
    }
}
</code></pre>
<h3 id="heading-step-by-step-guide-to-google-accessibility-scanner">Step-by-Step Guide to Google Accessibility Scanner</h3>
<p><strong>Google Accessibility Scanner</strong> is an Android diagnostic tool that inspects your app's rendered UI and flags accessibility violations.</p>
<h4 id="heading-how-to-install-and-set-up-accessibility-scanner">How to Install and Set Up Accessibility Scanner</h4>
<ol>
<li><p><strong>Install from Google Play:</strong> Open the Google Play Store on your test device and install <strong>Accessibility Scanner</strong> (published by Google LLC).</p>
</li>
<li><p>Enable in Accessibility Settings:</p>
<ul>
<li><p>Go to Settings then Accessibility and then Accessibility Scanner.</p>
</li>
<li><p>Toggle the switch to <strong>On</strong> and grant the required screen-reading permissions.</p>
</li>
</ul>
</li>
<li><p><strong>Locate the Floating Button:</strong> A blue floating action button with a checkmark icon <code>(✓)</code> will appear overlaid on your screen.</p>
</li>
</ol>
<h4 id="heading-running-a-scan-and-interpreting-results">Running a Scan and Interpreting Results</h4>
<p>Open your Android application and navigate to the screen you want to audit. Tap the floating blue <strong>Scanner button</strong>.</p>
<p>Then tap the <strong>Snapshot</strong> (camera) icon to analyze the static screen, or tap <strong>Record</strong> to audit a multi-step user flow.</p>
<p>The Scanner outlines each flagged UI component with an <strong>orange rectangle</strong>. Typical findings include interactive touch targets smaller than 48dp, missing labels on buttons and images, insufficient text or image contrast, and duplicate or redundant descriptions.</p>
<p>Tap any highlighted result to view a detailed breakdown explaining the problem, a link to the relevant accessibility guidance, and suggested remediation steps. Keep in mind that Scanner is a diagnostic aid. A clean scan doesn't guarantee that your app is fully accessible.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68ad0d824bbb144f1edc8183/7b7f782d-25f4-4b7a-bfc5-177648b34007.gif" alt="7b7f782d-25f4-4b7a-bfc5-177648b34007" style="display: block;" width="480" height="1040" loading="lazy">

<p><em>Figure: Google Accessibility Scanner auditing the AccessibilityDemo app, flagging an undersized 24dp touch target and displaying remediation guidance.</em></p>
<h3 id="heading-how-to-inspect-semantics-with-android-studio-layout-inspector">How to Inspect Semantics with Android Studio Layout Inspector</h3>
<p>Android Studio's <strong>Layout Inspector</strong> allows you to inspect your running Compose hierarchy in real time and view the exact Semantics Tree exposed to the operating system.</p>
<h4 id="heading-how-to-inspect-semantics-step-by-step">How to Inspect Semantics Step-by-Step</h4>
<ol>
<li><p>Run your Compose application on an emulator or physical device connected via USB debugging.</p>
</li>
<li><p>In Android Studio, open <strong>View</strong> then <strong>Tool Windows</strong> and then <strong>Layout Inspector</strong>.</p>
</li>
<li><p>In the process selector dropdown, select your application's process (such as <code>com.example.accessibilitydemo</code>).</p>
</li>
<li><p>In the Layout Inspector component tree panel on the left, navigate to the composable you want to inspect (such as the <code>Card</code> under Semantic Merging).</p>
</li>
<li><p>Look at the <strong>Attributes</strong> panel on the right. You'll see dedicated sections for:</p>
<ul>
<li><p><strong>Merged Semantics:</strong> Displays the collapsed semantic metadata exposed to accessibility services when <code>mergeDescendants = true</code> is used (including merged <code>ContentDescription</code>, <code>Text</code> lists, <code>OnClick</code> actions, and container flags).</p>
</li>
<li><p><strong>Declared Semantics:</strong> Shows the explicit semantic properties directly attached to that specific composable node.</p>
</li>
</ul>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/68ad0d824bbb144f1edc8183/c9d918df-794b-41ff-9771-3ec4b772a5f8.png" alt="c9d918df-794b-41ff-9771-3ec4b772a5f8" style="display: block;" width="1024" height="534" loading="lazy">

<p><em>Figure: Android Studio Layout Inspector inspecting the AccessibilityDemo app, displaying the Component Tree on the left, visual wireframes in the center, and the Merged Semantics attributes panel on the right.</em></p>
<p>Using Layout Inspector provides visual confirmation that:</p>
<ul>
<li><p><code>semantics(mergeDescendants = true)</code> is properly grouping disparate children into a single node.</p>
</li>
<li><p>Decorative icons are successfully excluded from the accessibility tree.</p>
</li>
<li><p>Click actions (<code>OnClick: AccessibilityAction</code>) and custom actions are correctly wired to the composable.</p>
</li>
</ul>
<h2 id="heading-real-world-accessible-ui-patterns">Real-World Accessible UI Patterns</h2>
<p>Here are complete, production-ready implementations of common UI patterns:</p>
<h3 id="heading-fully-accessible-login-form">Fully Accessible Login Form</h3>
<p>Forms are critical entry points. This implementation combines heading semantics, email input validation, dynamic error announcements, and proper touch target sizing:</p>
<pre><code class="language-kotlin">@Composable
fun AccessibleLoginForm(
    onLoginSubmitted: (String, String) -&gt; Unit
) {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    var isSubmitted by remember { mutableStateOf(false) }

    val isEmailInvalid = isSubmitted &amp;&amp; !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()
    val isPasswordInvalid = isSubmitted &amp;&amp; password.length &lt; 8

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp)
    ) {
        // 1. Designated Screen Title Heading
        Text(
            text = "Welcome Back",
            style = MaterialTheme.typography.headlineLarge,
            modifier = Modifier.semantics { heading() }
        )

        Text(
            text = "Sign in to access your account",
            style = MaterialTheme.typography.bodyMedium,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )

        Spacer(modifier = Modifier.height(24.dp))

        // 2. Accessible Email Input
        OutlinedTextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Email Address") },
            isError = isEmailInvalid,
            singleLine = true,
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Email,
                imeAction = ImeAction.Next
            ),
            supportingText = {
                if (isEmailInvalid) {
                    Text(
                        text = "Please enter a valid email address",
                        color = MaterialTheme.colorScheme.error
                    )
                }
            },
            modifier = Modifier
                .fillMaxWidth()
                .semantics {
                    if (isEmailInvalid) {
                        error("Please enter a valid email address")
                    }
                }
        )

        Spacer(modifier = Modifier.height(16.dp))

        // 3. Accessible Password Input
        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("Password") },
            isError = isPasswordInvalid,
            singleLine = true,
            visualTransformation = PasswordVisualTransformation(),
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Password,
                imeAction = ImeAction.Done
            ),
            supportingText = {
                if (isPasswordInvalid) {
                    Text(
                        text = "Password must be at least 8 characters long",
                        color = MaterialTheme.colorScheme.error
                    )
                }
            },
            modifier = Modifier
                .fillMaxWidth()
                .semantics {
                    if (isPasswordInvalid) {
                        error("Password must be at least 8 characters long")
                    }
                }
        )

        Spacer(modifier = Modifier.height(24.dp))

        // 4. Accessible Submit Button
        Button(
            onClick = {
                isSubmitted = true
                if (!isEmailInvalid &amp;&amp; !isPasswordInvalid) {
                    onLoginSubmitted(email, password)
                }
            },
            modifier = Modifier
                .fillMaxWidth()
                .minimumInteractiveComponentSize()
                .semantics {
                    contentDescription = "Sign in to your account"
                }
        ) {
            Text("Sign In")
        }
    }
}
</code></pre>
<p>Why this pattern works:</p>
<ol>
<li><p><strong>Screen Heading:</strong> TalkBack users can instantly jump to "Welcome Back" when entering the screen.</p>
</li>
<li><p><strong>Error Semantics:</strong> If validation fails, <code>error("...")</code> ensures TalkBack immediately speaks the validation requirement when the text field receives focus.</p>
</li>
<li><p><strong>Keyboard Routing:</strong> <code>ImeAction.Next</code> and <code>ImeAction.Done</code> guide keyboard and switch users smoothly between input fields.</p>
</li>
</ol>
<h3 id="heading-e-commerce-product-card-with-custom-actions">E-Commerce Product Card with Custom Actions</h3>
<p>This pattern demonstrates how to combine <code>mergeDescendants = true</code> with <code>customActions</code> to create a concise, power-user-friendly card:</p>
<pre><code class="language-kotlin">data class Product(
    val id: String,
    val title: String,
    val priceFormatted: String,
    val rating: Float,
    val imageRes: Int
)

@Composable
fun AccessibleProductCard(
    product: Product,
    onCardClick: () -&gt; Unit,
    onToggleFavorite: () -&gt; Unit,
    onAddToCart: () -&gt; Unit
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .clickable(onClickLabel = "View product details") { onCardClick() }
            .semantics(mergeDescendants = true) {
                // Group all descriptive properties into one coherent announcement
                contentDescription = "${product.title}, Price: ${product.priceFormatted}, Rated ${product.rating} out of 5 stars"
                
                // Expose secondary operations as custom accessibility actions
                customActions = listOf(
                    CustomAccessibilityAction("Add to shopping cart") {
                        onAddToCart()
                        true
                    },
                    CustomAccessibilityAction("Save to favorites") {
                        onToggleFavorite()
                        true
                    }
                )
            }
    ) {
        Row(
            modifier = Modifier.padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Image(
                painter = painterResource(product.imageRes),
                contentDescription = null, // Decorative: covered by merged description
                modifier = Modifier
                    .size(80.dp)
                    .clip(RoundedCornerShape(8.dp))
            )

            Spacer(modifier = Modifier.width(16.dp))

            Column(modifier = Modifier.weight(1f)) {
                Text(
                    text = product.title,
                    style = MaterialTheme.typography.titleMedium
                )
                Text(
                    text = product.priceFormatted,
                    style = MaterialTheme.typography.bodyLarge,
                    fontWeight = FontWeight.Bold
                )
                Text(
                    text = "★ ${product.rating}",
                    style = MaterialTheme.typography.bodySmall,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )
            }
        }
    }
}
</code></pre>
<p>Why this pattern works:</p>
<ol>
<li><p><strong>Single Semantic Stop:</strong> Instead of swiping four times through separate image and text views, TalkBack reads the entire card in one unified statement.</p>
</li>
<li><p><strong>Action Shortcuts:</strong> Users can add items to their cart or favorites directly from the TalkBack actions menu without navigating into the details page.</p>
</li>
</ol>
<h3 id="heading-tab-navigation-with-selection-state">Tab Navigation with Selection State</h3>
<p>This pattern demonstrates accessible tab bar navigation using <code>stateDescription</code> and custom tab announcements:</p>
<pre><code class="language-kotlin">@Composable
fun AccessibleTabNavigation(
    tabs: List&lt;String&gt;,
    selectedTabIndex: Int,
    onTabSelected: (Int) -&gt; Unit
) {
    TabRow(
        selectedTabIndex = selectedTabIndex,
        modifier = Modifier.semantics {
            contentDescription = "Navigation tabs, ${tabs[selectedTabIndex]} selected"
        }
    ) {
        tabs.forEachIndexed { index, title -&gt;
            val isSelected = selectedTabIndex == index
            Tab(
                selected = isSelected,
                onClick = { onTabSelected(index) },
                modifier = Modifier.semantics {
                    role = Role.Tab
                    stateDescription = if (isSelected) "Selected" else "Not selected"
                    contentDescription = "$title tab"
                }
            ) {
                Text(
                    text = title,
                    modifier = Modifier.padding(vertical = 16.dp)
                )
            }
        }
    }
}
</code></pre>
<p>Why this pattern works:</p>
<ol>
<li><p><strong>Explicit Role:</strong> Marking each item with <code>Role.Tab</code> informs accessibility services that this is a selectable tab container.</p>
</li>
<li><p><strong>Selection Feedback:</strong> The <code>stateDescription</code> tells the user whether a tab is currently active before they tap it.</p>
</li>
</ol>
<h3 id="heading-confirmation-dialog-with-action-descriptions">Confirmation Dialog with Action Descriptions</h3>
<p>This pattern shows how to structure an accessible confirmation dialog:</p>
<pre><code class="language-kotlin">@Composable
fun AccessibleDeleteConfirmationDialog(
    itemName: String,
    onDismissRequest: () -&gt; Unit,
    onConfirmDelete: () -&gt; Unit
) {
    AlertDialog(
        onDismissRequest = onDismissRequest,
        title = {
            Text(
                text = "Delete Item?",
                style = MaterialTheme.typography.headlineSmall,
                modifier = Modifier.semantics { heading() }
            )
        },
        text = {
            Text("Are you sure you want to permanently delete \"$itemName\"? This action cannot be undone.")
        },
        confirmButton = {
            TextButton(
                onClick = onConfirmDelete,
                modifier = Modifier.semantics {
                    contentDescription = "Confirm deletion of $itemName"
                }
            ) {
                Text("Delete", color = MaterialTheme.colorScheme.error)
            }
        },
        dismissButton = {
            TextButton(
                onClick = onDismissRequest,
                modifier = Modifier.semantics {
                    contentDescription = "Cancel deletion and close dialog"
                }
            ) {
                Text("Cancel")
            }
        },
        modifier = Modifier.semantics {
            contentDescription = "Delete item confirmation dialog"
        }
    )
}
</code></pre>
<p>Why this pattern works:</p>
<ol>
<li><p><strong>Clear Focus:</strong> When the dialog appears, TalkBack automatically traps focus within the dialog bounds so users can't accidentally click background elements.</p>
</li>
<li><p><strong>Context-Rich Buttons:</strong> Button descriptions explain the exact consequence of clicking ("Confirm deletion of Shopping List" rather than just "Delete").</p>
</li>
</ol>
<h2 id="heading-accessibility-audit-checklist">Accessibility Audit Checklist</h2>
<p>Before releasing your app to production or submitting it to app stores, run through this accessibility audit checklist:</p>
<h3 id="heading-visual-and-typography">Visual and Typography</h3>
<ul>
<li><p><strong>Scalable Text:</strong> Define all font sizes in <code>sp</code> (never fixed <code>dp</code>) so text scales cleanly up to 200%.</p>
</li>
<li><p><strong>Color Contrast:</strong> Maintain at least a <strong>4.5:1</strong> contrast ratio for normal text and <strong>3.0:1</strong> for large text and essential UI components against their backgrounds.</p>
</li>
<li><p><strong>Color Independence:</strong> Never convey information through color alone. Always pair color cues with text labels or distinct icons.</p>
</li>
</ul>
<h3 id="heading-interactive-elements-and-touch-targets">Interactive Elements and Touch Targets</h3>
<ul>
<li><p><strong>Touch Target Sizing:</strong> Ensure every clickable or interactive component has a minimum hit target of <strong>48dp × 48dp</strong> using <code>Modifier.minimumInteractiveComponentSize()</code>.</p>
</li>
<li><p><strong>Actionable Descriptions:</strong> Provide descriptive, action-oriented <code>contentDescription</code> strings on all functional icon buttons and interactive images.</p>
</li>
<li><p><strong>Decorative Elements:</strong> Set <code>contentDescription = null</code> on purely decorative icons and illustrations to keep TalkBack feedback concise.</p>
</li>
<li><p><strong>Click Context:</strong> Provide custom <code>onClickLabel</code> parameters on cards, list items, and custom buttons to clarify the outcome before activation.</p>
</li>
</ul>
<h3 id="heading-screen-structure-and-navigation">Screen Structure and Navigation</h3>
<ul>
<li><p><strong>Accessibility Headings:</strong> Mark major section titles and screen headers with <code>Modifier.semantics { heading() }</code> for rapid heading navigation.</p>
</li>
<li><p><strong>Semantic Merging:</strong> Group related visual sub-elements (such as ratings or multi-text card headers) using <code>Modifier.semantics(mergeDescendants = true)</code>.</p>
</li>
<li><p><strong>Live Announcements:</strong> Designate asynchronous UI updates with <code>liveRegion = LiveRegionMode.Polite</code> so TalkBack announces background changes.</p>
</li>
<li><p><strong>Form Optimization:</strong> Specify appropriate <code>keyboardType</code> and <code>imeAction</code> configurations on all text inputs.</p>
</li>
<li><p><strong>Validation Feedback:</strong> Declare active input error states using <code>Modifier.semantics { error("...") }</code>.</p>
</li>
</ul>
<h3 id="heading-testing-and-verification">Testing and Verification</h3>
<ul>
<li><p><strong>Automated Unit Tests:</strong> Add Compose accessibility assertions in continuous integration to catch touch target and missing label regressions.</p>
</li>
<li><p><strong>On-Device Diagnostic Audit:</strong> Run Google Accessibility Scanner across all key app screens to catch contrast or sizing defects.</p>
</li>
<li><p><strong>Semantics Tree Inspection:</strong> Use Android Studio Layout Inspector to verify merged semantics and declared accessibility actions.</p>
</li>
<li><p><strong>Manual Screen Reader Walkthrough:</strong> Complete end-to-end user journeys (login, checkout, navigation) with TalkBack enabled.</p>
</li>
</ul>
<h2 id="heading-conclusion-and-next-steps">Conclusion and Next Steps</h2>
<p>Building accessible applications in Jetpack Compose doesn't mean just retrofitting code at the end of a project. You need to understand Compose's dual-tree architecture and design your user interface so that both visual pixels and semantic metadata accurately represent your application's intent.</p>
<p>By applying semantic properties, ensuring 48dp touch targets, maintaining WCAG contrast ratios, and verifying your work with TalkBack and Google Accessibility Scanner, you ensure that your Android applications are welcoming, compliant, and intuitive for all users worldwide.</p>
<h2 id="heading-essential-resources">Essential Resources</h2>
<p>To deepen your understanding of Android accessibility, consult these essential references:</p>
<ul>
<li><p><a href="https://developer.android.com/develop/ui/compose/accessibility">Android Developers Official Guide: Accessibility in Jetpack Compose</a></p>
</li>
<li><p><a href="https://developer.android.com/develop/ui/compose/accessibility/semantics">Android Developers Official Guide: Semantics in Compose</a></p>
</li>
<li><p><a href="https://www.w3.org/WAI/WCAG21/quickref/">W3C Web Content Accessibility Guidelines (WCAG) 2.1 Quick Reference</a></p>
</li>
<li><p><a href="https://m3.material.io/foundations/accessible-design/overview">Google Material Design 3: Accessible Design Foundations</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Tooltips in Jetpack Compose ]]>
                </title>
                <description>
                    <![CDATA[ When I wrote my last article about Jetpack Compose, I stated there that Jetpack Compose is missing some (in my opinion) basic components, and one of them is the tooltip. At the time, there was no built-in composable to display tooltips and there were... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-tooltips-in-jetpack-compose/</link>
                <guid isPermaLink="false">66fd516514798d90f2228542</guid>
                
                    <category>
                        <![CDATA[ Android ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Jetpack Compose ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tooltip ]]>
                    </category>
                
                    <category>
                        <![CDATA[ UI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tomer ]]>
                </dc:creator>
                <pubDate>Wed, 02 Oct 2024 13:57:57 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1727813989960/b0a7ab29-d87c-4d87-9847-70b7e1c341b1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When I wrote my <a target="_blank" href="https://medium.com/better-programming/is-jetpack-compose-ready-for-you-eae6c93ad3f8">last article about Jetpack Compose</a>, I stated there that Jetpack Compose is missing some (in my opinion) basic components, and one of them is the tooltip.</p>
<p>At the time, there was no built-in composable to display tooltips and there were several alternative solutions circling online. The problem with those solutions was that once Jetpack Compose released newer versions, those solutions might break. So it wasn’t ideal and the community was left hoping that sometime in the future, support would be added for tooltips.</p>
<p>I’m glad to say that since <a target="_blank" href="https://developer.android.com/jetpack/androidx/releases/compose-material3#1.1.0">version 1.1.0 of Compose Material 3</a>, we now have built in tooltip support. 👏</p>
<p>While this in itself is great, more than a year has passed since that version was released. And with subsequent versions, the API related to tooltips changed drastically as well.</p>
<p>If you go over the changelog, you will see how the public and internal APIs have changed. So bear in mind, that when you read this article, things may have continued to change as everything related to Tooltips is still marked by the annotation <strong>ExperimentalMaterial3Api::class</strong>.</p>
<p>❗️ The version of material 3 used for this article is 1.2.1, which was released on March 6th, 2024</p>
<h2 id="heading-tooltip-types">Tooltip Types</h2>
<p>We now have support for two different types of tooltips:</p>
<ol>
<li><p>Plain tooltip</p>
</li>
<li><p>Rich media tooltip</p>
</li>
</ol>
<h3 id="heading-plain-tooltip">Plain Tooltip</h3>
<p>You can use the first kind to provide information about an icon button that wouldn’t be clear otherwise. For example, you can use a plain tooltip to indicate to a user what the icon button represents.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727602449314/94cf84bf-dec0-462c-a8a0-6f878e0d5db3.gif" alt="Basic tooltip example" class="image--center mx-auto" width="213" height="450" loading="lazy"></p>
<p>To add a tooltip to your application, you use the <strong>TooltipBox</strong> composable. This composable takes several arguments:</p>
<pre><code class="lang-kotlin"><span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">TooltipBox</span><span class="hljs-params">(
    positionProvider: <span class="hljs-type">PopupPositionProvider</span>,
    tooltip: @<span class="hljs-type">Composable</span> <span class="hljs-type">TooltipScope</span>.() -&gt; <span class="hljs-type">Unit</span>,
    state: <span class="hljs-type">TooltipState</span>,
    modifier: <span class="hljs-type">Modifier</span> = Modifier,
    focusable: <span class="hljs-type">Boolean</span> = <span class="hljs-literal">true</span>,
    enableUserInput: <span class="hljs-type">Boolean</span> = <span class="hljs-literal">true</span>,
    content: @<span class="hljs-type">Composable</span> () -&gt; <span class="hljs-type">Unit</span>,
)</span></span>
</code></pre>
<p>Some of these should be familiar to you if you have used Composables before. I’ll highlight the ones that have a specific use case here:</p>
<ul>
<li><p>positionProvider - Of <strong>PopupPositionProvider</strong> type, and is used to calculate the position of the tooltip.</p>
</li>
<li><p>tooltip - This is where you can design the UI of how the tooltip will look like.</p>
</li>
<li><p>state - This holds the state that is associated with a specific Tooltip instance. It exposes methods like showing/dismissing the tooltip and when instantiating an instance of one, you can declare if the tooltip should be persistent or not (meaning if it should keep displaying on the screen until a user performs a click action outside the tooltip).</p>
</li>
<li><p>content - This is the UI that the tooltip will display above/below.</p>
</li>
</ul>
<p>Here is an example of instantiating a <strong>BasicTooltipBox</strong> with all the relevant arguments filled in:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)</span>
<span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">BasicTooltip</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">val</span> tooltipPosition = TooltipDefaults.rememberPlainTooltipPositionProvider()
    <span class="hljs-keyword">val</span> tooltipState = rememberBasicTooltipState(isPersistent = <span class="hljs-literal">false</span>)

    BasicTooltipBox(positionProvider = tooltipPosition,
        tooltip =  { Text(<span class="hljs-string">"Hello World"</span>) } ,
        state = tooltipState) {
        IconButton(onClick = { }) {
            Icon(imageVector = Icons.Filled.Favorite, 
                 contentDescription = <span class="hljs-string">"Your icon's description"</span>)
        }
    }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727602558759/e00e0bed-6a95-489e-af5c-a7d9dcc33fe6.gif" alt="A basic tooltip" class="image--center mx-auto" width="213" height="450" loading="lazy"></p>
<p>Jetpack Compose has a built in class called TooltipDefaults. You can use this class to help you instantiate arguments that make up a TooltipBox. For instance, you could use <strong>TooltipDefaults.rememberPlainTooltipPositionProvider</strong> to correctly position the tooltip in relation to the anchor element.</p>
<h3 id="heading-rich-tooltip">Rich Tooltip</h3>
<p>A rich media tooltip takes more space than a plain tooltip and can be used to provide more context about the functionality of an icon button. When the tooltip is shown, you can add buttons and links to it to provide further explanation or definitions.</p>
<p>It is instantiated in a similar way as a plain tooltip, inside of a TooltipBox, but you use the RichTooltip composable.</p>
<pre><code class="lang-kotlin">TooltipBox(positionProvider = tooltipPosition,
        tooltip = {
                  RichTooltip(
                      title = { Text(<span class="hljs-string">"RichTooltip"</span>) },
                      caretSize = caretSize,
                      action = {
                          TextButton(onClick = {
                              scope.launch {
                                  tooltipState.dismiss()
                                  tooltipState.onDispose()
                              }
                          }) {
                              Text(<span class="hljs-string">"Dismiss"</span>)
                          }
                      }
                  ) {
                        Text(<span class="hljs-string">"This is where a description would go."</span>)
                  }
        },
        state = tooltipState) {
        IconButton(onClick = {
            <span class="hljs-comment">/* Icon button's click event */</span>
        }) {
            Icon(imageVector = tooltipIcon,
                contentDescription = <span class="hljs-string">"Your icon's description"</span>,
                tint = iconColor)
        }
    }
</code></pre>
<p>A few things to notice about a Rich tooltip:</p>
<ol>
<li><p>A Rich tooltip has support for a caret.</p>
</li>
<li><p>You can add an action (that is, a button) to the tooltip to give users an option to find out more information.</p>
</li>
<li><p>You can add logic to dismiss the tooltip.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727602624042/40160d88-4e8a-4487-835d-1b74a9dd7c72.png" alt="Rich tooltip without a caret" class="image--center mx-auto" width="375" height="792" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727602651265/f3e6f7fe-c4e1-4f98-972d-b20a273900b4.png" alt="Rich tooltip with a caret" class="image--center mx-auto" width="375" height="792" loading="lazy"></p>
<h3 id="heading-edge-cases">Edge Cases</h3>
<p>When you choose to mark your <strong>tooltip state as persistent</strong>, it means that once the user interacts with the UI that shows your tooltip, it will stay visible until the user presses anywhere else on the screen.</p>
<p>If you looked at the example of a Rich tooltip from above, you might have noticed that we have added a button to dismiss the tooltip once it’s clicked.</p>
<p>There is a problem that happens once a user presses that button. Since the dismiss action is performed on the tooltip, if a user wants to perform another long press on the UI item that invokes this tooltip, the tooltip won’t be shown again. This means that the state of the tooltip is persistent on it being dismissed. So, how do we go about and resolve this?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727602690256/a31b56bb-77c4-4444-bab6-7ffcca3f5207.gif" alt="Second long press does not trigger the tooltip" class="image--center mx-auto" width="213" height="450" loading="lazy"></p>
<p>In order to “reset” the state of the tooltip, we have to call the <strong>onDispose</strong> method that is exposed through the tooltip state. Once we do that, the tooltip state is reset and the tooltip will be shown again when the user performs a long press on the UI item.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@OptIn(ExperimentalMaterial3Api::class)</span>
<span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">RichTooltip</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">val</span> tooltipPosition = TooltipDefaults.rememberRichTooltipPositionProvider()
    <span class="hljs-keyword">val</span> tooltipState = rememberTooltipState(isPersistent = <span class="hljs-literal">true</span>)
    <span class="hljs-keyword">val</span> scope = rememberCoroutineScope()

    TooltipBox(positionProvider = tooltipPosition,
        tooltip = {
                  RichTooltip(
                      title = { Text(<span class="hljs-string">"RichTooltip"</span>) },
                      caretSize = TooltipDefaults.caretSize,
                      action = {
                          TextButton(onClick = {
                              scope.launch {
                                  tooltipState.dismiss()
                                  tooltipState.onDispose()  <span class="hljs-comment">/// &lt;---- HERE</span>
                              }
                          }) {
                              Text(<span class="hljs-string">"Dismiss"</span>)
                          }
                      }
                  ) {

                  }
        },
        state = tooltipState) {
        IconButton(onClick = {  }) {
            Icon(imageVector = Icons.Filled.Call, contentDescription = <span class="hljs-string">"Your icon's description"</span>)
        }
    }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727602730404/60f31668-ea66-4127-b6fc-41f3aca952ae.gif" alt="onDispose solves the issue" class="image--center mx-auto" width="213" height="450" loading="lazy"></p>
<p>Another scenario where the tooltip state does not reset is if instead of calling ourselves for the dismiss method per a user’s action, the user clicks outside of the tooltip, causing it to be dismissed. This calls the dismiss method behind the scenes and the tooltip state is set to dismissed. Long pressing on the UI element to see our tooltip again will result in nothing.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727602758707/60387e08-72e6-45d4-bd47-ffb2708e0efe.gif" alt="The tooltip does not show again" class="image--center mx-auto" width="213" height="450" loading="lazy"></p>
<p>Our logic that calls the tooltip’s onDispose method does not get triggered, so how can we reset the tooltip’s state?</p>
<p>Currently, I haven’t been able to figure this out. It might be related to the tooltip’s <a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/foundation/MutatorMutex">MutatorMutex</a>. Maybe with upcoming releases, there will be an API for this. I did notice that if other tooltips are present on the screen and they are pressed, this resets the previously clicked upon tooltip.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727602790121/25a81994-a508-4c71-8424-c45370a7999d.gif" alt="25a81994-a508-4c71-8424-c45370a7999d" class="image--center mx-auto" width="213" height="450" loading="lazy"></p>
<p>If you would like to see the code featured here, you can go to <a target="_blank" href="https://github.com/TomerPacific/MediumArticles/tree/master/TooltipExample">this GitHub repository</a></p>
<p>If you would like to see tooltips in an application, you can check it out <a target="_blank" href="https://play.google.com/store/apps/details?id=com.tomerpacific.laundry">here</a>.</p>
<h4 id="heading-references">References</h4>
<ul>
<li><p><a target="_blank" href="https://m3.material.io/components/tooltips/overview">Material3 Tooltip Overview</a></p>
</li>
<li><p><a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/material3/TooltipDefaults">Tooltip Defaults</a></p>
</li>
<li><p><a target="_blank" href="https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tooltip.kt">Tooltip Source Code</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Request Location Permissions in Jetpack Compose ]]>
                </title>
                <description>
                    <![CDATA[ Getting a user’s location can be a bit of hassle. Over the years, the permissions required to do this and the logic associated with it have changed quite drastically.  If your application depends on getting the user’s location, you want to ensure tha... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/requesting-location-permissions-in-jetpack-compose/</link>
                <guid isPermaLink="false">66ba5034158e6c6a8cb8c7a3</guid>
                
                    <category>
                        <![CDATA[ Jetpack Compose ]]>
                    </category>
                
                    <category>
                        <![CDATA[ user experience ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tomer ]]>
                </dc:creator>
                <pubDate>Thu, 05 Oct 2023 19:28:42 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/10/andrew-stutesman-l68Z6eF2peA-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Getting a user’s location can be a bit of hassle. Over the years, the permissions required to do this and the logic associated with it have changed quite drastically. </p>
<p>If your application depends on getting the user’s location, you want to ensure that the user has a good experience when the application requests this info. So it's crucial to handle all the edge cases and allow the user to select the option the they're most comfortable with.</p>
<p>With Jetpack Compose, the logic associated with getting the user’s location has changed a bit, and it’s important to know the in’s and out’s of how it can be done.</p>
<p>As with most things, there is a library you can use to handle getting permissions. It’s from Accompanist (read Google) and you can find it <a target="_blank" href="https://google.github.io/accompanist/permissions/#:~:text=A%20library%20which%20provides%20Android%20runtime%20permissions%20support%20for%20Jetpack%20Compose.&amp;text=The%20permission%20APIs%20are%20currently,marked%20with%20the%20%40ExperimentalPermissionsApi%20annotation.">here</a>. <strong>But you are here to learn how to do things yourself,</strong> right? So read on. 🕵️‍♀️</p>
<p>Before we get into the code and the logic, it is important to understand that requesting a permission from a user is a path that can have many decision points. As such, it is best described by having states that represent the current status of the permission (approved/rejected/denied) and the current status of the operating system. </p>
<p>The diagram below illustrates this flow:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/location.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Later on in this article, you will see how we will represent these states in variables in our code.</p>
<h2 id="heading-save-our-souls-sos">Save Our Souls (S.O.S)</h2>
<p>Before we get to the logic of asking for a permission, we’ll take care of the boilerplate around it. As always, add the necessary permissions to your AndroidManifest.xml file:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.ACCESS_COARSE_LOCATION"</span> /&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">uses-permission</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.permission.ACCESS_FINE_LOCATION"</span> /&gt;</span>
</code></pre>
<p>Then, we’ll create a composable screen where we ask for the necessary location permissions. The first step in this screen is to check whether the user has previously granted the required permissions. You can do this with something that is not new to Jetpack Compose – using the checkSelfPermission method:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> locationPermissionsAlreadyGranted = ContextCompat.checkSelfPermission(
            <span class="hljs-keyword">this</span>,
            Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
</code></pre>
<p>If the permission is not granted, we have to request it. We will use the <a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/activity/compose/package-summary#rememberlauncherforactivityresult">rememberLauncherForActivityResult</a> object to do this. This allows us in Jetpack Compose to get a result from an Activity.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> locationPermissions = arrayOf(
    Manifest.permission.ACCESS_FINE_LOCATION,
    Manifest.permission.ACCESS_COARSE_LOCATION)

<span class="hljs-keyword">val</span> locationPermissionLauncher = rememberLauncherForActivityResult(
                contract = ActivityResultContracts.RequestMultiplePermissions(),
                onResult = { permissions -&gt;
                    <span class="hljs-keyword">val</span> permissionsGranted = permissions.values.reduce { acc, isPermissionGranted -&gt;
                        acc &amp;&amp; isPermissionGranted
                    }

                    <span class="hljs-keyword">if</span> (!permissionsGranted) {
                       <span class="hljs-comment">//Logic when the permissions were not granted by the user</span>
                    }
                })
</code></pre>
<p>The arguments that we need to pass to <code>rememberLauncherForActivityResult</code> are:</p>
<ol>
<li>An <a target="_blank" href="https://developer.android.com/reference/androidx/activity/result/contract/ActivityResultContract">ActivityResultContract</a> – specifies the input of the activity and the output</li>
<li>onResult – a callback when the result is received</li>
</ol>
<p>In the code snippet above, we are using a multiple permissions contract, since we are requesting several location permissions. There is also a contract for requesting just one permission, <strong>ActivityResultContracts.RequestPermission().</strong></p>
<p>This piece of code doesn’t not run immediately as we need to request the permissions. To do that we use the launch method to start the activity:</p>
<pre><code class="lang-kotlin">locationPermissionLauncher.launch(locationPermissions)
</code></pre>
<p>In the case the user gave all or several of the required permissions, we can continue the logic of the application. </p>
<p>But, if the user did not approve any of the permissions, we need to find a way to make the user understand why these permissions are necessary.</p>
<h2 id="heading-explaining-the-rationale">Explaining the Rationale</h2>
<p>In case the user declined the permission, but did not select the “Deny And Don’t Ask Me Again” option, we have a way of giving the user a brief explanation on why they should grant the required permission(s). </p>
<p>To figure out if we should present this rationale, we use the <a target="_blank" href="https://developer.android.com/reference/androidx/core/app/ActivityCompat#shouldShowRequestPermissionRationale(android.app.Activity,java.lang.String)">shouldShowRequestPermissionRationale</a> from the Activity class:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> shouldShowPermissionRationale: <span class="hljs-built_in">Boolean</span> = shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_COARSE_LOCATION)
</code></pre>
<p>Once we know that we can display this explanation, there are two ways to go about it:</p>
<ol>
<li>We can present it to the user with an AlertDialog</li>
<li>We can use the Snackbar</li>
</ol>
<p>Presenting an alert dialog is pretty straightforward. All we have to do is make sure we describe clearly to the user why it is required to approve this permission:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ShowLocationPermissionRationale</span><span class="hljs-params">()</span></span> {
        AlertDialog(
            onDismissRequest = {
               <span class="hljs-comment">//Logic when dismiss happens</span>
            },
        title = {
            Text(<span class="hljs-string">"Permission Required"</span>)
                },
        text = {
            Text(<span class="hljs-string">"You need to approve this permission in order to..."</span>)
        },
        confirmButton = {
            TextButton(onClick = {
              <span class="hljs-comment">//Logic when user confirms to accept permissions</span>
            }) {
                Text(<span class="hljs-string">"Confirm"</span>)
            }
        },
        dismissButton = {
            TextButton(onClick = {
              <span class="hljs-comment">//Logic when user denies to accept permissions</span>
            }) {
                Text(<span class="hljs-string">"Deny"</span>)
            }
        })
    }
</code></pre>
<p>If we want to present the Snackbar, we need to be aware that we have to use a Scaffold container since that is the only container that supports showing a Snackbar. If we don’t use one, a Snackbar won’t appear. </p>
<p>Below is a snippet that shows you how to do this:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> scope = rememberCoroutineScope()
<span class="hljs-keyword">val</span> snackbarHostState = remember { SnackbarHostState() }

Scaffold(snackbarHost = {
        SnackbarHost(hostState = snackbarHostState)
    }) { contentPadding -&gt;
        <span class="hljs-keyword">if</span> (shouldShowPermissionRationale) {
            LaunchedEffect(key1 = shouldShowPermissionRationale, block = {
                scope.launch {
                    <span class="hljs-keyword">val</span> userAction = snackbarHostState.showSnackbar(
                        message =<span class="hljs-string">"Please authorize location permissions"</span>,
                        actionLabel = <span class="hljs-string">"Approve"</span>,
                        duration = SnackbarDuration.Indefinite,
                        withDismissAction = <span class="hljs-literal">true</span>
                    )
                    <span class="hljs-keyword">when</span> (userAction) {
                        SnackbarResult.ActionPerformed -&gt; {
                            <span class="hljs-comment">//User approved to grant the permission</span>
                            <span class="hljs-comment">//Ask for permissions again</span>
                        }
                        SnackbarResult.Dismissed -&gt; {
                            <span class="hljs-comment">//User dismissed snackbar</span>
                        }
                    }
                }
            })
        }
}
</code></pre>
<p>We allowed the Snackbar itself to be dismissible using the withDismissAction attribute and listened in to the action performed by the user.</p>
<h2 id="heading-lifecycle-observer">Lifecycle Observer</h2>
<p>One thing we have glossed over is the fact that we need to make sure our permission request adheres to the composable lifecycle. This means that once a user chooses their preferences regarding the permission request, we need the UI to adapt accordingly. </p>
<p>If you try and put the code above inside the activity’s onCreate method, you will be surprised with the outcome, since the application will crash with the following exception:</p>
<blockquote>
<p><em>java.lang.IllegalStateException: Launcher has not been initialized</em></p>
</blockquote>
<p>This is because Composable functions are supposed to be side effect free. What is a side effect? According to <a target="_blank" href="https://developer.android.com/jetpack/compose/side-effects">Google’s documentation</a> it is:</p>
<blockquote>
<p>… a change to the state of the app that happens outside the scope of a composable function</p>
</blockquote>
<p>So, in our use case, launching an activity for the permissions is the side effect happening here. </p>
<p>To circumvent this scenario, we need to use one of the side effect options. Since we don’t want to ask the user for permissions without remembering what their choices were previously, we can’t use the general <strong>SideEffect</strong>. And <strong>LaunchedEffect</strong> is used for calling suspend methods inside of a Composable, which is not our use case here. </p>
<p>So we are left with <strong>DisposableEffect</strong>. Reading the <a target="_blank" href="https://developer.android.com/jetpack/compose/side-effects#disposableeffect">documentation</a>, we can see that <a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#DisposableEffect(kotlin.Any,kotlin.Function1)">DisposableEffect</a> can be combined with Lifecycle events, which is what we are after.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> lifecycleOwner = LocalLifecycleOwner.current
            DisposableEffect(key1 = lifecycleOwner, effect = {
                <span class="hljs-keyword">val</span> observer = LifecycleEventObserver { _, event -&gt;
                    <span class="hljs-keyword">if</span> (event == Lifecycle.Event.ON_START &amp;&amp; !locationPermissionsAlreadyGranted) {
                        locationPermissionLauncher.launch(locationPermissions)
                       }
                    }
                    lifecycleOwner.lifecycle.addObserver(observer)
                    onDispose {
                        lifecycleOwner.lifecycle.removeObserver(observer)
                    }
                }
            )
</code></pre>
<p>In the code snippet above, we are adding a lifecycle observer that runs only in the case of the onStart lifecycle event. We also combine it with the boolean we have declared at the start of this section, locationPermissionsAlreadyGranted. This is so we won’t show the dialog for asking permissions if they are already granted. </p>
<p>As with all lifecycle observers, we need to remove our observer once the composition ends. We have that logic inside DisposableEffect’s onDispose clause.</p>
<h2 id="heading-location-not-found">Location Not Found</h2>
<p>The last case we need to deal with is when the user chooses the “Deny And Don’t Ask Me Again” option. When this happens, we cannot ask the user to grant the required permissions. </p>
<p>The only way the user can revert their choice is to go to the settings screen of our application and change the permissions there. So we need to direct the user to go there. </p>
<p>To open the settings screen of our application, we need to use an intent with the action of <strong>ACTION_APPLICATION_DETAILS_SETTINGS.</strong></p>
<pre><code class="lang-kotlin">Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts(<span class="hljs-string">"package"</span>, packageName, <span class="hljs-literal">null</span>)).also {
            startActivity(it)
        }
</code></pre>
<p>Taking the logic above, we can add it to our code when we know that the user has chosen to deny the permissions and not be asked again. This happens inside our request for permissions when the user has not granted the permissions and the option to show the rationale is false.</p>
<h2 id="heading-location-confirmed">Location Confirmed</h2>
<p>If we take everything we discussed in this article and put it inside one file, we will get the following code:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainActivity</span> : <span class="hljs-type">ComponentActivity</span></span>() {

    <span class="hljs-meta">@OptIn(ExperimentalMaterial3Api::class)</span>
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCreate</span><span class="hljs-params">(savedInstanceState: <span class="hljs-type">Bundle</span>?)</span></span> {
        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState)

        setContent {

            <span class="hljs-keyword">var</span> locationPermissionsGranted <span class="hljs-keyword">by</span> remember { mutableStateOf(areLocationPermissionsAlreadyGranted()) }
            <span class="hljs-keyword">var</span> shouldShowPermissionRationale <span class="hljs-keyword">by</span> remember {
                mutableStateOf(
                    shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_COARSE_LOCATION)
                )
            }

            <span class="hljs-keyword">var</span> shouldDirectUserToApplicationSettings <span class="hljs-keyword">by</span> remember {
                mutableStateOf(<span class="hljs-literal">false</span>)
            }

            <span class="hljs-keyword">var</span> currentPermissionsStatus <span class="hljs-keyword">by</span> remember {
                mutableStateOf(decideCurrentPermissionStatus(locationPermissionsGranted, shouldShowPermissionRationale))
            }

            <span class="hljs-keyword">val</span> locationPermissions = arrayOf(
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION
            )

            <span class="hljs-keyword">val</span> locationPermissionLauncher = rememberLauncherForActivityResult(
                contract = ActivityResultContracts.RequestMultiplePermissions(),
                onResult = { permissions -&gt;
                    locationPermissionsGranted = permissions.values.reduce { acc, isPermissionGranted -&gt;
                        acc &amp;&amp; isPermissionGranted
                    }

                    <span class="hljs-keyword">if</span> (!locationPermissionsGranted) {
                        shouldShowPermissionRationale =
                            shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_COARSE_LOCATION)
                    }
                    shouldDirectUserToApplicationSettings = !shouldShowPermissionRationale &amp;&amp; !locationPermissionsGranted
                    currentPermissionsStatus = decideCurrentPermissionStatus(locationPermissionsGranted, shouldShowPermissionRationale)
                })

            <span class="hljs-keyword">val</span> lifecycleOwner = LocalLifecycleOwner.current
            DisposableEffect(key1 = lifecycleOwner, effect = {
                <span class="hljs-keyword">val</span> observer = LifecycleEventObserver { _, event -&gt;
                    <span class="hljs-keyword">if</span> (event == Lifecycle.Event.ON_START &amp;&amp;
                        !locationPermissionsGranted &amp;&amp;
                        !shouldShowPermissionRationale) {
                        locationPermissionLauncher.launch(locationPermissions)
                    }
                }
                lifecycleOwner.lifecycle.addObserver(observer)
                onDispose {
                    lifecycleOwner.lifecycle.removeObserver(observer)
                    }
                }
            )

            <span class="hljs-keyword">val</span> scope = rememberCoroutineScope()
            <span class="hljs-keyword">val</span> snackbarHostState = remember { SnackbarHostState() }

            LocationPermissionsTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Scaffold(snackbarHost = {
                        SnackbarHost(hostState = snackbarHostState)
                    }) { contentPadding -&gt;
                        Column(modifier = Modifier.fillMaxSize(),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally){
                            Text(modifier = Modifier
                                .padding(contentPadding)
                                .fillMaxWidth(),
                                text = <span class="hljs-string">"Location Permissions"</span>,
                                textAlign = TextAlign.Center)
                            Spacer(modifier = Modifier.padding(<span class="hljs-number">20</span>.dp))
                            Text(modifier = Modifier
                                .padding(contentPadding)
                                .fillMaxWidth(),
                                text = <span class="hljs-string">"Current Permission Status: <span class="hljs-variable">$currentPermissionsStatus</span>"</span>,
                                textAlign = TextAlign.Center,
                                fontWeight = FontWeight.Bold
                            )
                        }
                        <span class="hljs-keyword">if</span> (shouldShowPermissionRationale) {
                            LaunchedEffect(<span class="hljs-built_in">Unit</span>) {
                                scope.launch {
                                    <span class="hljs-keyword">val</span> userAction = snackbarHostState.showSnackbar(
                                        message =<span class="hljs-string">"Please authorize location permissions"</span>,
                                        actionLabel = <span class="hljs-string">"Approve"</span>,
                                        duration = SnackbarDuration.Indefinite,
                                        withDismissAction = <span class="hljs-literal">true</span>
                                    )
                                    <span class="hljs-keyword">when</span> (userAction) {
                                        SnackbarResult.ActionPerformed -&gt; {
                                            shouldShowPermissionRationale = <span class="hljs-literal">false</span>
                                            locationPermissionLauncher.launch(locationPermissions)
                                        }
                                        SnackbarResult.Dismissed -&gt; {
                                            shouldShowPermissionRationale = <span class="hljs-literal">false</span>
                                        }
                                    }
                                }
                            }
                        }
                        <span class="hljs-keyword">if</span> (shouldDirectUserToApplicationSettings) {
                            openApplicationSettings()
                        }
                    }
                }
            }
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">areLocationPermissionsAlreadyGranted</span><span class="hljs-params">()</span></span>: <span class="hljs-built_in">Boolean</span> {
        <span class="hljs-keyword">return</span> ContextCompat.checkSelfPermission(
            <span class="hljs-keyword">this</span>,
            Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">openApplicationSettings</span><span class="hljs-params">()</span></span> {
        Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts(<span class="hljs-string">"package"</span>, packageName, <span class="hljs-literal">null</span>)).also {
            startActivity(it)
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">decideCurrentPermissionStatus</span><span class="hljs-params">(locationPermissionsGranted: <span class="hljs-type">Boolean</span>,
                                              shouldShowPermissionRationale: <span class="hljs-type">Boolean</span>)</span></span>: String {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> (locationPermissionsGranted) <span class="hljs-string">"Granted"</span>
        <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (shouldShowPermissionRationale) <span class="hljs-string">"Rejected"</span>
        <span class="hljs-keyword">else</span> <span class="hljs-string">"Denied"</span>
    }
}
</code></pre>
<p>And this is how it looks like:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/10/location2.gif" alt="Image" width="600" height="400" loading="lazy"></p>
<p>I have put all the logic in one file just for the purpose of this article. It is by no means the most esthetic and correct approach to handling the logic with requesting permissions. </p>
<p>You could easily refactor out the logic variables associated with holding the different state of the request for permissions to a view model class that is attached to this screen.</p>
<p>You can see all of the code described in this article by going to this project:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/TomerPacific/MediumArticles/tree/master/LocationPermissions">https://github.com/TomerPacific/MediumArticles/tree/master/LocationPermissions</a></div>
<p>And if you would like to read more of my articles, you can go view them below:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/TomerPacific/MediumArticles">https://github.com/TomerPacific/MediumArticles</a></div>
<p>I have also used this logic in an application that you can try out <a target="_blank" href="https://play.google.com/store/apps/details?id=com.tomerpacific.scheduler">here</a>.</p>
<p>Thank you for reading!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Create Tabs in Jetpack Compose ]]>
                </title>
                <description>
                    <![CDATA[ We’ve all seen it. We’ve all done it. Ain’t nothing like good ol’ tabs to organize content in a complex application. So how do we go about creating a tab layout in Jetpack Compose?  In this tutorial, we’ll go over all of the basics, but also show som... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/tabs-in-jetpack-compose/</link>
                <guid isPermaLink="false">66ba503cf8a814ef73b78bcc</guid>
                
                    <category>
                        <![CDATA[ Jetpack Compose ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tomer ]]>
                </dc:creator>
                <pubDate>Tue, 28 Feb 2023 19:09:07 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/02/chiara-f-MI8He1NWPWg-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>We’ve all seen it.</p>
<p>We’ve all done it.</p>
<p>Ain’t nothing like good ol’ tabs to organize content in a complex application. So how do we go about creating a tab layout in Jetpack Compose? </p>
<p>In this tutorial, we’ll go over all of the basics, but also show some things that are more advanced.</p>
<h2 id="heading-how-to-create-simple-tabs">How to Create Simple Tabs</h2>
<p>To create a tab layout, you need to start with a <a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#TabRow(kotlin.Int,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function1,kotlin.Function0,kotlin.Function0)"><strong>TabRow</strong></a>. This will be a container element that will hold your tabs.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-meta">@UiComposable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">TabRow</span><span class="hljs-params">(
    selectedTabIndex: <span class="hljs-type">Int</span>,
    modifier: <span class="hljs-type">Modifier</span> = Modifier,
    backgroundColor: <span class="hljs-type">Color</span> = MaterialTheme.colors.primarySurface,
    contentColor: <span class="hljs-type">Color</span> = contentColorFor(backgroundColor)</span></span>,
    indicator: <span class="hljs-meta">@Composable</span> <span class="hljs-meta">@UiComposable</span> (tabPositions: List&lt;TabPosition&gt;) -&gt; <span class="hljs-built_in">Unit</span> = <span class="hljs-meta">@Composable</span> { tabPositions -&gt;
            TabRowDefaults.Indicator(
                Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
            )
        },
    divider: <span class="hljs-meta">@Composable</span> <span class="hljs-meta">@UiComposable</span> () -&gt; <span class="hljs-built_in">Unit</span> = <span class="hljs-meta">@Composable</span> {
            TabRowDefaults.Divider()
        },
    tabs: <span class="hljs-meta">@Composable</span> <span class="hljs-meta">@UiComposable</span> () -&gt; <span class="hljs-built_in">Unit</span>
): <span class="hljs-built_in">Unit</span>
</code></pre>
<ul>
<li><strong>selectedTabIndex</strong> indicates the index of the tab that is currently selected</li>
<li><strong>indicator</strong> represents the UI that indicates which tab is currently selected</li>
<li><strong>divider</strong> is a composable that is drawn at the bottom of the TabRow under the indicator</li>
<li>If you don’t have a need to custom style your tabs, you can use <a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/material/TabRowDefaults"><strong>TabRowDefaults</strong></a> as it contains the default values and implementation used for TabRow (you can see it being used inside divider)</li>
</ul>
<p>Let’s see the usage of TabRow with an example. We will create a simple layout that will have three tabs:</p>
<ol>
<li>Home</li>
<li>About</li>
<li>Settings</li>
</ol>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">TabScreen</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">var</span> tabIndex <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-number">0</span>) }

    <span class="hljs-keyword">val</span> tabs = listOf(<span class="hljs-string">"Home"</span>, <span class="hljs-string">"About"</span>, <span class="hljs-string">"Settings"</span>)

    Column(modifier = Modifier.fillMaxWidth()) {
        TabRow(selectedTabIndex = tabIndex) {
            tabs.forEachIndexed { index, title -&gt;
                Tab(text = { Text(title) },
                    selected = tabIndex == index,
                    onClick = { tabIndex = index }
                )
            }
        }
        <span class="hljs-keyword">when</span> (tabIndex) {
            <span class="hljs-number">0</span> -&gt; HomeScreen()
            <span class="hljs-number">1</span> -&gt; AboutScreen()
            <span class="hljs-number">2</span> -&gt; SettingsScreen()
        }
    }
}
</code></pre>
<p>A couple things to pay attention to:</p>
<ul>
<li>The TabRow composable holds inside of itself a <strong>Tab</strong> composable</li>
<li>After the TabRow composable, we have a when clause to handle what happens when each tab is clicked (in our specific case we are opening different screens)</li>
<li>We are using a variable called tabIndex to keep track of which Tab is selected</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/02/1.jpg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Pretty bland, right?</p>
<p>Let’s spice things up with icons by using the icon attribute of the Tab composable.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">TabScreen</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">var</span> tabIndex <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-number">0</span>) }

    <span class="hljs-keyword">val</span> tabs = listOf(<span class="hljs-string">"Home"</span>, <span class="hljs-string">"About"</span>, <span class="hljs-string">"Settings"</span>)

    Column(modifier = Modifier.fillMaxWidth()) {
        TabRow(selectedTabIndex = tabIndex) {
            tabs.forEachIndexed { index, title -&gt;
                Tab(text = { Text(title) },
                    selected = tabIndex == index,
                    onClick = { tabIndex = index },
                    icon = {
                        <span class="hljs-keyword">when</span> (index) {
                            <span class="hljs-number">0</span> -&gt; Icon(imageVector = Icons.Default.Home, contentDescription = <span class="hljs-literal">null</span>)
                            <span class="hljs-number">1</span> -&gt; Icon(imageVector = Icons.Default.Info, contentDescription = <span class="hljs-literal">null</span>)
                            <span class="hljs-number">2</span> -&gt; Icon(imageVector = Icons.Default.Settings, contentDescription = <span class="hljs-literal">null</span>)
                        }
                    }
                )
            }
        }
        <span class="hljs-keyword">when</span> (tabIndex) {
            <span class="hljs-number">0</span> -&gt; HomeScreen()
            <span class="hljs-number">1</span> -&gt; AboutScreen()
            <span class="hljs-number">2</span> -&gt; SettingsScreen()
        }
    }
}
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/02/1-1.jpg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Looking better, but a question does arise: What if we have more tabs than the screen can show?</p>
<p>Luckily, the answer is simple.</p>
<p>There is an option to make our TabRow scrollable. Instead of using the TabRow element, you can use the <a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#ScrollableTabRow(kotlin.Int,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp,kotlin.Function1,kotlin.Function0,kotlin.Function0)"><strong>ScrollableTabRow</strong></a> composable.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-meta">@UiComposable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ScrollableTabRow</span><span class="hljs-params">(
    selectedTabIndex: <span class="hljs-type">Int</span>,
    modifier: <span class="hljs-type">Modifier</span> = Modifier,
    backgroundColor: <span class="hljs-type">Color</span> = MaterialTheme.colors.primarySurface,
    contentColor: <span class="hljs-type">Color</span> = contentColorFor(backgroundColor)</span></span>,
    edgePadding: Dp = TabRowDefaults.ScrollableTabRowPadding,
    indicator: <span class="hljs-meta">@Composable</span> <span class="hljs-meta">@UiComposable</span> (tabPositions: List&lt;TabPosition&gt;) -&gt; <span class="hljs-built_in">Unit</span> = <span class="hljs-meta">@Composable</span> { tabPositions -&gt;
            TabRowDefaults.Indicator(
                Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
            )
        },
    divider: <span class="hljs-meta">@Composable</span> <span class="hljs-meta">@UiComposable</span> () -&gt; <span class="hljs-built_in">Unit</span> = <span class="hljs-meta">@Composable</span> {
            TabRowDefaults.Divider()
        },
    tabs: <span class="hljs-meta">@Composable</span> <span class="hljs-meta">@UiComposable</span> () -&gt; <span class="hljs-built_in">Unit</span>
): <span class="hljs-built_in">Unit</span>
</code></pre>
<p>So if we convert our example from above, we will get this:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">TabScreen</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">var</span> tabIndex <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-number">0</span>) }

    <span class="hljs-keyword">val</span> tabs = listOf(<span class="hljs-string">"Home"</span>, <span class="hljs-string">"About"</span>, <span class="hljs-string">"Settings"</span>, <span class="hljs-string">"More"</span>, <span class="hljs-string">"Something"</span>, <span class="hljs-string">"Everything"</span>)

    Column(modifier = Modifier.fillMaxWidth()) {
        ScrollableTabRow(selectedTabIndex = tabIndex) {
            tabs.forEachIndexed { index, title -&gt;
                Tab(text = { Text(title) },
                    selected = tabIndex == index,
                    onClick = { tabIndex = index },
                    icon = {
                        <span class="hljs-keyword">when</span> (index) {
                            <span class="hljs-number">0</span> -&gt; Icon(imageVector = Icons.Default.Home, contentDescription = <span class="hljs-literal">null</span>)
                            <span class="hljs-number">1</span> -&gt; Icon(imageVector = Icons.Default.Info, contentDescription = <span class="hljs-literal">null</span>)
                            <span class="hljs-number">2</span> -&gt; Icon(imageVector = Icons.Default.Settings, contentDescription = <span class="hljs-literal">null</span>)
                            <span class="hljs-number">3</span> -&gt; Icon(imageVector = Icons.Default.Lock, contentDescription = <span class="hljs-literal">null</span>)
                            <span class="hljs-number">4</span> -&gt; Icon(imageVector = Icons.Default.HeartBroken, contentDescription = <span class="hljs-literal">null</span>)
                            <span class="hljs-number">5</span> -&gt; Icon(imageVector = Icons.Default.Star, contentDescription = <span class="hljs-literal">null</span>)
                        }
                    }
                )
            }
        }
        <span class="hljs-keyword">when</span> (tabIndex) {
            <span class="hljs-number">0</span> -&gt; HomeScreen()
            <span class="hljs-number">1</span> -&gt; AboutScreen()
            <span class="hljs-number">2</span> -&gt; SettingsScreen()
            <span class="hljs-number">3</span> -&gt; MoreScreen()
            <span class="hljs-number">4</span> -&gt; SomethingScreen()
            <span class="hljs-number">5</span> -&gt; EverythingScreen()
        }
    }
}
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/02/2.gif" alt="Image" width="600" height="400" loading="lazy"></p>
<h2 id="heading-how-to-create-tabs-with-swiping-enabled">How to Create Tabs with Swiping Enabled</h2>
<p>Scrollable tabs are nice, but swiping between tabs is even better. Most users will feel that it's more intuitive to swipe between the tabs rather than clicking on each one. If you look at the documentation, you will notice that there are a few options to go with:</p>
<ol>
<li>The <a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#(androidx.compose.ui.Modifier).swipeable(androidx.compose.material.SwipeableState,kotlin.collections.Map,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Function2,androidx.compose.material.ResistanceConfig,androidx.compose.ui.unit.Dp)">swipeable</a> modifier</li>
<li>The <a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/foundation/gestures/package-summary#(androidx.compose.ui.input.pointer.PointerInputScope).detectDragGestures(kotlin.Function1,kotlin.Function0,kotlin.Function0,kotlin.Function2)">detectDragGestures</a> modifier</li>
<li>The <a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/foundation/gestures/package-summary#(androidx.compose.ui.Modifier).draggable(androidx.compose.foundation.gestures.DraggableState,androidx.compose.foundation.gestures.Orientation,kotlin.Boolean,androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Boolean,kotlin.coroutines.SuspendFunction2,kotlin.coroutines.SuspendFunction2,kotlin.Boolean)">draggable</a> modifier</li>
</ol>
<p>Not all of these will help us in achieving our goal, each one for its own reasons. If you don’t want to go through the “hassle” of doing things yourself, there is a library from Accompanist called <a target="_blank" href="https://google.github.io/accompanist/pager/#usage">pager</a> that you can use. It allows you to add the ability to either horizontally or vertically create a row/column that reacts to swipes.</p>
<p>Steps to implement it have been covered already and you can use the resources below to learn how to do it:</p>
<ul>
<li><a target="_blank" href="https://johncodeos.com/how-to-create-tabs-with-jetpack-compose/">https://johncodeos.com/how-to-create-tabs-with-jetpack-compose/</a></li>
<li><a target="_blank" href="https://www.rockandnull.com/jetpack-compose-swipe-pager/">https://www.rockandnull.com/jetpack-compose-swipe-pager/</a></li>
</ul>
<p>If you are like me and you like to do things for yourself and are up for getting your hands dirty, read on.</p>
<h2 id="heading-option-1-the-swipeable-modifier">Option 1: the <code>Swipeable</code> Modifier</h2>
<p>The first thing to know about the swipeable modifier is that it is annotated with the @<a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/material/ExperimentalMaterialApi"><strong>ExperimentalMaterialApi</strong></a>. This means that this API can change between versions of Jetpack Compose and that it isn’t stable. </p>
<p>Apart from that, we need to go over the mechanism that the swipeable modifier uses. It has 3 building blocks:</p>
<ol>
<li>A swipeable state – Denoting the current state and holding data about any on going swipe or swipe related animation.</li>
<li>Anchors – A map of values (Float based) restricting the swipe action from the minimum value to the maximum value. It maps anchor points to swipeable states.</li>
<li>Thresholds – A value denoting the difference between two known anchors.</li>
</ol>
<pre><code class="lang-kotlin"><span class="hljs-meta">@ExperimentalMaterialApi</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-type">&lt;T : Any?&gt;</span> Modifier.<span class="hljs-title">swipeable</span><span class="hljs-params">(
    state: <span class="hljs-type">SwipeableState</span>&lt;<span class="hljs-type">T</span>&gt;,
    anchors: <span class="hljs-type">Map</span>&lt;<span class="hljs-type">Float</span>, T&gt;,
    orientation: <span class="hljs-type">Orientation</span>,
    enabled: <span class="hljs-type">Boolean</span> = <span class="hljs-literal">true</span>,
    reverseDirection: <span class="hljs-type">Boolean</span> = <span class="hljs-literal">false</span>,
    interactionSource: <span class="hljs-type">MutableInteractionSource</span>? = <span class="hljs-literal">null</span>,
    thresholds: (<span class="hljs-type">from</span>, <span class="hljs-type">to</span>) -&gt; <span class="hljs-type">ThresholdConfig</span> = { _, _ -&gt; FixedThreshold(<span class="hljs-number">56.</span>dp)</span></span> },
    resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
    velocityThreshold: Dp = VelocityThreshold
): Modifier
</code></pre>
<p>Regardless of this API being experimental, it just isn’t meant to be used for the swiping gesture we are seeking. </p>
<p>You can. use this modifier for a switch button that the user can drag between on/off positions (as an example). But what would be our anchors in our example? How do we define the thresholds? The swipe a user performs cannot be constrained between two points. Therefore, we’ll let this one go and move on to detectDragGestures.</p>
<h2 id="heading-option-2-the-detectdraggestures-modifier">Option 2: the <code>detectDragGestures</code> Modifier</h2>
<p>As the name implies, this modifier detects the drag gesture, which can be quite similar to swiping.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> PointerInputScope.<span class="hljs-title">detectDragGestures</span><span class="hljs-params">(
    onDragStart: (<span class="hljs-type">Offset</span>) -&gt; <span class="hljs-type">Unit</span> = { },
    onDragEnd: () -&gt; <span class="hljs-type">Unit</span> = { },
    onDragCancel: () -&gt; <span class="hljs-type">Unit</span> = { },
    onDrag: (<span class="hljs-type">change</span>: <span class="hljs-type">PointerInputChange</span>, <span class="hljs-type">dragAmount</span>: <span class="hljs-type">Offset</span>) -&gt; <span class="hljs-type">Unit</span>
)</span></span>: <span class="hljs-built_in">Unit</span>
</code></pre>
<p>As you can see, the <strong>onDrag</strong> callback has two arguments:</p>
<ol>
<li><code>change</code> – of <code>PointerInputChange</code> type, denoting the change in pointer when dragging</li>
<li><code>dragAmount</code> – of <code>Offset</code> type, denoting the amount dragged in x,y values</li>
</ol>
<p>This callback is called when:</p>
<blockquote>
<p><em>“… waits for pointer down and touch stop in any direction and then calls <code>onDrag</code> for each drag event.”</em></p>
</blockquote>
<p>The upside to use this modifier instead of the draggable one is that it provides you with information about the change in both x and y coordinates.</p>
<p>The downside of it is that it isn’t going to offer a smooth and elegant solution for swiping. This is because of the amount of times the onDrag callback is triggered. </p>
<p>When a user performs a swipe gesture, the onDrag callback is triggered multiple times. This makes it harder to discern when the “drag” gesture has ended completely. </p>
<p>When experimenting with this, I saw the onDrag callback being triggered three times for each swipe gesture. This won’t be a good fit for our use case so let’s check out the draggable modifier.</p>
<h2 id="heading-option-3-the-draggable-modifier">Option 3: the <code>Draggable</code> Modifier</h2>
<p>Think of this modifier as the stripped down version of the one before. This one measures changes in the UI when the user performs a drag gesture in only one orientation (vertical/horizontal). Since we only care about horizontal swipes, this can be a good option.</p>
<pre><code class="lang-kotlin"><span class="hljs-function"><span class="hljs-keyword">fun</span> Modifier.<span class="hljs-title">draggable</span><span class="hljs-params">(
    state: <span class="hljs-type">DraggableState</span>,
    orientation: <span class="hljs-type">Orientation</span>,
    enabled: <span class="hljs-type">Boolean</span> = <span class="hljs-literal">true</span>,
    interactionSource: <span class="hljs-type">MutableInteractionSource</span>? = <span class="hljs-literal">null</span>,
    startDragImmediately: <span class="hljs-type">Boolean</span> = <span class="hljs-literal">false</span>,
    onDragStarted: <span class="hljs-type">suspend</span> <span class="hljs-type">CoroutineScope</span>.(<span class="hljs-type">startedPosition</span>: <span class="hljs-type">Offset</span>) -&gt; <span class="hljs-type">Unit</span> = {},
    onDragStopped: <span class="hljs-type">suspend</span> <span class="hljs-type">CoroutineScope</span>.(<span class="hljs-type">velocity</span>: <span class="hljs-type">Float</span>) -&gt; <span class="hljs-type">Unit</span> = {},
    reverseDirection: <span class="hljs-type">Boolean</span> = <span class="hljs-literal">false</span>
)</span></span>: Modifier
</code></pre>
<p>Here as well there is no similarity to the two other modifiers and we will point out the things to pay attention to:</p>
<ul>
<li><code>state</code> – Similar to the state in the swipeable modifier, only here we are talking about a drag motion.</li>
<li><code>onDragStarted</code> – A callback triggered when the drag motion has begun.</li>
<li><code>onDragStopped</code> – A callback triggered when the drag motion has ended.</li>
</ul>
<p>Unlike <strong><code>detectDragGestures</code></strong>, here <code>onDragStopped</code> is called once for every swipe gesture, making this modifier the best candidate for the job.</p>
<p>It’s implementation as a swipe gesture detector in our example is quite robust, so let’s start with some prerequisites:</p>
<ol>
<li>We will be saving the index of the tab currently being viewed in a view model class</li>
<li>This index will be of <code>MutableLiveData</code> so that our composables will be able to recompose when the value is changed</li>
<li>Each of our screens will add the <code>draggable</code> modifier to its layout</li>
<li>We will need to add the runtime-livedata library as we are going to use the <a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/runtime/livedata/package-summary#(androidx.lifecycle.LiveData).observeAsState(kotlin.Any)"><code>observeAsState</code></a> method.</li>
</ol>
<p>We will start with #4.</p>
<p>Go to your application’s build.gradle file and add the following dependency:</p>
<pre><code>implementation <span class="hljs-string">"androidx.compose.runtime:runtime-livedata:$compose_version"</span>
</code></pre><p>where <strong><code>$compose_version</code></strong> is the version of Jetpack Compose you are using.</p>
<p>We have also minimized our previous example to hold three screens instead of six, as the solution works for either case and there is no need to create extra boiler plate.</p>
<p>Below is the view model:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainViewModel</span></span>(application: Application) : AndroidViewModel(application) {

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _tabIndex: MutableLiveData&lt;<span class="hljs-built_in">Int</span>&gt; = MutableLiveData(<span class="hljs-number">0</span>)
    <span class="hljs-keyword">val</span> tabIndex: LiveData&lt;<span class="hljs-built_in">Int</span>&gt; = _tabIndex
    <span class="hljs-keyword">val</span> tabs = listOf(<span class="hljs-string">"Home"</span>, <span class="hljs-string">"About"</span>, <span class="hljs-string">"Settings"</span>)

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">updateTabIndexBasedOnSwipe</span><span class="hljs-params">(isSwipeToTheLeft: <span class="hljs-type">Boolean</span>)</span></span> {
        _tabIndex.value = <span class="hljs-keyword">when</span> (isSwipeToTheLeft) {
            <span class="hljs-literal">true</span> -&gt; Math.floorMod(_tabIndex.value!!.plus(<span class="hljs-number">1</span>), tabs.size)
            <span class="hljs-literal">false</span> -&gt; Math.floorMod(_tabIndex.value!!.minus(<span class="hljs-number">1</span>), tabs.size)
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">updateTabIndex</span><span class="hljs-params">(i: <span class="hljs-type">Int</span>)</span></span> {
        _tabIndex.value = i
    }

}
</code></pre>
<ul>
<li><strong><code>tabIndex</code></strong> is in charge of holding the currently selected index.</li>
<li><strong><code>index</code></strong> is the exposed tabIndex.</li>
<li><strong><code>tabs</code></strong> is the list of tab names.</li>
<li>The method <strong><code>updateTabIndexBasedOnSwipe</code></strong> is triggered when a swipe happens and performs the calculation of where to move the tabIndex to.</li>
</ul>
<p>Each screen is made up of the same layout:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">AboutScreen</span><span class="hljs-params">(viewModel: <span class="hljs-type">MainViewModel</span>)</span></span> {

    <span class="hljs-keyword">var</span> isSwipeToTheLeft <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-literal">false</span>) }
    <span class="hljs-keyword">val</span> dragState = rememberDraggableState(onDelta = { delta -&gt;
        isSwipeToTheLeft = delta &gt; <span class="hljs-number">0</span>
    })

    Column(modifier = Modifier.fillMaxSize().draggable(
        state = dragState,
        orientation = Orientation.Horizontal,
        onDragStarted = {  },
        onDragStopped = {
            viewModel.updateTabIndexBasedOnSwipe(isSwipeToTheLeft = isSwipeToTheLeft)
        }),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center) {
        Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
            Text(
                text = <span class="hljs-string">"About"</span>,
                textAlign = TextAlign.Center,
                fontSize = <span class="hljs-number">20</span>.sp,
                fontWeight = FontWeight.Bold
            )
        }
    }
}
</code></pre>
<ul>
<li><strong><code>isSwipeToTheLeft</code></strong> is a Boolean indicating the direction of the swipe.</li>
<li><strong><code>dragState</code></strong> holds the state of the drag being performed and updates isSwipeToTheLeft according to the delta.</li>
<li>When the callback <strong><code>onDragStopped</code></strong> is called, we are calling the exposed viewModel method updateTabIndexBasedOnSwipe.</li>
</ul>
<p>And finally, our <code>TabLayout</code>:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">TabLayout</span><span class="hljs-params">(viewModel: <span class="hljs-type">MainViewModel</span>)</span></span> {
    <span class="hljs-keyword">val</span> tabIndex = viewModel.tabIndex.observeAsState()
    Column(modifier = Modifier.fillMaxWidth()) {
        TabRow(selectedTabIndex = tabIndex.value!!) {
            viewModel.tabs.forEachIndexed { index, title -&gt;
                Tab(text = { Text(title) },
                    selected = tabIndex.value!! == index,
                    onClick = { viewModel.updateTabIndex(index) },
                    icon = {
                        <span class="hljs-keyword">when</span> (index) {
                            <span class="hljs-number">0</span> -&gt; Icon(imageVector = Icons.Default.Home, contentDescription = <span class="hljs-literal">null</span>)
                            <span class="hljs-number">1</span> -&gt; Icon(imageVector = Icons.Default.Info, contentDescription = <span class="hljs-literal">null</span>)
                            <span class="hljs-number">2</span> -&gt; Icon(imageVector = Icons.Default.Settings, contentDescription = <span class="hljs-literal">null</span>)
                        }
                    }
                )
            }
        }

        <span class="hljs-keyword">when</span> (tabIndex.value) {
            <span class="hljs-number">0</span> -&gt; HomeScreen(viewModel = viewModel)
            <span class="hljs-number">1</span> -&gt; AboutScreen(viewModel = viewModel)
            <span class="hljs-number">2</span> -&gt; SettingsScreen(viewModel = viewModel)
        }
    }
}
</code></pre>
<ul>
<li>Notice that when a tab is selected, we are updating the currently selected tab in the <code>viewModel</code> with <strong><code>updateTabIndex</code></strong>.</li>
</ul>
<p>Putting it all together yields:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/02/2-1.gif" alt="Image" width="600" height="400" loading="lazy"></p>
<p>A few words regarding what we have accomplished. You might have noticed that there is some boilerplate we are adding for each of our screens that results in repetition. Each screen is saving the state of the drag. </p>
<p>To improve on that, we can move the <code>draggableState</code> to the view model, like so:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainViewModel</span></span>(application: Application) : AndroidViewModel(application) {

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _tabIndex: MutableLiveData&lt;<span class="hljs-built_in">Int</span>&gt; = MutableLiveData(<span class="hljs-number">0</span>)
    <span class="hljs-keyword">val</span> tabIndex: LiveData&lt;<span class="hljs-built_in">Int</span>&gt; = _tabIndex
    <span class="hljs-keyword">val</span> tabs = listOf(<span class="hljs-string">"Home"</span>, <span class="hljs-string">"About"</span>, <span class="hljs-string">"Settings"</span>)

    <span class="hljs-keyword">var</span> isSwipeToTheLeft: <span class="hljs-built_in">Boolean</span> = <span class="hljs-literal">false</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> draggableState = DraggableState { delta -&gt;
        isSwipeToTheLeft= delta &gt; <span class="hljs-number">0</span>
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _dragState = MutableLiveData&lt;DraggableState&gt;(draggableState)
    <span class="hljs-keyword">val</span> dragState: LiveData&lt;DraggableState&gt; = _dragState

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">updateTabIndexBasedOnSwipe</span><span class="hljs-params">()</span></span> {
        _tabIndex.value = <span class="hljs-keyword">when</span> (isSwipeToTheLeft) {
            <span class="hljs-literal">true</span> -&gt; Math.floorMod(_tabIndex.value!!.plus(<span class="hljs-number">1</span>), tabs.size)
            <span class="hljs-literal">false</span> -&gt; Math.floorMod(_tabIndex.value!!.minus(<span class="hljs-number">1</span>), tabs.size)
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">updateTabIndex</span><span class="hljs-params">(i: <span class="hljs-type">Int</span>)</span></span> {
        _tabIndex.value = i
    }

}
</code></pre>
<p>And that reduces the boilerplate a bit, since each screen now looks like:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">AboutScreen</span><span class="hljs-params">(viewModel: <span class="hljs-type">MainViewModel</span>)</span></span> {

    Column(modifier = Modifier.fillMaxSize().draggable(
        state = viewModel.dragState.value!!,
        orientation = Orientation.Horizontal,
        onDragStarted = {  },
        onDragStopped = {
            viewModel.updateTabIndexBasedOnSwipe()
        }),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center) {
        Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
            Text(
                text = <span class="hljs-string">"About"</span>,
                textAlign = TextAlign.Center,
                fontSize = <span class="hljs-number">20</span>.sp,
                fontWeight = FontWeight.Bold
            )
        }
    }
}
</code></pre>
<p>I hope this article gave you the necessary tools to create your own tabs UI in Jetpack Compose. </p>
<p>The example shown above can be found <a target="_blank" href="https://github.com/TomerPacific/MediumArticles/tree/master/JetpackComposeTabs">here</a>.</p>
<p>And if you would like to read other articles I have written, you can check them out <a target="_blank" href="https://github.com/TomerPacific/MediumArticles">here</a>.</p>
<p>References:</p>
<ul>
<li><a target="_blank" href="https://m3.material.io/components/tabs/overview">Material Design page about Tabs</a></li>
<li><a target="_blank" href="https://developer.android.com/jetpack/compose/touch-input/gestures">Gestures In Jetpack Compose</a></li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Serialize Your Data in Kotlin and Jetpack Compose ]]>
                </title>
                <description>
                    <![CDATA[ Serialization is the process of transforming data that's in one format into another format that can be stored.  If you have ever worked with a database or fetching data from a server, this should all be familiar to you. If not, you have come to the r... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/serializing-your-data-in-kotlin/</link>
                <guid isPermaLink="false">66ba503a158e6c6a8cb8c7a5</guid>
                
                    <category>
                        <![CDATA[ Jetpack Compose ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Kotlin ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tomer ]]>
                </dc:creator>
                <pubDate>Wed, 01 Feb 2023 21:19:13 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/01/fineas-anton-cnoMG2034k8-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Serialization is the process of transforming data that's in one format into another format that can be stored. </p>
<p>If you have ever worked with a database or fetching data from a server, this should all be familiar to you. If not, you have come to the right place. </p>
<p>In this tutorial, we will go over:</p>
<ul>
<li>How to setup serialization in a Jetpack Compose project</li>
<li>How to serialize a data class</li>
<li>How to de-serialize a data class</li>
</ul>
<p>You might be asking yourself, what’s so special about serialization in Jetpack Compose? In essence, there isn’t a lot of difference than with a regular Kotlin Android project. The only difference is in the setup.</p>
<h2 id="heading-how-to-set-everything-up">How to Set Everything Up</h2>
<p>Each version of Jetpack Compose corresponds with a version of Kotiln that it is compatible with. Each version of the kotlin-serialization library is also compatible with a specific version of Kotlin. So you need to make sure that each of the three parts in this tripod are compatible with one another.</p>
<p>How can you that?</p>
<p>Your first resource you'll want to consult is the <a target="_blank" href="https://developer.android.com/jetpack/androidx/releases/compose-kotlin">Compose to Kotlin Compatibility Map</a>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/01/1_5brVwILW54aNaFFimDF87Q.jpeg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Here you can see which version of Jetpack Compose corresponds to which Kotlin version.</p>
<p>The second resource you will need is the <a target="_blank" href="https://github.com/Kotlin/kotlinx.serialization/releases">releases page</a> for the kotlin-serialization library. There you will find which library version is compatible with which Kotlin version.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/01/1_y6Ba1fROOcSSXXm-Nll4Ew.jpeg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Confused? 😕</p>
<p>Let’s illustrate this with an example:</p>
<ul>
<li>Your Jetpack Compose version is <strong>1.1.0</strong>.</li>
<li>Looking over the compatibility map, you see it is compatible with Kotlin version <strong>1.6.10</strong>.</li>
<li>Heading to the releases page of kotlin-serialization library, you see that the version of the kotlin-serialization library that you need to use is <strong>1.3.2</strong>.</li>
</ul>
<p>Head into your project level build.gradle file, and inside the buildscript object, in the dependencies section, put in classpath for the kotlin-serialization library with the version you need.</p>
<pre><code class="lang-kotlin">dependencies {
        ...
        classpath <span class="hljs-string">"org.jetbrains.kotlin:kotlin-serialization:X.Y.Z"</span>
 }
</code></pre>
<p>Then, head over to your application build.gradle file and do these two things:</p>
<ol>
<li>Add the <strong>id ‘org.jetbrains.kotlin.plugin.serialization’</strong> inside of the plugins at the top of the file:</li>
</ol>
<pre><code class="lang-kotlin">plugins {
   ...
   id <span class="hljs-string">'org.jetbrains.kotlin.plugin.serialization'</span>
}
</code></pre>
<ol start="2">
<li>At the bottom of the file, inside the dependencies section add <strong>implementation ‘org.jetbrains.kotlinx:kotlinx-serialization-json:X.Y.Z’</strong>:</li>
</ol>
<pre><code class="lang-kotlin">dependencies {
   ...
   implementation <span class="hljs-string">'org.jetbrains.kotlinx:kotlinx-serialization-json:X.Y.Z'</span>
}
</code></pre>
<p>Sync your project and you should be good to go.</p>
<p>Note that we are using the <strong>json</strong> format of the library, but there are other formats that are supported as well:</p>
<ul>
<li>Protocol Buffers</li>
<li>CBOR (Concise Binary Object Representation)</li>
<li>Properties</li>
<li>HOCON (Human Optimized Config Object Notation)</li>
</ul>
<blockquote>
<p>⚠️ If you encounter any errors, make sure the versions you put are correct</p>
</blockquote>
<h2 id="heading-how-to-build-your-data-class">How to Build Your Data Class</h2>
<p>In order to have something we can serialize and later de-serialize, we need to work with data classes. </p>
<p>Creating a data class is simple. If you are using Android Studio, just right click inside your project’s module and choose New Kotlin file. Enter your class name and then append the data keyword before it.</p>
<p>For the sake of this article, let's say we are working with an API that returns a list of users. Each user object has a range of attributes it can have (just to name a few):</p>
<ul>
<li>First name</li>
<li>Last name</li>
<li>Age</li>
<li>Birthdate</li>
<li>Id</li>
</ul>
<p>To make our data class serializable, all you need to do is add the <strong>@Serializable</strong> annotation above the class declaration.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Serializable</span>
<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserModel</span></span>(
   <span class="hljs-keyword">val</span> firstName: String,
   <span class="hljs-keyword">val</span> lastName: String,
   <span class="hljs-keyword">val</span> age: <span class="hljs-built_in">Int</span>,
   <span class="hljs-keyword">val</span> birthdate: Date,
   <span class="hljs-keyword">val</span> id: <span class="hljs-built_in">Long</span>
)
</code></pre>
<p>Pretty nifty, right?</p>
<p>Well, there’s more.</p>
<p>The variable that will hold the user’s first name is written as firstName. That means that in the response from our server, it needs to return in a field with the same name. </p>
<p>Sometimes, in API responses, the keys are not written in camelCase, but rather in kebab_case. That would mean that the key for first name, might be first_name. In that case, we would have to write it out like this:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Serializable</span>
<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserModel</span></span>(
   <span class="hljs-keyword">val</span> first_name: String,
   <span class="hljs-keyword">val</span> lastName: String,
   <span class="hljs-keyword">val</span> age: <span class="hljs-built_in">Int</span>,
   <span class="hljs-keyword">val</span> birthdate: Date,
   <span class="hljs-keyword">val</span> id: <span class="hljs-built_in">Long</span>
)
</code></pre>
<p>But that is not the <a target="_blank" href="https://kotlinlang.org/docs/coding-conventions.html">convention</a> for property names in Kotlin.</p>
<p>So what can we do?</p>
<p>We can use the <strong>@SerialName</strong> annotation. This allows us to mark what the name of the field will be from the response and then write anything as the property for it.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Serializable</span>
<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserModel</span></span>(
   <span class="hljs-meta">@SerialName(<span class="hljs-meta-string">"first_name"</span>)</span>
   <span class="hljs-keyword">val</span> firstName: String,
   <span class="hljs-keyword">val</span> lastName: String,
   <span class="hljs-keyword">val</span> age: <span class="hljs-built_in">Int</span>,
   <span class="hljs-keyword">val</span> birthdate: Date,
   <span class="hljs-keyword">val</span> id: <span class="hljs-built_in">Long</span>
)
</code></pre>
<h2 id="heading-how-to-serialize-and-de-serialize">How to Serialize and De-Serialize</h2>
<p>Now that our data class is set up, let’s enjoy the fruits of our labor. Whenever we need to serialize our data class, we will use the <a target="_blank" href="https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/encode-to-string.html">Json.encodeToString</a> method:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> dataAsString: String = Json.encodeToString(user)
</code></pre>
<p>When we run the above line of code, we will get our data class in string form.</p>
<p>De-serializing our data is as simple as serializing it. We will use the <a target="_blank" href="https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/decode-from-string.html">Json.decodeFromString</a> method:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> user: UserModel = Json.decodeFromString&lt;UserModel&gt;(dataAsString)
</code></pre>
<blockquote>
<p>✋ Notice that we specified which type of data we want to de-serialize to with the type parameter ().</p>
</blockquote>
<p><img src="https://images.unsplash.com/photo-1600176842064-635fe81d2441?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDMwfHxyZW1vdGUlMjBjb250cm9sfGVufDB8fHx8MTY3NTAxODM0NQ&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Image" width="2000" height="1325" loading="lazy">
_Photo by [Unsplash](https://unsplash.com/@macroman?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit"&gt;Immo Wegmann / &lt;a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm<em>campaign=api-credit)</em></p>
<p>Time for some extra credit.</p>
<p>Let’s say that in your data class you have a field that you don’t want to serialize. If we take our UserModel class, imagine that we want to have a user’s actual picture (bitmap).</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Serializable</span>
<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserModel</span></span>(
   <span class="hljs-meta">@SerialName(<span class="hljs-meta-string">"first-name"</span>)</span>
   <span class="hljs-keyword">val</span> firstName: String,
   <span class="hljs-keyword">val</span> lastName: String,
   <span class="hljs-keyword">val</span> age: <span class="hljs-built_in">Int</span>,
   <span class="hljs-keyword">val</span> birthdate: Date,
   <span class="hljs-keyword">val</span> id: <span class="hljs-built_in">Long</span>,
   <span class="hljs-keyword">var</span> photo: Bitmap?
)
</code></pre>
<p>This is not something we will get from our API call, so how can we exclude it? Because if we don’t, our serialization will fail.</p>
<p>Here to our rescue is the <a target="_blank" href="https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-transient/"><strong>@Transient</strong> annotation</a>.</p>
<pre><code>@Serializable
data <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserModel</span>(
   @<span class="hljs-title">SerialName</span>("<span class="hljs-title">first</span>-<span class="hljs-title">name</span>")
   <span class="hljs-title">val</span> <span class="hljs-title">firstName</span>: <span class="hljs-title">String</span>,
   <span class="hljs-title">val</span> <span class="hljs-title">lastName</span>: <span class="hljs-title">String</span>,
   <span class="hljs-title">val</span> <span class="hljs-title">age</span>: <span class="hljs-title">Int</span>,
   <span class="hljs-title">val</span> <span class="hljs-title">birthdate</span>: <span class="hljs-title">Date</span>,
   <span class="hljs-title">val</span> <span class="hljs-title">id</span>: <span class="hljs-title">Long</span>,
   @<span class="hljs-title">Transient</span>
   <span class="hljs-title">var</span> <span class="hljs-title">photo</span>: <span class="hljs-title">Bitmap</span>?
)</span>
</code></pre><p>This will exclude the marked field from being serialized and de-serialized.</p>
<ul>
<li>If you want to see a real life example of using serialization inside a project, you can check out a project I made <a target="_blank" href="https://medium.com/r?url=https%3A%2F%2Fgithub.com%2FTomerPacific%2Fmovies-presenter">here</a></li>
<li>And if you would like to read other articles I have written, you can go <a target="_blank" href="https://medium.com/r?url=https%3A%2F%2Fgithub.com%2FTomerPacific%2FMediumArticles">here</a></li>
<li>For more information about the kotlin-serialization library, you can go <a target="_blank" href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/basic-serialization.md#json-encoding">here</a></li>
</ul>
<p>Thank you for reading! Happy serializing.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Handle UI Events in Jetpack Compose ]]>
                </title>
                <description>
                    <![CDATA[ In this short and practical article, we will talk about how to handle UI events in Jetpack Compose. In the old system, we used OnClickListeners and other interfaces. In Compose, we can take full advantage of Kotlin’s Sealed Classes, Function Types an... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-handle-ui-events-in-jetpack-compose/</link>
                <guid isPermaLink="false">66d460c99f2bec37e2da066a</guid>
                
                    <category>
                        <![CDATA[ Android ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Android Studio ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Jetpack Compose ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Kotlin ]]>
                    </category>
                
                    <category>
                        <![CDATA[ UI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ User Interface ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Ryan Michael Kay ]]>
                </dc:creator>
                <pubDate>Tue, 16 Mar 2021 18:22:24 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2021/03/cat-4793068_1280-5.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this short and practical article, we will talk about how to handle UI events in Jetpack Compose.</p>
<p>In the old system, we used OnClickListeners and other interfaces. In Compose, we can take full advantage of Kotlin’s <strong>Sealed Classes</strong>, <strong>Function Types</strong> and <strong>Lambda Expressions</strong>.</p>
<p>If you do not know what a composable is, consider reading <a target="_blank" href="https://www.freecodecamp.org/news/jetpack-compose-beginner-tutorial-composables-recomposition/">this article which explains the fundamentals</a>.</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/LrNPw1LQHEw" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p> </p>
<h2 id="heading-how-to-model-ui-events-with-a-sealed-class">How to Model UI Events with a Sealed Class</h2>
<p>First, we must learn what is meant by UI Events and how to model them with Sealed Classes.</p>
<p>I have described this same process for <a target="_blank" href="https://medium.com/swlh/simplify-your-ui-interactions-with-events-java-kotlin-any-language-5062c1b1e0e4">Java and Kotlin</a> (with the old view system) before, so I will keep this brief.</p>
<h3 id="heading-the-process">The Process</h3>
<p>For each screen or sub-screen of your UI, ask yourself this question: What are all the different ways which the user can interact with it?</p>
<p>Let's take an example from my first app built fully in compose, <a target="_blank" href="https://play.google.com/store/apps/details?id=com.bracketcove.graphsudoku">Graph Sudoku</a>:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/graph_sudoku_small_screen.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Screenshot of a Sudoku Android App</em></p>
<p>The sealed class I use to represent the UI interactions of this screen looks like this:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">sealed</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ActiveGameEvent</span> </span>{
    <span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OnInput</span></span>(<span class="hljs-keyword">val</span> input: <span class="hljs-built_in">Int</span>) : ActiveGameEvent()
    <span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OnTileFocused</span></span>(<span class="hljs-keyword">val</span> x: <span class="hljs-built_in">Int</span>, 
    <span class="hljs-keyword">val</span> y: <span class="hljs-built_in">Int</span>) : ActiveGameEvent()
    <span class="hljs-keyword">object</span> OnNewGameClicked : ActiveGameEvent()
    <span class="hljs-keyword">object</span> OnStart : ActiveGameEvent()
    <span class="hljs-keyword">object</span> OnStop : ActiveGameEvent()
}
</code></pre>
<p>To explain briefly:</p>
<ul>
<li><p>OnInput represents a user touching an input button (like 0, 1, 2, 3, 4)</p>
</li>
<li><p>OnTileFocused represents a user selecting a tile (like the amber highlighted one)</p>
</li>
<li><p>OnNewGameClicked is self-explanatory</p>
</li>
<li><p>OnStart and OnStop are lifecycle events which my composables do not care about, but they are used in the Activity which acts as a Container for the composables</p>
</li>
</ul>
<p>Once you have your sealed class set up, you can now handle a wide variety of events using a single event handler function. Sometimes it might make more sense to have multiple event handler functions, so keep in mind that <strong>this approach must be adapted to your project's specific requirements</strong>.</p>
<h2 id="heading-how-to-connect-your-software-architecture">How to Connect Your Software Architecture</h2>
<p>What you have handling these events is totally up to you. Some people think that MVVM is the golden standard of software architectures, but it seems like more and more people are realizing that <strong>there is no single architecture which works best for every situation</strong>.</p>
<p>For Android with Compose, my current approach is to use a very 3rd party minimalist approach which typically has these things in each feature (screen):</p>
<ul>
<li><p>A (Presentation) Logic class <strong>as an event handler</strong></p>
</li>
<li><p>A ViewModel to store the data necessary to render the View (as the name implies)</p>
</li>
<li><p>An Activity which acts as a Container (not a god object)</p>
</li>
<li><p>Composables to form the View</p>
</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/model_view_whatever-3.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><em>Model-View-Whatever</em></p>
<p>I do not care what you use as long as you are applying <a target="_blank" href="https://youtu.be/B_C41SF0KbI">separation of concerns</a>. This is how I arrived at this architecture, by simply asking what should and should not be put together in the same class.</p>
<p>Whether you want your ViewModel, a Fragment, or an Activity to be your event handler, all of them can be set up the same way: <strong>Function Types!</strong></p>
<p>Within your class of choice, set up an event handler function which accepts your sealed class as its argument:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ActiveGameLogic</span></span>(
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> container: ActiveGameContainer?,
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> viewModel: ActiveGameViewModel,
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> gameRepo: IGameRepository,
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> statsRepo: IStatisticsRepository,
    dispatcher: DispatcherProvider
) : BaseLogic&lt;ActiveGameEvent&gt;(dispatcher),
    CoroutineScope {
    <span class="hljs-comment">//...</span>
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onEvent</span><span class="hljs-params">(event: <span class="hljs-type">ActiveGameEvent</span>)</span></span> {
        <span class="hljs-keyword">when</span> (event) {
            <span class="hljs-keyword">is</span> ActiveGameEvent.OnInput -&gt; onInput(
                event.input,
                viewModel.timerState
            )
            ActiveGameEvent.OnNewGameClicked -&gt; onNewGameClicked()
            ActiveGameEvent.OnStart -&gt; onStart()
            ActiveGameEvent.OnStop -&gt; onStop()
            <span class="hljs-keyword">is</span> ActiveGameEvent.OnTileFocused -&gt; onTileFocused(event.x, event.y)
        }
    }
    <span class="hljs-comment">//...</span>
}
</code></pre>
<p>This approach is very organized and makes it easy to test every Unit in this 3rd party library free class through a single entry point.</p>
<p>However, we are not done yet. Naturally, we need a way to get a reference to this event handler function, <code>onEvent</code>, to our Composables. We can do this using a <strong>function reference</strong>:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ActiveGameActivity</span> : <span class="hljs-type">AppCompatActivity</span></span>(), ActiveGameContainer {
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> logic: ActiveGameLogic

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCreate</span><span class="hljs-params">(savedInstanceState: <span class="hljs-type">Bundle</span>?)</span></span> {
        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState)

        <span class="hljs-keyword">val</span> viewModel = ActiveGameViewModel()

        setContent {
            ActiveGameScreen(
                onEventHandler = logic::onEvent,
                viewModel
            )
        }

        logic = buildActiveGameLogic(<span class="hljs-keyword">this</span>, viewModel, applicationContext)
    }

      <span class="hljs-comment">//...</span>
}
</code></pre>
<p>I am sure some of you are wondering why I am using an Activity. You can ask me during a <a target="_blank" href="https://youtu.be/-xV8k-4UW50">livestream Q&amp;A sometime for a detailed answer</a>.</p>
<p>In short, Fragments appear to be a bit pointless with Compose with my approach to architecture (I do not use Jetpack Navigation), and there is nothing wrong with using Activities as a feature specific container. <strong>Just avoid writing god activities, basically.</strong></p>
<p>To be specific, the way you make a reference to a function in Kotlin, is by providing the <strong>class/interface name</strong> (or <strong>skip that if it is a Top-Level function</strong>), followed by <strong>two colons</strong>, and the <strong>name of the function without any arguments or brackets</strong>:</p>
<pre><code class="lang-pgsql">onEventHandler = logic::onEvent
</code></pre>
<h2 id="heading-how-to-replace-onclicklistener-with-jetpack-compose-onclick-modifier">How to Replace onClickListener With Jetpack Compose onClick Modifier</h2>
<p>With that stuff ready, we can look at how this works within the composable. Naturally, your root composable will need the event handler function as a parameter:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ActiveGameScreen</span><span class="hljs-params">(
    onEventHandler: (<span class="hljs-type">ActiveGameEvent</span>) -&gt; <span class="hljs-type">Unit</span>,
    viewModel: <span class="hljs-type">ActiveGameViewModel</span>
)</span></span> {
<span class="hljs-comment">//...</span>
}
</code></pre>
<p>It can be a bit tricky to get function type syntax correctly, but understand that this <strong>really is a reference to a function,</strong> which is not so different from a reference to a class.</p>
<p>Just as you should not build god objects, you should not build giant composables:</p>
<ol>
<li><p>Break your UI down into the <strong>smallest reasonable parts</strong></p>
</li>
<li><p>Wrap them in a composable function</p>
</li>
<li><p>For each composable which has a UI interaction associated with it, <strong>it must be given a reference to your event handler function</strong></p>
</li>
</ol>
<p>Here is a composable which represents the input buttons of the Sudoku app, which is given the event handler by reference:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">SudokuInputButton</span><span class="hljs-params">(
    onEventHandler: (<span class="hljs-type">ActiveGameEvent</span>) -&gt; <span class="hljs-type">Unit</span>,
    number: <span class="hljs-type">Int</span>
)</span></span> {
    Button(
        onClick = { onEventHandler.invoke(ActiveGameEvent.OnInput(number)) },
        modifier = Modifier
            .requiredSize(<span class="hljs-number">56</span>.dp)
            .padding(<span class="hljs-number">2</span>.dp)
    ) {
        Text(
            text = number.toString(),
            style = inputButton.copy(color = MaterialTheme.colors.onPrimary),
            modifier = Modifier.fillMaxSize()
        )
    }
}
</code></pre>
<p>To actually pass the event to the logic class, we must use the <code>invoke</code> function, which will accept arguments as per the function type definition (which accepts an <code>ActiveGameEvent</code> in this case).</p>
<p>At this point, you are ready to handle UI interaction events in Kotlin (compose or not) by taking full advantage of this beautiful and modern programming language.</p>
<p>If you liked this article, share it on social media and consider checking out the resources below to support an independent programmer and content creator.</p>
<h3 id="heading-social">Social</h3>
<p>You can find me on <a target="_blank" href="https://www.instagram.com/rkay301/">Instagram here</a> and on <a target="_blank" href="https://twitter.com/wiseAss301">Twitter here</a>.</p>
<h3 id="heading-here-are-some-of-my-tutorials-amp-courses">Here are some of my tutorials &amp; courses</h3>
<p><a target="_blank" href="https://www.youtube.com/channel/UCSwuCetC3YlO1Y7bqVW5GHg">https://youtube.com/wiseass</a> <a target="_blank" href="https://www.freecodecamp.org/news/author/ryan-michael-kay/">https://www.freecodecamp.org/news/author/ryan-michael-kay/</a> <a target="_blank" href="https://skl.sh/35IdKsj">https://skl.sh/35IdKsj</a> (introduction to Android with Android Studio)</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
