<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" 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/"><channel><title>Alex Zhukovich</title><description>Alex Zhukovich writes about building high-quality mobile apps: testing, architecture, performance, Jetpack Compose and Kotlin.</description><link>https://alexzh.com/</link><language>en</language><atom:link href="https://alexzh.com/rss.xml" rel="self" type="application/rss+xml"/><item><title>Handling Asynchronous Images in Android Screenshot Tests</title><link>https://alexzh.com/handling-asynchronous-images-in-android-screenshot-tests/</link><guid isPermaLink="false">6a8fe736c2ac3f0001e2a1dc</guid><description>Images that load asynchronously make screenshot tests flaky. Two ways to make them deterministic: a fake image loader from coil-test, or Inspection Mode.</description><pubDate>Thu, 27 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://alexzh.com/_astro/cover.BalBxfYG_1n7eIy.webp&quot; alt=&quot;&quot; width=&quot;1080&quot; height=&quot;654&quot;&gt;&lt;blockquote&gt;
&lt;p&gt;This article is based on a chapter from my book, &lt;a href=&quot;https://leanpub.com/mastering-android-screenshot-testing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Mastering Android Screenshot Testing&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Modern applications frequently load images asynchronously, which is a common cause of unstable screenshot tests or confusion when you review newly generated screenshots. A test can pass locally and fail on CI, produce a different image on every run, or leave an empty space where the image should be placed.&lt;/p&gt;
&lt;p&gt;This article covers two ways to make screenshot tests deterministic: injecting a fake image loader with the &lt;em&gt;coil-test&lt;/em&gt; library, or using Compose’s inspection mode as a fallback when the first option isn’t available.&lt;/p&gt;
&lt;p&gt;Screenshot testing frameworks wait for the UI to be idle before capturing an image, but do not wait for asynchronous image loading to complete. What happens next depends on the framework. Compose Preview Screenshot Testing and Paparazzi render through &lt;em&gt;layoutlib&lt;/em&gt;, which has no access to the network, so the image never loads and you are left with an empty space. Other frameworks might capture a screenshot while the image request is still in progress.&lt;/p&gt;
&lt;p&gt;The primary purpose of screenshot tests is to verify that components or screens render correctly across diverse system configurations, such as varying font scales, light and dark color modes, and different locales. In most cases, the actual image content is irrelevant—you can replace images with placeholders to verify your layouts are rendered correctly regardless of image content.&lt;/p&gt;
&lt;p&gt;If you’re using &lt;em&gt;Coil&lt;/em&gt; for asynchronous image loading, you can use the &lt;em&gt;coil-test&lt;/em&gt; library to inject custom images for all image requests, ensuring predictable and consistent results.&lt;/p&gt;
&lt;h2 id=&quot;providing-fake-images-using-the-coil-test-library&quot;&gt;Providing Fake Images Using the coil-test Library&lt;/h2&gt;
&lt;p&gt;The &lt;em&gt;coil-test&lt;/em&gt; library allows you to provide custom images for all loading requests.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val imageLoaderEngine = FakeImageLoaderEngine.Builder()
    .intercept({ request -&amp;gt; true }, ColorImage(Color.Green.toArgb()))
    .build()

val imageLoader = ImageLoader.Builder(ApplicationProvider.getApplicationContext())
    .components { add(imageLoaderEngine) }
    .build()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To replace the default &lt;code&gt;ImageLoader&lt;/code&gt; with a custom one, you can use &lt;code&gt;SingletonImageLoader.setUnsafe(imageLoader)&lt;/code&gt;. To revert it back to the default &lt;code&gt;ImageLoader&lt;/code&gt;, you need to call &lt;code&gt;SingletonImageLoader.reset()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Let’s look at an example that uses the Android Testify framework.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@RunWith(AndroidJUnit4::class)
class AsyncImageScreenshotTest {

    @get:Rule
    val composableScreenshotRule = ComposableScreenshotRule()

    @ScreenshotInstrumentation
    @Test
    fun asyncImage() {
        // Providing a fake image
        val imageLoaderEngine = FakeImageLoaderEngine.Builder()
            .intercept({ request -&amp;gt; true }, ColorImage(Color.Green.toArgb()))
            .build()
        val imageLoader = ImageLoader.Builder(ApplicationProvider.getApplicationContext())
            .components { add(imageLoaderEngine) }
            .build()
        SingletonImageLoader.setUnsafe(imageLoader)

        composableScreenshotRule
            .setCompose {
                AppTheme {
                    AsyncImage(
                        modifier = Modifier.size(200.dp),
                        uri = &quot;content://media/external/images/media/1&quot;.toUri()
                    )
                }
            }
            .assertSame()

        // Resetting to provide real images again
        SingletonImageLoader.reset()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can avoid duplicating code for setting up and resetting an image loader in each test by creating a custom JUnit rule. This rule will handle the setup and reset process for you. To implement this, create a new class and extend the &lt;code&gt;TestWatcher&lt;/code&gt; class, which serves as the base class for JUnit rules.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class FakeImageLoaderRule(
    private val contextProvider: () -&amp;gt; Context,
    val image: Image = ColorImage(Color.Green.toArgb())
): TestWatcher() {

    @OptIn(DelicateCoilApi::class)
    override fun starting(description: Description?) {
        super.starting(description)
        val imageLoaderEngine = FakeImageLoaderEngine.Builder()
            .intercept({ request -&amp;gt; true }, image)
            .build()
        val imageLoader = ImageLoader.Builder(contextProvider())
            .components { add(imageLoaderEngine) }
            .build()
        SingletonImageLoader.setUnsafe(imageLoader)
    }

    @OptIn(DelicateCoilApi::class)
    override fun finished(description: Description?) {
        super.finished(description)
        SingletonImageLoader.reset()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;starting&lt;/code&gt; function runs before the test case executes, and the &lt;code&gt;finished&lt;/code&gt; after it, including when the test fails.&lt;/p&gt;
&lt;p&gt;Once that’s done, you can make the test class simpler.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@RunWith(AndroidJUnit4::class)
class AsyncImageScreenshotTest {

    @get:Rule(order = 0)
    val fakeImageLoaderRule = FakeImageLoaderRule(
        contextProvider = { ApplicationProvider.getApplicationContext() }
    )

    @get:Rule(order = 1)
    val composableScreenshotRule = ComposableScreenshotRule()

    @ScreenshotInstrumentation
    @Test
    fun asyncImage() {
        composableScreenshotRule
            .setCompose {
                AppTheme {
                    AsyncImage(
                        modifier = Modifier.size(200.dp),
                        uri = &quot;content://media/external/images/media/1&quot;.toUri()
                    )
                }
            }
            .assertSame()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The current implementation of the &lt;code&gt;FakeImageLoaderRule&lt;/code&gt; isn’t tied to the screenshot testing framework, it depends on the &lt;em&gt;coil-test&lt;/em&gt; library, and if you decide to use another framework instead of &lt;em&gt;Coil&lt;/em&gt;, the implementation of it should be changed. So, the same rule can also be used with Paparazzi, Roborazzi, Dropshots and Shot.&lt;/p&gt;
&lt;p&gt;However, you cannot use the &lt;code&gt;FakeImageLoaderRule&lt;/code&gt; test rule for &lt;code&gt;@Preview&lt;/code&gt; functions. Instead, you need to provide an &lt;code&gt;AsyncImagePreviewHandler&lt;/code&gt; object through &lt;code&gt;LocalAsyncImagePreviewHandler&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Preview
@Composable
fun Preview_AsyncImage() {
    AppTheme {
        val previewHandler = AsyncImagePreviewHandler {
            ColorImage(Color.Green.toArgb())
        }
        CompositionLocalProvider(
            LocalAsyncImagePreviewHandler provides previewHandler
        ) {
            AsyncImage(
                modifier = Modifier.size(200.dp),
                model = &quot;content://media/external/images/media/1&quot;.toUri(),
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;using-inspection-mode-to-display-fake-images&quot;&gt;Using Inspection Mode to Display Fake Images&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;LocalInspectionMode.current&lt;/code&gt; returns a &lt;code&gt;Boolean&lt;/code&gt; value showing whether the composable function is rendering within a preview. Let’s assume in your project, an &lt;code&gt;AsyncImage&lt;/code&gt; component is used, which is a wrapper around the &lt;code&gt;ThirdPartyAsyncImage&lt;/code&gt; component from the library, which doesn’t provide a way to inject fake images. To display fake images for preview functions and screenshot tests, you need to check whether inspection mode returns &lt;code&gt;true&lt;/code&gt;. If so, use an &lt;code&gt;Image&lt;/code&gt; component instead of the &lt;code&gt;ThirdPartyAsyncImage&lt;/code&gt; one, with a green background and a placeholder image.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun AsyncImage(
    modifier: Modifier = Modifier,
    uri: Uri,
    contentDescription: String? = null,
    placeholder: ImageVector = ImagePlaceholderIcon,
    contentScale: ContentScale = ContentScale.Crop
) {
    if (LocalInspectionMode.current.not()) {
        ThirdPartyAsyncImage(
            modifier = modifier,
            model = uri,
            contentDescription = contentDescription,
            placeholder = rememberVectorPainter(placeholder),
            contentScale = contentScale
        )
    } else {
        Image(
            modifier = modifier
                .fillMaxSize()
                .background(Color.Green),
            imageVector = placeholder,
            contentDescription = null,
            colorFilter = ColorFilter.tint(Color.White)
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This requires no additional configuration for Compose Preview Screenshot Testing, since preview functions already render in inspection mode. With any other framework, you need to enable it manually by providing &lt;code&gt;LocalInspectionMode&lt;/code&gt; via &lt;code&gt;CompositionLocalProvider&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@RunWith(AndroidJUnit4::class)
class AsyncImageScreenshotTest {

    @get:Rule
    val composableScreenshotRule = ComposableScreenshotRule()

    @ScreenshotInstrumentation
    @Test
    fun asyncImage_inspectionMode() {
        composableScreenshotRule
            .setCompose {
                CompositionLocalProvider(LocalInspectionMode provides true) {
                    AppTheme {
                        AsyncImage(
                            modifier = Modifier.size(200.dp),
                            uri = &quot;content://media/external/images/media/1&quot;.toUri()
                        )
                    }
                }
            }
            .assertSame()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using this approach, you can achieve the same result, but I’d treat it as a last resort, for two reasons.&lt;/p&gt;
&lt;p&gt;First, it introduces test-related code into the production codebase. Second, you need to keep the &lt;code&gt;Image&lt;/code&gt; and &lt;code&gt;ThirdPartyAsyncImage&lt;/code&gt; configurations in sync, as any change to parameters like contentScale or modifier must be mirrored; otherwise your screenshot tests won’t accurately reflect the real UI.&lt;/p&gt;
&lt;p&gt;Here is an example of how the mood card component with photos looks when fake images are provided.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1800&quot; height=&quot;266&quot; src=&quot;https://alexzh.com/_astro/moodcard-component-fake-images.BVLo_Ww1_12SltV.webp&quot; srcset=&quot;https://alexzh.com/_astro/moodcard-component-fake-images.BVLo_Ww1_Zrez3a.webp 720w, https://alexzh.com/_astro/moodcard-component-fake-images.BVLo_Ww1_Z1GryJw.webp 1080w, https://alexzh.com/_astro/moodcard-component-fake-images.BVLo_Ww1_ZMgI6K.webp 1440w&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Asynchronous images can lead to unpredictable screenshot tests, as screenshot testing frameworks capture the screen before image loading has finished. This can result in either a blank space where the image should be, or a different image on every run. The solution is to replace all asynchronously loaded images with fake ones. You can do that by injecting a fake image loader with &lt;em&gt;coil-test&lt;/em&gt; or a similar tool, or fall back to Compose’s inspection mode if it doesn’t. Both methods provide the same result, but a fake image loader is preferable: it keeps test-specific code out of production, and you don’t need to keep testing and production implementations in sync.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;This article is based on a chapter from my book, &lt;a href=&quot;https://leanpub.com/mastering-android-screenshot-testing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Mastering Android Screenshot Testing&lt;/a&gt;. It covers the fundamental principles, frameworks, and best practices to catch visual regressions across devices and configurations before they reach production. A free sample chapter is available &lt;a href=&quot;https://leanpub.com/mastering-android-screenshot-testing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;on Leanpub&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;Originally published on &lt;a href=&quot;https://alexzh.com/handling-asynchronous-images-in-android-screenshot-tests/&quot;&gt;alexzh.com&lt;/a&gt;.&lt;/p&gt;</content:encoded><media:content url="https://alexzh.com/_astro/cover.BalBxfYG_ZmLhnq.jpg" medium="image"/></item><item><title>My laptop died last week, and here is what it took with it</title><link>https://alexzh.com/laptop-backup-system/</link><guid isPermaLink="false">6a4c07808cfabc0001b954fd</guid><description>Last week I spilled water on my laptop. This is the story of what I lost, what I learned, and the 3-2-1 backup system I wish I&apos;d had a week earlier.</description><pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://alexzh.com/_astro/cover.Bk0jvlj5_ZxHn3l.webp&quot; alt=&quot;&quot; width=&quot;1080&quot; height=&quot;654&quot;&gt;&lt;p&gt;A few days ago, I accidentally spilled water on my laptop.&lt;/p&gt;
&lt;p&gt;It restarted a few times and turned off entirely.&lt;/p&gt;
&lt;p&gt;I did everything the internet told me to. Dried it out and didn’t touch the power button.&lt;/p&gt;
&lt;p&gt;Every repair shop was already closed. So I waited overnight, not knowing if anything on that laptop was still alive.&lt;/p&gt;
&lt;p&gt;The next morning, the guy at the shop checked it and said: “The chances of recovery are small. Do you want us to try?”&lt;/p&gt;
&lt;p&gt;Until that moment, I had hope. It was just water, not coffee, not something sticky.&lt;/p&gt;
&lt;p&gt;But now, all I could focus on was what hadn’t been backed up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My photos&lt;/li&gt;
&lt;li&gt;The fixes for the next release of my book&lt;/li&gt;
&lt;li&gt;The code for one of my personal projects&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Gone. Maybe forever.&lt;/p&gt;
&lt;p&gt;Now let me ask you something.&lt;/p&gt;
&lt;p&gt;If your laptop died right now, without any warning, what would you lose?&lt;/p&gt;
&lt;p&gt;Do you actually back up the things you can’t replace?&lt;/p&gt;
&lt;p&gt;If you just pictured something you wouldn’t want to lose, you’re exactly where I was a week ago. Here’s the simple system I wish I’d had back then:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start with what you can’t replace.&lt;/li&gt;
&lt;li&gt;Automate a backup process. A backup you have to remember is a backup you’ll forget.&lt;/li&gt;
&lt;li&gt;Follow the 3-2-1 rule: 3 copies of your data (the original included) on 2 different types of storage with 1 copy somewhere remote.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If I’d followed step 2, the changes that I made last week for my book would be safe right now.&lt;/p&gt;
&lt;p&gt;So here’s what I’d suggest.&lt;/p&gt;
&lt;p&gt;Tonight, before you close your laptop, pick the one folder you wouldn’t want to lose, and copy it somewhere else: cloud, USB drive, anywhere.&lt;/p&gt;
&lt;p&gt;That’s it. Five minutes. You can build the full 3-2-1 setup this weekend. But tonight, get your irreplaceable stuff to two places instead of one.&lt;/p&gt;
&lt;p&gt;Take care of your data (and your drinks near your laptop),&lt;/p&gt;
&lt;p&gt;Alex&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; I’m writing this from a new laptop.&lt;/p&gt;
&lt;p&gt;The repair shop called me back a few days later. They gave me the old one and said there was nothing they could do.&lt;/p&gt;
&lt;p&gt;I’m telling you this because “I’ll back it up later” was working fine for me too, right until the moment it wasn’t.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;Originally published on &lt;a href=&quot;https://alexzh.com/laptop-backup-system/&quot;&gt;alexzh.com&lt;/a&gt;.&lt;/p&gt;</content:encoded><media:content url="https://alexzh.com/_astro/cover.Bk0jvlj5_Z2iATak.jpg" medium="image"/><category>Personal</category><category>Backup</category></item><item><title>How to fix the Compose preview naming problem</title><link>https://alexzh.com/how-to-fix-the-compose-preview-naming-problem/</link><guid isPermaLink="false">697158a7f27f92000137791c</guid><description>Starting with ui-tooling-preview 1.10.0, you can give PreviewParameterProvider states custom display names instead of &quot;uiState 0&quot; and &quot;uiState 1&quot;.</description><pubDate>Thu, 22 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://alexzh.com/_astro/cover.Ca7CJTzN_5WFEF.webp&quot; alt=&quot;&quot; width=&quot;1080&quot; height=&quot;654&quot;&gt;&lt;p&gt;If you use &lt;code&gt;PreviewParameterProvider&lt;/code&gt; to avoid duplicating preview functions, you’ve probably dealt with this frustration: index-based naming like &lt;em&gt;uiState 0&lt;/em&gt;, &lt;em&gt;uiState 1&lt;/em&gt;, &lt;em&gt;uiState 2&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;When you see these names, it’s hard to tell which one relates to the empty state or error state. When you have 20+ previews across different states, themes, and font scales, index-based naming becomes something that slows you down.&lt;/p&gt;
&lt;p&gt;Good news: the recent stable release of &lt;code&gt;ui-tooling-preview&lt;/code&gt; finally solves this.&lt;/p&gt;
&lt;h2 id=&quot;quick-context-why-previewparameterprovider-matters&quot;&gt;Quick Context: Why PreviewParameterProvider Matters&lt;/h2&gt;
&lt;p&gt;If you’re already using &lt;code&gt;PreviewParameterProvider&lt;/code&gt; you can skip this section. For everyone else, here’s the short version.&lt;/p&gt;
&lt;p&gt;Instead of writing separate preview functions for each UI state (empty, loading, success, error), you create one provider that supplies multiple states to a single preview function. Let’s look at examples of testing different states of the Statistics screen. Here we want to verify empty and two states when different data are available.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1598&quot; height=&quot;1192&quot; src=&quot;https://alexzh.com/_astro/previews-demo.BOlN8Ke-_Z2ddsVB.webp&quot; srcset=&quot;https://alexzh.com/_astro/previews-demo.BOlN8Ke-_1pv8Rk.webp 720w, https://alexzh.com/_astro/previews-demo.BOlN8Ke-_Z1TF7dW.webp 1080w, https://alexzh.com/_astro/previews-demo.BOlN8Ke-_2fgUKs.webp 1440w&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Now, let’s take a look at the source code of the provider:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class StatisticsScreenUiStateProvider : PreviewParameterProvider&amp;lt;StatisticsScreenUiState&amp;gt; {
    override val values: Sequence&amp;lt;StatisticsScreenUiState&amp;gt;
        get() = sequenceOf(
            StatisticsScreenUiState(
                isLoading = false,
                selectedDateRange = SelectedDateRangeData(...)
            ),
            StatisticsScreenUiState(
                isLoading = false,
                selectedDateRange = SelectedDateRangeData(...),
                averageDailyMoodChartData = AverageDailyMoodChartData(...),
                actionImpactChartData = ActionImpactChartData(...)
            ),
            StatisticsScreenUiState(
                isLoading = false,
                selectedDateRange = SelectedDateRangeData(...),
                averageDailyMoodChartData = AverageDailyMoodChartData(...)
            )
        )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The main problem with this approach is the default index-based naming for the preview functions, like &lt;em&gt;uiState 0&lt;/em&gt;, &lt;em&gt;uiState 1&lt;/em&gt;, &lt;em&gt;uiState 2&lt;/em&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1598&quot; height=&quot;1192&quot; src=&quot;https://alexzh.com/_astro/highlighted-naming-problem.kVJkw0Oe_ZhoVMg.webp&quot; srcset=&quot;https://alexzh.com/_astro/highlighted-naming-problem.kVJkw0Oe_ZdFwqa.webp 720w, https://alexzh.com/_astro/highlighted-naming-problem.kVJkw0Oe_2wtYym.webp 1080w, https://alexzh.com/_astro/highlighted-naming-problem.kVJkw0Oe_R3O4n.webp 1440w&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;solution-provide-a-name-for-every-state&quot;&gt;Solution: Provide a name for every state&lt;/h2&gt;
&lt;p&gt;Starting with &lt;code&gt;ui-tooling-preview:1.10.0&lt;/code&gt;, you can replace default index-based names with descriptive labels. The &lt;code&gt;getDisplayName(index: Int): String?&lt;/code&gt; function gives you full control over the names of preview functions displayed in the preview panel.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class StatisticsScreenUiStateProvider : PreviewParameterProvider&amp;lt;StatisticsScreenUiState&amp;gt; {

    ...
    
    override fun getDisplayName(index: Int): String? {
        return when (index) {
            0 -&amp;gt; &quot;Empty&quot;
            1 -&amp;gt; &quot;Average Daily Mood &amp;amp; Action Impact&quot;
            2 -&amp;gt; &quot;Average Daily Mood&quot;
            else -&amp;gt; super.getDisplayName(index)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your preview panel now shows what each state represents.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1596&quot; height=&quot;1196&quot; src=&quot;https://alexzh.com/_astro/highlighted-solution-demo.CUHLpulQ_CqLrq.webp&quot; srcset=&quot;https://alexzh.com/_astro/highlighted-solution-demo.CUHLpulQ_Xt6Rv.webp 720w, https://alexzh.com/_astro/highlighted-solution-demo.CUHLpulQ_Z1GwJtT.webp 1080w, https://alexzh.com/_astro/highlighted-solution-demo.CUHLpulQ_2vu42v.webp 1440w&quot;&gt;&lt;/figure&gt;
&lt;aside class=&quot;callout callout--note&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Requirement&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
This feature works correctly in Android Studio Narwhal 3 Feature Drop (2025.1.3) or newer.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;The basic approach from the previous example works, but it has a big problem: your state objects live separately from labels. If you add a new state and forget to update the name mapping, your labels will get mixed up again.&lt;/p&gt;
&lt;p&gt;To fix this problem, we will use the &lt;code&gt;Pair&amp;lt;String, StatisticsUiState&amp;gt;&lt;/code&gt; to connect each name with its state.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class StatisticsScreenUiStateProvider : PreviewParameterProvider&amp;lt;StatisticsScreenUiState&amp;gt; {
        val data = listOf(
            Pair(
                &quot;Empty&quot;,
                StatisticsScreenUiState(
                    isLoading = false,
                    selectedDateRange = SelectedDateRangeData(...)
                )
            ),
            Pair(
                &quot;Average Daily Mood &amp;amp; Action Impact&quot;,
                StatisticsScreenUiState(
                    isLoading = false,
                    selectedDateRange = SelectedDateRangeData(...),
                    averageDailyMoodChartData = AverageDailyMoodChartData(...),
                    actionImpactChartData = ActionImpactChartData(...)
                )
            ),
            Pair(
                &quot;Average Daily Mood&quot;,
                StatisticsScreenUiState(
                    isLoading = false,
                    selectedDateRange = SelectedDateRangeData(...),
                    averageDailyMoodChartData = AverageDailyMoodChartData(...)
            )
        )

        override val values: Sequence&amp;lt;StatisticsScreenUiState&amp;gt;
            get() = data.map { it.second }.asSequence()

        override fun getDisplayName(index: Int): String {
            return data[index].first
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, it’s impossible to add a state without naming it.&lt;/p&gt;
&lt;aside class=&quot;callout callout--note&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Limitation&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
The Compose Screenshot Testing Tool (0.0.1-alpha13) doesn’t yet support custom display names. Your screenshot test output will still show index-based naming even if Android Studio displays your custom names correctly.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;If you’re using Jetpack Compose BOM &lt;em&gt;2026.01.00&lt;/em&gt; or later, you already have &lt;code&gt;ui-tooling-preview:1.10.1&lt;/code&gt; and this feature is ready to use.&lt;/p&gt;
&lt;h2 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Instead of writing multiple preview functions for different UI states, create a single provider that supplies many states to one preview function.&lt;/li&gt;
&lt;li&gt;Starting with &lt;code&gt;ui-tooling-preview:1.10.0&lt;/code&gt;, you can replace default index-based naming (e.g., “uiState 0”) with descriptive custom names. This naming feature requires Android Studio Narwhal 3 Feature Drop (2025.1.3) or newer.&lt;/li&gt;
&lt;li&gt;Custom display names don’t yet work with the Compose Screenshot Testing Tool (0.0.1-alpha13), which still uses index-based naming.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;&lt;p&gt;Originally published on &lt;a href=&quot;https://alexzh.com/how-to-fix-the-compose-preview-naming-problem/&quot;&gt;alexzh.com&lt;/a&gt;.&lt;/p&gt;</content:encoded><media:content url="https://alexzh.com/_astro/cover.Ca7CJTzN_Z1DUPrj.jpg" medium="image"/><category>Jetpack Compose</category></item><item><title>Why Visual Testing Is Critical for Mobile App Quality</title><link>https://alexzh.com/why-visual-testing-is-critical-for-mobile-app-quality/</link><guid isPermaLink="false">67d3451c1c9e7b000150bd97</guid><description>Visual tests catch UI imperfections that traditional UI tests miss, like misalignments, color issues and layout problems across devices and themes.</description><pubDate>Thu, 13 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://alexzh.com/_astro/cover.DiPiEJF2_v9AUW.webp&quot; alt=&quot;&quot; width=&quot;1080&quot; height=&quot;654&quot;&gt;&lt;p&gt;Have you ever shipped an app with a broken UI?&lt;/p&gt;
&lt;p&gt;In today’s fast-paced development environment, it is happening more often, even for teams who have manual QA and automated UI tests in place.&lt;/p&gt;
&lt;p&gt;We often consider UI issues less important, but they can be much more important than just misalignment of a button. Many UI issues can make your application inaccessible because the text color may have not enough contrast, which leads to unreadable text. On the other side, when users see an unpolished UI, they lose trust in your brand, and bad app reviews can lead to a reduced number of downloads.&lt;/p&gt;
&lt;p&gt;But wait… I already have UI tests.&lt;/p&gt;
&lt;p&gt;Even if you have UI tests, this doesn’t mean that they verify pixel perfectness. Many UI tests check the behavior of components, screens, or an application, but not pixel-perfectness.&lt;/p&gt;
&lt;p&gt;Different types of UI tests verify different aspects of the application. End-To-End and UI tests with fake data focus on user behavior, but they don’t check alignment between elements, color correctness for light and dark modes, and other visual details.&lt;/p&gt;
&lt;p&gt;There is a solution - visual testing.&lt;/p&gt;
&lt;h2 id=&quot;what-is-visual-testing&quot;&gt;What is Visual Testing?&lt;/h2&gt;
&lt;p&gt;Visual tests focus on identifying pixel imperfections in your components and screens. In Android development, visual tests are implemented through screenshot comparison. This involves comparing the current state of the UI against a baseline screenshot (commonly called “golden screenshots”) that represents the expected state of a component or a screen.&lt;/p&gt;
&lt;p&gt;The screenshot comparison allows you to catch the following problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Misaligned components and spacing inconsistencies&lt;/li&gt;
&lt;li&gt;Incorrect colors in light mode, dark mode, and custom themes&lt;/li&gt;
&lt;li&gt;Broken layouts for various devices (phones, tablets, foldable devices) and font sizes&lt;/li&gt;
&lt;li&gt;Rendering errors in Right-To-Left (RTL) and Left-to-Right (LTR) layouts, often related to locale settings&lt;/li&gt;
&lt;li&gt;Localization problems, such as text overflow when content in certain languages exceeds available space&lt;/li&gt;
&lt;li&gt;Accessibility concerns related to displaying content, such as color contrast&lt;/li&gt;
&lt;li&gt;Unexpected rendering problems in specific scenarios&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1034&quot; height=&quot;720&quot; src=&quot;https://alexzh.com/_astro/ui-issues.C2OEoDS1_20MHOR.webp&quot; srcset=&quot;https://alexzh.com/_astro/ui-issues.C2OEoDS1_2cJmY9.webp 720w, https://alexzh.com/_astro/ui-issues.C2OEoDS1_20MHOR.webp 1034w&quot;&gt;&lt;/figure&gt;
&lt;p&gt;There are multiple frameworks for screenshot testing in Android. Here are the most popular frameworks:&lt;/p&gt;
&lt;table data-stack=&quot;rows&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Local vs Instrumentation tests&lt;/th&gt;
&lt;th&gt;Rendering engine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Framework&quot;&gt;&lt;a href=&quot;https://github.com/pedrovgs/Shot&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Shot&lt;/a&gt;&lt;/td&gt;
&lt;td data-label=&quot;Local vs Instrumentation tests&quot;&gt;Instrumentation tests&lt;/td&gt;
&lt;td data-label=&quot;Rendering engine&quot;&gt;Device rendering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Framework&quot;&gt;&lt;a href=&quot;https://github.com/takahirom/roborazzi&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Roborazzi&lt;/a&gt;&lt;/td&gt;
&lt;td data-label=&quot;Local vs Instrumentation tests&quot;&gt;Local tests&lt;/td&gt;
&lt;td data-label=&quot;Rendering engine&quot;&gt;Robolectric Native Graphics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Framework&quot;&gt;&lt;a href=&quot;https://github.com/cashapp/paparazzi&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Paparazzi&lt;/a&gt;&lt;/td&gt;
&lt;td data-label=&quot;Local vs Instrumentation tests&quot;&gt;Local tests&lt;/td&gt;
&lt;td data-label=&quot;Rendering engine&quot;&gt;Layoutlib&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Framework&quot;&gt;&lt;a href=&quot;https://developer.android.com/studio/preview/compose-screenshot-testing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Compose Preview Screenshot Testing&lt;/a&gt;&lt;/td&gt;
&lt;td data-label=&quot;Local vs Instrumentation tests&quot;&gt;Local tests&lt;/td&gt;
&lt;td data-label=&quot;Rendering engine&quot;&gt;Layoutlib&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Note: The “Layoutlib” renders @Previews in Android Studio.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A detailed framework comparison will be covered in one of the following articles.&lt;/p&gt;
&lt;p&gt;These frameworks follow a similar workflow with two commands:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The “record” command generates golden screenshots for your tests.&lt;/li&gt;
&lt;li&gt;The “verify” command compares the current UI state against the golden screenshots.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Command syntax varies between frameworks. For example, when using the Shot framework, you’ll use the &lt;code&gt;-Precord&lt;/code&gt; parameter to generate baseline images:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./gradlew :app:debugExecuteScreenshotTests -Precord&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ok, it sounds simple, but show me the test case.&lt;/p&gt;
&lt;h2 id=&quot;first-visual-tests-with-shot-framework&quot;&gt;First Visual Tests with Shot Framework&lt;/h2&gt;
&lt;aside class=&quot;callout callout--note&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;&lt;p&gt;To ensure consistency of your screenshot tests, it’s crucial to use consistent fake data and use the same emulator or device (if you use instrumentation tests). Using different devices or emulators will cause resolution variations and lead to test failures.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Let’s explore two screenshot tests for the statistics screen for the mood tracker app using the Shot framework. The tests will focus on the UI states representing Empty and Success states (data available for chart rendering).&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class StatisticsScreenScreenshotTest : ScreenshotTest {
        companion object {
        val TEST_DATE = LocalDate(2024, Month.SEPTEMBER, 15)
    }
    
    @get:Rule
    val composeTestRule = createComposeRule()

    private val dateProvider = mockk&amp;lt;DateProvider&amp;gt;()
    private val moodHistoryRepository = mockk&amp;lt;MoodHistoryRepository&amp;gt;()

    @Before
    fun setUp() {
        initDI()
    }
    
    @Test
    fun statisticsScreen_noData() {
        val startDate = TEST_DATE.atStartOfMonth().atStartOfDayIn(
            defaultTimeZone
        )
        val endDate = TEST_DATE.atEndOfMonth().atEndOfDay()

        every { dateProvider.getCurrentDate() } returns TEST_DATE

        every {
            moodHistoryRepository.getAverageDayToHappiness(startDate, endDate)
        } returns flowOf(emptyList())

        every {
            moodHistoryRepository.getActivityToHappinessByDate(startDate, endDate)
        } returns flowOf(emptyList())

        composeTestRule.setContent {
            FeelTrackerAppTheme {
                StatisticsScreen(
                    viewModel = koinViewModel(),
                    onHome = { },
                    onBreathingPatternSelection = { },
                    onSettings = { }
                )
            }
        }

        compareScreenshot(
            rule = composeTestRule,
            name = &quot;statisticsScreen_noData&quot;
        )
    }

    @Test
    fun statisticsScreen_hasData() {
        val startDate = TEST_DATE.atStartOfMonth().atStartOfDayIn(
            defaultTimeZone
        )
        val endDate = TEST_DATE.atEndOfMonth().atEndOfDay()
        val firstDateOfTheMonth = TEST_DATE.atStartOfMonth()

        every { dateProvider.getCurrentDate() } returns StatisticsScreenScreenshotTest.TEST_DATE

        every {
            moodHistoryRepository.getAverageDayToHappiness(startDate, endDate)
        } returns averageDayToHappinessChartData(firstDateOfTheMonth)

        every {
            moodHistoryRepository.getActivityToHappinessByDate(startDate, endDate)
        } returns activityToHappinessData()

        composeTestRule.setContent {
            FeelTrackerAppTheme {
                StatisticsScreen(
                    viewModel = koinViewModel(),
                    onHome = { },
                    onBreathingPatternSelection = { },
                    onSettings = { }
                )
            }
        }

        compareScreenshot(
            rule = composeTestRule,
            name = &quot;statisticsScreen_hasData&quot;
        )
    }
    
    private fun initDI() {
        stopKoin()
        startKoin {
            allowOverride(true)
            androidContext(InstrumentationRegistry.getInstrumentation().targetContext)
            modules(
                ...
                module {
                    single { dateProvider }
                    single { moodHistoryRepository }
                }
            )
        }
    }

    private fun averageDayToHappinessChartData(startDate: LocalDate): Flow&amp;lt;List&amp;lt;MoodDayToHappiness&amp;gt;&amp;gt; {
        return flowOf(listOf(...))
    }

    private fun activityToHappinessData(): Flow&amp;lt;List&amp;lt;ActivityToHappiness&amp;gt;&amp;gt; {
        return flowOf(listOf(...))
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Important notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Test class should implement the “ScreenshotTest” interface to screenshot comparison functionality&lt;/li&gt;
&lt;li&gt;Fixed date is needed for reproducibility of the screenshot test&lt;/li&gt;
&lt;li&gt;The test uses mock instances of data provider and repository to emulate different screen states. It uses the “Koin” framework.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To generate baseline (golden) screenshots, run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./gradlew :app:debugExecuteScreenshotTests -Precord&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command saves the current state of UI as the reference point for future comparisons. Now, let’s simulate the real-world scenario by changing the title of the average daily mood chart from “AVERAGE DAILY MOOD” to “DAILY MOOD” and run the verification:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./gradlew :app:debugExecuteScreenshotTests&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After execution, the Shot framework generates a report that highlights the differences:&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; style=&quot;max-width: calc(640px * 2000 / 1791)&quot; sizes=&quot;(min-width: 763px) 715px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;2000&quot; height=&quot;1791&quot; src=&quot;https://alexzh.com/_astro/screenshot-tests-report.IfxfiTcN_ZaWThD.webp&quot; srcset=&quot;https://alexzh.com/_astro/screenshot-tests-report.IfxfiTcN_Z74AAc.webp 720w, https://alexzh.com/_astro/screenshot-tests-report.IfxfiTcN_Z1jSWkI.webp 1080w, https://alexzh.com/_astro/screenshot-tests-report.IfxfiTcN_Z2pptuD.webp 1440w&quot;&gt;&lt;/figure&gt;
&lt;p&gt;&lt;em&gt;Note: This image includes the zoom effect only for demonstration purposes.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This visual feedback is significantly more efficient than manually inspecting each component and screen.&lt;/p&gt;
&lt;p&gt;To better understand the difference between different types of UI tests and how they complement each other, check out the &lt;a href=&quot;https://alexzh.com/not-all-ui-tests-are-the-same/&quot;&gt;“Not all UI tests are the same”&lt;/a&gt; article.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Visual testing is critical for delivering applications without visual imperfections. While UI tests focus on behavior verification, they cannot check pixel perfectness of your application.&lt;/p&gt;
&lt;p&gt;By implementing visual testing, development teams can catch visual imperfections before releasing an application. These tests will catch component misalignments, color incorrectness across various themes, visual issues on different devices, and much more.&lt;/p&gt;
&lt;p&gt;Unpolished UI always looks unprofessional and can damage trust in your brand. By incorporating UI tests into the development process, you’re not just checking pixel perfectness, but also protecting brand reputation.&lt;/p&gt;
&lt;p&gt;Stay tuned for my upcoming articles where I’ll dive deeper into framework comparisons and share best practices for integrating visual testing into Android projects.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;Originally published on &lt;a href=&quot;https://alexzh.com/why-visual-testing-is-critical-for-mobile-app-quality/&quot;&gt;alexzh.com&lt;/a&gt;.&lt;/p&gt;</content:encoded><media:content url="https://alexzh.com/_astro/cover.DiPiEJF2_Z1eIUb2.jpg" medium="image"/><category>Android Testing</category><category>Visual Testing</category><category>Screenshot Testing</category></item><item><title>A Visual Guide to TopAppBar Variants in Jetpack Compose</title><link>https://alexzh.com/visual-guide-to-topappbar-variants-in-jetpack-compose/</link><guid isPermaLink="false">67ae7fc7ea77d9000133f82a</guid><description>Material 3 offers several TopAppBar components. This visual guide explores each variant, including centered titles, expandable layouts and subtitles.</description><pubDate>Fri, 14 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://alexzh.com/_astro/cover.COJW-M--_2uMEwo.webp&quot; alt=&quot;&quot; width=&quot;1080&quot; height=&quot;654&quot;&gt;&lt;p&gt;The TopAppBar is a commonly used component in Jetpack Compose as many screens use it. While developers commonly customize the TopAppBar. Material 3 library provides predefined variations to simplify development and follow Material Design guidelines.&lt;/p&gt;
&lt;p&gt;In this article, we will explore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The different TopAppBar components offered by the Material 3 library.&lt;/li&gt;
&lt;li&gt;The default typography and height in each TopAppBar variant.&lt;/li&gt;
&lt;/ul&gt;
&lt;aside class=&quot;callout callout--note&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;&lt;p&gt;This article focuses on TopAppBar variations with consistent title, navigation icons and actions. All expandable/collapsable components will have a consistent scrolling experience.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Let’s look at the default text styles for titles and subtitles, as well as the component height, to help choose the best &lt;code&gt;TopAppBar&lt;/code&gt; components for your design and use case. These parameters can also be customized.&lt;/p&gt;
&lt;p&gt;For easy comparison, I created a comprehensive table summarizing all the available options. Below, you’ll find detailed information about each TopAppBar variant, alongside with a basic example.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1920&quot; height=&quot;1080&quot; src=&quot;https://alexzh.com/_astro/topappbar-variants-table.Q8aedpwZ_1BpepG.webp&quot; srcset=&quot;https://alexzh.com/_astro/topappbar-variants-table.Q8aedpwZ_RL24P.webp 720w, https://alexzh.com/_astro/topappbar-variants-table.Q8aedpwZ_26hU7M.webp 1080w, https://alexzh.com/_astro/topappbar-variants-table.Q8aedpwZ_1QIFfC.webp 1440w&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;topappbar&quot;&gt;TopAppBar&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;TopAppBar&lt;/code&gt; is the most widespread component.&lt;/p&gt;
&lt;p&gt;&lt;video src=&quot;https://alexzh.com/media/visual-guide-to-topappbar-variants-in-jetpack-compose/Demo_TopAppBar-1.mp4&quot; poster=&quot;https://alexzh.com/media/visual-guide-to-topappbar-variants-in-jetpack-compose/Demo_TopAppBar-1_poster.webp&quot; width=&quot;1280&quot; height=&quot;720&quot; loop muted playsinline preload=&quot;none&quot; data-autoplay&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;Default values:&lt;/p&gt;
&lt;table data-stack=&quot;rows&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;TopAppBar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Title Text Style&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;TopAppBar&quot;&gt;TitleLarge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Height&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;TopAppBar&quot;&gt;64dp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun Demo_TopAppBar() {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(&quot;Title&quot;) },
                navigationIcon = {
                    IconButton(onClick = {}) {
                        Icon(Icons.AutoMirrored.Default.ArrowBack, &quot;Back&quot;)
                    }
                },
                actions = {
                    IconButton(onClick = {}) {
                        Icon(Icons.Default.AttachFile, &quot;Attach&quot;)
                    }
                    IconButton(onClick = {}) {
                        Icon(Icons.Default.MoreVert, &quot;More&quot;)
                    }
                }
            )
        }
    ) { innerPadding -&amp;gt;
        Column(
            Modifier.fillMaxWidth().padding(innerPadding).verticalScroll(rememberScrollState())
        ) {
            Text(
                modifier = Modifier.padding(16.dp),
                text = remember { LoremIpsum().values.first() },
                fontSize = 16.sp
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;centeralignedtopappbar&quot;&gt;CenterAlignedTopAppBar&lt;/h2&gt;
&lt;p&gt;Similar to the standard &lt;code&gt;TopAppBar&lt;/code&gt;, but with one key difference: the title is horizontally centered.&lt;/p&gt;
&lt;p&gt;&lt;video src=&quot;https://alexzh.com/media/visual-guide-to-topappbar-variants-in-jetpack-compose/Demo_CenterAlignedTopAppBar.mp4&quot; poster=&quot;https://alexzh.com/media/visual-guide-to-topappbar-variants-in-jetpack-compose/Demo_CenterAlignedTopAppBar_poster.webp&quot; width=&quot;1280&quot; height=&quot;720&quot; loop muted playsinline preload=&quot;none&quot; data-autoplay&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;Default values:&lt;/p&gt;
&lt;table data-stack=&quot;rows&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;CenterAlignedTopAppBar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Title Text Style&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;CenterAlignedTopAppBar&quot;&gt;TitleLarge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Height&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;CenterAlignedTopAppBar&quot;&gt;64dp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun Demo_CenterAlignedTopAppBar() {
    Scaffold(
        topBar = {
            CenterAlignedTopAppBar(
                title = { Text(&quot;Title&quot;) },
                navigationIcon = {
                    IconButton(onClick = {}) {
                        Icon(Icons.AutoMirrored.Default.ArrowBack, &quot;Back&quot;)
                    }
                },
                actions = {
                    IconButton(onClick = {}) {
                        Icon(Icons.Default.AttachFile, &quot;Attach&quot;)
                    }
                    IconButton(onClick = {}) {
                        Icon(Icons.Default.MoreVert, &quot;More&quot;)
                    }
                }
            )
        }
    ) { innerPadding -&amp;gt;
        Column(
            Modifier.fillMaxWidth().padding(innerPadding).verticalScroll(rememberScrollState())
        ) {
            Text(
                modifier = Modifier.padding(16.dp),
                text = remember { LoremIpsum().values.first() },
                fontSize = 16.sp
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;mediumtopappbar-and-largetopappbar&quot;&gt;MediumTopAppBar and LargeTopAppBar&lt;/h2&gt;
&lt;p&gt;These components introduce dynamic behavior, allowing the components to expand and collapse. They can display titles, navigation icons, and actions.&lt;/p&gt;
&lt;p&gt;&lt;video src=&quot;https://alexzh.com/media/visual-guide-to-topappbar-variants-in-jetpack-compose/Demo_MediumTopAppBar_And_LargeTopAppBar.mp4&quot; poster=&quot;https://alexzh.com/media/visual-guide-to-topappbar-variants-in-jetpack-compose/Demo_MediumTopAppBar_And_LargeTopAppBar_poster.webp&quot; width=&quot;1280&quot; height=&quot;720&quot; loop muted playsinline preload=&quot;none&quot; data-autoplay&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;Default values:&lt;/p&gt;
&lt;table data-stack=&quot;rows&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;MediumTopAppBar&lt;/th&gt;
&lt;th&gt;LargeTopAppBar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Title Text Style (Collapsed)&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;MediumTopAppBar&quot;&gt;TitleLarge&lt;/td&gt;
&lt;td data-label=&quot;LargeTopAppBar&quot;&gt;TitleLarge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Title Text Style (Expanded)&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;MediumTopAppBar&quot;&gt;HeadlineSmall&lt;/td&gt;
&lt;td data-label=&quot;LargeTopAppBar&quot;&gt;HeadlineMedium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Collapsed Height&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;MediumTopAppBar&quot;&gt;64dp&lt;/td&gt;
&lt;td data-label=&quot;LargeTopAppBar&quot;&gt;64dp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Expanded Height&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;MediumTopAppBar&quot;&gt;112dp&lt;/td&gt;
&lt;td data-label=&quot;LargeTopAppBar&quot;&gt;152dp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun Demo_MediumTopAppBar() {
    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()

    Scaffold(
        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
        topBar = {
            MediumTopAppBar(
                title = { Text(&quot;Title&quot;) },
                navigationIcon = {
                    IconButton(onClick = {}) {
                        Icon(Icons.AutoMirrored.Default.ArrowBack, &quot;Back&quot;)
                    }
                },
                actions = {
                    IconButton(onClick = {}) {
                        Icon(Icons.Default.AttachFile, &quot;Attach&quot;)
                    }
                    IconButton(onClick = {}) {
                        Icon(Icons.Default.MoreVert, &quot;More&quot;)
                    }
                },
                scrollBehavior = scrollBehavior
            )
        }
    ) { innerPadding -&amp;gt;
        Column(
            Modifier.fillMaxWidth().padding(innerPadding).verticalScroll(rememberScrollState())
        ) {
            Text(
                modifier = Modifier.padding(16.dp),
                text = remember { LoremIpsum().values.first() },
                fontSize = 16.sp
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;mediumflexibletopappbar-and-largeflexibletopappbar&quot;&gt;MediumFlexibleTopAppBar and LargeFlexibleTopAppBar&lt;/h2&gt;
&lt;p&gt;These components add the capability to display subtitle alongside with titles, navigation icons, and actions.&lt;/p&gt;
&lt;p&gt;&lt;video src=&quot;https://alexzh.com/media/visual-guide-to-topappbar-variants-in-jetpack-compose/Demo_MediumFlexibleTopAppBar_And_LargeFlexibleTopAppBar.mp4&quot; poster=&quot;https://alexzh.com/media/visual-guide-to-topappbar-variants-in-jetpack-compose/Demo_MediumFlexibleTopAppBar_And_LargeFlexibleTopAppBar_poster.webp&quot; width=&quot;1280&quot; height=&quot;720&quot; loop muted playsinline preload=&quot;none&quot; data-autoplay&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;Default values:&lt;/p&gt;
&lt;table data-stack=&quot;rows&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;MediumFlexibleTopAppBar&lt;/th&gt;
&lt;th&gt;LargeFlexibleTopAppBar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Title Text Style (Collapsed)&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;MediumFlexibleTopAppBar&quot;&gt;TitleLarge&lt;/td&gt;
&lt;td data-label=&quot;LargeFlexibleTopAppBar&quot;&gt;TitleLarge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Title Text Style (Expanded)&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;MediumFlexibleTopAppBar&quot;&gt;HeadlineMedium&lt;/td&gt;
&lt;td data-label=&quot;LargeFlexibleTopAppBar&quot;&gt;DisplaySmall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Subtitle Text Style (Collapsed)&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;MediumFlexibleTopAppBar&quot;&gt;LabelMedium&lt;/td&gt;
&lt;td data-label=&quot;LargeFlexibleTopAppBar&quot;&gt;LabelMedium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Subtitle Text Style (Expanded)&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;MediumFlexibleTopAppBar&quot;&gt;LabelLarge&lt;/td&gt;
&lt;td data-label=&quot;LargeFlexibleTopAppBar&quot;&gt;TitleMedium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Collapsed Height&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;MediumFlexibleTopAppBar&quot;&gt;64dp&lt;/td&gt;
&lt;td data-label=&quot;LargeFlexibleTopAppBar&quot;&gt;64dp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Expanded Height (with subtitle)&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;MediumFlexibleTopAppBar&quot;&gt;136dp&lt;/td&gt;
&lt;td data-label=&quot;LargeFlexibleTopAppBar&quot;&gt;152dp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Expanded Height (without subtitle)&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;MediumFlexibleTopAppBar&quot;&gt;112dp&lt;/td&gt;
&lt;td data-label=&quot;LargeFlexibleTopAppBar&quot;&gt;120dp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Preview
@Composable
fun Demo_MediumFlexibleTopAppBar() {
    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
    Scaffold(
        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
        topBar = {
            MediumFlexibleTopAppBar(
                title = { Text(&quot;Title&quot;) },
                subtitle = { Text(&quot;Subtitle&quot;) },
                navigationIcon = {
                    IconButton(onClick = {}) {
                        Icon(Icons.AutoMirrored.Default.ArrowBack, &quot;Back&quot;)
                    }
                },
                actions = {
                    IconButton(onClick = {}) {
                        Icon(Icons.Default.AttachFile, &quot;Attach&quot;)
                    }
                    IconButton(onClick = {}) {
                        Icon(Icons.Default.MoreVert, &quot;More&quot;)
                    }
                },
                scrollBehavior = scrollBehavior
            )
        }
    ) { innerPadding -&amp;gt;
        Column(
            Modifier.fillMaxWidth().padding(innerPadding).verticalScroll(rememberScrollState())
        ) {
            Text(
                modifier = Modifier.padding(16.dp),
                text = remember { LoremIpsum().values.first() },
                fontSize = 16.sp
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;tworowstopappbar&quot;&gt;TwoRowsTopAppBar&lt;/h2&gt;
&lt;p&gt;This component also supports expanded and collapsed states. It includes title, subtitle, navigation icons, and actions. The &lt;code&gt;TwoRowsTopAppBar&lt;/code&gt; component simplifies the management of different text strings for expanded and collapsed states compared to &lt;code&gt;MediumFlexibleTopAppBar&lt;/code&gt;.&lt;/p&gt;
&lt;aside class=&quot;callout callout--note&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;&lt;p&gt;The &lt;code&gt;TwoRowsTopAppBar&lt;/code&gt; was added in version 1.4.0-alpha07.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;&lt;video src=&quot;https://alexzh.com/media/visual-guide-to-topappbar-variants-in-jetpack-compose/Demo_TwoRowsTopAppBar-1.mp4&quot; poster=&quot;https://alexzh.com/media/visual-guide-to-topappbar-variants-in-jetpack-compose/Demo_TwoRowsTopAppBar-1_poster.webp&quot; width=&quot;1280&quot; height=&quot;720&quot; loop muted playsinline preload=&quot;none&quot; data-autoplay&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;Default values:&lt;/p&gt;
&lt;table data-stack=&quot;rows&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;TwoRowsTopAppBar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Title Text Style (Collapsed)&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;TwoRowsTopAppBar&quot;&gt;TitleLarge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Title Text Style (Expanded)&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;TwoRowsTopAppBar&quot;&gt;HeadlineMedium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Subtitle Text Style (Collapsed)&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;TwoRowsTopAppBar&quot;&gt;LabelMedium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Subtitle Text Style (Expanded)&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;TwoRowsTopAppBar&quot;&gt;LabelLarge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Collapsed Height&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;TwoRowsTopAppBar&quot;&gt;64dp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Expanded Height (with subtitle)&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;TwoRowsTopAppBar&quot;&gt;136dp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Property&quot;&gt;&lt;strong&gt;Expanded Height (without subtitle)&lt;/strong&gt;&lt;/td&gt;
&lt;td data-label=&quot;TwoRowsTopAppBar&quot;&gt;112dp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Composable
fun Demo_TwoRowsTopAppBar() {
    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
    Scaffold(
        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
        topBar = {
            TwoRowsTopAppBar(
                title = { expanded -&amp;gt;
                    Text(
                        text = if (expanded) &quot;Expanded Title&quot; else &quot;Collapsed Title&quot;,
                        maxLines = 1,
                        overflow = TextOverflow.Ellipsis
                    )
                },
                subtitle = { expanded -&amp;gt;
                    Text(
                        text = if (expanded) &quot;Expanded Subtitle&quot; else &quot;Collapsed Subtitle&quot;,
                        maxLines = 1,
                        overflow = TextOverflow.Ellipsis
                    )
                },
                navigationIcon = {
                    IconButton(onClick = { /* doSomething() */ }) {
                        Icon(
                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                            contentDescription = &quot;Localized description&quot;
                        )
                    }
                },
                scrollBehavior = scrollBehavior
            )
        }
    ) { innerPadding -&amp;gt;
        Column(
            Modifier.fillMaxWidth().padding(innerPadding).verticalScroll(rememberScrollState())
        ) {
            Text(
                modifier = Modifier.padding(16.dp),
                text = remember { LoremIpsum().values.first() },
                fontSize = 16.sp
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;By exploring different &lt;code&gt;TopAppBar&lt;/code&gt; variants, we’ve seen that Material 3 offers pre-built solutions for many common use cases.&lt;/p&gt;
&lt;p&gt;Understanding these predefined options saves development time and effort. Instead of reinventing the wheel and customizing the basic &lt;code&gt;TopAppBar&lt;/code&gt; for each use case, you can leverage the pre-built solutions provided by the Material 3 library.&lt;/p&gt;
&lt;p&gt;The next time, explore the available options in the Material 3 library. It may save you time and you will already use a polished solution.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;Originally published on &lt;a href=&quot;https://alexzh.com/visual-guide-to-topappbar-variants-in-jetpack-compose/&quot;&gt;alexzh.com&lt;/a&gt;.&lt;/p&gt;</content:encoded><media:content url="https://alexzh.com/_astro/cover.COJW-M--_JT8pp.jpg" medium="image"/><category>Jetpack Compose</category></item><item><title>Overcoming Comparison and Finding Joy in Writing My First Book</title><link>https://alexzh.com/overcoming-comparison-and-finding-joy-in-writing-my-first-book/</link><guid isPermaLink="false">679cf48080755900015c173b</guid><description>Writing my first book was tough. I lost the joy of writing until I changed my approach. Learn how small changes can reignite your writing passion.</description><pubDate>Fri, 31 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://alexzh.com/_astro/cover.CV7NBhOP_2t6vcU.webp&quot; alt=&quot;&quot; width=&quot;1080&quot; height=&quot;654&quot;&gt;&lt;p&gt;The past year was a roller coaster for me. In 2024, I began taking the first steps to write my first book. I started working on planning and creating the structure of my book and the writing process started pretty easily, but after the first chapter, I made a big mistake.&lt;/p&gt;
&lt;p&gt;I started reading other technical books and compared the polished version of these books to my raw first draft. It’s a good idea to read books in your genre, but doing this made me doubt my writing skills. I forgot to remind myself that I was comparing my first iteration to a polished version that had gone through multiple iterations.&lt;/p&gt;
&lt;p&gt;I spent weeks writing and comparing, but it made writing a book more complicated and less enjoyable. I only wrote a small number of words each writing session, pretending that I was writing a book. Then I finally admitted to myself that I wasn’t actually writing a book; I was just imitating the process and finding no enjoyment in it at all.&lt;/p&gt;
&lt;p&gt;I decided that I had to either take writing seriously or stop it altogether. I set a goal to write 15,000 words in 3 weeks. If I couldn’t meet this goal, I knew it was time to stop lying to myself and move on. During one week of those three, I went on vacation with my family. We enjoyed the sun and went swimming, but I also made time for writing. After 19 days, I had written almost 17,000 words and after that I completely changed my approach for writing the first draft of my book.&lt;/p&gt;
&lt;p&gt;I wrote down all my thoughts about the topic of my book, reminding myself that I could always revise my manuscript later. My focus was not on writing perfectly, but on expressing everything I wanted to say. And in the meantime, I stopped reading other books in the genre until I finished the first draft. This helped me complete the manuscript in 3 months. Writing this way made me enjoy writing again.&lt;/p&gt;
&lt;p&gt;Of course, I found a lot of mistakes while self-reviewing my first draft, but the first draft is not a polished version of the book.&lt;/p&gt;
&lt;p&gt;This experience taught me several valuable lessons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Creating something great takes time and practice.&lt;/li&gt;
&lt;li&gt;Enjoyment is an important part of the process.&lt;/li&gt;
&lt;li&gt;Even small adjustments can bring back enjoyment and motivation.&lt;/li&gt;
&lt;li&gt;Break down your goal into small chunks, take action, and reflect. Without this approach, progress will be slow and efficiency will suffer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’m still on my journey to writing my first book, and it’s already been a roller coaster ride full of unexpected twists and turns. However, it’s a journey of self-discovery. It reminded me that passion and enjoyment are crucial for creative success.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;Originally published on &lt;a href=&quot;https://alexzh.com/overcoming-comparison-and-finding-joy-in-writing-my-first-book/&quot;&gt;alexzh.com&lt;/a&gt;.&lt;/p&gt;</content:encoded><media:content url="https://alexzh.com/_astro/cover.CV7NBhOP_IcY5V.jpg" medium="image"/><category>Book Writing</category></item><item><title>How to add platform-specific icons to a Compose Multiplatform project</title><link>https://alexzh.com/how-to-add-platform-specific-icons-to-a-compose-multiplatform-project/</link><guid isPermaLink="false">67522a28c39ba2000169febd</guid><description>Explore how to add platform-specific icons to a Compose Multiplatform project, using different icons for Android, iOS, Desktop and Web.</description><pubDate>Thu, 05 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://alexzh.com/_astro/cover.D-XLU_fW_ZgaDVA.webp&quot; alt=&quot;&quot; width=&quot;1080&quot; height=&quot;655&quot;&gt;&lt;p&gt;Make step towards native experience on different platforms by adopting platform-specific icons in Compose Multiplatform. This helps improve user experience by providing familiar elements on different devices, while still maintaining common code.&lt;/p&gt;
&lt;p&gt;In this article, we will explore how to support platform-specific icons in Compose Multiplatform project.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;440&quot; src=&quot;https://alexzh.com/_astro/demo-platform-specific-icons.DfzWtv1l_1B9XsA.webp&quot; srcset=&quot;https://alexzh.com/_astro/demo-platform-specific-icons.DfzWtv1l_2jh60C.webp 720w, https://alexzh.com/_astro/demo-platform-specific-icons.DfzWtv1l_Z25zQiB.webp 1080w, https://alexzh.com/_astro/demo-platform-specific-icons.DfzWtv1l_1B9XsA.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;p&gt;If you like learning through videos, this is for you:&lt;/p&gt;
&lt;figure class=&quot;kg-card kg-embed-card&quot;&gt;&lt;a class=&quot;youtube&quot; href=&quot;https://www.youtube.com/watch?v=AMZ91EYURjk&quot; data-youtube=&quot;AMZ91EYURjk&quot; data-title=&quot;How to add platform-specific icons to a Compose Multiplatform project&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;img alt=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://alexzh.com/_astro/platform-specific-icons-video.Ddn4-MKU_ZaCJIv.webp&quot; srcset=&quot;https://alexzh.com/_astro/platform-specific-icons-video.Ddn4-MKU_Z19w5G9.webp 720w, https://alexzh.com/_astro/platform-specific-icons-video.Ddn4-MKU_Z2vqtEp.webp 1080w, https://alexzh.com/_astro/platform-specific-icons-video.Ddn4-MKU_ZaCJIv.webp 1280w&quot;&gt;&lt;span class=&quot;youtube__play&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg viewBox=&quot;0 0 68 48&quot; width=&quot;68&quot; height=&quot;48&quot;&gt;&lt;path d=&quot;M66.5 7.7c-.8-2.9-2.5-5.4-5.4-6.2C55.8.1 34 0 34 0S12.2.1 6.9 1.6C4 2.4 2.3 4.9 1.5 7.7.1 13 0 24 0 24s.1 11 1.5 16.3c.8 2.9 2.5 5.4 5.4 6.2 5.3 1.5 27.1 1.6 27.1 1.6s21.8-.1 27.1-1.6c2.9-.8 4.6-3.3 5.4-6.2C67.9 35 68 24 68 24s-.1-11-1.5-16.3z&quot; fill=&quot;#f00&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M45 24 27 14v20&quot; fill=&quot;#fff&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;youtube__label&quot;&gt;Play video: How to add platform-specific icons to a Compose Multiplatform project&lt;/span&gt;&lt;/a&gt;&lt;/figure&gt;
&lt;h2 id=&quot;setting-up-a-compose-multiplatform-project&quot;&gt;Setting up a Compose Multiplatform Project&lt;/h2&gt;
&lt;p&gt;For the demo purposes, I want to have a display a separate “Back”, “Edit” and “Delete” icons for the following platforms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Android&lt;/li&gt;
&lt;li&gt;iOS&lt;/li&gt;
&lt;li&gt;Web &amp;amp; Desktop&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We will implement it using icons from the &lt;strong&gt;material-icons-core&lt;/strong&gt; (which is part of compose dependency) and &lt;strong&gt;.svg&lt;/strong&gt; icons. For iOS, we will use SVG icons that can be added to the &lt;strong&gt;composeResources/drawable&lt;/strong&gt; folder. Icons for other platforms will come from the “material-icons” library.&lt;/p&gt;
&lt;h2 id=&quot;getting-information-about-the-platform&quot;&gt;Getting information about the platform&lt;/h2&gt;
&lt;p&gt;In the end, I want to have an icon object, which will contain platform-specific icons which I can reuse in Compose Multiplatform code. Kotlin Multiplatform provides a possibility to use &lt;a href=&quot;https://kotlinlang.org/docs/multiplatform-expect-actual.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;expect/actual&lt;/a&gt; mechanism to use different implementation depending on the platform. Let’s use it to get information about the platform.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;sealed class Platform {
    data object Android : Platform()
    data object Desktop : Platform()
    data object Ios : Platform()
    data object Web : Platform()
}

expect val platform: Platform&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is an implementation for each platform:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;// Android
actual val platform: Platform = Platform.Android

// Desktop
actual val platform: Platform = Platform.Desktop

// iOS
actual val platform: Platform = Platform.Ios

//Web
actual val platform: Platform = Platform.Web&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;creating-platform-specific-icons&quot;&gt;Creating Platform-Specific Icons&lt;/h2&gt;
&lt;p&gt;The next step is to define an icon. So, we will create an &lt;code&gt;ImageVector&lt;/code&gt; object which defines an icon base on the platform. Let’s check out the definitions for the &lt;em&gt;BackIcon&lt;/em&gt;, &lt;em&gt;EditIcon&lt;/em&gt; and &lt;em&gt;DeleteIcon&lt;/em&gt; objects which provides platform-specific icons.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val BackIcon: ImageVector
    @Composable
    get() {
        return when (platform) {
            Platform.Android -&amp;gt; Icons.AutoMirrored.Default.ArrowBack
            Platform.Ios -&amp;gt; vectorResource(Res.drawable.ic_back_ios)
            else -&amp;gt; Icons.AutoMirrored.Default.KeyboardArrowLeft
        }
    }

val EditIcon: ImageVector
    @Composable
    get() {
        return when (platform) {
            Platform.Android -&amp;gt; Icons.Outlined.Edit
            Platform.Ios -&amp;gt; vectorResource(Res.drawable.ic_edit_ios)
            else -&amp;gt; Icons.Filled.Edit
        }
    }

val DeleteIcon: ImageVector
    @Composable
    get() {
        return when (platform) {
            Platform.Android -&amp;gt; Icons.Outlined.Delete
            Platform.Ios -&amp;gt; vectorResource(Res.drawable.ic_delete_ios)
            else -&amp;gt; Icons.Filled.Delete
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, we can use the “BackIcon” as a navigation icon of the &lt;code&gt;TopAppBar&lt;/code&gt;, and display other icons on the screen.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun IconList(
    onBack: () -&amp;gt; Unit
) {
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(&quot;Demo: Icons&quot;) },
                navigationIcon = {
                    IconButton(onClick = onBack) {
                        Icon(
                            imageVector = BackIcon,
                            contentDescription = &quot;Back&quot;
                        )
                    }
                }
            )
        },
    ) {
        Column(
            modifier = Modifier.fillMaxSize().padding(vertical = 16.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            Card(modifier = Modifier.size(100.dp)) {
                Image(
                    imageVector = EditIcon,
                    contentDescription = &quot;Edit&quot;
                )
            }

            Card(modifier = Modifier.size(100.dp)) {
                Image(
                    imageVector = DeleteIcon,
                    contentDescription = &quot;Delete&quot;
                )
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;440&quot; src=&quot;https://alexzh.com/_astro/demo-platform-specific-icons.DfzWtv1l_1B9XsA.webp&quot; srcset=&quot;https://alexzh.com/_astro/demo-platform-specific-icons.DfzWtv1l_2jh60C.webp 720w, https://alexzh.com/_astro/demo-platform-specific-icons.DfzWtv1l_Z25zQiB.webp 1080w, https://alexzh.com/_astro/demo-platform-specific-icons.DfzWtv1l_1B9XsA.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;p&gt;You can find the full source code in &lt;a href=&quot;https://github.com/AlexZhukovich/KotlinMultiplatformPlayground/blob/main/composeApp/src/commonMain/kotlin/com/alexzh/kmp/playground/presentation/features/icons/IconList.kt&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;IconList.kt in the KotlinMultiplatformPlayground repository&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In a Compose Multiplatform project, we can easily leverage the &lt;code&gt;expect/actual&lt;/code&gt; mechanism to display different icons for every platform.&lt;/p&gt;
&lt;p&gt;If you want to use material design icons, you can use the &lt;strong&gt;material-icons-core&lt;/strong&gt; library, and if you need other icons, you can use the &lt;strong&gt;SVG&lt;/strong&gt; files.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;Originally published on &lt;a href=&quot;https://alexzh.com/how-to-add-platform-specific-icons-to-a-compose-multiplatform-project/&quot;&gt;alexzh.com&lt;/a&gt;.&lt;/p&gt;</content:encoded><media:content url="https://alexzh.com/_astro/cover.D-XLU_fW_1yG5BA.jpg" medium="image"/><category>Compose Multiplatform</category><category>Kotlin</category></item><item><title>How to create a scrollable chart in Compose Multiplatform</title><link>https://alexzh.com/how-to-create-a-scrollable-chart-in-compose-multiplatform/</link><guid isPermaLink="false">67408963c4d7880001c0fb0b</guid><description>Explore how to create a dynamic, scrollable chart in Compose Multiplatform using the KoalaPlot library.</description><pubDate>Fri, 22 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://alexzh.com/_astro/cover.DCorXHnQ_1aitr4.webp&quot; alt=&quot;&quot; width=&quot;1080&quot; height=&quot;655&quot;&gt;&lt;p&gt;The “koalaplot” library got an update &lt;a href=&quot;https://github.com/KoalaPlot/koalaplot-core/releases/tag/v0.7.0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;“v0.7.0”&lt;/a&gt; which added zoom and pan support for all platforms for &lt;code&gt;XYGraph&lt;/code&gt; component. These changes allow us to create scrollable charts in Compose Multiplatform which will be available for Android, iOS, Web and Desktop platforms.&lt;/p&gt;
&lt;p&gt;In this article, we will create a “Vertical Bar Chart” which will work on all platforms.&lt;/p&gt;
&lt;p&gt;&lt;video src=&quot;https://alexzh.com/media/how-to-create-a-scrollable-chart-in-compose-multiplatform/demo-scrollable-chart-mobile-desktop.mp4&quot; poster=&quot;https://alexzh.com/media/how-to-create-a-scrollable-chart-in-compose-multiplatform/demo-scrollable-chart-mobile-desktop_poster.webp&quot; width=&quot;1280&quot; height=&quot;720&quot; loop muted playsinline preload=&quot;none&quot; data-autoplay&gt;&lt;/video&gt;&lt;/p&gt;
&lt;aside class=&quot;callout callout--note&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;&lt;p&gt;In this version of the library, the following approach will work only you use the &lt;code&gt;LinearAxisModel&lt;/code&gt;s.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;If you like learning through videos, this is for you:&lt;/p&gt;
&lt;figure class=&quot;kg-card kg-embed-card&quot;&gt;&lt;a class=&quot;youtube&quot; href=&quot;https://www.youtube.com/watch?v=WtYJuUodiuQ&quot; data-youtube=&quot;WtYJuUodiuQ&quot; data-title=&quot;How to create a scrollable chart in Compose Multiplatform&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;img alt=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://alexzh.com/_astro/scrollable-chart-video.DjGxTeQG_Z2b8CaO.webp&quot; srcset=&quot;https://alexzh.com/_astro/scrollable-chart-video.DjGxTeQG_1eP2yz.webp 720w, https://alexzh.com/_astro/scrollable-chart-video.DjGxTeQG_ZfJGvh.webp 1080w, https://alexzh.com/_astro/scrollable-chart-video.DjGxTeQG_Z2b8CaO.webp 1280w&quot;&gt;&lt;span class=&quot;youtube__play&quot; aria-hidden=&quot;true&quot;&gt;&lt;svg viewBox=&quot;0 0 68 48&quot; width=&quot;68&quot; height=&quot;48&quot;&gt;&lt;path d=&quot;M66.5 7.7c-.8-2.9-2.5-5.4-5.4-6.2C55.8.1 34 0 34 0S12.2.1 6.9 1.6C4 2.4 2.3 4.9 1.5 7.7.1 13 0 24 0 24s.1 11 1.5 16.3c.8 2.9 2.5 5.4 5.4 6.2 5.3 1.5 27.1 1.6 27.1 1.6s21.8-.1 27.1-1.6c2.9-.8 4.6-3.3 5.4-6.2C67.9 35 68 24 68 24s-.1-11-1.5-16.3z&quot; fill=&quot;#f00&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M45 24 27 14v20&quot; fill=&quot;#fff&quot;&gt;&lt;/path&gt;&lt;/svg&gt;&lt;/span&gt;&lt;span class=&quot;youtube__label&quot;&gt;Play video: How to create a scrollable chart in Compose Multiplatform&lt;/span&gt;&lt;/a&gt;&lt;/figure&gt;
&lt;h2 id=&quot;setting-up-the-project&quot;&gt;Setting up the project&lt;/h2&gt;
&lt;p&gt;To create the “Vertical Bar Chart”, let’s start with creating the generating Compose Multiplatform project, using the &lt;a href=&quot;https://kmp.jetbrains.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Kotlin Multiplatform Wizard&lt;/a&gt; website.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;2000&quot; height=&quot;1240&quot; src=&quot;https://alexzh.com/_astro/kotlin-multiplatform-wizard-website.BcO-9z3v_Z1VIydh.webp&quot; srcset=&quot;https://alexzh.com/_astro/kotlin-multiplatform-wizard-website.BcO-9z3v_Z1eIzLb.webp 720w, https://alexzh.com/_astro/kotlin-multiplatform-wizard-website.BcO-9z3v_1MHPSy.webp 1080w, https://alexzh.com/_astro/kotlin-multiplatform-wizard-website.BcO-9z3v_ZGJTqq.webp 1440w&quot;&gt;&lt;/figure&gt;
&lt;p&gt;The next step is to add the library to the project. To do so, we need to library definition and version to the &lt;strong&gt;gradle/libs.versions.toml&lt;/strong&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[versions]
...
koalaplot-core = &quot;0.7.0&quot;

[libraries]
...
koalaplot-core = { module = &quot;io.github.koalaplot:koalaplot-core&quot;, version.ref = &quot;koalaplot-core&quot; }

...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And, we need to add dependency to the &lt;strong&gt;composeApp/build.gradle.kts&lt;/strong&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;...
kotlin {
         sourceSets {
        ...
        commonMain.dependencies {
                ...
                implementation(libs.koalaplot.core)
        }
        ...
    }
}
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Afterwards, we need to sync the project.&lt;/p&gt;
&lt;h2 id=&quot;creating-a-chart&quot;&gt;Creating a Chart&lt;/h2&gt;
&lt;p&gt;Let’s create a chart which has a lot of data on the “X-axis”. For example, let’s create the “Average Daily Mood” chart for a month which has 30 days. This chart will use fake data, and we will use this function to generate fake data instead of getting data from the database.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;data class DemoDailyMood(
    val dayOfMonth: Int,
    val happiness: Float
)

private fun generateChartData(numDays: Int = 30) = (1..numDays).map { day -&amp;gt;
    DemoDailyMood(day, Random.nextFloat() * (5f - 1f) + 1f)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we’ll create a composable function to generate a chart. This function will use a chart component that needs separate data for the X-axis and Y-axis. So, we’ll provide this data to the composable function as parameters.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@OptIn(ExperimentalKoalaPlotApi::class)
@Composable
fun DailyMoodChart(
    modifier: Modifier = Modifier,
    xData: List&amp;lt;Int&amp;gt;,
    yData: List&amp;lt;Float&amp;gt;
) {
    ChartLayout(
        modifier = modifier.padding(16.dp),
    ) {
        val xAxisModel = rememberIntLinearAxisModel(1..xData.size)
        val yAxisModel = rememberFloatLinearAxisModel(0f..5f)

        XYGraph(
            xAxisModel = xAxisModel,
            yAxisModel = yAxisModel,
            yAxisLabels = { value -&amp;gt;
                when (value) {
                    1.0f -&amp;gt; &quot;Crying&quot;
                    2.0f -&amp;gt; &quot;Sad&quot;
                    3.0f -&amp;gt; &quot;OK&quot;
                    4.0f -&amp;gt; &quot;Good&quot;
                    5.0f -&amp;gt; &quot;Happy&quot;
                    else -&amp;gt; &quot;&quot;
                }
            }
        ) {
            VerticalBarPlot(
                xData = xData,
                yData = yData,
                barWidth = 0.6f,
                bar = {
                    val color = when (yData[it]) {
                        in 0f..1.65f -&amp;gt; Color(0xFFF2554A)
                        in 1.66f..3.3f -&amp;gt; Color(0xFFFFB31A)
                        else -&amp;gt; Color(0xFF8FBF41)
                    }

                    DefaultVerticalBar(
                        brush = SolidColor(color),
                        shape = RoundedCornerShape(100)
                    )
                }
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you add the &lt;code&gt;DailyMoodChart&lt;/code&gt; to the screen, you’ll notice that there is too much data. As a result, the chart becomes almost unreadable on mobile devices.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;720&quot; src=&quot;https://alexzh.com/_astro/mobile-30-days-chart.CR0vy7tC_lD0OE.webp&quot; srcset=&quot;https://alexzh.com/_astro/mobile-30-days-chart.CR0vy7tC_Z1s8rhD.webp 720w, https://alexzh.com/_astro/mobile-30-days-chart.CR0vy7tC_2lozR0.webp 1080w, https://alexzh.com/_astro/mobile-30-days-chart.CR0vy7tC_lD0OE.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;p&gt;This is how we can add it to the screen:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun ScrollableChartScreen(onBack: () -&amp;gt; Unit) {
    val data = generateChartData()
    val xValues = data.map { it.dayOfMonth }
    val yValues = data.map { it.happiness }

    Scaffold(topBar = { TopAppBar(...) }) {
        DailyMoodChart(
            xData = xValues,
            yData = yValues
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We created data inside the composable function for simplicity. Ideally, it should be done outside and passed as a state to the function.&lt;/p&gt;
&lt;h2 id=&quot;adding-scroll-functionality&quot;&gt;Adding Scroll Functionality&lt;/h2&gt;
&lt;p&gt;We can improve the situation by zooming in on a specific part of the chart. Instead of displaying all the data, we can show data from the first day to the 15th day of each month. This way, the user can interact with the chart and scroll horizontally.&lt;/p&gt;
&lt;p&gt;To achieve this, we can use the &lt;code&gt;setViewRange&lt;/code&gt; function for the “X-axis”. Additionally, we need to enable the pan gesture for both the &lt;code&gt;xAxisModel&lt;/code&gt; and &lt;code&gt;XYGraph&lt;/code&gt; components.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@OptIn(ExperimentalKoalaPlotApi::class)
@Composable
fun DailyMoodChart(
    modifier: Modifier = Modifier,
    xData: List&amp;lt;Int&amp;gt;,
    yData: List&amp;lt;Float&amp;gt;
) {
    ChartLayout(
        modifier = modifier.padding(16.dp),
    ) {
        val xAxisModel = rememberIntLinearAxisModel(
            range = 1..xData.size,
            allowPanning = true
        ).apply { setViewRange(1..15) }
                ...
                
        XYGraph(
            ...,
            panEnabled = true
        ) {
            ...
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;video src=&quot;https://alexzh.com/media/how-to-create-a-scrollable-chart-in-compose-multiplatform/demo-scrollable-chart-mobile.mp4&quot; poster=&quot;https://alexzh.com/media/how-to-create-a-scrollable-chart-in-compose-multiplatform/demo-scrollable-chart-mobile_poster.webp&quot; width=&quot;1280&quot; height=&quot;720&quot; loop muted playsinline preload=&quot;none&quot; data-autoplay&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;To have a vertically scrollable chart, you can use the same approach for &lt;code&gt;yAxisModel&lt;/code&gt; instead of &lt;code&gt;xAxisModel&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; To add zoom functionality to the chart, update the &lt;code&gt;zoomEnabled&lt;/code&gt; parameter of the &lt;code&gt;XYGraph&lt;/code&gt; component. Remember to enable it for the &lt;code&gt;LinearAxisModel&lt;/code&gt; values as well. You can also choose to enable zoom for either the X-axis or Y-axis separately by updating the &lt;code&gt;allowIndependentZoom&lt;/code&gt; property of the &lt;code&gt;XYGraph&lt;/code&gt; component.&lt;/p&gt;
&lt;p&gt;You can find the source code in &lt;a href=&quot;https://github.com/AlexZhukovich/KotlinMultiplatformPlayground/blob/main/composeApp/src/commonMain/kotlin/com/alexzh/kmp/playground/presentation/features/chart/ScrollableChart.kt&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ScrollableChart.kt in the KotlinMultiplatformPlayground repository&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The “koalaplot:0.7.0” release offers a scrollable chart option for Kotlin Multiplatform projects. This means you can create a component once and use it on Android, iOS, Desktop, and Web versions of your application.&lt;/p&gt;
&lt;p&gt;However, the current solution only works with linear axis models. It does not work for the &lt;code&gt;CategoryAxisModel&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;setViewRange&lt;/code&gt; function lets us zoom to a specific range. When combined with the pan gesture, you can create charts that can be scrolled.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;Originally published on &lt;a href=&quot;https://alexzh.com/how-to-create-a-scrollable-chart-in-compose-multiplatform/&quot;&gt;alexzh.com&lt;/a&gt;.&lt;/p&gt;</content:encoded><media:content url="https://alexzh.com/_astro/cover.DCorXHnQ_Z251TNG.jpg" medium="image"/><category>Compose Multiplatform</category><category>Kotlin</category><category>Kotlin Multiplatform</category></item><item><title>Date and Time Formatting in Kotlin with the DateTime Library</title><link>https://alexzh.com/date-and-time-formatting-in-kotlin-with-the-datetime-library/</link><guid isPermaLink="false">65e1d1bab30b010001a10f26</guid><description>Format dates and times in Kotlin with the Kotlinx-datetime library, using Unicode patterns and the Kotlin DSL. Ideal for Kotlin Multiplatform projects.</description><pubDate>Fri, 01 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://alexzh.com/_astro/cover.CI_zSkra_qoup7.webp&quot; alt=&quot;&quot; width=&quot;1080&quot; height=&quot;654&quot;&gt;&lt;p&gt;The “kotlinx-datetime” library got an update &lt;a href=&quot;https://github.com/Kotlin/kotlinx-datetime/releases/tag/v0.6.0-RC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;“v0.6.0-RC”&lt;/a&gt; which introduces an API for locale-invariant parsing and formatting. These changes will allow for cleaning up platform-specific date and time formatting code.&lt;/p&gt;
&lt;p&gt;In this article, we will explore options for parsing and formatting &lt;code&gt;LocalDateTime&lt;/code&gt;, &lt;code&gt;LocalDate&lt;/code&gt; and &lt;code&gt;LocalTime&lt;/code&gt; types.&lt;/p&gt;
&lt;p&gt;Several predefined formatting options exist for different date and time types. Let’s check how they format a specific date and time: &lt;em&gt;March 1, 2024, at 9:15 AM&lt;/em&gt;, represented in code as &lt;code&gt;LocalDateTime(2024, 3, 1, 9, 15, 0, 0)&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LocalDateTime&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LocalDateTime.Formats.ISO&lt;/code&gt; formats it as &lt;code&gt;2024-03-01T09:15:00&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LocalDate&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LocalDate.Formats.ISO&lt;/code&gt; formats it as &lt;code&gt;2024-03-01&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LocalDate.Formats.ISO_BASIC&lt;/code&gt; formats it as &lt;code&gt;20240301&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LocalTime&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LocalTime.Formats.ISO&lt;/code&gt; formats it as &lt;code&gt;09:15:00&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To use the version “0.6.0-RC” of the “kotlinx-datetime” library, you need to add a dependency to your &lt;code&gt;build.gradle&lt;/code&gt; file or update &lt;code&gt;gradle/libs.versions.toml&lt;/code&gt; (if you use the version catalog):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;implementation &quot;org.jetbrains.kotlinx:kotlinx-datetime:0.6.0-RC.2&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;formatting&quot;&gt;Formatting&lt;/h2&gt;
&lt;p&gt;There are 2 ways of formatting date and time by using unicode pattern or by using Kotlin DSL. Let’s explore both options.&lt;/p&gt;
&lt;p&gt;Let’s format &lt;code&gt;LocalDateTime&lt;/code&gt;, &lt;code&gt;LocalDate&lt;/code&gt; and &lt;code&gt;LocalTime&lt;/code&gt; objects using different approaches.&lt;/p&gt;
&lt;p&gt;To format different date and time types, we need to use the &lt;code&gt;format(...)&lt;/code&gt; function.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val currentDateTime = LocalDateTime(2024, 3, 1, 9, 15, 0, 0)
val currentDate = currentDateTime.date
val currentTime = currentDateTime.time

currentDateTime.format(LocalDateTime.Format { 
    // Use Unicode Pattern or Kotlin DSL
})

currentDate.format(LocalDate.Format { 
    // Use Unicode Pattern or Kotlin DSL
})
        
currentTime.format(LocalTime.Format { 
    // Use Unicode Pattern or Kotlin DSL
})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, if we want to format &lt;code&gt;LocalDate&lt;/code&gt; we need to use &lt;code&gt;LocalDate.Format { ... }&lt;/code&gt; type as a parameter for the format function.&lt;/p&gt;
&lt;h3 id=&quot;formatting-by-using-unicode-pattern&quot;&gt;Formatting by using Unicode pattern&lt;/h3&gt;
&lt;p&gt;Let’s take a look at examples of formatting date and times using the different unicode patterns:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val currentDateTime = LocalDateTime(2024, 3, 1, 9, 15, 0, 0)
val currentDate = currentDateTime.date
val currentTime = currentDateTime.time

// 2024 03 01 - 09:15
currentDateTime.format(LocalDateTime.Format { byUnicodePattern(&quot;yyyy MM dd - HH:mm&quot;) })

// 2024/03/01
currentDate.format(LocalDate.Format { byUnicodePattern(&quot;yyyy/MM/dd&quot;) })

// 09:15 
currentTime.format(LocalTime.Format { byUnicodePattern(&quot;HH:mm&quot;) })&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;formatting-by-using-kotlin-dsl&quot;&gt;Formatting by using Kotlin DSL&lt;/h3&gt;
&lt;p&gt;Let’s examine formatting dates and times using Kotlin DSL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val currentDateTime = LocalDateTime(2024, 3, 1, 9, 15, 0, 0)
val currentDate = currentDateTime.date
val currentTime = currentDateTime.time

// Mar 01 2024 - 09:15
currentDateTime.format(
    LocalDateTime.Format {
        date(
            LocalDate.Format {
                monthName(MonthNames.ENGLISH_ABBREVIATED)
                char(&apos; &apos;)
                dayOfMonth()
                chars(&quot; &quot;)
                year()
            }
        )
        chars(&quot; - &quot;)
        time(
            LocalTime.Format {
                hour(); char(&apos;:&apos;); minute()
            }
        )
    }
)

// Mar 01, 2024
currentDate.format(LocalDate.Format {
    monthName(MonthNames.ENGLISH_ABBREVIATED)
    char(&apos; &apos;)
    dayOfMonth()
    chars(&quot;, &quot;)
    year()
})

// 09:15
currentTime.format(LocalTime.Format {
    hour()
    char(&apos;:&apos;)
    minute()
})&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;hour()&lt;/code&gt; function returns the hour value between 0 and 23 hours, but if you want the hour of the day in the 12-hour clock, you need to use the &lt;code&gt;amPmHour()&lt;/code&gt; function.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When you use the Kotlin DSL approach, you can pass the &lt;code&gt;padding&lt;/code&gt; parameters to functions like: &lt;code&gt;year()&lt;/code&gt;, &lt;code&gt;monthNumber()&lt;/code&gt;, &lt;code&gt;dayOfMonth()&lt;/code&gt;, &lt;code&gt;hour()&lt;/code&gt;, &lt;code&gt;minute()&lt;/code&gt; and it will change the style of padding to use when formatting a value. To understand it better, let’s take a look at an example of formatting the “09:08 (AM)” time.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val time = LocalTime(hour = 9, minute = 8)

// &quot;9:8&quot;
time.format(LocalTime.Format { 
    hour(padding = Padding.NONE)
    char(&apos;:&apos;)
    minute(padding = Padding.NONE) 
})

// &quot;09:08&quot;
time.format(LocalTime.Format { 
    hour(padding = Padding.ZERO)
    char(&apos;:&apos;)
    minute(padding = Padding.ZERO) 
})

// &quot; 9: 8&quot;
time.format(LocalTime.Format { 
    hour(padding = Padding.SPACE)
    char(&apos;:&apos;)
    minute(padding = Padding.SPACE) 
})&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;parsing&quot;&gt;Parsing&lt;/h2&gt;
&lt;p&gt;To parse a formatted date and time into &lt;code&gt;LocalDateTime&lt;/code&gt;, &lt;code&gt;LocalDate&lt;/code&gt;, or &lt;code&gt;LocalTime&lt;/code&gt;, we use the &lt;code&gt;parse&lt;/code&gt; method on the respective type.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val formattedDateTime = &quot;2024 03 01 - 09:15&quot;
LocalDateTime.parse(
    input = formattedDateTime,
    format = LocalDateTime.Format { byUnicodePattern(&quot;yyyy MM dd - HH:mm&quot;) }
)

val formattedDate = &quot;2024/03/01&quot;
LocalDate.parse(
    input = formattedDate,
    format = LocalDate.Format { byUnicodePattern(&quot;yyyy/MM/dd&quot;) }
)

val formattedTime = &quot;09:15&quot;
LocalTime.parse(
    input = formattedTime,
    format = LocalTime.Formats.ISO
)&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;The “kotlinx-datetime:v0.6.0-RC” release provides multiple options for working with date and time independently of the platforms.&lt;/p&gt;
&lt;p&gt;Date and time can be formatted using one of the available formatting options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;unicode pattern&lt;/li&gt;
&lt;li&gt;Kotlin DSL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Date and time types can be parsed using the &lt;code&gt;parse&lt;/code&gt; function on an appropriate class, like &lt;code&gt;LocalTime.parse&lt;/code&gt; and providing the format of the value.&lt;/p&gt;
&lt;p&gt;Remember to clean up the platform-specific code for date formatting if you decided to use the new pre-release version of the “kotlinx-datetime” library.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;Originally published on &lt;a href=&quot;https://alexzh.com/date-and-time-formatting-in-kotlin-with-the-datetime-library/&quot;&gt;alexzh.com&lt;/a&gt;.&lt;/p&gt;</content:encoded><media:content url="https://alexzh.com/_astro/cover.CI_zSkra_Z1ju1GR.jpg" medium="image"/><category>Kotlin</category></item><item><title>Jetpack Compose: Divider</title><link>https://alexzh.com/jetpack-compose-divider/</link><guid isPermaLink="false">65c4ba0f14393900015e0c62</guid><description>How to use and customize dividers from Material 2 and Material 3 in Jetpack Compose, and how Divider, HorizontalDivider and VerticalDivider differ.</description><pubDate>Thu, 08 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://alexzh.com/_astro/cover.CLPVfRd3_Z550J5.webp&quot; alt=&quot;&quot; width=&quot;1080&quot; height=&quot;655&quot;&gt;&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Divider is a widespread component used to separate content.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;460&quot; src=&quot;https://alexzh.com/_astro/dividers-example.CYkYnKbS_Z1EbyVJ.webp&quot; srcset=&quot;https://alexzh.com/_astro/dividers-example.CYkYnKbS_IyckU.webp 720w, https://alexzh.com/_astro/dividers-example.CYkYnKbS_1sS4A6.webp 1080w, https://alexzh.com/_astro/dividers-example.CYkYnKbS_Z1EbyVJ.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;p&gt;In this article, we will explore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What is the difference between dividers in the “Material 2” and “Material 3” libraries?&lt;/li&gt;
&lt;li&gt;How to use horizontal and vertical dividers in Jetpack Compose?&lt;/li&gt;
&lt;li&gt;How to customize dividers from the “Material 3” library?&lt;/li&gt;
&lt;li&gt;How to create a custom divider?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;jetpack-compose-divider&quot;&gt;Jetpack Compose Divider&lt;/h2&gt;
&lt;p&gt;Let’s start by exploring the differences between dividers from the “Material 2” and “Material 3” libraries.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Divider&lt;/code&gt; component from the “Material 2” library uses the &lt;code&gt;Box&lt;/code&gt; container to display a divider on the screen.&lt;/p&gt;
&lt;p&gt;Here is the source code of the &lt;code&gt;Divider&lt;/code&gt; component from the “Material 2” library.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun Divider(
    modifier: Modifier = Modifier,
    color: Color = MaterialTheme.colors.onSurface.copy(alpha = DividerAlpha),
    thickness: Dp = 1.dp,
    startIndent: Dp = 0.dp
) {
    ...
    Box(
        modifier.then(indentMod)
            .fillMaxWidth()
            .height(targetThickness)
            .background(color = color)
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;HorizontalDivider&lt;/code&gt; and &lt;code&gt;VerticalDivider&lt;/code&gt; components from the “Material 3” library draw a line on a &lt;code&gt;Canvas&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun VerticalDivider(
    modifier: Modifier = Modifier,
    thickness: Dp = DividerDefaults.Thickness,
    color: Color = DividerDefaults.color,
) = Canvas(modifier.fillMaxHeight().width(thickness)) {
    drawLine(
        color = color,
        strokeWidth = thickness.toPx(),
        start = Offset(thickness.toPx() / 2, 0f),
        end = Offset(thickness.toPx() / 2, size.height),
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;All the following examples will be based on the “Material 3” implementation.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Both horizontal and vertical dividers have the &lt;code&gt;modifier&lt;/code&gt;, &lt;code&gt;thickness&lt;/code&gt;, and &lt;code&gt;color&lt;/code&gt; parameters. By default, they have the following values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;thickness&lt;/code&gt;: &lt;code&gt;1.dp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;color&lt;/code&gt;: &lt;code&gt;MaterialTheme.colorScheme.outlineVariant&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;horizontal-divider&quot;&gt;Horizontal Divider&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;HorizontalDivider&lt;/code&gt; represents a thin horizontal line that splits different elements. By default, it uses all available &lt;code&gt;width&lt;/code&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;320&quot; src=&quot;https://alexzh.com/_astro/horizontaldivider-defaultcolor-demo.DUDP6ddz_Z1jVbTz.webp&quot; srcset=&quot;https://alexzh.com/_astro/horizontaldivider-defaultcolor-demo.DUDP6ddz_Zl3Fdm.webp 720w, https://alexzh.com/_astro/horizontaldivider-defaultcolor-demo.DUDP6ddz_Z1iUnJV.webp 1080w, https://alexzh.com/_astro/horizontaldivider-defaultcolor-demo.DUDP6ddz_Z1jVbTz.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun Demo_HorizontalDivider() {
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Text(&quot;Item 1&quot;, fontSize = 18.sp)
        HorizontalDivider()
        Text(&quot;Item 2&quot;, fontSize = 18.sp)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;vertical-divider&quot;&gt;Vertical Divider&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;VerticalDivider&lt;/code&gt; represents a thin vertical line that splits different elements. By default, it uses all available height, which is why we need to specify the &lt;code&gt;height&lt;/code&gt; of the &lt;code&gt;Row&lt;/code&gt; container. I recommend using &lt;code&gt;.height(IntrinsicSize.Min)&lt;/code&gt; because it will properly render all children inside the &lt;code&gt;Row&lt;/code&gt; using the minimum possible height.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;VerticalDivider&lt;/code&gt; doesn’t add any space before and after the divider.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;320&quot; src=&quot;https://alexzh.com/_astro/verticaldivider-demo.DKR4ExBB_1S49gY.webp&quot; srcset=&quot;https://alexzh.com/_astro/verticaldivider-demo.DKR4ExBB_ZCQUHB.webp 720w, https://alexzh.com/_astro/verticaldivider-demo.DKR4ExBB_Z1fNXAq.webp 1080w, https://alexzh.com/_astro/verticaldivider-demo.DKR4ExBB_1S49gY.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun Demo_VerticalDivider() {
    Row(
        modifier = Modifier.padding(16.dp)
            .height(IntrinsicSize.Min)
    ) {
        Text(&quot;Item 1&quot;, fontSize = 18.sp)
        VerticalDivider(
            modifier = Modifier.padding(horizontal = 16.dp),
            thickness = 1.dp,
            color = Color.DarkGray
        )
        Text(&quot;Item 2&quot;, fontSize = 18.sp)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;customizing-the-divider&quot;&gt;Customizing the Divider&lt;/h2&gt;
&lt;p&gt;Both &lt;code&gt;HorizontalDivider&lt;/code&gt; and &lt;code&gt;VerticalDivider&lt;/code&gt; have relatively limited customization options, as they have only the following parameters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;modifier: Modifier&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;thickness: Dp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;color: Color&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If we want to change the size of a divider or add padding, we need to use the &lt;code&gt;modifier: Modifier&lt;/code&gt; parameter for it.&lt;/p&gt;
&lt;h3 id=&quot;adjusting-divider-colors&quot;&gt;Adjusting Divider Colors&lt;/h3&gt;
&lt;p&gt;To set a color, we need to pass it via a parameter. We can use the Jetpack Compose &lt;code&gt;Color&lt;/code&gt; type or pass a color from the &lt;code&gt;colors.xml&lt;/code&gt; file using &lt;code&gt;colorResource(id = R.color.myColor)&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun Demo_HorizontalDivider() {
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Text(&quot;Item 1&quot;, fontSize = 18.sp)
        HorizontalDivider(color = Color.DarkGray)
        Text(&quot;Item 2&quot;, fontSize = 18.sp)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;320&quot; src=&quot;https://alexzh.com/_astro/horizontaldivider-demo.BcOu_4s-_DNGXH.webp&quot; srcset=&quot;https://alexzh.com/_astro/horizontaldivider-demo.BcOu_4s-_Z1SGYb2.webp 720w, https://alexzh.com/_astro/horizontaldivider-demo.BcOu_4s-_1iSY27.webp 1080w, https://alexzh.com/_astro/horizontaldivider-demo.BcOu_4s-_DNGXH.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;h3 id=&quot;changing-divider-thickness&quot;&gt;Changing Divider Thickness&lt;/h3&gt;
&lt;p&gt;To specify the thickness of the horizontal and vertical dividers, we need to pass the value for the &lt;code&gt;thickness: Dp&lt;/code&gt; parameter.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun Demo_HorizontalDivider_thickness() {
    Column(
        modifier = Modifier.fillMaxWidth(),
        verticalArrangement = Arrangement.spacedBy(10.dp)
    ) {
        for (thicknessValue in 1..10) {
            HorizontalDivider(
                thickness = thicknessValue.dp
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;360&quot; src=&quot;https://alexzh.com/_astro/horizontaldivider-thickness-demo.BqeT2iys_10bt30.webp&quot; srcset=&quot;https://alexzh.com/_astro/horizontaldivider-thickness-demo.BqeT2iys_cb0Ej.webp 720w, https://alexzh.com/_astro/horizontaldivider-thickness-demo.BqeT2iys_Z1HDJYW.webp 1080w, https://alexzh.com/_astro/horizontaldivider-thickness-demo.BqeT2iys_10bt30.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;creating-a-custom-divider&quot;&gt;Creating a Custom Divider&lt;/h2&gt;
&lt;p&gt;Of course, the &lt;code&gt;HorizontalDivider&lt;/code&gt; and &lt;code&gt;VerticalDivider&lt;/code&gt; don’t cover all use cases. To create a custom divider, we can adopt a similar approach by drawing on a canvas to suit the specific needs of our application.&lt;/p&gt;
&lt;p&gt;Let’s imagine we want to build a file manager application and we need to create a divider component that will separate segments of the path to the file.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;460&quot; src=&quot;https://alexzh.com/_astro/file-manager-demo.DoELLuXY_IqN93.webp&quot; srcset=&quot;https://alexzh.com/_astro/file-manager-demo.DoELLuXY_1VfYjX.webp 720w, https://alexzh.com/_astro/file-manager-demo.DoELLuXY_N8un.webp 1080w, https://alexzh.com/_astro/file-manager-demo.DoELLuXY_IqN93.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;p&gt;So, let’s create a custom divider that helps us implement a UI similar to what we see above.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun SegmentPathDivider(
    modifier: Modifier = Modifier,
    width: Dp = 20.dp,
    thickness: Dp = DividerDefaults.Thickness,
    color: Color = DividerDefaults.color,
) = Canvas(modifier.fillMaxHeight().width(width)) {
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Drawing a diagonal line requires more width than a vertical line, so we need to consider the &lt;code&gt;width&lt;/code&gt; parameter. In our case, we will draw a line between 25% and 75% of the width. (By default, the &lt;code&gt;VerticalDivider&lt;/code&gt; doesn’t add any additional space before and after the divider.)&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;360&quot; src=&quot;https://alexzh.com/_astro/custom-segmentpathdivider-drawing-structure.B7cpdzsm_6wh5l.webp&quot; srcset=&quot;https://alexzh.com/_astro/custom-segmentpathdivider-drawing-structure.B7cpdzsm_ZYpLNG.webp 720w, https://alexzh.com/_astro/custom-segmentpathdivider-drawing-structure.B7cpdzsm_ZzbKs1.webp 1080w, https://alexzh.com/_astro/custom-segmentpathdivider-drawing-structure.B7cpdzsm_6wh5l.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun SegmentPathDivider(
    modifier: Modifier = Modifier,
    width: Dp = 20.dp,
    thickness: Dp = DividerDefaults.Thickness,
    color: Color = DividerDefaults.color,
) = Canvas(
    modifier
        .fillMaxHeight()
        .width(width)) {
    drawLine(
        color = color,
        strokeWidth = thickness.toPx(),
        start = Offset(thickness.toPx() / 2 + width.toPx() * 0.25f, size.height),
        end = Offset(thickness.toPx() / 2 + width.toPx() * 0.75f, 0f)
    )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The final step is to apply the &lt;code&gt;SegmentPathDivider&lt;/code&gt; in the user interface of our application.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;320&quot; src=&quot;https://alexzh.com/_astro/custom-segmentpathdivider-demo.CFQYmyWf_Zf7MK.webp&quot; srcset=&quot;https://alexzh.com/_astro/custom-segmentpathdivider-demo.CFQYmyWf_DxssO.webp 720w, https://alexzh.com/_astro/custom-segmentpathdivider-demo.CFQYmyWf_Z1LI1pU.webp 1080w, https://alexzh.com/_astro/custom-segmentpathdivider-demo.CFQYmyWf_Zf7MK.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
fun Demo_SegmentPathDivider() {
    val pathSegments = listOf(&quot;Projects&quot;, &quot;Important project&quot;, &quot;src&quot;)
    Row(
        modifier = Modifier
            .padding(16.dp)
            .height(IntrinsicSize.Min)
    ) {
        pathSegments.forEachIndexed { index, segment -&amp;gt;
            Text(text = segment, fontSize = 20.sp)
            if (index &amp;lt; pathSegments.size - 1) {
                SegmentPathDivider(color = Color.Black)
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;As you can see, the &lt;code&gt;HorizontalDivider&lt;/code&gt; and &lt;code&gt;VerticalDivider&lt;/code&gt; from the “Material 3” library are easy to use, but they are relatively limited in customizations.&lt;/p&gt;
&lt;p&gt;Under the hood, this is just a &lt;code&gt;Canvas&lt;/code&gt; with a drawn line. So, we can easily create a &lt;em&gt;custom divider&lt;/em&gt; and reuse it in many places of our application.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;Originally published on &lt;a href=&quot;https://alexzh.com/jetpack-compose-divider/&quot;&gt;alexzh.com&lt;/a&gt;.&lt;/p&gt;</content:encoded><media:content url="https://alexzh.com/_astro/cover.CLPVfRd3_1JLIO6.jpg" medium="image"/><category>Jetpack Compose</category><category>UI components</category></item><item><title>Review of 2023</title><link>https://alexzh.com/review-of-2023/</link><guid isPermaLink="false">6597084aee87200001d9ac11</guid><description>Looking back at 2023: the habits that improved my mental state, the articles and tips I published, my public speaking, and plans for 2024.</description><pubDate>Thu, 04 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://alexzh.com/_astro/cover.DnobO2O2_ZW8XIQ.webp&quot; alt=&quot;&quot; width=&quot;1080&quot; height=&quot;655&quot;&gt;&lt;p&gt;The first week of 2024 is an excellent opportunity to reflect on the previous year and set goals for the upcoming one. The 2023 was pretty challenging for me in multiple areas of my life, like mental state and content creation. I’d like to share a concise recap of last year and share plans for 2024 in this article. &lt;/p&gt;
&lt;p&gt;I want to start with activities which helped me to improve my mental state:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Morning reading&lt;/li&gt;
&lt;li&gt;Mental walks (no music &amp;amp; podcasts)&lt;/li&gt;
&lt;li&gt;Journalling&lt;/li&gt;
&lt;li&gt;No phone first 2 hours of the day&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;articles&quot;&gt;Articles&lt;/h2&gt;
&lt;p&gt;In 2023, I published 8 articles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://alexzh.com/new-logcat-5-features-for-effective-android-app-debugging/&quot;&gt;New Logcat: 5 Features for Effective Android App Debugging&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://alexzh.com/jetpack-compose-dropdownmenu/&quot;&gt;Jetpack Compose: DropDown Menu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://alexzh.com/how-to-test-android-app-shortcuts/&quot;&gt;How to test Android App Shortcuts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://alexzh.com/jetpack-compose-switch/&quot;&gt;Jetpack Compose: Switch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://alexzh.com/get-access-to-string-resources-in-uiautomator/&quot;&gt;Get access to string resources in UiAutomator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://alexzh.com/regional-preferences-in-android-14/&quot;&gt;Regional Preferences in Android 14&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://alexzh.com/jetpack-compose-pull-to-refresh/&quot;&gt;Jetpack Compose: Pull to Refresh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://alexzh.com/not-all-ui-tests-are-the-same/&quot;&gt;Not all UI tests are the same&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;a href=&quot;https://alexzh.com/&quot;&gt;alexzh.com&lt;/a&gt; received over 150k page views 🎉. (Last year, the yearly result was around 112k).&lt;/p&gt;
&lt;h2 id=&quot;android-development-and-testing-tips&quot;&gt;Android Development and Testing Tips&lt;/h2&gt;
&lt;p&gt;In 2023, I continued to post tips about Android development and testing on &lt;a href=&quot;https://twitter.com/Alex_Zhukovich&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;X (Twitter)&lt;/a&gt;, &lt;a href=&quot;https://www.linkedin.com/in/alex-zhukovich&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;LinkedIn&lt;/a&gt; and &lt;a href=&quot;https://androiddev.social/@Alex_Zhukovich&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Mastodon&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here are 3 most popular tips:&lt;/p&gt;
&lt;figure class=&quot;kg-card kg-embed-card&quot;&gt;&lt;div&gt;&lt;div class=&quot;tweet&quot; data-tweet=&quot;1722124319143440869&quot;&gt;&lt;blockquote class=&quot;tweet__fallback&quot;&gt;&lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&gt;Do you want to sort your dependencies alphabetically or variables inside the class?&lt;br&gt;&lt;br&gt;Here&apos;s the simple solution:&lt;br&gt;1. Select multiple lines.&lt;br&gt;2. Click &quot;Edit/Sort Lines&quot;.&lt;br&gt;3. Enjoy the result.&lt;a href=&quot;https://twitter.com/hashtag/AndroidDev?src=hash&amp;amp;ref_src=twsrc%5Etfw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#AndroidDev&lt;/a&gt; &lt;a href=&quot;https://twitter.com/hashtag/AndroidStudio?src=hash&amp;amp;ref_src=twsrc%5Etfw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#AndroidStudio&lt;/a&gt; &lt;a href=&quot;https://twitter.com/hashtag/IntelliJIDEATips?src=hash&amp;amp;ref_src=twsrc%5Etfw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#IntelliJIDEATips&lt;/a&gt; &lt;a href=&quot;https://t.co/Sa0cU2E6zP&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;pic.twitter.com/Sa0cU2E6zP&lt;/a&gt;&lt;/p&gt;— Alex Zhukovich (@Alex_Zhukovich) &lt;a href=&quot;https://twitter.com/Alex_Zhukovich/status/1722124319143440869?ref_src=twsrc%5Etfw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;November 8, 2023&lt;/a&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;
&lt;figure class=&quot;kg-card kg-embed-card&quot;&gt;&lt;div&gt;&lt;div class=&quot;tweet&quot; data-tweet=&quot;1694930107478278641&quot;&gt;&lt;blockquote class=&quot;tweet__fallback&quot;&gt;&lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&gt;Jetpack Compose Tip:&lt;br&gt;&lt;br&gt;Make use of the &quot;ContentAlpha&quot; object to easily access default alpha levels for Material components. It includes:&lt;br&gt;• high (text in a selected TextField)&lt;br&gt;• medium (placeholder text)&lt;br&gt;• disabled (disabled components)&lt;a href=&quot;https://twitter.com/hashtag/AndroidDev?src=hash&amp;amp;ref_src=twsrc%5Etfw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#AndroidDev&lt;/a&gt; &lt;a href=&quot;https://twitter.com/hashtag/JetpackCompose?src=hash&amp;amp;ref_src=twsrc%5Etfw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#JetpackCompose&lt;/a&gt; &lt;a href=&quot;https://t.co/NfQxC0g9o4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;pic.twitter.com/NfQxC0g9o4&lt;/a&gt;&lt;/p&gt;— Alex Zhukovich (@Alex_Zhukovich) &lt;a href=&quot;https://twitter.com/Alex_Zhukovich/status/1694930107478278641?ref_src=twsrc%5Etfw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;August 25, 2023&lt;/a&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;
&lt;figure class=&quot;kg-card kg-embed-card&quot;&gt;&lt;div&gt;&lt;div class=&quot;tweet&quot; data-tweet=&quot;1689512346505601024&quot;&gt;&lt;blockquote class=&quot;tweet__fallback&quot;&gt;&lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&gt;Jetpack Compose Tip:&lt;br&gt;&lt;br&gt;Did you know that you can combine multiple preview annotations and use a single annotation to generate multiple previews?&lt;a href=&quot;https://twitter.com/hashtag/JetpackCompose?src=hash&amp;amp;ref_src=twsrc%5Etfw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#JetpackCompose&lt;/a&gt; &lt;a href=&quot;https://twitter.com/hashtag/AndroidDev?src=hash&amp;amp;ref_src=twsrc%5Etfw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#AndroidDev&lt;/a&gt; &lt;a href=&quot;https://t.co/X04RQY37Rh&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;pic.twitter.com/X04RQY37Rh&lt;/a&gt;&lt;/p&gt;— Alex Zhukovich (@Alex_Zhukovich) &lt;a href=&quot;https://twitter.com/Alex_Zhukovich/status/1689512346505601024?ref_src=twsrc%5Etfw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;August 10, 2023&lt;/a&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;/div&gt;&lt;/figure&gt;
&lt;h2 id=&quot;public-speaking&quot;&gt;Public speaking&lt;/h2&gt;
&lt;p&gt;This year I gave workshops and talks about UI testing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full-day Workshop: Modern Android UI Testing @ AppDevCon (&lt;a href=&quot;https://speakerdeck.com/alexzhukovich/workshop-android-ui-testing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;slides&lt;/a&gt;, &lt;a href=&quot;https://github.com/AlexZhukovich/AndroidUiTestingWorkshop&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;project&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Common Mistakes in UI Testing @ AppDevCon (&lt;a href=&quot;https://speakerdeck.com/alexzhukovich/common-mistakes-in-ui-testing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;slides&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;4h workshop: UI Testing of Jetpack Compose Apps @ Droidcon Italy&lt;/li&gt;
&lt;li&gt;UI Testing of Jetpack Compose Apps @ Droidcon Italy (&lt;a href=&quot;https://speakerdeck.com/alexzhukovich/ui-testing-of-jetpack-compose-apps-droidcon-italy-2023&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;slides&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;plans-for-2024&quot;&gt;Plans for 2024&lt;/h2&gt;
&lt;p&gt;Previous year was pretty chaotic for me and I want to make this year much more consistent. My focus for 2024 is writing and I want to deliver more high-quality content on my blog and in social media. &lt;/p&gt;
&lt;p&gt;Here are my main goals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Publish a book&lt;/li&gt;
&lt;li&gt;Release 15+ articles&lt;/li&gt;
&lt;li&gt;Publish 10 videos&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Follow my &lt;a href=&quot;https://twitter.com/Alex_Zhukovich&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;X (Twitter)&lt;/a&gt;, &lt;a href=&quot;https://www.linkedin.com/in/alex-zhukovich&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;LinkedIn&lt;/a&gt; and &lt;a href=&quot;https://androiddev.social/@Alex_Zhukovich&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Mastodon&lt;/a&gt; accounts if you want to get more content about mobile development and testing.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;Originally published on &lt;a href=&quot;https://alexzh.com/review-of-2023/&quot;&gt;alexzh.com&lt;/a&gt;.&lt;/p&gt;</content:encoded><media:content url="https://alexzh.com/_astro/cover.DnobO2O2_RHKOk.jpg" medium="image"/><category>Annual Review</category></item><item><title>Not all UI tests are the same</title><link>https://alexzh.com/not-all-ui-tests-are-the-same/</link><guid isPermaLink="false">655695cca18551000103637e</guid><description>End-to-end tests, UI tests with fake data, pixel perfection tests and accessibility tests: what each type of UI testing checks and how to get started.</description><pubDate>Fri, 17 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://alexzh.com/_astro/cover._FKM2DmX_1fC91C.webp&quot; alt=&quot;&quot; width=&quot;1080&quot; height=&quot;654&quot;&gt;&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Typically, when we think of UI testing, we think of simulating user behavior, such as when a user enters their email and password and clicks the login button. However, modern UI testing can do much more than just emulate user behavior.&lt;/p&gt;
&lt;p&gt;In this article, we will explore the following topics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Different types of UI tests&lt;/li&gt;
&lt;li&gt;Understanding what different UI tests CAN and CANNOT verify&lt;/li&gt;
&lt;li&gt;What to check with each type of UI tests&lt;/li&gt;
&lt;li&gt;How to start with UI testing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All examples will be based on the “Mood Tracker” application. Let’s explore 3 screens, as all our test cases will be based on these screens.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1224&quot; height=&quot;650&quot; src=&quot;https://alexzh.com/_astro/mood-tracker-app-screens.De4pmyM3_Z1uDaf1.webp&quot; srcset=&quot;https://alexzh.com/_astro/mood-tracker-app-screens.De4pmyM3_ZUD6DQ.webp 720w, https://alexzh.com/_astro/mood-tracker-app-screens.De4pmyM3_Z2sFIiY.webp 1080w, https://alexzh.com/_astro/mood-tracker-app-screens.De4pmyM3_Z1uDaf1.webp 1224w&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;different-types-of-ui-tests&quot;&gt;Different types of UI tests&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;End-To-End tests&lt;/strong&gt; ensure that the application works with the production environment. This means that all application layers work correctly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UI tests with fake data&lt;/strong&gt; verify different states of the screen (no data, successfully loaded data, network is unavailable, and much more).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Screenshot tests&lt;/strong&gt; ensure that a component or screen looks correct in different states.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Accessibility tests&lt;/strong&gt; check if everyone can use a user interface, including people with disabilities.&lt;/p&gt;
&lt;h3 id=&quot;end-to-end-tests&quot;&gt;End-To-End tests&lt;/h3&gt;
&lt;p&gt;End-to-end tests verify the interaction between the UI, back-end, database, and other application layers.&lt;/p&gt;
&lt;p&gt;Let’s explore the main characteristics of such tests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Entry point:&lt;/strong&gt; This is similar to the entry point of the application (we often start such tests from the splash or home screen).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Navigation:&lt;/strong&gt; They usually navigate through several screens starting from the entry point.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data:&lt;/strong&gt; We use the production or staging environment (staging environment: replica or copy of the production environment) to ensure interaction verification with the production environment.&lt;/li&gt;
&lt;/ul&gt;
&lt;table data-stack=&quot;cells&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Pros&quot;&gt;E2E tests emulate real-world user scenarios and provide a realistic assessment of how the application will behave in a production environment.&lt;/td&gt;
&lt;td data-label=&quot;Cons&quot;&gt;E2E tests are slow because of interaction with the real environment and a lot of navigating is required.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Pros&quot;&gt;We make sure that our application works well with the real environment.&lt;/td&gt;
&lt;td data-label=&quot;Cons&quot;&gt;They can be flaky as they have multiple dependencies (network access, network speed, VPN access, etc.).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Pros&quot;&gt;These tests effectively detect integration issues and other complex defects that are not easily detected by unit or integration testing.&lt;/td&gt;
&lt;td data-label=&quot;Cons&quot;&gt;It is not possible to test all cases because it is not always possible to reproduce all types of issues within the real environment.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td data-label=&quot;Cons&quot;&gt;E2E tests update data in the prod/staging environment and can affect other tests that use the same environment.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;aside class=&quot;callout callout--note&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Test Case:&lt;/strong&gt;&lt;/strong&gt; Add an emotional state to the application and verify the entered data on the screen.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Steps:&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
• &lt;strong&gt;[Action]:&lt;/strong&gt; Open the application.&lt;br&gt;
• &lt;strong&gt;[Assertion]:&lt;/strong&gt; The “No data” text is displayed on the home screen.&lt;br&gt;
• &lt;strong&gt;[Action]:&lt;/strong&gt; Tap on the “Add” button.&lt;br&gt;
• &lt;strong&gt;[Action]:&lt;/strong&gt; Select the “Excited” emotion.&lt;br&gt;
• &lt;strong&gt;[Action]:&lt;/strong&gt; Select the specific date and time.&lt;br&gt;
• &lt;strong&gt;[Action]:&lt;/strong&gt; Select “Relax” activity.&lt;br&gt;
• &lt;strong&gt;[Action]:&lt;/strong&gt; Tap on the “Save” button.&lt;br&gt;
• &lt;strong&gt;[Assertion]:&lt;/strong&gt; Check that the emotion item (with “Excited” emotion, “Relax” activity, and a specific time) is displayed on the screen.&lt;/p&gt;&lt;/aside&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1224&quot; height=&quot;650&quot; src=&quot;https://alexzh.com/_astro/e2e-test-case-flow.nWDgdAcs_bT2Tu.webp&quot; srcset=&quot;https://alexzh.com/_astro/e2e-test-case-flow.nWDgdAcs_Vbvuu.webp 720w, https://alexzh.com/_astro/e2e-test-case-flow.nWDgdAcs_Z1a4gfO.webp 1080w, https://alexzh.com/_astro/e2e-test-case-flow.nWDgdAcs_bT2Tu.webp 1224w&quot;&gt;&lt;/figure&gt;
&lt;h3 id=&quot;ui-tests-with-fake-data&quot;&gt;UI tests with fake data&lt;/h3&gt;
&lt;aside class=&quot;callout callout--note&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Naming&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
As we all know, naming is a complicated topic in software development, and we have the same situation with UI testing. This type of test has different names in different articles:&lt;br&gt;
• Component UI tests&lt;br&gt;
• Functional tests&lt;br&gt;
• Integration tests&lt;br&gt;
• Isolated UI tests&lt;br&gt;
• UI tests&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;&lt;strong&gt;UI tests with fake data&lt;/strong&gt; verify the different states of the components or screens of the application and use the fake data to simulate all cases.&lt;/p&gt;
&lt;p&gt;Main characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Entry point:&lt;/strong&gt; Required screen (if the UI testing framework allows us to do it)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Navigation:&lt;/strong&gt; Depends on the goal of the test case (they use fake data):
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Screen tests (Screen component verification):&lt;/em&gt; No navigation needed&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Navigation tests (Navigation verification):&lt;/em&gt; Navigation needed&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data:&lt;/strong&gt; Fake data&lt;/li&gt;
&lt;/ul&gt;
&lt;table data-stack=&quot;cells&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Pros&quot;&gt;These tests are fast and stable.&lt;/td&gt;
&lt;td data-label=&quot;Cons&quot;&gt;These tests use fake data and we need to synchronize fake responses with responses from a real server.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Pros&quot;&gt;We can emulate all cases.&lt;/td&gt;
&lt;td data-label=&quot;Cons&quot;&gt;Testing with fake data may not fully represent the complexity and variability of real-world data.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;aside class=&quot;callout callout--note&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Test Case:&lt;/strong&gt;&lt;/strong&gt; Validate the different screen states:&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Single item on the screens&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
• &lt;strong&gt;[Action]:&lt;/strong&gt; Load the screen with predefined data.&lt;br&gt;
• &lt;strong&gt;[Assertion]:&lt;/strong&gt; Check that the emotion item is displayed on the screen (with emotion, activity and a specific time).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Multiple items on the screens&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;&lt;strong&gt;•&lt;/strong&gt;&lt;/strong&gt; &lt;strong&gt;[Action]:&lt;/strong&gt; Load the screen with predefined data.&lt;br&gt;
• &lt;strong&gt;[Assertion]:&lt;/strong&gt; Check that emotion items are displayed on the screen (with emotion, activity, and a specific time).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;No data&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;&lt;strong&gt;•&lt;/strong&gt;&lt;/strong&gt; &lt;strong&gt;[Action]:&lt;/strong&gt; Load the screen with predefined data.&lt;br&gt;
• &lt;strong&gt;[Assertion]:&lt;/strong&gt; Check that “No data” text is displayed on the screen.&lt;/p&gt;&lt;/aside&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1207&quot; height=&quot;345&quot; src=&quot;https://alexzh.com/_astro/ui-tests-with-fake-data-init-exampes.C2hxU4sI_1pNxDb.webp&quot; srcset=&quot;https://alexzh.com/_astro/ui-tests-with-fake-data-init-exampes.C2hxU4sI_1onAzQ.webp 720w, https://alexzh.com/_astro/ui-tests-with-fake-data-init-exampes.C2hxU4sI_Z1gYns9.webp 1080w, https://alexzh.com/_astro/ui-tests-with-fake-data-init-exampes.C2hxU4sI_1pNxDb.webp 1207w&quot;&gt;&lt;/figure&gt;
&lt;p&gt;This screen displays the time and we can verify that the time is displayed in the correct time format without updating the values in the “Settings” screen. So, let’s update our scenarios.&lt;/p&gt;
&lt;aside class=&quot;callout callout--note&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Updated test case&lt;/strong&gt;&lt;/strong&gt;: Verify the different screen states&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Single item on the screens with 12h time format&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
• &lt;strong&gt;[Action]:&lt;/strong&gt; Load the screen with predefined data.&lt;br&gt;
• &lt;strong&gt;[Assertion]:&lt;/strong&gt; Check that the emotion item is displayed on the screen (with emotion, activity, and a specific time in 12h format).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Single item on the screens with 24h time format&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;&lt;strong&gt;•&lt;/strong&gt;&lt;/strong&gt; &lt;strong&gt;[Action]:&lt;/strong&gt; Load the screen with predefined data.&lt;br&gt;
• &lt;strong&gt;[Assertion]:&lt;/strong&gt; Check that the emotion item is displayed on the screen (with emotion, activity, and a specific time in 24h format).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Multiple items on the screens with 12h time format&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
• &lt;strong&gt;[Action]:&lt;/strong&gt; Load the screen with predefined data.&lt;br&gt;
• &lt;strong&gt;[Assertion]:&lt;/strong&gt; Check that emotion items are displayed on the screen (with emotion, activity, and a specific time in 12h format).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Multiple items on the screens with 24h time format&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
• &lt;strong&gt;[Action]:&lt;/strong&gt; Load the screen with predefined data&lt;br&gt;
• &lt;strong&gt;[Assertion]:&lt;/strong&gt; Check that emotion items are displayed on the screen (with emotion, activity, and a specific time in 24h format).&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;No data&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
• &lt;strong&gt;[Action]:&lt;/strong&gt; Load the screen with predefined data.&lt;br&gt;
• &lt;strong&gt;[Assertion]:&lt;/strong&gt; Check that “No data” text is displayed on the screen.&lt;/p&gt;&lt;/aside&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1224&quot; height=&quot;740&quot; src=&quot;https://alexzh.com/_astro/ui-tests-with-fake-data-exampes.BsuNiCtY_ZjL4PF.webp&quot; srcset=&quot;https://alexzh.com/_astro/ui-tests-with-fake-data-exampes.BsuNiCtY_1NEfEM.webp 720w, https://alexzh.com/_astro/ui-tests-with-fake-data-exampes.BsuNiCtY_ZQ7Pjk.webp 1080w, https://alexzh.com/_astro/ui-tests-with-fake-data-exampes.BsuNiCtY_ZjL4PF.webp 1224w&quot;&gt;&lt;/figure&gt;
&lt;h3 id=&quot;screenshot-tests&quot;&gt;Screenshot tests&lt;/h3&gt;
&lt;aside class=&quot;callout callout--note&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Naming&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
These tests are also commonly called:&lt;br&gt;
• Visual Tests&lt;br&gt;
• Snapshot Tests&lt;br&gt;
• Pixel Perfectness Tests&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;&lt;em&gt;End-To-End&lt;/em&gt; and &lt;em&gt;UI tests with fake data&lt;/em&gt; can find many issues in the app. However, they cannot find pixel perfectness issues because they verify that data is displayed on the screen, but they don’t check that data is displayed in the right place, that views don’t overlap each other, that text doesn’t cut off, that UI elements use the right color, font size, and much more.&lt;/p&gt;
&lt;p&gt;If we look at this image, we can find the difference even if both screens have similar data.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1224&quot; height=&quot;340&quot; src=&quot;https://alexzh.com/_astro/screenshot-tests-issue-example.CmJhvBE3_Z4mCgA.webp&quot; srcset=&quot;https://alexzh.com/_astro/screenshot-tests-issue-example.CmJhvBE3_axN2D.webp 720w, https://alexzh.com/_astro/screenshot-tests-issue-example.CmJhvBE3_Z2oQCSE.webp 1080w, https://alexzh.com/_astro/screenshot-tests-issue-example.CmJhvBE3_Z4mCgA.webp 1224w&quot;&gt;&lt;/figure&gt;
&lt;p&gt;So, screenshot tests verify how content is rendered on the screen. We test components and screens with static data using this type of tests. When the UI is changed, the screenshots should be updated (usually it’s done with a single command).&lt;/p&gt;
&lt;table data-stack=&quot;cells&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Pros&quot;&gt;These tests provide a visual validation of the user interface.&lt;/td&gt;
&lt;td data-label=&quot;Cons&quot;&gt;The maintenance of screenshot tests can be challenging, especially if the UI changes frequently.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Pros&quot;&gt;Visual changes are easily detected in reports after test case execution.&lt;/td&gt;
&lt;td data-label=&quot;Cons&quot;&gt;These tests focus on the visual aspect only and will not detect any functional issues.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;aside class=&quot;callout callout--note&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Test Cases&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Custom components:&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
• Weekly Calendar&lt;br&gt;
• Date Picker&lt;br&gt;
• Time Picker&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Screen configurations:&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;&lt;strong&gt;•&lt;/strong&gt;&lt;/strong&gt; Color Mode (light and dark modes)&lt;br&gt;
• Font Sizes&lt;br&gt;
• Layout Direction&lt;br&gt;
• Translations into Different Languages&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;So, let’s choose a few basic test scenarios which can be covered for the home screen with screenshot tests:&lt;/p&gt;
&lt;aside class=&quot;callout callout--note&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Test Cases&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Home screen without emotions&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
• Light mode &amp;amp; small font size&lt;br&gt;
• Light mode &amp;amp; large font size&lt;br&gt;
• Dark mode &amp;amp; small font size&lt;br&gt;
• Dark mode &amp;amp; large font size&lt;/p&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;Home screen with multiple emotions&lt;/strong&gt;&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;&lt;strong&gt;•&lt;/strong&gt;&lt;/strong&gt; Light mode &amp;amp; small font size&lt;br&gt;
• Light mode &amp;amp; large font size&lt;br&gt;
• Dark mode &amp;amp; small font size&lt;br&gt;
• Dark mode &amp;amp; large font size&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;Often you can find a situation where a UI component or screen works with the default font size, but when the font size is large, you can see some problems in the UI. In this example, when the font size is large, some of the day abbreviations contain 2 symbols instead of 3 (e.g. “Mo” instead of “Mon”).&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1224&quot; height=&quot;400&quot; src=&quot;https://alexzh.com/_astro/screenshot-tests-large-font-size-issues.BS7OdK3z_ZPa5He.webp&quot; srcset=&quot;https://alexzh.com/_astro/screenshot-tests-large-font-size-issues.BS7OdK3z_7bP6U.webp 720w, https://alexzh.com/_astro/screenshot-tests-large-font-size-issues.BS7OdK3z_ZCdHK6.webp 1080w, https://alexzh.com/_astro/screenshot-tests-large-font-size-issues.BS7OdK3z_ZPa5He.webp 1224w&quot;&gt;&lt;/figure&gt;
&lt;p&gt;I highly recommend checking screens with different font sizes and scales. I saw such issues in many apps.&lt;/p&gt;
&lt;h3 id=&quot;accessibility-tests&quot;&gt;Accessibility tests&lt;/h3&gt;
&lt;p&gt;The application may function well and have a good design, but that doesn’t make it accessible for everyone. Often accessibility has a low priority and people with disabilities do not have the best experience when using various applications. Accessibility tests verify that our application is accessible to everyone.&lt;/p&gt;
&lt;p&gt;Here are the common issues that can be verified with accessibility tests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Incorrect screen structure&lt;/li&gt;
&lt;li&gt;Missing labels for UI components&lt;/li&gt;
&lt;li&gt;Incorrect labels for complex components&lt;/li&gt;
&lt;li&gt;Small clickable elements, such as buttons and icons&lt;/li&gt;
&lt;/ul&gt;
&lt;table data-stack=&quot;cells&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Pros&quot;&gt;These tests make sure the app is accessible to a broader audience, including people with disabilities.&lt;/td&gt;
&lt;td data-label=&quot;Cons&quot;&gt;Accessibility testing often requires specialized knowledge and skills, and it may be that not all members of the team are familiar with the best practices and guidelines.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Android recommendations&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Before creating UI tests, I recommend verifying your application with the “Accessibility Scanner” app.&lt;/li&gt;
&lt;li&gt;Recently, the “Smart UI Check Mode” feature was added to the Android Studio Iguana, and you can verify if your composable functions are accessible without running the application.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&quot;kg-card kg-embed-card&quot;&gt;&lt;div class=&quot;tweet&quot; data-tweet=&quot;1719587614519611526&quot;&gt;&lt;blockquote class=&quot;tweet__fallback&quot;&gt;&lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&gt;Not sure if your composable function is accessible?&lt;br&gt;&lt;br&gt;The &quot;Smart UI Check Mode&quot; feature of Android Studio Iguana allows you to verify your composable for common accessibility issues.&lt;a href=&quot;https://twitter.com/hashtag/JetpackCompose?src=hash&amp;amp;ref_src=twsrc%5Etfw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#JetpackCompose&lt;/a&gt; &lt;a href=&quot;https://twitter.com/hashtag/AndroidDev?src=hash&amp;amp;ref_src=twsrc%5Etfw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#AndroidDev&lt;/a&gt; &lt;a href=&quot;https://twitter.com/hashtag/AndroidStudio?src=hash&amp;amp;ref_src=twsrc%5Etfw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;#AndroidStudio&lt;/a&gt; &lt;a href=&quot;https://t.co/C2a25yFFIZ&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;pic.twitter.com/C2a25yFFIZ&lt;/a&gt;&lt;/p&gt;— Alex Zhukovich (@Alex_Zhukovich) &lt;a href=&quot;https://twitter.com/Alex_Zhukovich/status/1719587614519611526?ref_src=twsrc%5Etfw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;November 1, 2023&lt;/a&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;/figure&gt;
&lt;p&gt;I recommend covering all custom components (components from the design system) with accessibility tests.&lt;/p&gt;
&lt;h2 id=&quot;how-to-start-ui-testing&quot;&gt;How to start UI testing?&lt;/h2&gt;
&lt;p&gt;As you can see, the different types of tests have their advantages and disadvantages. If your application already has had UI tests, you will see what needs to be improved or which types of tests you can introduce to your project. However, if you are new to UI testing, you may have the question, “What is a good way to introduce UI testing to the project?”.&lt;/p&gt;
&lt;p&gt;Here are a few recommendations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First, we should prioritize E2E testing for the most critical use cases to ensure that the features work correctly and interact smoothly with the production/staging environment. Keep in mind that E2E testing tends to be slow and flaky. So it’s a good place to start, but I recommend focusing more on other types of tests.&lt;/li&gt;
&lt;li&gt;Next, you can start adding &lt;em&gt;UI tests with fake data&lt;/em&gt; and &lt;em&gt;Screenshot tests&lt;/em&gt;, as they both check components/screens in isolation, and you can check all states and errors for your screens.&lt;/li&gt;
&lt;li&gt;To find the important screens to test, I suggest looking at the analytics. It will show you which screens and scenarios are used more than others in your application.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;There are several types of UI tests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;End-To-End tests&lt;/strong&gt; ensure that your application works properly with the production/staging environment, but they are slower and less reliable than other types of test.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI tests with fake data&lt;/strong&gt; check various states of your screens and use fake data; this makes these tests fast, but you cannot be sure that your application works well with the real environment using only this type of test.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Screenshot tests&lt;/strong&gt; verify that all your components and screens are rendered properly and placed correctly on the screen. You can also check your screen with different configurations like color modes, font sizes, display scales, layout directions, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accessibility tests&lt;/strong&gt; checks that everyone can use your application using gestures, voice, or special devices.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As you can see, if we want to make sure that your application works correctly, we need to check many scenarios:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Verify that your application has no problems interacting with the production environment&lt;/li&gt;
&lt;li&gt;Check all states and errors for components and screens&lt;/li&gt;
&lt;li&gt;Verify that everything is correctly rendered and positioned on the screen&lt;/li&gt;
&lt;li&gt;Make sure that your application is accessible to everyone&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This requires us to combine different types of UI testing.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I would like to thank &lt;a href=&quot;https://twitter.com/testableapple&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Alexey Alter-Pesotskiy&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/nonewsss&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Alexey Bykov&lt;/a&gt;, &lt;a href=&quot;https://www.linkedin.com/in/ekatrys/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Ekaterina Basova&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/e_matsyuk&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Evgenii Matsiuk&lt;/a&gt;, &lt;a href=&quot;https://twitter.com/jovchem&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Jovche Mitrejchevski&lt;/a&gt;, and &lt;a href=&quot;https://twitter.com/Gio_Sastre&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Sergio Sastre Florez&lt;/a&gt; for reviewing the article.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;Originally published on &lt;a href=&quot;https://alexzh.com/not-all-ui-tests-are-the-same/&quot;&gt;alexzh.com&lt;/a&gt;.&lt;/p&gt;</content:encoded><media:content url="https://alexzh.com/_astro/cover._FKM2DmX_Zugn5m.jpg" medium="image"/><category>Android Testing</category><category>UI testing</category></item><item><title>Jetpack Compose: Pull to Refresh</title><link>https://alexzh.com/jetpack-compose-pull-to-refresh/</link><guid isPermaLink="false">6533fcea74d39300015173eb</guid><description>How to add and customize the &quot;Pull to Refresh&quot; action in Jetpack Compose, and how to trigger it in UI tests.</description><pubDate>Fri, 16 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://alexzh.com/_astro/cover.CJxAAmcz_1CEEwE.webp&quot; alt=&quot;&quot; width=&quot;1080&quot; height=&quot;655&quot;&gt;&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Pull to refresh is a widespread User Interface pattern that is used to refresh data on the screen. The “Material” library for the Jetpack Compose contains the “pullRefresh” modifier and the “PullRefreshIndicator” composable function.&lt;/p&gt;
&lt;p&gt;In this article, we will explore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How to add pull to refresh to the Android app with Jetpack Compose?&lt;/li&gt;
&lt;li&gt;How to customize the indicator for pull to refresh?&lt;/li&gt;
&lt;li&gt;How to trigger pull to refresh action in UI tests?&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;The content of this article is based on the “material” library of the “androidx.compose:compose-bom:2023.05.01”. The &lt;code&gt;pullRefresh&lt;/code&gt; modifier and the &lt;code&gt;PullRefreshIndicator&lt;/code&gt; composable function are still in the experimental stage.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;460&quot; src=&quot;https://alexzh.com/_astro/jetpack-compose-pull-to-refresh-orders.C6qXdl4a_dt5N3.webp&quot; srcset=&quot;https://alexzh.com/_astro/jetpack-compose-pull-to-refresh-orders.C6qXdl4a_Z2hrGs5.webp 720w, https://alexzh.com/_astro/jetpack-compose-pull-to-refresh-orders.C6qXdl4a_ZFJG3R.webp 1080w, https://alexzh.com/_astro/jetpack-compose-pull-to-refresh-orders.C6qXdl4a_dt5N3.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;aside class=&quot;callout callout--note&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;&lt;p&gt;If you do not use the scrollable container on your screen, like LazyColumn and want to use the pullRefresh modifier, you need to add the verticalScroll to be able to trigger the pull to refresh action.&lt;/p&gt;&lt;/aside&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;Box(
    modifier = Modifier
        .fillMaxSize()
        .pullRefresh(pullRefreshState)
        .verticalScroll(rememberScrollState())
) { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;adding-the-pull-to-refresh-to-an-app&quot;&gt;Adding the Pull to Refresh to an app&lt;/h2&gt;
&lt;p&gt;Let’s start with creating a demo application which will emulate getting data from the network. For the sake of simplicity, we will add items to the list after a small delay instead of request to the server.&lt;/p&gt;
&lt;p&gt;The next steps is to a add dependency:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;dependencies {
    ...
    implementation &quot;androidx.compose.material:material:$material_version&quot;
    // Needed to get a view model reference in Jetpack Compose
    implementation &quot;androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The demo application shows the list of orders. So, let’s start with adding the &lt;code&gt;Order&lt;/code&gt; class:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;data class Order(
    val title: String,
    val price: Double
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After it, we can create an &lt;code&gt;OrderViewModel&lt;/code&gt; which will provide the orders and the loading state.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;data class OrderScreenState(
    val isLoading: Boolean = false,
    val orders: List&amp;lt;Order&amp;gt; = emptyList()
)

class OrderViewModel : ViewModel() {
    private val _state = mutableStateOf(OrderScreenState())
    val state: State&amp;lt;OrderScreenState&amp;gt; = _state

    init {
        loadOrders()
    }

    fun loadOrders() {
        viewModelScope.launch {
            _state.value = _state.value.copy(isLoading = true)
            delay(1_000L)
            _state.value = _state.value.copy(
                isLoading = false,
                orders = _state.value.orders.toMutableList().also {
                    it.add(
                        index = 0,
                        element = Order(
                            title = &quot;Order #${it.size + 1}&quot;,
                            price = Random.nextDouble(10.0, 100.0)
                        )
                    )
                }
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, we can add the UI of the screen.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Preview
@Composable
fun OrderScreen() {
    val viewModel = viewModel&amp;lt;OrderViewModel&amp;gt;()
    val state by viewModel.state
    val pullRefreshState = rememberPullRefreshState(
        refreshing = state.isLoading,
        onRefresh = viewModel::loadOrders
    )

    Scaffold(
        topBar = { TopAppBar(title = { Text(&quot;Items&quot;) }) }
    ) { padding -&amp;gt;
        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(padding)
                .pullRefresh(pullRefreshState)
        ) {
            LazyColumn {
                items(state.orders) {
                    ListItem(
                        headlineContent = { Text(text = &quot;${it.title} (€ ${&quot;%.2f&quot;.format(it.price)})&quot; ) },
                    )
                }
            }

            PullRefreshIndicator(
                refreshing = viewModel.state.value.isLoading,
                state = pullRefreshState,
                modifier = Modifier.align(Alignment.TopCenter),
                backgroundColor = if (viewModel.state.value.isLoading) Color.Red else Color.Green,
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;460&quot; src=&quot;https://alexzh.com/_astro/jetpack-compose-pull-to-refresh-orders.C6qXdl4a_dt5N3.webp&quot; srcset=&quot;https://alexzh.com/_astro/jetpack-compose-pull-to-refresh-orders.C6qXdl4a_Z2hrGs5.webp 720w, https://alexzh.com/_astro/jetpack-compose-pull-to-refresh-orders.C6qXdl4a_ZFJG3R.webp 1080w, https://alexzh.com/_astro/jetpack-compose-pull-to-refresh-orders.C6qXdl4a_dt5N3.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;pull-to-refresh-customization&quot;&gt;Pull to Refresh customization&lt;/h2&gt;
&lt;p&gt;We can customize the refresh threshold, background and content color of the pull to refresh indicator.&lt;/p&gt;
&lt;h3 id=&quot;customizing-the-pull-to-refresh-state&quot;&gt;Customizing the Pull to Refresh state&lt;/h3&gt;
&lt;p&gt;Let’s start with exploring customization options from the &lt;code&gt;rememberPullRefreshState&lt;/code&gt; function, which creates the instance of the &lt;code&gt;PullRefreshState&lt;/code&gt; class.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Composable
@ExperimentalMaterialApi
fun rememberPullRefreshState(
    refreshing: Boolean,
    onRefresh: () -&amp;gt; Unit,
    refreshThreshold: Dp = PullRefreshDefaults.RefreshThreshold,
    refreshingOffset: Dp = PullRefreshDefaults.RefreshingOffset,
): PullRefreshState { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;refreshThreshold&lt;/code&gt; allows us to define the minimum values which should be achieved to trigger the &lt;code&gt;onRefresh&lt;/code&gt; function. By default, this value to set to &lt;code&gt;80.dp&lt;/code&gt; (&lt;code&gt;val RefreshThreshold = 80.dp&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;refreshingOffset&lt;/code&gt; allows us to define the offset for rendering the pull to refresh indicator. By default, this value to set to &lt;code&gt;56.dp&lt;/code&gt; (&lt;code&gt;val RefreshingOffset = 56.dp&lt;/code&gt;).&lt;/p&gt;
&lt;h3 id=&quot;customizing-the-pull-to-refresh-indicator&quot;&gt;Customizing the Pull to Refresh indicator&lt;/h3&gt;
&lt;p&gt;We can customize the background and content colors of the pull to refresh indicator with the &lt;code&gt;backgroundColor&lt;/code&gt; and the &lt;code&gt;contentColor&lt;/code&gt; parameters. If we want to combine the &lt;code&gt;backgroundColor&lt;/code&gt; (or &lt;code&gt;contentColor&lt;/code&gt;) with the refreshing state, we can have different colors for the refreshing and non-refreshing states of the indicator.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;...
PullRefreshIndicator(
    refreshing = isLoading,
    state = pullRefreshState,
    modifier = Modifier.align(Alignment.TopCenter),
    backgroundColor = if (isLoading) Color.Red else Color.Green,
)
...&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;video src=&quot;https://alexzh.com/media/jetpack-compose-pull-to-refresh/jetpack-compose-pull-to-refresh-orders-diff-colors-1.mp4&quot; poster=&quot;https://alexzh.com/media/jetpack-compose-pull-to-refresh/jetpack-compose-pull-to-refresh-orders-diff-colors-1_poster.webp&quot; width=&quot;1280&quot; height=&quot;460&quot; loop muted playsinline preload=&quot;none&quot; data-autoplay&gt;&lt;/video&gt;&lt;/figure&gt;
&lt;h2 id=&quot;testing-pull-to-refresh-state&quot;&gt;Testing Pull to Refresh state&lt;/h2&gt;
&lt;p&gt;Let’s explore how to trigger update content use pull to refresh.&lt;/p&gt;
&lt;p&gt;We will test a screen that shows “Item 1” by default, and “Item 1” and “Item 2” after a pull to refresh action.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;420&quot; src=&quot;https://alexzh.com/_astro/pull-to-refresh-states.Dx0_yuso_gWOHr.webp&quot; srcset=&quot;https://alexzh.com/_astro/pull-to-refresh-states.Dx0_yuso_ftmmx.webp 720w, https://alexzh.com/_astro/pull-to-refresh-states.Dx0_yuso_1P7fR3.webp 1080w, https://alexzh.com/_astro/pull-to-refresh-states.Dx0_yuso_gWOHr.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;p&gt;Here is the source code of the screen:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
private fun DemoPullToRefresh() {
    val coroutineScope = rememberCoroutineScope()
    val listItems = remember { mutableListOf(&quot;Item 1&quot;) }
    var isRefreshing by remember { mutableStateOf(false) }
    val pullRefreshState = rememberPullRefreshState(
        refreshing = isRefreshing,
        onRefresh = {
            isRefreshing = true
            coroutineScope.launch {
                listItems.add(&quot;Item ${listItems.size + 1}&quot;)
                isRefreshing = false
            }
        }
    )
    Scaffold(
        topBar = { TopAppBar(title = { Text(&quot;Items&quot;) }) }
    ) { padding -&amp;gt;
        Box(
            modifier = Modifier
                .padding(padding)
                .pullRefresh(pullRefreshState)
        ) {
            LazyColumn(Modifier.fillMaxSize()) {
                items(listItems) {
                    ListItem(headlineContent = { Text(text = it) })
                }
            }

            PullRefreshIndicator(
                refreshing = isRefreshing,
                state = pullRefreshState,
                modifier = Modifier.align(Alignment.TopCenter)
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To trigger pull to refresh action, we need to use the &lt;code&gt;.performTouchInput { swipeDown() }&lt;/code&gt; action on scrollable node. We can find a scrollable node on the screen using the &lt;code&gt;hasScrollToIndexAction()&lt;/code&gt; or &lt;code&gt;hasScrollToIndexAction()&lt;/code&gt; semantic matcher.&lt;/p&gt;
&lt;p&gt;Let’s create a test case for the following scenario:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Initially “Item 1” is displayed and “Item 2” is not displayed&lt;/li&gt;
&lt;li&gt;Trigger pull to refresh action&lt;/li&gt;
&lt;li&gt;Verify that “Item 2” is displayed on the screen&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;@Test
fun shouldDisplaySecondItemAfterPullToRefresh() {
    composeTestRule.apply {
        setContent {
            DemoPullToRefresh()
        }

        onNodeWithText(&quot;Item 1&quot;)
            .assertIsDisplayed()

        onNodeWithText(&quot;Item 2&quot;)
            .assertDoesNotExist()

        onNode(hasScrollToIndexAction())
            .performTouchInput { swipeDown() }

        onNodeWithText(&quot;Item 2&quot;)
            .assertIsDisplayed()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The “Material” library for Jetpack Compose provides the &lt;code&gt;pullRefresh&lt;/code&gt; modifier and the &lt;code&gt;PullRefreshIndicator&lt;/code&gt; component. They allow us to implement the pull to refresh UI patter in a screen which is built with Jetpack Compose.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;460&quot; src=&quot;https://alexzh.com/_astro/jetpack-compose-pull-to-refresh-orders.C6qXdl4a_dt5N3.webp&quot; srcset=&quot;https://alexzh.com/_astro/jetpack-compose-pull-to-refresh-orders.C6qXdl4a_Z2hrGs5.webp 720w, https://alexzh.com/_astro/jetpack-compose-pull-to-refresh-orders.C6qXdl4a_ZFJG3R.webp 1080w, https://alexzh.com/_astro/jetpack-compose-pull-to-refresh-orders.C6qXdl4a_dt5N3.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;p&gt;When we want to trigger pull to refresh action in UI tests, we need to find a scrollable node and execute the &lt;code&gt;swipeDown()&lt;/code&gt; action.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;Originally published on &lt;a href=&quot;https://alexzh.com/jetpack-compose-pull-to-refresh/&quot;&gt;alexzh.com&lt;/a&gt;.&lt;/p&gt;</content:encoded><media:content url="https://alexzh.com/_astro/cover.CJxAAmcz_Z1BEII6.jpg" medium="image"/><category>Jetpack Compose</category><category>UI components</category></item><item><title>Regional Preferences in Android 14</title><link>https://alexzh.com/regional-preferences-in-android-14/</link><guid isPermaLink="false">6533fcea74d39300015173e9</guid><description>This article explains how to get access to values from a &quot;Regional Preferences&quot; settings screen in Android 14.</description><pubDate>Fri, 26 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://alexzh.com/_astro/cover.DmcN-ZgX_HSk1o.webp&quot; alt=&quot;&quot; width=&quot;1080&quot; height=&quot;654&quot;&gt;&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The “Regional Preferences” settings screen appeared in Android 14, and it allows users to set a few regional preferences:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First Day of Week&lt;/li&gt;
&lt;li&gt;Temperature Unit&lt;/li&gt;
&lt;/ul&gt;
&lt;aside class=&quot;callout callout--note&quot;&gt;&lt;p class=&quot;callout__title&quot;&gt;Note&lt;/p&gt;&lt;p&gt;The content of this article is based on the Android 14 Beta 5.&lt;/p&gt;&lt;/aside&gt;
&lt;p&gt;This feature is convenient for people who are used to one temperature unit, but live in the country where another one is used. Many applications have their own preferences, and users can set temperature units, the first day of the week, and hours cycle preferences for every app.&lt;/p&gt;
&lt;p&gt;To find the Regional Preferences setting, go to &lt;strong&gt;Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;System&lt;/strong&gt; &amp;gt; &lt;strong&gt;Languages&lt;/strong&gt; and press the &lt;strong&gt;Regional Preferences&lt;/strong&gt; menu item.&lt;/p&gt;
&lt;figure&gt;&lt;img alt=&quot;&quot; sizes=&quot;(min-width: 768px) 720px, calc(100vw - 32px)&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;1280&quot; height=&quot;540&quot; src=&quot;https://alexzh.com/_astro/regional-preferences.CTmIFasU_17ho7d.webp&quot; srcset=&quot;https://alexzh.com/_astro/regional-preferences.CTmIFasU_Z17fsWc.webp 720w, https://alexzh.com/_astro/regional-preferences.CTmIFasU_Z1xOrQk.webp 1080w, https://alexzh.com/_astro/regional-preferences.CTmIFasU_17ho7d.webp 1280w&quot;&gt;&lt;/figure&gt;
&lt;h2 id=&quot;getting-regional-preference-values&quot;&gt;Getting Regional Preference Values&lt;/h2&gt;
&lt;p&gt;We can use the following data by using new &lt;code&gt;LocalePreferences&lt;/code&gt; API:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The temperature units&lt;/li&gt;
&lt;li&gt;The first day of the week&lt;/li&gt;
&lt;li&gt;The hour cycle&lt;/li&gt;
&lt;li&gt;The calendar type&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To use the API to get this data, we need to use the &lt;code&gt;androidx.core:core-ktx&lt;/code&gt; dependency with version &lt;code&gt;1.12.0-alpha01&lt;/code&gt; or higher.&lt;/p&gt;
&lt;p&gt;You can find information about all versions of the “androidx.core:core-ktx” dependency on the &lt;a href=&quot;https://developer.android.com/jetpack/androidx/releases/core&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;androidx.core releases page&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;dependencies {
    ...
    implementation &quot;androidx.core:core:1.12.0-rc01&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;getting-the-temperature-units&quot;&gt;Getting the temperature units&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;LocalePreferences.getTemperatureUnit()&lt;/code&gt; returns a &lt;code&gt;String&lt;/code&gt; value which represents a selected temperature unit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“celsius” (°C)&lt;/li&gt;
&lt;li&gt;“fahrenhe” (°F)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We have more functions for getting the temperature units for a specific locale:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getTemperatureUnit(Locale locale)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getTemperatureUnit(Locale locale, boolean resolved)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also compare these values with a specific temperature unit by using constants (&lt;code&gt;LocalePreferences.TemperatureUnit.FAHRENHEIT&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;You can find all the constants for the &lt;code&gt;TemperatureUnit&lt;/code&gt; class in the &lt;a href=&quot;https://developer.android.com/reference/androidx/core/text/util/LocalePreferences.TemperatureUnit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;LocalePreferences.TemperatureUnit reference&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;getting-the-first-day-of-the-week&quot;&gt;Getting the first day of the week&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;LocalePreferences.getFirstDayOfWeek()&lt;/code&gt; returns a &lt;code&gt;String&lt;/code&gt; value which represents a selected day of the week:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“sun” - Sunday&lt;/li&gt;
&lt;li&gt;“mon” - Monday&lt;/li&gt;
&lt;li&gt;“tue” - Tuesday&lt;/li&gt;
&lt;li&gt;“wed” - Wednesday&lt;/li&gt;
&lt;li&gt;“thu” - Thursday&lt;/li&gt;
&lt;li&gt;“fri” - Friday&lt;/li&gt;
&lt;li&gt;“sat” - Saturday&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We have more functions to for getting the first day of the week for a specific locale:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getFirstDayOfWeek(Locale locale)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getFirstDayOfWeek(Locale locale, boolean resolved)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also compare these values with a specific day of the week by using constants (&lt;code&gt;LocalePreferences.FirstDayOfWeek.MONDAY&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;You can find all the constants for the &lt;code&gt;FirstDayOfWeek&lt;/code&gt; class in the &lt;a href=&quot;https://developer.android.com/reference/androidx/core/text/util/LocalePreferences.FirstDayOfWeek&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;LocalePreferences.FirstDayOfWeek reference&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;getting-the-hour-cycle&quot;&gt;Getting the hour cycle&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;LocalePreferences.getHourCycle()&lt;/code&gt; returns a &lt;code&gt;String&lt;/code&gt; value which represents the hour cycle for the current &lt;code&gt;Locale&lt;/code&gt;. It returns the following values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“h11” - 12 Hour System (0-11)&lt;/li&gt;
&lt;li&gt;“h12” - 12 Hour System (1-12)&lt;/li&gt;
&lt;li&gt;“h23” - 24 Hour System (0-23)&lt;/li&gt;
&lt;li&gt;“h24” - 24 Hour System (1-24)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We have more functions to for getting the hour cycle for a specific locale:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getHourCycle(Locale locale)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getHourCycle(Locale locale, boolean resolved)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also compare these values with a specific hour cycle by using constants (&lt;code&gt;LocalePreferences.HourCycle.H24&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;You can find all the constants for the &lt;code&gt;HourCycle&lt;/code&gt; class in the &lt;a href=&quot;https://developer.android.com/reference/androidx/core/text/util/LocalePreferences.HourCycle&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;LocalePreferences.HourCycle reference&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;getting-the-calendar-type&quot;&gt;Getting the calendar type&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;LocalePreferences.getCalendarType()&lt;/code&gt; returns a &lt;code&gt;String&lt;/code&gt; value which represents the calendar type for the current locale. It returns the following values:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“chinese” - Chinese Calendar&lt;/li&gt;
&lt;li&gt;“dangi” - Dangi Calendar (Korea Calendar)&lt;/li&gt;
&lt;li&gt;“gregorian” - Gregorian Calendar&lt;/li&gt;
&lt;li&gt;“hebrew” - Hebrew Calendar&lt;/li&gt;
&lt;li&gt;“indian” - Indian National Calendar&lt;/li&gt;
&lt;li&gt;“islamic” - Islamic Calendar&lt;/li&gt;
&lt;li&gt;“islamic-civil” - Islamic Calendar (tabular, civil epoch)&lt;/li&gt;
&lt;li&gt;“islamic-rgsa” - Islamic Calendar (Saudi Arabia, sighting)&lt;/li&gt;
&lt;li&gt;“islamic-tbla” - Islamic Calendar (tabular, astronomical epoch)&lt;/li&gt;
&lt;li&gt;“islamic-umalqura” - Islamic Calendar (Umm al-Qura)&lt;/li&gt;
&lt;li&gt;“persian” - Persian Calendar&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We have more functions for getting the calendar type for a specific locale:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getCalendarType(Locale locale)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getCalendarType(Locale locale, boolean resolved)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also compare these values with a specific calendar type by using constants (&lt;code&gt;LocalePreferences.CalendarType.CHINESE&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;You can find all the constants for the &lt;code&gt;CalendarType&lt;/code&gt; class in the &lt;a href=&quot;https://developer.android.com/reference/androidx/core/text/util/LocalePreferences.CalendarType&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;LocalePreferences.CalendarType reference&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;example-and-use-cases&quot;&gt;Example and Use Cases&lt;/h2&gt;
&lt;p&gt;One of the simplest ways to compare values between different locales is to create a &lt;code&gt;BroadcastReceiver&lt;/code&gt; which logs new values for a selected locale after changing it in the &lt;strong&gt;Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;System&lt;/strong&gt; &amp;gt; &lt;strong&gt;Languages&lt;/strong&gt; &amp;gt; &lt;strong&gt;System Languages&lt;/strong&gt; screen.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;class LocaleReceiver : BroadcastReceiver() {
    
    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    override fun onReceive(context: Context, intent: Intent) {
        val localePreferenceData = &quot;&quot;&quot;
            getTemperatureUnit: ${LocalePreferences.getTemperatureUnit()}
            getFirstDayOfWeek: ${LocalePreferences.getFirstDayOfWeek()}
            getHourCycle: ${LocalePreferences.getHourCycle()}
            getCalendarType: ${LocalePreferences.getCalendarType()}
        &quot;&quot;&quot;.trimIndent()

        Log.d(&quot;New locale prefs values&quot;, localePreferenceData)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The next step is to register the &lt;code&gt;LocaleReceiver&lt;/code&gt; receiver in the &lt;code&gt;AndroidManifest.xml&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
  ...
    &amp;lt;application
    ...&amp;gt;
    &amp;lt;receiver
        android:name=&quot;.LocaleReceiver&quot;
        android:enabled=&quot;true&quot;
        android:exported=&quot;true&quot;&amp;gt;
        &amp;lt;intent-filter&amp;gt;
        &amp;lt;action android:name=&quot;android.intent.action.LOCALE_CHANGED&quot; /&amp;gt;
        &amp;lt;/intent-filter&amp;gt;
    &amp;lt;/receiver&amp;gt;
        ...
    &amp;lt;/application&amp;gt;
&amp;lt;/manifest&amp;gt;    &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s compare values for the “English (United States)” and “English (Netherlands)” locales.&lt;/p&gt;
&lt;table data-stack=&quot;rows&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Preference&lt;/th&gt;
&lt;th&gt;English (United States)&lt;/th&gt;
&lt;th&gt;English (Netherlands)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Preference&quot;&gt;Temperature Unit&lt;/td&gt;
&lt;td data-label=&quot;English (United States)&quot;&gt;fahrenhe&lt;/td&gt;
&lt;td data-label=&quot;English (Netherlands)&quot;&gt;celsius&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Preference&quot;&gt;First Day of Week&lt;/td&gt;
&lt;td data-label=&quot;English (United States)&quot;&gt;sun&lt;/td&gt;
&lt;td data-label=&quot;English (Netherlands)&quot;&gt;mon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Preference&quot;&gt;Hour Cycle&lt;/td&gt;
&lt;td data-label=&quot;English (United States)&quot;&gt;h12&lt;/td&gt;
&lt;td data-label=&quot;English (Netherlands)&quot;&gt;h23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td data-label=&quot;Preference&quot;&gt;Calendar Type&lt;/td&gt;
&lt;td data-label=&quot;English (United States)&quot;&gt;gregorian&lt;/td&gt;
&lt;td data-label=&quot;English (Netherlands)&quot;&gt;gregorian&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Let’s take a look at a few use cases when this data can be helpful.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Temperature Unit&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Many applications show weather data and they usually have their own settings for selecting preferred temperature units. Default data for this screen can be based on the values from the “Regional Preferences” screen.&lt;/p&gt;
&lt;p&gt;The date in the application can be converted to a specific temperature unit based on user preferences from the “Regional Preferences” screen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First Day of Week&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The “DatePicker” component is used in many applications for selecting the date and we can decide if we want to start the week in this component from Saturday or Monday. When we get information about the first day of the week, we can make a better experience for the user.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hour Cycle&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can find time in many applications. Usually, we see a specific time when a user creates a note, pays for a product, or checks information about delivery time. All data can have benefits from using the hour cycle preference and time will be formatted according to user preferences.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The “Regional Preferences” settings screens appeared in Android 14 and developers can start using API to get temperature units and the first day of the week.&lt;/p&gt;
&lt;p&gt;We can get user preferences for &lt;strong&gt;temperature units&lt;/strong&gt; and &lt;strong&gt;first day of week&lt;/strong&gt; using the following functions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LocalePreferences.getTemperatureUnit()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LocalePreferences.getFirstDayOfWeek()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In addition to that, we can get information about &lt;strong&gt;hour cycle&lt;/strong&gt; and &lt;strong&gt;calendar type&lt;/strong&gt; from the current locale:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LocalePreferences.getHourCycle()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LocalePreferences.getCalendarType()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I have doubts that this API will be widely used in the nearest future because many apps already have their own built-in implementation of configuring these properties in app settings and it works with all Android OS versions. However, I think that this API will be used in the future.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;Originally published on &lt;a href=&quot;https://alexzh.com/regional-preferences-in-android-14/&quot;&gt;alexzh.com&lt;/a&gt;.&lt;/p&gt;</content:encoded><media:content url="https://alexzh.com/_astro/cover.DmcN-ZgX_Z120c5A.jpg" medium="image"/><category>Android 14</category></item><item><title>Get access to string resources in UiAutomator</title><link>https://alexzh.com/get-access-to-string-resources-in-uiautomator/</link><guid isPermaLink="false">6533fcea74d39300015173e8</guid><description>UiAutomator can&apos;t read strings through the R class. How to access the app&apos;s string resources in UiAutomator tests, so they pass in any locale.</description><pubDate>Fri, 24 Mar 2023 00:00:00 GMT</pubDate><content:encoded>&lt;img src=&quot;https://alexzh.com/_astro/cover.qONyzO8f_Z1C0aFS.webp&quot; alt=&quot;&quot; width=&quot;1080&quot; height=&quot;654&quot;&gt;&lt;p&gt;String resources provide multiple benefits, such as localization and accessibility support, dynamic string formatting and maintenance of the application.&lt;/p&gt;
&lt;p&gt;When we create UI test cases, we often want to reuse strings from the app to avoid issues when a test device has different locale than the default language of the application. Unfortunately, the “UiAutomator” doesn’t allow us to get strings using the generated &lt;code&gt;R&lt;/code&gt; class.&lt;/p&gt;
&lt;p&gt;There are a few popular cases of using the UiAutomator framework:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://alexzh.com/guide-testing-android-notifications/&quot;&gt;Testing notifications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://alexzh.com/ui-testing-of-android-runtime-permissions/&quot;&gt;Testing runtime permissions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We cannot verify both cases with the Espresso based framework because notifications and permission dialog are part of the OS (Operating System), not part of the application. However, when we test notifications, we use string values from resources and we want to reuse string resources to avoid failure when we update the string resource value.&lt;/p&gt;
&lt;p&gt;Let’s take a look at part of the test cases that interact with a notification:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val expectedText = TestData.getCityById(amsterdamId).description
...

uiDevice.openNotification()
uiDevice.wait(Until.hasObject(By.text(&quot;DemoApp&quot;)), TIMEOUT)
uiDevice.findObject(By.text(expectedText))
    .click()
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The major problem in this example is the hard-coded string. Let’s explore how we can use string resources with the UiAutomator framework.&lt;/p&gt;
&lt;p&gt;The first option is to use the &lt;code&gt;By.res(PACKAGE, RESOURCE_ID)&lt;/code&gt; function. Here, we need to pass package and resource ID as strings and we can easily make a typo.&lt;/p&gt;
&lt;p&gt;Let’s explore how to get a value from a string resources by using the &lt;code&gt;R&lt;/code&gt; class, like &lt;code&gt;R.string.app_name&lt;/code&gt;. To get a value from the string resource, we need to have application context, which we can get from the &lt;code&gt;InstrumentationRegistry&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;InstrumentationRegistry.getInstrumentation().targetContext&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The next step is to create a function that returns a string value from resource id.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;private fun stringResource(
    @StringRes resId: Int,
    context: Context = InstrumentationRegistry.getInstrumentation().targetContext
): String {
    return context.getString(resId)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we can use string resources in our test case:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-kotlin&quot;&gt;val expectedText = TestData.getCityById(amsterdamId).description
...

uiDevice.openNotification()
uiDevice.wait(Until.hasObject(By.text(stringResource(R.string.app_name))), TIMEOUT)
uiDevice.findObject(By.textStartsWith(expectedText))
        .click()
...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When we use string resources (&lt;code&gt;R.string.*&lt;/code&gt;) in our test case, we have a single point of truth for our strings. In addition to that, we will have a compilation error if we will update the name of the string resource. It helps us to find and fix an error faster.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;Originally published on &lt;a href=&quot;https://alexzh.com/get-access-to-string-resources-in-uiautomator/&quot;&gt;alexzh.com&lt;/a&gt;.&lt;/p&gt;</content:encoded><media:content url="https://alexzh.com/_astro/cover.qONyzO8f_1Hir14.jpg" medium="image"/><category>UI testing</category><category>UiAutomator</category></item></channel></rss>