<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Architecting Android: The Right way]]></title><description><![CDATA[Exploring Clean Architecture in Android with Kotlin and Jetpack Compose — simplifying design patterns, testing, and app structure for curious developers.]]></description><link>https://androidavatar.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 15 Sep 2026 16:02:12 GMT</lastBuildDate><atom:link href="https://androidavatar.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Chapter 31 : ImageSearchScreen Composable Testing (Final Code)]]></title><description><![CDATA[package androidavatar.learning.artlearnersapp.presentation.ui

import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
import org.junit.Test

class ImageSearchScreenTest {

    @get:Rule
    va...]]></description><link>https://androidavatar.hashnode.dev/chapter-31-imagesearchscreen-composable-testing-final-code</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-31-imagesearchscreen-composable-testing-final-code</guid><category><![CDATA[Composable Architecture]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[kotlin beginner]]></category><category><![CDATA[kotlin coroutines]]></category><category><![CDATA[junit]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><category><![CDATA[androidx]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Wed, 12 Nov 2025 21:38:56 GMT</pubDate><content:encoded><![CDATA[<pre><code class="lang-kotlin"><span class="hljs-keyword">package</span> androidavatar.learning.artlearnersapp.presentation.ui

<span class="hljs-keyword">import</span> androidx.compose.ui.test.*
<span class="hljs-keyword">import</span> androidx.compose.ui.test.junit4.createComposeRule
<span class="hljs-keyword">import</span> org.junit.Rule
<span class="hljs-keyword">import</span> org.junit.Test

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ImageSearchScreenTest</span> </span>{

    <span class="hljs-meta">@get:Rule</span>
    <span class="hljs-keyword">val</span> composeTestRule = createComposeRule()

    <span class="hljs-comment">/**
     * Test 1 – Verify basic UI elements (TextField + Search icon)
     */</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">initialScreen_displaysSearchFieldAndButton</span><span class="hljs-params">()</span></span> {
        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {},
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState()
            )
        }

        composeTestRule.onNodeWithText(<span class="hljs-string">"Search Image"</span>).assertIsDisplayed()
        composeTestRule.onNodeWithContentDescription(<span class="hljs-string">"Search"</span>).assertIsDisplayed()
    }

    <span class="hljs-comment">/**
     * Test 2 – Loading indicator is visible when state.isLoading = true
     */</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">whenLoading_showsCircularProgressIndicator</span><span class="hljs-params">()</span></span> {
        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {},
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState(isLoading = <span class="hljs-literal">true</span>)
            )
        }

        <span class="hljs-comment">// Add test tag to your CircularProgressIndicator in the composable if not already:</span>
        <span class="hljs-comment">// CircularProgressIndicator(modifier = Modifier.testTag("Progress"))</span>
        composeTestRule.onNodeWithTag(<span class="hljs-string">"Progress"</span>).assertExists()
    }

    <span class="hljs-comment">/**
     * Test 3 – Error message and "Go Back" button appear on error
     */</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">whenErrorMessagePresent_displaysErrorAndButton</span><span class="hljs-params">()</span></span> {
        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {},
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState(errorMessage = <span class="hljs-string">"Network Error"</span>)
            )
        }

        composeTestRule.onNodeWithText(<span class="hljs-string">"Network Error"</span>).assertIsDisplayed()
        composeTestRule.onNodeWithText(<span class="hljs-string">"Go Back"</span>).assertIsDisplayed()
    }

    <span class="hljs-comment">/**
     * Test 4 – Search callback triggers when valid query entered (≥3 chars)
     */</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">enteringValidQuery_triggersOnSearchCallback</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">var</span> queryTriggered = <span class="hljs-literal">false</span>
        <span class="hljs-keyword">var</span> receivedQuery: String? = <span class="hljs-literal">null</span>

        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {
                    queryTriggered = <span class="hljs-literal">true</span>
                    receivedQuery = it
                },
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState()
            )
        }

        composeTestRule.onNodeWithText(<span class="hljs-string">"Search Image"</span>).performTextInput(<span class="hljs-string">"mountain"</span>)
        composeTestRule.onNodeWithContentDescription(<span class="hljs-string">"Search"</span>).performClick()

        assert(queryTriggered)
        assert(receivedQuery == <span class="hljs-string">"mountain"</span>)
    }

    <span class="hljs-comment">/**
     * Test 5 – Invalid short query (&lt;3 chars) should not trigger callback
     */</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">enteringShortQuery_doesNotTriggerSearch</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">var</span> searchTriggered = <span class="hljs-literal">false</span>

        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = { searchTriggered = <span class="hljs-literal">true</span> },
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState()
            )
        }

        composeTestRule.onNodeWithText(<span class="hljs-string">"Search Image"</span>).performTextInput(<span class="hljs-string">"ab"</span>)
        composeTestRule.onNodeWithContentDescription(<span class="hljs-string">"Search"</span>).performClick()

        assert(!searchTriggered)
    }

    <span class="hljs-comment">/**
     * Test 6 – Clicking an image triggers onImageClick with correct URL
     */</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">clickingImage_triggersImageClickCallback</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">val</span> fakeUrls = listOf(<span class="hljs-string">"url1"</span>, <span class="hljs-string">"url2"</span>)
        <span class="hljs-keyword">var</span> clickedUrl: String? = <span class="hljs-literal">null</span>

        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {},
                onImageClick = { clickedUrl = it },
                onBack = {},
                state = SearchImageUiState(searchResults = fakeUrls)
            )
        }

        composeTestRule.onAllNodes(hasContentDescription(<span class="hljs-string">"Search result image"</span>))
            .onFirst()
            .performClick()

        assert(clickedUrl == <span class="hljs-string">"url1"</span>)
    }

    <span class="hljs-comment">/**
     * Test 7 – Verify loading state hides images and error UI
     */</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">loadingState_hidesImagesAndErrorMessage</span><span class="hljs-params">()</span></span> {
        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {},
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState(isLoading = <span class="hljs-literal">true</span>)
            )
        }

        composeTestRule.onNodeWithTag(<span class="hljs-string">"Progress"</span>).assertExists()
        composeTestRule.onNodeWithText(<span class="hljs-string">"Go Back"</span>).assertDoesNotExist()
        composeTestRule.onAllNodes(hasContentDescription(<span class="hljs-string">"Search result image"</span>))
            .assertCountEquals(<span class="hljs-number">0</span>)
    }

    <span class="hljs-comment">/**
     * Test 8 – Verify results are displayed correctly when available
     */</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">whenResultsAvailable_displaysGridOfImages</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">val</span> fakeUrls = listOf(<span class="hljs-string">"url1"</span>, <span class="hljs-string">"url2"</span>, <span class="hljs-string">"url3"</span>)

        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {},
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState(searchResults = fakeUrls)
            )
        }

        composeTestRule.onAllNodes(hasContentDescription(<span class="hljs-string">"Search result image"</span>))
            .assertCountEquals(<span class="hljs-number">3</span>)
    }

    <span class="hljs-comment">/**
     * Test 9 – Verify error screen hides loading and image grid
     */</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">whenError_displaysOnlyErrorUI</span><span class="hljs-params">()</span></span> {
        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {},
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState(errorMessage = <span class="hljs-string">"Something went wrong"</span>)
            )
        }

        composeTestRule.onNodeWithText(<span class="hljs-string">"Something went wrong"</span>).assertIsDisplayed()
        composeTestRule.onNodeWithTag(<span class="hljs-string">"Progress"</span>).assertDoesNotExist()
        composeTestRule.onAllNodes(hasContentDescription(<span class="hljs-string">"Search result image"</span>))
            .assertCountEquals(<span class="hljs-number">0</span>)
    }
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Chapter 30 - Testing the AddArtScreen for Inserting Art Object into Room DB]]></title><description><![CDATA[In this chapter, we do the following:
Test that AddArtScreen correctly collects text inputs and triggers the save action (which inserts into DB through ViewModel).
We will test three layers of behaviour:

Input field typing: verifies that the user ca...]]></description><link>https://androidavatar.hashnode.dev/chapter-30-testing-the-addartscreen-for-inserting-art-object-into-room-db</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-30-testing-the-addartscreen-for-inserting-art-object-into-room-db</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[kotlin-flow]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Android]]></category><category><![CDATA[compose]]></category><category><![CDATA[instrumentation]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Wed, 12 Nov 2025 21:05:09 GMT</pubDate><content:encoded><![CDATA[<p>In this chapter, we do the following:</p>
<p><strong>Test that AddArtScreen correctly collects text inputs and triggers the save action (which inserts into DB through ViewModel).</strong></p>
<p>We will test <strong>three layers of behaviour:</strong></p>
<ol>
<li><p><strong>Input field typing:</strong> verifies that the user can type into text fields.</p>
</li>
<li><p><strong>Save button click:</strong> triggers the onSaveClick callback.</p>
</li>
<li><p><strong>Data propagation:</strong> ensures that correct data (name, artist, year, imageUrl) is passed upward.</p>
</li>
</ol>
<p>We’re not testing the Room DB directly — only verifying that the UI correctly sends the event (the ViewModel can handle DB insertion separately with its own unit test). We have already covered test earlier.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@HiltAndroidTest</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AddArtScreenTest</span> </span>{

    <span class="hljs-meta">@get:Rule</span>
    <span class="hljs-keyword">val</span> composeRule = createComposeRule()

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> savedArt: Art? = <span class="hljs-literal">null</span>

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">addArtScreen_userInputAndSaveButton_callsOnSaveClickWithCorrectData</span><span class="hljs-params">()</span></span> {
        <span class="hljs-comment">// Given</span>
        composeRule.setContent {
            AddArtScreen(
                imageUrl = <span class="hljs-string">"url1"</span>,
                onSaveClick = { name, artist, year, imageUrl -&gt;
                    savedArt = Art(
                        name = name,
                        artistName = artist,
                        year = year,
                        imageUrl = imageUrl
                    )
                },
                onNavigateToImageSearch = {},
                onNavigateBack = {},
                uiEventFlow = MutableSharedFlow()
            )
        }

        <span class="hljs-comment">// When - simulate typing text</span>
        composeRule.onNodeWithText(<span class="hljs-string">"Enter Name"</span>).performTextInput(<span class="hljs-string">"Mona Lisa"</span>)
        composeRule.onNodeWithText(<span class="hljs-string">"Enter Artist"</span>).performTextInput(<span class="hljs-string">"Da Vinci"</span>)
        composeRule.onNodeWithText(<span class="hljs-string">"Enter Year"</span>).performTextInput(<span class="hljs-string">"1503"</span>)

        <span class="hljs-comment">// And click save</span>
        composeRule.onNodeWithText(<span class="hljs-string">"Save"</span>).performClick()

        <span class="hljs-comment">// Then - verify the callback got correct values</span>
        composeRule.runOnIdle {
            assertThat(savedArt).isNotNull()
            assertThat(savedArt!!.name).isEqualTo(<span class="hljs-string">"Mona Lisa"</span>)
            assertThat(savedArt!!.artistName).isEqualTo(<span class="hljs-string">"Da Vinci"</span>)
            assertThat(savedArt!!.year).isEqualTo(<span class="hljs-string">"1503"</span>)
            assertThat(savedArt!!.imageUrl).isEqualTo(<span class="hljs-string">"url1"</span>)
        }
    }

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">addArtScreen_clicksImage_navigatesToImageSearch</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">var</span> navigated = <span class="hljs-literal">false</span>

        composeRule.setContent {
            AddArtScreen(
                imageUrl = <span class="hljs-literal">null</span>,
                onSaveClick = { _, _, _, _ -&gt; },
                onNavigateToImageSearch = { navigated = <span class="hljs-literal">true</span> },
                onNavigateBack = {},
                uiEventFlow = MutableSharedFlow()
            )
        }

        composeRule.onNodeWithContentDescription(<span class="hljs-string">"Art Image"</span>).performClick()
        composeRule.runOnIdle { assertThat(navigated).isTrue() }
    }

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">addArtScreen_showsToastOnShowToastEvent</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">val</span> sharedFlow = MutableSharedFlow&lt;ArtViewModel.UiEvent&gt;()

        composeRule.setContent {
            AddArtScreen(
                imageUrl = <span class="hljs-string">"url1"</span>,
                onSaveClick = { _, _, _, _ -&gt; },
                onNavigateToImageSearch = {},
                onNavigateBack = {},
                uiEventFlow = sharedFlow
            )
        }

        composeRule.runOnIdle {
            runBlocking {
                sharedFlow.emit(ArtViewModel.UiEvent.ShowToast(<span class="hljs-string">"Test message"</span>))
            }
        }
    }
}
</code></pre>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Section</strong></td><td><strong>Purpose</strong></td></tr>
</thead>
<tbody>
<tr>
<td>savedArt</td><td>A holder for the callback’s emitted art object.</td></tr>
<tr>
<td>performTextInput</td><td>Simulates typing in text fields.</td></tr>
<tr>
<td>performClick</td><td>Simulates pressing the Save button.</td></tr>
<tr>
<td>assertThat</td><td>Verifies correctness of the callback data.</td></tr>
<tr>
<td>Second test</td><td>Ensures the image click triggers navigation.</td></tr>
<tr>
<td>Third test</td><td>Emits a fake toast event from SharedFlow (ensures no crash).</td></tr>
</tbody>
</table>
</div>]]></content:encoded></item><item><title><![CDATA[Chapter 29 - Testing the ArtListScreen for Left Swipe and Deletion of Art Record]]></title><description><![CDATA[We have six Instrumentation tests in the final completed test class for ArtListScreen.




Test NamePurpose



1artListScreen_displaysArtNamesFromViewModelVerifies list shows all items

2artListScreen_showsLoadingIndicatorWhenLoadingChecks loading sp...]]></description><link>https://androidavatar.hashnode.dev/chapter-29-testing-the-artlistscreen-for-left-swipe-and-deletion-of-art-record</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-29-testing-the-artlistscreen-for-left-swipe-and-deletion-of-art-record</guid><category><![CDATA[instrumentation]]></category><category><![CDATA[Testing]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Wed, 12 Nov 2025 20:52:15 GMT</pubDate><content:encoded><![CDATA[<p>We have six Instrumentation tests in the final completed test class for ArtListScreen.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td><strong>Test Name</strong></td><td><strong>Purpose</strong></td></tr>
</thead>
<tbody>
<tr>
<td>1</td><td>artListScreen_displaysArtNamesFromViewModel</td><td>Verifies list shows all items</td></tr>
<tr>
<td>2</td><td>artListScreen_showsLoadingIndicatorWhenLoading</td><td>Checks loading spinner visibility</td></tr>
<tr>
<td>3</td><td>artListScreen_displaysErrorMessageWhenErrorOccurs</td><td>Checks error message rendering</td></tr>
<tr>
<td>4</td><td>artListScreen_displaysEmptyMessageWhenListIsEmpty</td><td>Tests empty state UI</td></tr>
<tr>
<td>5</td><td>artListScreen_fabClickTriggersCallback</td><td>Verifies FAB click triggers navigation</td></tr>
<tr>
<td>6</td><td>artListScreen_swipeLeftShowsDeleteDialogAndConfirmsDelete</td><td>Verifies swipe-to-delete and dialog logic</td></tr>
</tbody>
</table>
</div><pre><code class="lang-kotlin"><span class="hljs-meta">@RunWith(AndroidJUnit4::class)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ArtListScreenTest</span> </span>{

    <span class="hljs-meta">@get:Rule</span>
    <span class="hljs-keyword">val</span> composableRule = createComposeRule()

    <span class="hljs-comment">// ---------- Fake Repository ----------</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> fakeRepo = <span class="hljs-keyword">object</span> : ArtRepositoryInterface {
        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getArt</span><span class="hljs-params">()</span></span>: Flow&lt;List&lt;Art&gt;&gt; = flowOf(
            listOf(
                Art(id = <span class="hljs-number">1</span>, name = <span class="hljs-string">"Starry Horse"</span>, artistName = <span class="hljs-string">"Viru Pandey"</span>, year = <span class="hljs-string">"1904"</span>, imageUrl = <span class="hljs-string">"url1"</span>),
                Art(id = <span class="hljs-number">2</span>, name = <span class="hljs-string">"Mano ya na mano"</span>, artistName = <span class="hljs-string">"Satish Shah"</span>, year = <span class="hljs-string">"1998"</span>, imageUrl = <span class="hljs-string">"url2"</span>)
            )
        )

        <span class="hljs-keyword">override</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">insertArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {}
        <span class="hljs-keyword">override</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">deleteArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {}
        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">searchImage</span><span class="hljs-params">(userQuery: <span class="hljs-type">String</span>)</span></span>: Flow&lt;Resource&lt;List&lt;ImageResult&gt;&gt;&gt; =
            flow { emit(Resource.Success(emptyList())) }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> getArtUseCase = GetArtUseCase(fakeRepo)
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> searchImageUseCase = SearchImageUseCase(fakeRepo)
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> insertArtUseCase = InsertArtUseCase(fakeRepo)
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> deleteArtUseCase = DeleteArtUseCase(fakeRepo)

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> useCases = ArtUseCases(
        getArtUseCase, searchImageUseCase,
        insertArtUseCase = insertArtUseCase,
        deleteArtUseCase = deleteArtUseCase
    )
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> viewModel = ArtViewModel(useCases)

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> fakeArtList = listOf(
        Art(<span class="hljs-number">1</span>, <span class="hljs-string">"Starry Horse"</span>, <span class="hljs-string">"Viru Pandey"</span>, <span class="hljs-string">"1904"</span>, <span class="hljs-string">"url1"</span>),
        Art(<span class="hljs-number">2</span>, <span class="hljs-string">"Mano ya na mano"</span>, <span class="hljs-string">"Satish Shah"</span>, <span class="hljs-string">"1998"</span>, <span class="hljs-string">"url2"</span>)
    )

    <span class="hljs-comment">// -------------------------------------------------------------</span>
    <span class="hljs-comment">// 1. Normal data case — list should show all artworks</span>
    <span class="hljs-comment">// -------------------------------------------------------------</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">artListScreen_displaysArtNamesFromViewModel</span><span class="hljs-params">()</span></span> {
        composableRule.setContent {
            ArtListScreen(viewModel)
        }

        composableRule.onNodeWithText(<span class="hljs-string">"Starry Horse"</span>).assertIsDisplayed()
        composableRule.onNodeWithText(<span class="hljs-string">"Mano ya na mano"</span>).assertIsDisplayed()
    }

    <span class="hljs-comment">// -------------------------------------------------------------</span>
    <span class="hljs-comment">// 2. Loading state test — CircularProgressIndicator visible</span>
    <span class="hljs-comment">// -------------------------------------------------------------</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">artListScreen_showsLoadingIndicatorWhenLoading</span><span class="hljs-params">()</span></span> {
        composableRule.setContent {
            ArtListScreen(viewModel, isLoading = <span class="hljs-literal">true</span>)
        }

        composableRule.onNodeWithTag(<span class="hljs-string">"Progress"</span>, useUnmergedTree = <span class="hljs-literal">true</span>).assertExists()
    }

    <span class="hljs-comment">// -------------------------------------------------------------</span>
    <span class="hljs-comment">// 3. Error message test</span>
    <span class="hljs-comment">// -------------------------------------------------------------</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">artListScreen_displaysErrorMessageWhenErrorOccurs</span><span class="hljs-params">()</span></span> {
        composableRule.setContent {
            ArtListScreen(viewModel, errorMessage = <span class="hljs-string">"Something went wrong"</span>)
        }

        composableRule.onNodeWithText(<span class="hljs-string">"Something went wrong"</span>).assertIsDisplayed()
    }

    <span class="hljs-comment">// -------------------------------------------------------------</span>
    <span class="hljs-comment">// 4. Empty list test — should show "No artworks found!"</span>
    <span class="hljs-comment">// -------------------------------------------------------------</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">artListScreen_displaysEmptyMessageWhenListIsEmpty</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">val</span> emptyRepo = <span class="hljs-keyword">object</span> : ArtRepositoryInterface {
            <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getArt</span><span class="hljs-params">()</span></span>: Flow&lt;List&lt;Art&gt;&gt; = flowOf(emptyList())
            <span class="hljs-keyword">override</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">insertArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {}
            <span class="hljs-keyword">override</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">deleteArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {}
            <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">searchImage</span><span class="hljs-params">(userQuery: <span class="hljs-type">String</span>)</span></span>: Flow&lt;Resource&lt;List&lt;ImageResult&gt;&gt;&gt; =
                flow { emit(Resource.Success(emptyList())) }
        }

        <span class="hljs-keyword">val</span> emptyViewModel = ArtViewModel(
            ArtUseCases(GetArtUseCase(emptyRepo), SearchImageUseCase(emptyRepo),
                InsertArtUseCase(emptyRepo),
                DeleteArtUseCase(emptyRepo)
            )
        )

        composableRule.setContent {
            ArtListScreen(viewModel = emptyViewModel)
        }

        composableRule.onNodeWithText(<span class="hljs-string">"No artworks found!"</span>).assertIsDisplayed()
    }

    <span class="hljs-comment">// -------------------------------------------------------------</span>
    <span class="hljs-comment">// 5. FAB click test — verify callback is triggered</span>
    <span class="hljs-comment">// -------------------------------------------------------------</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">artListScreen_fabClickTriggersCallback</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">var</span> clicked = <span class="hljs-literal">false</span>

        composableRule.setContent {
            ArtListScreen(viewModel, onFabClick = { clicked = <span class="hljs-literal">true</span> })
        }

        composableRule.onNodeWithContentDescription(<span class="hljs-string">"Add Art"</span>).performClick()
        assertThat(clicked).isTrue()
    }

    <span class="hljs-comment">// -------------------------------------------------------------</span>
    <span class="hljs-comment">// 6. Swipe left to delete — confirm dialog and callback trigger</span>
    <span class="hljs-comment">// -------------------------------------------------------------</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">artListScreen_swipeLeftShowsDeleteDialogAndConfirmsDelete</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">var</span> deletedArt: Art? = <span class="hljs-literal">null</span>

        composableRule.setContent {
            ArtListScreen(
                viewModel = viewModel,
                onDeleteClick = { deletedArt = it }
            )
        }

        <span class="hljs-comment">// Perform a long swipe to the left on the first card</span>
        composableRule.onNodeWithTag(<span class="hljs-string">"SwipeItem_1"</span>)
            .performTouchInput { swipeLeft(startX = <span class="hljs-number">1000f</span>, endX = <span class="hljs-number">0f</span>) }

        <span class="hljs-comment">// Let Compose process the gesture</span>
        composableRule.waitForIdle()

        <span class="hljs-comment">// Now the dialog should be visible</span>
        composableRule.onNodeWithText(<span class="hljs-string">"Delete Artwork"</span>).assertIsDisplayed()
        composableRule.onNodeWithText(<span class="hljs-string">"Delete"</span>).performClick()

        assertThat(deletedArt?.name).isEqualTo(<span class="hljs-string">"Starry Horse"</span>)
}

}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Chapter 28 - Testing the ArtViewModel for Insertion and Deletion Use Cases]]></title><description><![CDATA[The testing will help to :

The ViewModel correctly invokes the use cases

The fake repository’s state updates

The UI state (like the art list) reflects those changes


/**
 * ## Unit test for [ArtViewModel].
 *
 * Verifies that:
 * 1. [ArtViewModel...]]></description><link>https://androidavatar.hashnode.dev/chapter-28-testing-the-artviewmodel-for-insertion-and-deletion-use-cases</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-28-testing-the-artviewmodel-for-insertion-and-deletion-use-cases</guid><category><![CDATA[Testing]]></category><category><![CDATA[test-automation]]></category><category><![CDATA[UI]]></category><category><![CDATA[state]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Android]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Wed, 12 Nov 2025 17:58:54 GMT</pubDate><content:encoded><![CDATA[<p>The testing will help to :</p>
<ul>
<li><p>The ViewModel correctly invokes the use cases</p>
</li>
<li><p>The fake repository’s state updates</p>
</li>
<li><p>The UI state (like the art list) reflects those changes</p>
</li>
</ul>
<pre><code class="lang-kotlin"><span class="hljs-comment">/**
 * ## Unit test for [ArtViewModel].
 *
 * Verifies that:
 * 1. [ArtViewModel] correctly collects from both [GetArtUseCase] and [SearchImageUseCase].
 * 2. Data from repository flows into StateFlows (`artList`, `imageList`, `errorMessage`, `isLoading`).
 * 3. Loading and error flags behave as expected for both success and failure.
 */</span>
<span class="hljs-meta">@OptIn(ExperimentalCoroutinesApi::class)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ArtViewModelRevisedTest</span> </span>{

    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-comment">// Fake repository (stateful)</span>
    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FakeRepository</span> : <span class="hljs-type">ArtRepositoryInterface {</span></span>

        <span class="hljs-comment">// initial list</span>
        <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _arts = MutableStateFlow(
            listOf(
                Art(id = <span class="hljs-number">1</span>, name = <span class="hljs-string">"Starry Horse"</span>, artistName = <span class="hljs-string">"Viru Pandey"</span>, year = <span class="hljs-string">"1904"</span>, imageUrl = <span class="hljs-string">"url1"</span>),
                Art(id = <span class="hljs-number">2</span>, name = <span class="hljs-string">"Mano ya na Mano"</span>, artistName = <span class="hljs-string">"Satish Shah"</span>, year = <span class="hljs-string">"1998"</span>, imageUrl = <span class="hljs-string">"url2"</span>)
            )
        )
        <span class="hljs-comment">// expose read-only StateFlow</span>
        <span class="hljs-keyword">val</span> artsState: StateFlow&lt;List&lt;Art&gt;&gt; <span class="hljs-keyword">get</span>() = _arts

        <span class="hljs-comment">// Return the live flow so collectors observe updates</span>
        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getArt</span><span class="hljs-params">()</span></span>: Flow&lt;List&lt;Art&gt;&gt; = _arts

        <span class="hljs-comment">// mutate and emit a new list (use setValue or emit)</span>
        <span class="hljs-keyword">override</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">insertArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {
            <span class="hljs-keyword">val</span> updated = _arts.value.toMutableList().apply { add(art) }
            _arts.emit(updated)
        }

        <span class="hljs-keyword">override</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">deleteArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {
            <span class="hljs-keyword">val</span> updated = _arts.value.toMutableList().apply { remove(art) }
            _arts.emit(updated)
        }

        <span class="hljs-comment">// simple searchImage implementation for tests</span>
        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">searchImage</span><span class="hljs-params">(userQuery: <span class="hljs-type">String</span>)</span></span>: Flow&lt;Resource&lt;List&lt;ImageResult&gt;&gt;&gt; = flow {
            emit(Resource.Loading())
            <span class="hljs-keyword">val</span> results = listOf(
                ImageResult(id = <span class="hljs-number">1</span>, previewURL = <span class="hljs-string">"preview1"</span>, largeImageURL = <span class="hljs-string">"large1"</span>),
                ImageResult(id = <span class="hljs-number">2</span>, previewURL = <span class="hljs-string">"preview2"</span>, largeImageURL = <span class="hljs-string">"large2"</span>)
            )
            emit(Resource.Success(results))
        }
    }

    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-comment">// Test deps</span>
    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> fakeRepo: FakeRepository
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> getArtUseCase: GetArtUseCase
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> searchImageUseCase: SearchImageUseCase
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> insertArtUseCase: InsertArtUseCase
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> deleteArtUseCase: DeleteArtUseCase
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> artUseCases: ArtUseCases
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> viewModel: ArtViewModel

    <span class="hljs-comment">// a second VM wired to mocked usecases to test search error flows etc.</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> mockSearchImageUseCase: SearchImageUseCase
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> fakeUseCases: ArtUseCases
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> viewModel2: ArtViewModel

    <span class="hljs-meta">@Before</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setup</span><span class="hljs-params">()</span></span> {
        fakeRepo = FakeRepository()
        getArtUseCase = GetArtUseCase(fakeRepo)
        searchImageUseCase = SearchImageUseCase(fakeRepo)
        insertArtUseCase = InsertArtUseCase(fakeRepo)
        deleteArtUseCase = DeleteArtUseCase(fakeRepo)

        artUseCases = ArtUseCases(
            getArtUseCase = getArtUseCase,
            searchImageUseCase = searchImageUseCase,
            insertArtUseCase = insertArtUseCase,
            deleteArtUseCase = deleteArtUseCase
        )

        <span class="hljs-comment">// VM wired to the real fakeRepo usecases</span>
        viewModel = ArtViewModel(artUseCases)

        <span class="hljs-comment">// second VM wired to mocked usecases for search tests</span>
        mockSearchImageUseCase = mock()
        fakeUseCases = mock {
            on { getArtUseCase } doReturn mock()
            on { searchImageUseCase } doReturn mockSearchImageUseCase
            on { insertArtUseCase } doReturn mock()
            on { deleteArtUseCase } doReturn mock()
        }
        viewModel2 = ArtViewModel(fakeUseCases)
    }

    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-comment">// TEST 1 — getArt collected initially</span>
    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `artList emits expected fake <span class="hljs-keyword">data</span> after initialization`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-keyword">val</span> expected = fakeRepo.artsState.value

        <span class="hljs-comment">// collect and assert the first emission equals expected</span>
        viewModel.arts.test {
            <span class="hljs-keyword">val</span> emission = awaitItem()
            assertThat(emission).isEqualTo(expected)
            cancelAndIgnoreRemainingEvents()
        }
    }

    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-comment">// TEST 2 — searchImage success flow (via mocked usecase)</span>
    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `searchImage emits loading then success updates imageList`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-keyword">val</span> fakeResults = listOf(
            ImageResult(previewURL = <span class="hljs-string">"preview1"</span>, id = <span class="hljs-number">0</span>, largeImageURL = <span class="hljs-string">"large1"</span>),
            ImageResult(previewURL = <span class="hljs-string">"preview2"</span>, id = <span class="hljs-number">2</span>, largeImageURL = <span class="hljs-string">"large2"</span>)
        )

        whenever(mockSearchImageUseCase.invoke(<span class="hljs-string">"tigers"</span>)).thenReturn(
            flowOf(Resource.Loading(), Resource.Success(fakeResults))
        )

        viewModel2.searchState.test {
            viewModel2.searchImage(<span class="hljs-string">"tigers"</span>)

            awaitItem() <span class="hljs-comment">// initial state</span>
            <span class="hljs-keyword">val</span> loading = awaitItem()
            assertThat(loading.isLoading).isTrue()

            <span class="hljs-keyword">val</span> success = awaitItem()
            assertThat(success.isLoading).isFalse()
            assertThat(success.searchResults).isEqualTo(fakeResults.map { it.previewURL })
            assertThat(success.errorMessage).isNull()

            cancelAndIgnoreRemainingEvents()
        }
    }

    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-comment">// TEST 3 — searchImage error flow</span>
    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `searchImage emits error updates errorMessage`<span class="hljs-params">()</span></span> = runTest {
        whenever(mockSearchImageUseCase.invoke(<span class="hljs-string">"dogs"</span>)).thenReturn(
            flowOf(Resource.Loading(), Resource.Error(<span class="hljs-string">"Network error"</span>))
        )

        viewModel2.searchState.test {
            viewModel2.searchImage(<span class="hljs-string">"dogs"</span>)

            awaitItem() <span class="hljs-comment">// initial state</span>
            <span class="hljs-keyword">val</span> loading = awaitItem()
            assertThat(loading.isLoading).isTrue()

            <span class="hljs-keyword">val</span> errorState = awaitItem()
            assertThat(errorState.isLoading).isFalse()
            assertThat(errorState.errorMessage).isEqualTo(<span class="hljs-string">"Network error"</span>)

            cancelAndIgnoreRemainingEvents()
        }
    }

    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-comment">// TEST 4 — initial search state values</span>
    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `initial state should be empty and not loading`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-keyword">val</span> state = viewModel.searchState.value
        assertFalse(state.isLoading)
        assertNull(state.errorMessage)
        assertTrue(state.searchResults.isEmpty())
    }

    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-comment">// TEST 5 — insert updates list (capture emission after action)</span>
    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `insert updates Art List`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-keyword">val</span> newArt = Art(id = <span class="hljs-number">12</span>, artistName = <span class="hljs-string">"Nah kehta hua"</span>, name = <span class="hljs-string">"pattar aur kanta"</span>, year = <span class="hljs-string">"1982"</span>, imageUrl = <span class="hljs-string">"yaq1"</span>)

        <span class="hljs-comment">// Start collecting BEFORE action so Turbine can capture subsequent emissions</span>
        viewModel.arts.test {
            <span class="hljs-keyword">val</span> first = awaitItem() <span class="hljs-comment">// initial list</span>

            <span class="hljs-comment">// act</span>
            viewModel.insertArt(newArt)
            <span class="hljs-comment">// allow VM/repo coroutines to run</span>
            advanceUntilIdle()

            <span class="hljs-comment">// the next emission should contain newArt</span>
            <span class="hljs-keyword">val</span> updated = awaitItem()
            assertThat(updated).contains(newArt)

            cancelAndIgnoreRemainingEvents()
        }
    }

    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-comment">// TEST 6 — delete removes art from list</span>
    <span class="hljs-comment">// -------------------------</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `deleteArt removes art from list`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-keyword">val</span> artToDelete = fakeRepo.artsState.value.first()

        <span class="hljs-comment">// Start collecting BEFORE action</span>
        viewModel.arts.test {
            <span class="hljs-keyword">val</span> first = awaitItem() <span class="hljs-comment">// initial emission</span>

            <span class="hljs-comment">// act</span>
            viewModel.deleteArt(artToDelete)
            advanceTimeBy(<span class="hljs-number">100</span>)
            advanceUntilIdle()

            <span class="hljs-keyword">val</span> afterDelete = awaitItem()
            assertThat(afterDelete).doesNotContain(artToDelete)

            cancelAndIgnoreRemainingEvents()
        }
    }
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Chapter 27 - Testing the Repository Implementation for the Insert and Delete Use Cases]]></title><description><![CDATA[//test 5: Ensure repository calls artDao.insertArt() with the correct converted entity.
    @Test
    fun `insertArt calls dao insert with correct data`() =runTest {

        //arrange
        val art = Art(name = "Maddy King", artistName = "Kimshi Y...]]></description><link>https://androidavatar.hashnode.dev/chapter-27-testing-the-repository-implementation-for-the-insert-and-delete-use-cases</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-27-testing-the-repository-implementation-for-the-insert-and-delete-use-cases</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[kotlin-flow]]></category><category><![CDATA[fake]]></category><category><![CDATA[repository]]></category><category><![CDATA[Implementation]]></category><category><![CDATA[unit testing]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Wed, 12 Nov 2025 17:00:32 GMT</pubDate><content:encoded><![CDATA[<pre><code class="lang-kotlin"><span class="hljs-comment">//test 5: Ensure repository calls artDao.insertArt() with the correct converted entity.</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `insertArt calls dao insert with correct <span class="hljs-keyword">data</span>`<span class="hljs-params">()</span></span> =runTest {

        <span class="hljs-comment">//arrange</span>
        <span class="hljs-keyword">val</span> art = Art(name = <span class="hljs-string">"Maddy King"</span>, artistName = <span class="hljs-string">"Kimshi Yako"</span>,id=<span class="hljs-number">4</span>, year = <span class="hljs-string">"1987"</span>, imageUrl = <span class="hljs-string">"url2"</span>)

        <span class="hljs-comment">//act</span>
        repositoryImpl.insertArt(art)

        <span class="hljs-comment">//assert</span>
        <span class="hljs-keyword">val</span> allArts = fakeDao.getAllArts().first()
        assertThat(allArts.size).isEqualTo(<span class="hljs-number">1</span>)
        assertThat(allArts[<span class="hljs-number">0</span>].name).isEqualTo(<span class="hljs-string">"Maddy King"</span>)
        assertThat(allArts[<span class="hljs-number">0</span>].artistName).isEqualTo(<span class="hljs-string">"Kimshi Yako"</span>)
        assertThat(allArts[<span class="hljs-number">0</span>].year).isEqualTo(<span class="hljs-string">"1987"</span>)

       <span class="hljs-comment">// This checks whether the domain object is mapped and persisted correctly.</span>
    }

    <span class="hljs-comment">//Test 6: Ensure repository correctly removes the item through DAO.</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `deleteArt removes item from dao`<span class="hljs-params">()</span></span> = runTest{

        <span class="hljs-comment">//arrange : add an art first</span>
        <span class="hljs-keyword">val</span> art = ArtEntity(id = <span class="hljs-number">3</span>, name = <span class="hljs-string">"Kashi Mora"</span>, artistName = <span class="hljs-string">"Kami Dhingra"</span>, year = <span class="hljs-string">"1932"</span>, imageUrl = <span class="hljs-string">"uri1"</span>)

        fakeDao.insertArt(art)

        <span class="hljs-comment">//act</span>
        repositoryImpl.deleteArt(art.toDomainModel())

        <span class="hljs-comment">//assert</span>
        <span class="hljs-keyword">val</span> allArts = fakeDao.getAllArts().first()
        assertThat(allArts).doesNotContain(art)
    }
</code></pre>
<h3 id="heading-insert-art-behaviour-verification">Insert Art — Behaviour Verification</h3>
<p>Goal → Ensure repository calls artDao.insertArt() with the correct converted entity.</p>
<h3 id="heading-delete-art-behaviour-verification">Delete Art — Behaviour Verification</h3>
<p>Goal → Ensure repository correctly removes the item through DAO.</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">/**
 * Tests the ArtRepositoryImpl class to verify that
 * - Data from DAO is correctly mapped to domain model.
 * - The Flow emitted matches the expected data.
 * - The repository interacts correctly with DAO and API layers.
 * - API returns expected Flow&lt;Resource&gt; for searchImage()
 */</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ArtRepositoryImplTest</span> </span>{

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> fakeDao: FakeArtDao
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> mockApi:ImageApi
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> repositoryImpl: ArtRepositoryImpl
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> dummyApiKey = <span class="hljs-string">"FAKE_KEY"</span>

    <span class="hljs-meta">@Before</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setup</span><span class="hljs-params">()</span></span>{

        fakeDao = FakeArtDao()
        mockApi = mock(ImageApi::<span class="hljs-keyword">class</span>.java)
        repositoryImpl = ArtRepositoryImpl(fakeDao,mockApi,dummyApiKey)
    }

    @Test
    <span class="hljs-keyword">fun</span> `getArt returns mapped domain objects`() = runTest {
        //arrange
        <span class="hljs-keyword">val</span> testArtEntity =
            ArtEntity(name = <span class="hljs-string">"jalwa fariq"</span>, artistName = <span class="hljs-string">"rehman malik"</span>, year = <span class="hljs-string">"1989"</span>, imageUrl = <span class="hljs-string">"url2"</span>)

        fakeDao.insertArt(testArtEntity)

        <span class="hljs-keyword">val</span> result = repositoryImpl.getArt().first()

        <span class="hljs-comment">//assert</span>
        assertThat(result.size).isEqualTo(<span class="hljs-number">1</span>)
        assertThat(result[<span class="hljs-number">0</span>].year).isEqualTo(<span class="hljs-string">"1989"</span>)
        assertThat(result[<span class="hljs-number">0</span>].name).isEqualTo(<span class="hljs-string">"jalwa fariq"</span>)
        assertThat(result[<span class="hljs-number">0</span>].artistName).isEqualTo(<span class="hljs-string">"rehman malik"</span>)
    }

    <span class="hljs-comment">// --- Test 2: searchImage returns success ---</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `searchImage emits loading and success <span class="hljs-keyword">when</span> API returns <span class="hljs-keyword">data</span>`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-comment">// Arrange fake API response</span>
        <span class="hljs-keyword">val</span> fakeImageResults = listOf(
            ImageResultDto(
                id = <span class="hljs-number">1</span>, previewURL = <span class="hljs-string">"url1"</span>, largeImageURL = <span class="hljs-string">"urlLarge"</span>,
                comments = <span class="hljs-number">0</span>,
                downloads = <span class="hljs-number">0</span>,
                favorites = <span class="hljs-number">0</span>,
                imageHeight = <span class="hljs-number">111</span>,
                imageSize = <span class="hljs-number">112</span>,
                imageWidth = <span class="hljs-number">111</span>,
                likes = <span class="hljs-number">12</span>,
                pageURL = <span class="hljs-string">"TODO()"</span>,
                previewHeight = <span class="hljs-number">12</span>,
                previewWidth = <span class="hljs-number">22</span>,
                tags = <span class="hljs-string">"TODO()"</span>,
                type = <span class="hljs-string">"TODO()"</span>,
                user = <span class="hljs-string">"TODO()"</span>,
                userId = <span class="hljs-number">0</span>,
                userImageURL = <span class="hljs-string">"TODO()"</span>,
                views = <span class="hljs-number">0</span>,
                webformatHeight = <span class="hljs-number">0</span>,
                webformatURL = <span class="hljs-string">"TODO()"</span>,
                webformatWidth = <span class="hljs-number">11</span>
            )
        )
        <span class="hljs-keyword">val</span> responseDto = ImageResponseDto(total = <span class="hljs-number">1</span>, totalHits = <span class="hljs-number">1</span>, hits = fakeImageResults)

        `<span class="hljs-keyword">when</span>`(mockApi.searchImages(dummyApiKey, <span class="hljs-string">"cats"</span>)).thenReturn(Response.success(responseDto))

        <span class="hljs-comment">// Act</span>
        <span class="hljs-keyword">val</span> results = repositoryImpl.searchImage(<span class="hljs-string">"cats"</span>).toList()

        <span class="hljs-comment">// Assert</span>
        assertThat(results[<span class="hljs-number">0</span>]).isInstanceOf(Resource.Loading::<span class="hljs-keyword">class</span>.java)
        assertThat(results[<span class="hljs-number">1</span>]).isInstanceOf(Resource.Success::<span class="hljs-keyword">class</span>.java)
        <span class="hljs-keyword">val</span> success = results[<span class="hljs-number">1</span>] <span class="hljs-keyword">as</span> Resource.Success
        assertThat(success.<span class="hljs-keyword">data</span>?.size).isEqualTo(<span class="hljs-number">1</span>)
        assertThat(success.<span class="hljs-keyword">data</span>?.<span class="hljs-keyword">get</span>(<span class="hljs-number">0</span>)?.previewURL).isEqualTo(<span class="hljs-string">"url1"</span>)
    }

    <span class="hljs-comment">// --- Test 3: searchImage emits error when API key empty ---</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `searchImage emits error <span class="hljs-keyword">when</span> api key empty`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-keyword">val</span> repo = ArtRepositoryImpl(fakeDao, mockApi, apiKey = <span class="hljs-string">""</span>)

        <span class="hljs-keyword">val</span> results = repo.searchImage(<span class="hljs-string">"cats"</span>).toList()

        assertThat(results[<span class="hljs-number">0</span>]).isInstanceOf(Resource.Loading::<span class="hljs-keyword">class</span>.java)
        assertThat(results[<span class="hljs-number">1</span>]).isInstanceOf(Resource.Error::<span class="hljs-keyword">class</span>.java)
        assertThat(results[<span class="hljs-number">1</span>].message).isEqualTo(<span class="hljs-string">"Empty api key"</span>)
    }

    <span class="hljs-comment">// --- Test 4: searchImage emits error when API fails ---</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `searchImage emits error <span class="hljs-keyword">when</span> API returns failure`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-keyword">val</span> errorResponse = Response.error&lt;ImageResponseDto&gt;(
            <span class="hljs-number">404</span>,
            ResponseBody.create(<span class="hljs-string">"application/json"</span>.toMediaTypeOrNull(), <span class="hljs-string">"Not Found"</span>)
        )

        `<span class="hljs-keyword">when</span>`(mockApi.searchImages(dummyApiKey, <span class="hljs-string">"dogs"</span>)).thenReturn(errorResponse)

        <span class="hljs-keyword">val</span> results = repositoryImpl.searchImage(<span class="hljs-string">"dogs"</span>).toList()

        assertThat(results[<span class="hljs-number">0</span>]).isInstanceOf(Resource.Loading::<span class="hljs-keyword">class</span>.java)
        assertThat(results[<span class="hljs-number">1</span>]).isInstanceOf(Resource.Error::<span class="hljs-keyword">class</span>.java)
        assertThat(results[<span class="hljs-number">1</span>].message).contains(<span class="hljs-string">"API error"</span>)
    }

    <span class="hljs-comment">//test 5: Ensure repository calls artDao.insertArt() with the correct converted entity.</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `insertArt calls dao insert with correct <span class="hljs-keyword">data</span>`<span class="hljs-params">()</span></span> =runTest {

        <span class="hljs-comment">//arrange</span>
        <span class="hljs-keyword">val</span> art = Art(name = <span class="hljs-string">"Maddy King"</span>, artistName = <span class="hljs-string">"Kimshi Yako"</span>,id=<span class="hljs-number">4</span>, year = <span class="hljs-string">"1987"</span>, imageUrl = <span class="hljs-string">"url2"</span>)

        <span class="hljs-comment">//act</span>
        repositoryImpl.insertArt(art)

        <span class="hljs-comment">//assert</span>
        <span class="hljs-keyword">val</span> allArts = fakeDao.getAllArts().first()
        assertThat(allArts.size).isEqualTo(<span class="hljs-number">1</span>)
        assertThat(allArts[<span class="hljs-number">0</span>].name).isEqualTo(<span class="hljs-string">"Maddy King"</span>)
        assertThat(allArts[<span class="hljs-number">0</span>].artistName).isEqualTo(<span class="hljs-string">"Kimshi Yako"</span>)
        assertThat(allArts[<span class="hljs-number">0</span>].year).isEqualTo(<span class="hljs-string">"1987"</span>)

       <span class="hljs-comment">// This checks whether the domain object is mapped and persisted correctly.</span>
    }

    <span class="hljs-comment">//Test 6: Ensure repository correctly removes the item through DAO.</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `deleteArt removes item from dao`<span class="hljs-params">()</span></span> = runTest{

        <span class="hljs-comment">//arrange : add an art first</span>
        <span class="hljs-keyword">val</span> art = ArtEntity(id = <span class="hljs-number">3</span>, name = <span class="hljs-string">"Kashi Mora"</span>, artistName = <span class="hljs-string">"Kami Dhingra"</span>, year = <span class="hljs-string">"1932"</span>, imageUrl = <span class="hljs-string">"uri1"</span>)

        fakeDao.insertArt(art)

        <span class="hljs-comment">//act</span>
        repositoryImpl.deleteArt(art.toDomainModel())

        <span class="hljs-comment">//assert</span>
        <span class="hljs-keyword">val</span> allArts = fakeDao.getAllArts().first()
        assertThat(allArts).doesNotContain(art)

    }
}

<span class="hljs-comment">/*
A Fake is a lightweight, in-memory implementation of the real ArtDao interface.
FakeArtDao mimics the behavior of the Room DAO but uses plain Kotlin collections and Flows instead of a real database.
It's designed for fast, deterministic tests of higher layers (repository, use cases, ViewModel) without using Room or an emulator.
Fake has real behavior (stores and returns data) so it's ideal for validating interactions and data flow.
*/</span>

<span class="hljs-comment">/*
Responsibilities (what FakeArtDao must do)
Implement the ArtDao interface exactly (same function signatures).
Expose a Flow&lt;List&lt;ArtEntity&gt;&gt; for getAllArts() that behaves like Room: emits the current list and emits new lists when data changes.
Provide working insertArt() and deleteArt() semantics (mutate internal state).
Be deterministic and resettable between tests.
Be coroutine-friendly (suspend functions behave like the real DAO).
 */</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FakeArtDao</span> : <span class="hljs-type">ArtDao {</span></span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> <span class="hljs-keyword">data</span> = mutableListOf&lt;ArtEntity&gt;()

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getAllArts</span><span class="hljs-params">()</span></span>: Flow&lt;List&lt;ArtEntity&gt;&gt; = flowOf(<span class="hljs-keyword">data</span>)

    <span class="hljs-keyword">override</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">insertArt</span><span class="hljs-params">(artEntity: <span class="hljs-type">ArtEntity</span>)</span></span> {
        <span class="hljs-keyword">data</span>.add(artEntity)
    }

    <span class="hljs-keyword">override</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">deleteArt</span><span class="hljs-params">(artEntity: <span class="hljs-type">ArtEntity</span>)</span></span> {
        <span class="hljs-keyword">data</span>.remove(artEntity)
    }
}
</code></pre>
<p>The Repository implementation class has all six tests as elucidated above.</p>
]]></content:encoded></item><item><title><![CDATA[Chapter 26 - Testing the Insert and Delete Use Cases]]></title><description><![CDATA[Since InsertArtUseCase and DeleteArtUseCase are pure domain logic, they’re easy to test — no Android dependencies, just Kotlin coroutines and a fake repository.
Let’s write clear, minimal, and clean unit tests using JUnit + Kotlin coroutines test.
We...]]></description><link>https://androidavatar.hashnode.dev/chapter-26-testing-the-insert-and-delete-use-cases</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-26-testing-the-insert-and-delete-use-cases</guid><category><![CDATA[insertion ]]></category><category><![CDATA[deletion]]></category><category><![CDATA[Usecase]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[kotlin coroutines]]></category><category><![CDATA[kotlin-flow]]></category><category><![CDATA[Testing]]></category><category><![CDATA[unit testing]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Wed, 12 Nov 2025 15:51:43 GMT</pubDate><content:encoded><![CDATA[<p>Since InsertArtUseCase and DeleteArtUseCase are <strong>pure domain logic</strong>, they’re easy to test — no Android dependencies, just Kotlin coroutines and a fake repository.</p>
<p>Let’s write clear, minimal, and clean <strong>unit tests</strong> using <strong>JUnit + Kotlin coroutines test</strong>.</p>
<p>We'll create a test class, e.g.<br />InsertArtUseCaseTest.kt and DeleteArtUseCaseTest.kt.</p>
<p>Each test will:</p>
<ol>
<li><p>Use a <strong>fake repository</strong> that implements ArtRepositoryInterface.</p>
</li>
<li><p>Call the use case.</p>
</li>
<li><p>Assert on the returned Resource or expected behaviour.</p>
</li>
</ol>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">InsertArtUseCaseTest</span> </span>{

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> fakeRepository: FakeArtRepository
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> insertArtUseCase: InsertArtUseCase

    <span class="hljs-meta">@Before</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setup</span><span class="hljs-params">()</span></span> {
        fakeRepository = FakeArtRepository()
        insertArtUseCase = InsertArtUseCase(fakeRepository)
    }

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `insert art with valid <span class="hljs-keyword">data</span> returns success`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-keyword">val</span> art = Art(
            name = <span class="hljs-string">"Mona Lisa"</span>,
            artistName = <span class="hljs-string">"Da Vinci"</span>,
            year = <span class="hljs-string">"1503"</span>,
            imageUrl = <span class="hljs-string">"https://example.com/mona.jpg"</span>
        )

        <span class="hljs-keyword">val</span> result = insertArtUseCase(art)


       Truth.assertThat(result.<span class="hljs-keyword">data</span>).isNotNull()
       Truth.assertThat(result).isInstanceOf(Resource.Success::<span class="hljs-keyword">class</span>.java)
       Truth.assertThat(result.<span class="hljs-keyword">data</span>?.year).isEqualTo(<span class="hljs-string">"1503"</span>)

    }

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `insert art with blank fields returns error`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-keyword">val</span> invalidArt = Art(
            name = <span class="hljs-string">""</span>,
            artistName = <span class="hljs-string">"Artist"</span>,
            year = <span class="hljs-string">"2000"</span>,
            imageUrl = <span class="hljs-string">""</span>
        )

        <span class="hljs-keyword">val</span> result = insertArtUseCase(invalidArt)

        Truth.assertThat(result).isInstanceOf(Resource.Error::<span class="hljs-keyword">class</span>.java)
        Truth.assertThat(result.<span class="hljs-keyword">data</span>).isNull()
        Truth.assertThat(fakeRepository.insertedArts).isEmpty()
        Truth.assertThat(result.message).isEqualTo(<span class="hljs-string">"Fields must not be empty and/or filled incorrectly"</span>)
    }
}
</code></pre>
<p>InsertArtUseCase correctly <strong>validates</strong> input before inserting. It calls the repository only for valid data.</p>
<p>DeleteArtUseCase safely removes items. Both use cases handle coroutines properly.</p>
<h3 id="heading-summary">SUMMARY</h3>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Concept</strong></td><td><strong>Explanation</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Fake Repository</strong></td><td>Keeps an in-memory list to verify that data was inserted (no DB needed).</td></tr>
<tr>
<td><strong>Positive &amp; Negative Tests</strong></td><td>Always test both valid and invalid data for validation logic.</td></tr>
<tr>
<td><strong>Resource Assertions</strong></td><td>Verifies correct return type (Success or Error) and proper message/data.</td></tr>
<tr>
<td><strong>No Flow Needed</strong></td><td>Since invoke() is suspend, we directly call it inside runTest.</td></tr>
<tr>
<td><strong>Truth Assertions</strong></td><td>More readable, null-safe, and semantic checks.</td></tr>
</tbody>
</table>
</div>]]></content:encoded></item><item><title><![CDATA[Chapter 25 - Coding the Composable ArtListScreen with the Left Swipe Functionality]]></title><description><![CDATA[@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ArtListScreen(
    viewModel: ArtViewModel = hiltViewModel(),
    isLoading: Boolean = false,
    onFabClick: () -> Unit = {},
    onDeleteClick: (Art) -> Unit = {},
    errorMessage: String? = n...]]></description><link>https://androidavatar.hashnode.dev/chapter-25-coding-the-composable-artlistscreen-with-the-left-swipe-functionality</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-25-coding-the-composable-artlistscreen-with-the-left-swipe-functionality</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[Android]]></category><category><![CDATA[Clean Architecture]]></category><category><![CDATA[Composable Architecture]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Wed, 12 Nov 2025 12:07:19 GMT</pubDate><content:encoded><![CDATA[<pre><code class="lang-kotlin">
<span class="hljs-meta">@OptIn(ExperimentalMaterialApi::class)</span>
<span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ArtListScreen</span><span class="hljs-params">(
    viewModel: <span class="hljs-type">ArtViewModel</span> = hiltViewModel()</span></span>,
    isLoading: <span class="hljs-built_in">Boolean</span> = <span class="hljs-literal">false</span>,
    onFabClick: () -&gt; <span class="hljs-built_in">Unit</span> = {},
    onDeleteClick: (Art) -&gt; <span class="hljs-built_in">Unit</span> = {},
    errorMessage: String? = <span class="hljs-literal">null</span>
) {
    <span class="hljs-comment">/*
    * Collects the current list of artworks (StateFlow&lt;List&lt;Art&gt;&gt;) from the ViewModel.
      collectAsState() turns the flow into a Compose state, so UI updates automatically when data changes.
      No need for manual observe() like LiveData — this is fully reactive.
* */</span>
    <span class="hljs-keyword">val</span> artList <span class="hljs-keyword">by</span> viewModel.arts.collectAsState()

    <span class="hljs-comment">/*
    * Provides a Material Design layout structure.
    * Handles FAB positioning, top/bottom insets, and screen padding.
    * safeDrawingPadding() ensures content doesn't overlap with status bar or camera cutouts.
    * */</span>
    Scaffold(
        floatingActionButton = {
            <span class="hljs-comment">/*
            Tapping FAB triggers onFabClick() — typically navigates to the Add Art screen.
            The icon visually indicates adding a new item.
            Safe padding avoids overlap with gesture navigation areas or corners.
            **/</span>
            FloatingActionButton(onClick = onFabClick,modifier = Modifier.padding(<span class="hljs-number">16</span>.dp).safeDrawingPadding()) {
                Icon(Icons.Default.AddBox, contentDescription = <span class="hljs-string">"Add Art"</span>)
            }
        },
        modifier = Modifier.safeDrawingPadding()
    ) { paddingValues -&gt;
        <span class="hljs-comment">//Box acts as a flexible container to layer different states (loading spinner, error text, or list)</span>
        <span class="hljs-comment">//The when expression ensures that only one state (loading/error/empty/data) is displayed at a time.</span>
        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)
        ) {
            <span class="hljs-keyword">when</span> {
                isLoading -&gt; {
                    <span class="hljs-comment">//A simple spinner shown in the center of the screen while fetching data.</span>
                    CircularProgressIndicator(Modifier.align(Alignment.Center))
                }

                <span class="hljs-comment">//Displays a red error message when something goes wrong (e.g., database fetch failed).</span>
                errorMessage != <span class="hljs-literal">null</span> -&gt; {
                    Text(
                        text = errorMessage,
                        color = Color.Red,
                        modifier = Modifier.align(Alignment.Center)
                    )
                }

                artList.isEmpty() -&gt; {
                    <span class="hljs-comment">/*
                    * Shown when the list is empty. A friendly way to handle “no data” without a blank screen.
                    * */</span>
                    Text(
                        text = <span class="hljs-string">"No artworks found!"</span>,
                        modifier = Modifier.align(Alignment.Center)
                    )
                }

                <span class="hljs-keyword">else</span> -&gt; {
                    <span class="hljs-comment">/*
                    * Displays the list efficiently (lazy loading like RecyclerView).
                    * Each item represents one artwork card.
                    * key ensures stable identity for efficient re-rendering.*/</span>
                    LazyColumn(modifier = Modifier.fillMaxSize().safeDrawingPadding()) {
                        items(artList, key = { it.id ?: it.hashCode() }) { art -&gt;
                            <span class="hljs-keyword">var</span> showDialog <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-literal">false</span>) }

                            <span class="hljs-comment">// correct parameter name: confirmStateChange</span>
                            <span class="hljs-comment">/*
                            * Tracks the swipe gesture and its progress.
                            * confirmStateChange lets us intercept and control when the item is actually dismissed.*/</span>
                            <span class="hljs-keyword">val</span> dismissState = rememberDismissState(
                                confirmStateChange = { value -&gt;
                                    <span class="hljs-keyword">if</span> (value == DismissValue.DismissedToStart) {
                                        showDialog = <span class="hljs-literal">true</span>
                                        <span class="hljs-literal">false</span> <span class="hljs-comment">// prevent auto-dismiss until user confirms</span>
                                    } <span class="hljs-keyword">else</span> {
                                        <span class="hljs-literal">false</span>
                                    }
                                }
                            )

                            <span class="hljs-comment">// Confirmation dialog shown after swipe</span>
                            <span class="hljs-comment">/*
                            * When a user swipes the card, a confirmation dialog appears before deletion.Prevents accidental data loss.
                            * */</span>
                            <span class="hljs-keyword">if</span> (showDialog) {
                                AlertDialog(
                                    onDismissRequest = { showDialog = <span class="hljs-literal">false</span> },
                                    title = { Text(<span class="hljs-string">"Delete Artwork"</span>) },
                                    text = { Text(<span class="hljs-string">"Are you sure you want to delete '<span class="hljs-subst">${art.name}</span>'?"</span>) },
                                    confirmButton = {
                                        TextButton(onClick = {
                                            showDialog = <span class="hljs-literal">false</span>
                                            onDeleteClick(art) <span class="hljs-comment">// perform actual delete</span>
                                        }) {
                                            Text(<span class="hljs-string">"Delete"</span>)
                                        }
                                    },
                                    dismissButton = {
                                        TextButton(onClick = { showDialog = <span class="hljs-literal">false</span> }) {
                                            Text(<span class="hljs-string">"Cancel"</span>)
                                        }
                                    }
                                )
                            }

                            SwipeToDismiss(
                                state = dismissState,
                                directions = setOf(DismissDirection.EndToStart),
                                background = {
                                    <span class="hljs-keyword">val</span> color = <span class="hljs-keyword">when</span> (dismissState.targetValue) {
                                        DismissValue.DismissedToStart -&gt; Color.Red.copy(alpha = <span class="hljs-number">0.8f</span>)
                                        <span class="hljs-keyword">else</span> -&gt; Color.Transparent
                                    }
                                    <span class="hljs-comment">/*
                                    * Displays a red background + trash icon when user swipes. Gives visual feedback that this is a delete action.
                                    * */</span>
                                    Box(
                                        modifier = Modifier
                                            .fillMaxSize()
                                            .padding(horizontal = <span class="hljs-number">20</span>.dp),
                                        contentAlignment = Alignment.CenterEnd
                                    ) {
                                        Icon(
                                            imageVector = Icons.Default.Delete,
                                            contentDescription = <span class="hljs-string">"Delete"</span>,
                                            tint = Color.White
                                        )
                                    }
                                },
                                dismissContent = {
                                    ListItem(
                                        text = { Text(art.name) },
                                        secondaryText = { Text(<span class="hljs-string">"<span class="hljs-subst">${art.artistName}</span> (<span class="hljs-subst">${art.year}</span>)"</span>) },
                                        icon = {
                                            Image(
                                                painter = <span class="hljs-keyword">if</span> (art.imageUrl.startsWith(<span class="hljs-string">"http"</span>, <span class="hljs-literal">true</span>)) {
                                                    rememberAsyncImagePainter(art.imageUrl)
                                                } <span class="hljs-keyword">else</span> {
                                                    painterResource(R.drawable.ic_launcher_foreground)
                                                },
                                                contentDescription = art.name,
                                                modifier = Modifier
                                                    .size(<span class="hljs-number">60</span>.dp)
                                                    .clip(RoundedCornerShape(<span class="hljs-number">12</span>.dp))
                                                    .border(
                                                        <span class="hljs-number">1</span>.dp,
                                                        MaterialTheme.colors.onSurface.copy(alpha = <span class="hljs-number">0.12f</span>),
                                                        RoundedCornerShape(<span class="hljs-number">12</span>.dp)
                                                    ),
                                                contentScale = ContentScale.Crop
                                            )
                                        }
                                    )
                                    Divider() <span class="hljs-comment">// material v1 Divider — not deprecated here</span>
                                }
                            )
                        }
                    }
                }
            }
        }
    }
}
</code></pre>
<h3 id="heading-notes">NOTES</h3>
<p>This screen displays a <strong>scrollable list of artworks</strong> fetched from the database via the <strong>ArtViewModel</strong>.</p>
<p>It supports:</p>
<ul>
<li><p>Viewing all saved artworks.</p>
</li>
<li><p>Adding a new artwork via FloatingActionButton (FAB).</p>
</li>
<li><p>Deleting artworks with swipe + confirmation dialog.</p>
</li>
<li><p>Handling loading and error states gracefully.</p>
</li>
</ul>
<h3 id="heading-diagram">DIAGRAM</h3>
<pre><code class="lang-mermaid">graph TD
    A[ArtListScreen] --&gt;|FAB Click| B[AddArtScreen]
    A --&gt;|Swipe Left| C[AlertDialog]
    C --&gt;|Confirm Delete| D[ViewModel.deleteArt]
    D --&gt;|Updates DB| E[StateFlow emits new list]
    E --&gt;|Recomposition| A
</code></pre>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Concept</strong></td><td><strong>Description</strong></td></tr>
</thead>
<tbody>
<tr>
<td>rememberDismissState()</td><td>Tracks swipe gesture state.</td></tr>
<tr>
<td>AlertDialog()</td><td>Prompts user confirmation before deleting.</td></tr>
<tr>
<td>LazyColumn</td><td>Efficient vertical list.</td></tr>
<tr>
<td>Scaffold</td><td>Provides layout structure with FAB and body.</td></tr>
<tr>
<td>safeDrawingPadding()</td><td>Prevents overlap with system UI areas.</td></tr>
<tr>
<td>collectAsState()</td><td>Observes ViewModel Flow reactively.</td></tr>
</tbody>
</table>
</div>]]></content:encoded></item><item><title><![CDATA[Chapter 24 - Coding the Composable AddArtScreen]]></title><description><![CDATA[@Composable
fun AddArtScreen(
    imageUrl: String?,                // Displayed image
    onSaveClick: (String, String, String,String) -> Unit, // Emits form data upward
    modifier: Modifier = Modifier,
    onNavigateToImageSearch:() -> Unit,
    ...]]></description><link>https://androidavatar.hashnode.dev/chapter-24-coding-the-composable-addartscreen</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-24-coding-the-composable-addartscreen</guid><category><![CDATA[LaunchedEffect]]></category><category><![CDATA[OutlinedTextField]]></category><category><![CDATA[Composable Architecture]]></category><category><![CDATA[composable]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><category><![CDATA[mutable]]></category><category><![CDATA[text]]></category><category><![CDATA[flow]]></category><category><![CDATA[context]]></category><category><![CDATA[Coil]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Wed, 12 Nov 2025 11:46:35 GMT</pubDate><content:encoded><![CDATA[<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">AddArtScreen</span><span class="hljs-params">(
    imageUrl: <span class="hljs-type">String</span>?,                <span class="hljs-comment">// Displayed image</span>
    onSaveClick: (<span class="hljs-type">String</span>, <span class="hljs-type">String</span>, <span class="hljs-type">String</span>,<span class="hljs-type">String</span>) -&gt; <span class="hljs-type">Unit</span>, <span class="hljs-comment">// Emits form data upward</span>
    modifier: <span class="hljs-type">Modifier</span> = Modifier,
    onNavigateToImageSearch:() -&gt; <span class="hljs-type">Unit</span>,
    onNavigateBack: () -&gt; <span class="hljs-type">Unit</span>,
    uiEventFlow: <span class="hljs-type">SharedFlow</span>&lt;<span class="hljs-type">ArtViewModel</span>.<span class="hljs-type">UiEvent</span>&gt;,
)</span></span> {
    <span class="hljs-comment">// Local text states</span>
    <span class="hljs-comment">/** 
    These store the current input text for each field.
    remember makes them survive recompositions (so typing doesn’t reset the field).
    These are local states; the final data is passed upward through onSaveClick.
    */</span>
    <span class="hljs-keyword">var</span> name <span class="hljs-keyword">by</span> remember { mutableStateOf(TextFieldValue(<span class="hljs-string">""</span>)) }
    <span class="hljs-keyword">var</span> artist <span class="hljs-keyword">by</span> remember { mutableStateOf(TextFieldValue(<span class="hljs-string">""</span>)) }
    <span class="hljs-keyword">var</span> year <span class="hljs-keyword">by</span> remember { mutableStateOf(TextFieldValue(<span class="hljs-string">""</span>)) }

    <span class="hljs-comment">//Provides access to the Android Context, used for things like Toast messages.</span>
    <span class="hljs-keyword">val</span> context = LocalContext.current

    <span class="hljs-comment">//show toast message only once per new error</span>
    <span class="hljs-comment">// Toast on error or success</span>
    <span class="hljs-comment">// collect events</span>
    <span class="hljs-comment">/**
      LaunchedEffect runs once when the composable enters composition.
      It collects from a SharedFlow of events emitted by the ViewModel 
      (e.g., success/error messages).

     Handles two events:
     ShowToast → Displays short message to user.
     NavigateBack → Calls parent callback to go back
     */</span>
    LaunchedEffect(<span class="hljs-built_in">Unit</span>) {
        uiEventFlow.collect { event -&gt;
            <span class="hljs-keyword">when</span> (event) {
                <span class="hljs-keyword">is</span> ArtViewModel.UiEvent.ShowToast -&gt;
                    Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()

                ArtViewModel.UiEvent.NavigateBack -&gt;  {
                    onNavigateBack()
                }
            }
        }
    }

    LaunchedEffect(imageUrl) {
        Log.d(<span class="hljs-string">"AddArtScreen"</span>, <span class="hljs-string">"Image URL changed to <span class="hljs-variable">$imageUrl</span>"</span>)
    }

   <span class="hljs-comment">/*
    Displays either:

   The selected image from search (via imageUrl), or
   A fallback default image if none is chosen.
   Uses Coil’s rememberAsyncImagePainter to load images asynchronously.
    * */</span>
    <span class="hljs-keyword">val</span> painter = <span class="hljs-keyword">if</span> (!imageUrl.isNullOrEmpty()) {
        rememberAsyncImagePainter(model = imageUrl)
    } <span class="hljs-keyword">else</span> {
        painterResource(R.drawable.ic_launcher_background)
    }

   <span class="hljs-comment">/*
    Used when the user saves without selecting any image.
    Prevents crashes from null URLs.
    * */</span>
    <span class="hljs-keyword">val</span> DEFAULT_IMAGE_URL = <span class="hljs-string">"android.resource://androidavatar.learning.artlearnersapp/drawable/ic_launcher_background"</span>

   <span class="hljs-comment">/* 
   Column stacks UI vertically.
   Padding ensures breathing room from screen edges.
   Content is centered both horizontally and vertically.
   */</span>
    Column(
        modifier = modifier
            .fillMaxSize()
            .padding(<span class="hljs-number">16</span>.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {

        <span class="hljs-comment">/*
        Displays the image in a rounded rectangle.
        clickable → navigates to Image Search screen.
        contentScale = Crop ensures image fills its box proportionally.
       */</span>
        Image(
            painter = painter,
            contentDescription = <span class="hljs-string">"Art Image"</span>,
            modifier = Modifier
                .size(<span class="hljs-number">250</span>.dp)
                .clip(RoundedCornerShape(<span class="hljs-number">12</span>.dp))
                .clickable { onNavigateToImageSearch() },
            contentScale = ContentScale.Crop
        )

        Spacer(modifier = Modifier.height(<span class="hljs-number">16</span>.dp))

        <span class="hljs-comment">/*
        The three OutlinedTextFields capture user input for art details.
        Each uses two-way data binding with local mutableStateOf.
        */</span>
        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text(<span class="hljs-string">"Enter Name"</span>) },
            modifier = Modifier.fillMaxWidth(<span class="hljs-number">0.8f</span>)
        )

        Spacer(modifier = Modifier.height(<span class="hljs-number">8</span>.dp))

        OutlinedTextField(
            value = artist,
            onValueChange = { artist = it },
            label = { Text(<span class="hljs-string">"Enter Artist"</span>) },
            modifier = Modifier.fillMaxWidth(<span class="hljs-number">0.8f</span>)
        )

        Spacer(modifier = Modifier.height(<span class="hljs-number">8</span>.dp))

        OutlinedTextField(
            value = year,
            onValueChange = { year = it },
            label = { Text(<span class="hljs-string">"Enter Year"</span>) },
            modifier = Modifier.fillMaxWidth(<span class="hljs-number">0.8f</span>)
        )

        Spacer(modifier = Modifier.height(<span class="hljs-number">24</span>.dp))

        <span class="hljs-comment">/*
        On click → collects form data and sends it to ViewModel through onSaveClick.

        Uses the default image if none selected.
        fillMaxWidth(0.5f) → gives button a medium width (aesthetic balance).
        */</span>
        Button(
            onClick = {
                <span class="hljs-keyword">val</span> finalUrl = imageUrl ?: DEFAULT_IMAGE_URL
                onSaveClick(name.text, artist.text, year.text,finalUrl)
            },
            modifier = Modifier.fillMaxWidth(<span class="hljs-number">0.5f</span>)
        ) {
            Text(<span class="hljs-string">"Save"</span>)
        }
    }
}
</code></pre>
<h3 id="heading-important-points">IMPORTANT POINTS</h3>
<ul>
<li><p>This screen allows users to:</p>
<ul>
<li><p>Enter details of an artwork (name, artist, year)</p>
</li>
<li><p>Pick an image (from another screen)</p>
</li>
<li><p>Save the artwork</p>
</li>
</ul>
</li>
</ul>
<p>    It connects the <strong>UI layer</strong> with the <strong>ViewModel</strong>, following Clean Architecture principles — the UI collects input, while business logic stays in the ViewModel.</p>
<ul>
<li>AddArtScreen - This is a <strong>UI function</strong> that describes <em>what to display</em> and <em>how it should react</em> to state changes. It receives <strong>callbacks</strong> like onSaveClick, onNavigateBack, etc.→ This keeps the screen <strong>stateless</strong> and <strong>reusable</strong>.</li>
</ul>
<pre><code class="lang-mermaid">graph LR
    UI[AddArtScreen] --&gt;|User input| VM[ArtViewModel]
    VM --&gt;|uiEvent : SharedFlow| UI
    UI --&gt;|onNavigateToImageSearch| Search[ImageSearchScreen]
    Search --&gt;|Selected Image URL| UI
</code></pre>
<ul>
<li><p>The UI is <strong>reactive</strong>: it updates based on StateFlow/SharedFlow from ViewModel.</p>
</li>
<li><p>Navigation and UI state are controlled <strong>indirectly</strong> via events and callbacks (no direct coupling).</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Chapter 23 - Coding the Navigation Graph]]></title><description><![CDATA[/**
 * ## AppNavGraph
 *
 * This composable function defines the **navigation graph** for the entire app.
 *
 * ### Responsibilities:
 * - Sets up navigation destinations (screens) for the app.
 * - Manages a **shared `ArtViewModel`** across multiple...]]></description><link>https://androidavatar.hashnode.dev/chapter-23-coding-the-navigation-graph</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-23-coding-the-navigation-graph</guid><category><![CDATA[navigation]]></category><category><![CDATA[Graph]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Android]]></category><category><![CDATA[android apps]]></category><category><![CDATA[Composable Architecture]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Wed, 12 Nov 2025 10:19:16 GMT</pubDate><content:encoded><![CDATA[<pre><code class="lang-kotlin"><span class="hljs-comment">/**
 * ## AppNavGraph
 *
 * This composable function defines the **navigation graph** for the entire app.
 *
 * ### Responsibilities:
 * - Sets up navigation destinations (screens) for the app.
 * - Manages a **shared `ArtViewModel`** across multiple screens:
 *   - Art List (`artList`)
 *   - Art Details (`artDetails`)
 *   - Image Search (`imageSearch`)
 * - Ensures ViewModel state (like selected image, search results, etc.)
 *   persists across these routes.
 *
 * ---
 * ### Why Shared ViewModel?
 * Normally, each composable gets its own ViewModel instance.
 * But when screens are logically connected (e.g., “Add Art” → “Search Image”),
 * you want shared state between them.
 *
 * Hence, we **scope the ViewModel to a parent navigation graph** using:
 * 
 * navController.getBackStackEntry("root_graph")

 *
 * This makes the ViewModel lifecycle tied to the `root_graph` (not each screen),
 * so the same ViewModel instance is reused across all nested destinations.
 *
 * ---
 * ### Structure:
 * - **root_graph** → parent graph route (ViewModel is scoped here)
 * - `artList` → main list of saved artworks
 * - `artDetails` → add/edit artwork details
 * - `imageSearch` → search for images via API
 *
 * ---
 * <span class="hljs-doctag">@param</span> navController NavHostController controlling navigation within the app
 * <span class="hljs-doctag">@param</span> modifier Optional [Modifier] for layout customization
 */</span>
<span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">AppNavGraph</span><span class="hljs-params">(
    navController: <span class="hljs-type">NavHostController</span>,
    modifier: <span class="hljs-type">Modifier</span> = Modifier
)</span></span> {
    NavHost(
        navController = navController,
        startDestination = <span class="hljs-string">"artList"</span>,
        route = <span class="hljs-string">"root_graph"</span>, <span class="hljs-comment">//define a root graph route</span>
        modifier = modifier
    ) {

        <span class="hljs-comment">// Art List Screen</span>
        composable(route = <span class="hljs-string">"artList"</span>) { backStackEntry -&gt;
            <span class="hljs-comment">// Do it inside the lambda</span>
            <span class="hljs-keyword">val</span> parentEntry = remember(backStackEntry) {
                navController.getBackStackEntry(<span class="hljs-string">"root_graph"</span>)
            }
            <span class="hljs-keyword">val</span> viewModel: ArtViewModel = hiltViewModel(parentEntry)

            ArtListScreen(
                viewModel,
                onFabClick = { navController.navigate(<span class="hljs-string">"artDetails"</span>) },
                onDeleteClick = { art -&gt; viewModel.deleteArt(art) }
            )
        }

        <span class="hljs-comment">// Art Details Screen</span>
        composable(route = <span class="hljs-string">"artDetails"</span>) { backStackEntry -&gt;
            <span class="hljs-keyword">val</span> parentEntry = remember(backStackEntry) {
                navController.getBackStackEntry(<span class="hljs-string">"root_graph"</span>)
            }
            <span class="hljs-keyword">val</span> viewModel: ArtViewModel = hiltViewModel(parentEntry)

            <span class="hljs-keyword">val</span> addState <span class="hljs-keyword">by</span> viewModel.addArtState.collectAsState()
            <span class="hljs-keyword">val</span> selectedImageUrl <span class="hljs-keyword">by</span> viewModel.selectedImageUrl.collectAsState()

            AddArtScreen(
                imageUrl = selectedImageUrl,
                onSaveClick = { name, artist, year, url -&gt;
                    viewModel.insertArt(
                        Art(name = name, artistName = artist, year = year, imageUrl = url)
                    )

                },
                onNavigateToImageSearch = { navController.navigate(<span class="hljs-string">"imageSearch"</span>) },

                uiEventFlow = viewModel.uiEvent,
                onNavigateBack = {
                    viewModel.clearSelectedImageUrl()
                    navController.popBackStack()

                }

            )
        }

        <span class="hljs-comment">// Image Search Screen</span>
        composable(route = <span class="hljs-string">"imageSearch"</span>) { backStackEntry -&gt;
            <span class="hljs-keyword">val</span> parentEntry = remember(backStackEntry) {
                navController.getBackStackEntry(<span class="hljs-string">"root_graph"</span>)
            }
            <span class="hljs-keyword">val</span> viewModel: ArtViewModel = hiltViewModel(parentEntry)

            <span class="hljs-keyword">val</span> searchState <span class="hljs-keyword">by</span> viewModel.searchState.collectAsState()

            ImageSearchScreen(
                onSearch = { query -&gt; viewModel.searchImage(query) },
                onImageClick = { selectedUrl -&gt;
                    viewModel.setSelectedImageUrl(selectedUrl)
                    viewModel.clearImageResults()
                    navController.popBackStack()
                },
                state = searchState,
                onBack = {
                    viewModel.clearImageResults()
                    navController.popBackStack()
                }
            )
        }
    }
}

<span class="hljs-meta">@AndroidEntryPoint</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainActivity</span> : <span class="hljs-type">ComponentActivity</span></span>() {

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

        setContent {
            <span class="hljs-keyword">val</span> navController = rememberNavController()
            AppNavGraph(navController = navController)
        }
    }
}
</code></pre>
<h3 id="heading-important-points">IMPORTANT POINTS</h3>
<ol>
<li><p>The <strong>AppNavGraph</strong> function defines all the navigation routes in the app — it connects the three main screens:</p>
<ul>
<li><p><strong>Art List</strong> → displays saved artworks.</p>
</li>
<li><p><strong>Art Details</strong> → lets users add or edit an artwork.</p>
</li>
<li><p><strong>Image Search</strong> → lets users find an image using the Pixabay API.</p>
</li>
</ul>
</li>
<li><p>The navigation graph uses a <strong>root route</strong> called "root_graph". The key idea is to <strong>share the same ArtViewModel</strong> across all these screens.<br /> This acts like a <strong>common parent container</strong> for all the child screens. That means any state (like the selected image, search results, or form data) remains the same when we navigate between them.</p>
</li>
<li><p>Normally, every screen would get its <strong>own instance</strong> of a ViewModel when using <code>hiltViewModel()</code>.<br /> But in this setup, we use:</p>
<pre><code class="lang-kotlin"> <span class="hljs-keyword">val</span> parentEntry = remember(backStackEntry) {
     navController.getBackStackEntry(<span class="hljs-string">"root_graph"</span>)
 }
 <span class="hljs-keyword">val</span> viewModel: ArtViewModel = hiltViewModel(parentEntry)
</code></pre>
<p> This makes sure the <strong>same ViewModel instance</strong> is reused for every screen inside the "root_graph".</p>
</li>
<li><p>Because of this shared scope:</p>
<ul>
<li><p>When we set an image URL in the ImageSearch screen, it’s still available in the ArtDetails screen after going back.</p>
</li>
<li><p>When we delete or insert an artwork, the ArtList screen automatically updates because the same ViewModel holds the data.</p>
</li>
</ul>
</li>
<li><p>Each composable (screen) <strong>collects StateFlow</strong> values from the ViewModel using collectAsState().</p>
<p> This lets the UI update automatically when the ViewModel state changes.</p>
</li>
<li><p>For cleanup:</p>
<ul>
<li><p>When we navigate back from Image Search → we call viewModel.clearImageResults().</p>
</li>
<li><p>When we go back from Art Details → we call viewModel.clearSelectedImageUrl().<br />  This ensures no leftover data appears when the user reopens those screens.</p>
</li>
</ul>
</li>
<li><p>The UiEvent system (inside the ViewModel) helps trigger <strong>navigation or toast messages</strong> based on ViewModel logic — separating UI concerns from the business logic layer.</p>
</li>
<li><p>This setup follows <strong>Clean Architecture principles</strong>:</p>
<ul>
<li><p>ViewModel holds logic and state.</p>
</li>
<li><p>Composables just display data and forward user actions.</p>
</li>
<li><p>Navigation and UI events are handled reactively.</p>
</li>
</ul>
</li>
</ol>
<h3 id="heading-diagram-navigation-shared-view-model">DIAGRAM : Navigation + Shared View Model</h3>
<pre><code class="lang-mermaid">graph TD

    subgraph RootGraph["root_graph(Shared ViewModel Scope)"]
        A[ArtListScreen]
        B[ArtDetailsScreen]
        C[ImageSearchScreen]
    end

    A --&gt;|FAB Click| B
    B --&gt;|Open Image Search| C
    C --&gt;|Select Image| B
    B --&gt;|Save Art| A

    subgraph VM["ArtViewModel (shared)"]
        V1[arts : StateFlow&amp;lt;List&amp;lt;Art&amp;gt;&amp;gt;]
        V2[selectedImageUrl : StateFlow&amp;lt;String?&amp;gt;]
        V3[searchResults : StateFlow&amp;lt;Resource&amp;lt;List&amp;lt;ImageResult&amp;gt;&amp;gt;&amp;gt;]
        V4[addArtState : StateFlow&amp;lt;AddArtUiState&amp;gt;]
        V5[uiEvent : SharedFlow&amp;lt;UiEvent&amp;gt;]
    end

    A --&gt;|Observes| VM
    B --&gt;|Observes| VM
    C --&gt;|Observes| VM

    VM --&gt;|Updates State| A
    VM --&gt;|Updates State| B
    VM --&gt;|Updates State| C
</code></pre>
<h3 id="heading-summary">SUMMARY</h3>
<ul>
<li><p><strong>root_graph</strong> is the shared parent that defines the scope of the shared <strong>ArtViewModel</strong>.</p>
</li>
<li><p><strong>Each screen uses hiltViewModel(parentEntry)</strong> to access the same instance.</p>
</li>
<li><p><strong>StateFlows</strong> keep the UI reactive and consistent.</p>
</li>
<li><p><strong>Manual clearing methods</strong> prevent old data from leaking between sessions.</p>
</li>
<li><p><strong>The ViewModel</strong> acts as a single source of truth for the entire feature (Art Management + Image Search).</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Chapter 22 - Updating the ArtViewModel for the Insert and Delete Use cases]]></title><description><![CDATA[The Insert and Delete Use Cases delegates the Room DB updates for insertion and deletion to the Repository. The ArtViewModel will implement the insert and delete cases.

    /**
     * Inserts a new artwork into the local database.
     *
     * @par...]]></description><link>https://androidavatar.hashnode.dev/chapter-22-updating-the-artviewmodel-for-the-insert-and-delete-use-cases</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-22-updating-the-artviewmodel-for-the-insert-and-delete-use-cases</guid><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Sun, 09 Nov 2025 21:08:26 GMT</pubDate><content:encoded><![CDATA[<p>The Insert and Delete Use Cases delegates the Room DB updates for insertion and deletion to the Repository. The ArtViewModel will implement the insert and delete cases.</p>
<pre><code class="lang-kotlin">
    <span class="hljs-comment">/**
     * Inserts a new artwork into the local database.
     *
     * <span class="hljs-doctag">@param</span> art The [Art] object containing artwork details (name, artist, year, image).
     *
     * This method calls the [InsertArtUseCase] and, upon success, reloads the updated art list.
     * If an error occurs, it updates the state with an error message.
     *
     * Internal helper to collect artworks and update the UI state.
     * Called during initialization and after insert/delete operations.
     * It continuously listens for changes from the Room database.
     */</span>

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">insertArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {
        <span class="hljs-comment">//Log.d("baxss", art.toString())</span>

        <span class="hljs-keyword">if</span> (art.name.isBlank() || art.artistName.isBlank() || art.year.isBlank() || art.imageUrl.isBlank()) {
            sendUiEvent(<span class="hljs-string">"All fields are required"</span>)
            <span class="hljs-keyword">return</span>
        }

        viewModelScope.launch {
            _addState.update { it.copy(isLoading = <span class="hljs-literal">true</span>) }

            <span class="hljs-keyword">when</span> (<span class="hljs-keyword">val</span> result = useCases.insertArtUseCase(art)) {
                <span class="hljs-keyword">is</span> Resource.Error -&gt; {
                    _addState.update { it.copy(isLoading = <span class="hljs-literal">false</span>) }
                    sendUiEvent(result.message ?: <span class="hljs-string">"Something went wrong"</span>)
                }

                <span class="hljs-keyword">is</span> Resource.Success -&gt; {
                    _addState.update { it.copy(isLoading = <span class="hljs-literal">false</span>, success = <span class="hljs-literal">true</span>) }
                    sendUiEvent(<span class="hljs-string">"Art saved successfully!"</span>)
                    loadArts()
                }

                <span class="hljs-keyword">else</span> -&gt; <span class="hljs-built_in">Unit</span>
            }
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _uiEvent = MutableSharedFlow&lt;UiEvent&gt;()
    <span class="hljs-keyword">val</span> uiEvent = _uiEvent.asSharedFlow()

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">sendUiEvent</span><span class="hljs-params">(message: <span class="hljs-type">String</span>)</span></span> {
        viewModelScope.launch {
            _uiEvent.emit(UiEvent.ShowToast(message))
        }
    }

    <span class="hljs-keyword">sealed</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UiEvent</span> </span>{
        <span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ShowToast</span></span>(<span class="hljs-keyword">val</span> message: String) : UiEvent()
    }


    <span class="hljs-comment">/**
     * Deletes an existing artwork from the local database.
     *
     * <span class="hljs-doctag">@param</span> art The [Art] object representing the artwork to be deleted.
     *
     * This function delegates the deletion logic to the [DeleteArtUseCase].
     * It does not emit UI state changes but can be extended for confirmation handling.
     */</span>

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">deleteArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {
        viewModelScope.launch {
            useCases.deleteArtUseCase(art)
        }
    }
</code></pre>
<p>The left swipe of the list item in the ArtListScreen triggers a call to the deletion function.</p>
<p>The Save button click verifies whether all image details are available. If yes, then the Art data is saved inside the Room DB by triggering the insert function. If the Art data is incomplete, then we display the toast message asking the user to modify.</p>
]]></content:encoded></item><item><title><![CDATA[Chapter 21 - Coding the Data layer for the InsertArtUseCase and DeleteArtUseCase]]></title><description><![CDATA[Here we implement the repository implementation class
@Singleton
class ArtRepositoryImpl @Inject constructor(private val artDao: ArtDao,
                                            private val imageApi:ImageApi,
                                      ...]]></description><link>https://androidavatar.hashnode.dev/chapter-21-coding-the-data-layer-for-the-insertartusecase-and-deleteartusecase</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-21-coding-the-data-layer-for-the-insertartusecase-and-deleteartusecase</guid><category><![CDATA[insertion ]]></category><category><![CDATA[deletion]]></category><category><![CDATA[room database]]></category><category><![CDATA[Room]]></category><category><![CDATA[DAOs]]></category><category><![CDATA[DAO]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Sun, 09 Nov 2025 17:36:01 GMT</pubDate><content:encoded><![CDATA[<p>Here we implement the repository implementation class</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Singleton</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ArtRepositoryImpl</span> <span class="hljs-meta">@Inject</span> <span class="hljs-keyword">constructor</span></span>(<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> artDao: ArtDao,
                                            <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> imageApi:ImageApi,
                                            <span class="hljs-meta">@Named(<span class="hljs-meta-string">"apiKey"</span>)</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> apiKey: String):ArtRepositoryInterface {
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getArt</span><span class="hljs-params">()</span></span>: Flow&lt;List&lt;Art&gt;&gt; {

        <span class="hljs-comment">//returns a stream (Flow) that emits a list of ArtEntity objects every time the ROOM DB table changes.</span>
        <span class="hljs-keyword">val</span> artStream = artDao.getAllArts()

       <span class="hljs-keyword">val</span> artObjs = artStream.map { entityList -&gt; entityList.map { it.toDomainModel() } }
        <span class="hljs-keyword">return</span> artObjs
    }

    <span class="hljs-keyword">override</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">insertArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {
         artDao.insertArt(art.toEntity())
    }

    <span class="hljs-keyword">override</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">deleteArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {
         artDao.insertArt(art.toEntity())
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">searchImage</span><span class="hljs-params">(imageString: <span class="hljs-type">String</span>)</span></span>: Flow&lt;Resource&lt;List&lt;ImageResult&gt;&gt;&gt; = flow {
        emit(Resource.loading())
        <span class="hljs-comment">//Log.d("PixabayRepo", "query code: ${imageString}")</span>

        <span class="hljs-keyword">if</span>(apiKey.isEmpty()){
            emit(Resource.error(<span class="hljs-string">"Empty api key"</span>))
            <span class="hljs-comment">//Log.d("PixabayRepo", "emptiness")</span>
            <span class="hljs-keyword">return</span><span class="hljs-symbol">@flow</span>
        }

        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">val</span> response = imageApi.searchImages(apiKey, imageString)
            <span class="hljs-comment">//Log.d("PixabayRepo", "Response code: ${response.code()}, success=${response.isSuccessful}")</span>

            <span class="hljs-keyword">if</span> (response.isSuccessful) {
                <span class="hljs-keyword">val</span> body = response.body()

                <span class="hljs-keyword">if</span> (body != <span class="hljs-literal">null</span>) {
                    <span class="hljs-comment">// Map the list of DTOs to domain models</span>
                    <span class="hljs-keyword">val</span> results = body.hits.map {it.toDomainModel()}
                    <span class="hljs-comment">//Log.d("PixabayRepo", "Received ${body.hits.size} images for query=$imageString")</span>

                    emit(Resource.success(results))
                } <span class="hljs-keyword">else</span> {
                    emit(Resource.error(<span class="hljs-string">"Empty response body"</span>))
                   <span class="hljs-comment">// Log.d("error pixa","error")</span>
                }
            } <span class="hljs-keyword">else</span> {
                emit(Resource.error(<span class="hljs-string">"API error: <span class="hljs-subst">${response.message()}</span>"</span>))
                <span class="hljs-comment">//Log.d("error API", response.message())</span>
            }

        } <span class="hljs-keyword">catch</span> (e: Exception) {
            emit(Resource.error(<span class="hljs-string">"Network error: <span class="hljs-subst">${e.message}</span>"</span>))
            <span class="hljs-comment">//e.message?.let { Log.d("error API", it) }</span>
        }
    }
}
</code></pre>
<p>The artDao object inserts or deletes an Art object in the Room DB.</p>
]]></content:encoded></item><item><title><![CDATA[Chapter 20 - Coding the Domain layer for InsertArtUseCase and DeleteArtUseCase]]></title><description><![CDATA[Let us update the ArtUseCases class.
data class ArtUseCases(
   val getArtUseCase: GetArtUseCase,
    val searchImageUseCase: SearchImageUseCase,
    val insertArtUseCase: InsertArtUseCase,
    val deleteArtUseCase:DeleteArtUseCase
)

We will now cod...]]></description><link>https://androidavatar.hashnode.dev/chapter-20-coding-the-domain-layer-for-insertartusecase-and-deleteartusecase</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-20-coding-the-domain-layer-for-insertartusecase-and-deleteartusecase</guid><category><![CDATA[Usecase]]></category><category><![CDATA[data class]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Sun, 09 Nov 2025 17:18:25 GMT</pubDate><content:encoded><![CDATA[<p>Let us update the ArtUseCases class.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ArtUseCases</span></span>(
   <span class="hljs-keyword">val</span> getArtUseCase: GetArtUseCase,
    <span class="hljs-keyword">val</span> searchImageUseCase: SearchImageUseCase,
    <span class="hljs-keyword">val</span> insertArtUseCase: InsertArtUseCase,
    <span class="hljs-keyword">val</span> deleteArtUseCase:DeleteArtUseCase
)
</code></pre>
<p>We will now code the two classes for insert and delete usecases. The code is easy to understand.</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">InsertArtUseCase</span></span>(<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> artRepository: ArtRepositoryInterface) {

    <span class="hljs-keyword">suspend</span> <span class="hljs-keyword">operator</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">invoke</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span>: Resource&lt;Art&gt; {
        <span class="hljs-comment">// Basic validation</span>
        <span class="hljs-keyword">if</span> (art.name.isBlank() || art.artistName.isBlank() || art.imageUrl.isBlank() || art.year.isBlank()) {
            <span class="hljs-keyword">return</span> Resource.error(<span class="hljs-string">"Fields must not be empty and/or filled incorrectly"</span>, <span class="hljs-literal">null</span>)
        }

        <span class="hljs-comment">// Save to DB through the repository interface</span>
        artRepository.insertArt(art)

        <span class="hljs-comment">// Return success</span>
        <span class="hljs-keyword">return</span> Resource.success(art)
    }

}

<span class="hljs-comment">//Purpose: Validate and save a new artwork to the Room DB.</span>
<span class="hljs-comment">//Domain logic: Ensure fields are valid before inserting.</span>
</code></pre>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DeleteArtUseCase</span></span>(<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> artRep: ArtRepositoryInterface) {

    <span class="hljs-keyword">suspend</span> <span class="hljs-keyword">operator</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">invoke</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span>{
        artRep.deleteArt(art)
    }
}
</code></pre>
<p>Now we will code the data layer for the above two usecases - the repository implementation class.</p>
]]></content:encoded></item><item><title><![CDATA[Chapter 19 - Testing the Domain,Data and  Presentation layers for SearchImageUseCase]]></title><description><![CDATA[The following objectives need to be satisfied by the use case:

Emits Loading, Success, and Error states in sequence.

Interacts properly with the repository’s searchImage() flow.

Handles both success and failure paths gracefully.


Testing SearchIm...]]></description><link>https://androidavatar.hashnode.dev/chapter-19-testing-the-domaindata-and-presentation-layers-for-searchimageusecase</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-19-testing-the-domaindata-and-presentation-layers-for-searchimageusecase</guid><category><![CDATA[fake]]></category><category><![CDATA[faker]]></category><category><![CDATA[repository]]></category><category><![CDATA[flow]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[ViewModel]]></category><category><![CDATA[Testing]]></category><category><![CDATA[testing tool]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Sat, 08 Nov 2025 16:54:25 GMT</pubDate><content:encoded><![CDATA[<p>The following objectives need to be satisfied by the use case:</p>
<ul>
<li><p>Emits <strong>Loading</strong>, <strong>Success</strong>, and <strong>Error</strong> states in sequence.</p>
</li>
<li><p>Interacts properly with the repository’s searchImage() flow.</p>
</li>
<li><p>Handles both success and failure paths gracefully.</p>
</li>
</ul>
<h3 id="heading-testing-searchimageusecase-with-a-mock-repository">Testing SearchImageUseCase with a Mock Repository</h3>
<pre><code class="lang-kotlin">**
 * ## SearchImageUseCaseTest
 *
 * This test verifies that [SearchImageUseCase] correctly reacts to
 * repository emissions. It does **not** hit the real API — instead,
 * the repository <span class="hljs-keyword">is</span> **mocked** using Mockito.
 *
 * ### Test Scope
 * - Domain Layer only
 * - No Retrofit or Room
 * - No Android framework dependencies
 *
 * ### What We’re Testing
 * <span class="hljs-number">1</span>. The flow of Resource states (Loading → Success / Error)
 * <span class="hljs-number">2</span>. That the use case correctly calls the repository method
 */
<span class="hljs-meta">@OptIn(ExperimentalCoroutinesApi::class)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SearchImageUseCaseTest</span> </span>{

    <span class="hljs-comment">/**
     * Mocked repository so that we don’t call the actual implementation.
     *
     * Mockito will intercept any calls like `repository.searchImage("cats")`
     * and return whatever fake Flow we configure it to emit.
     */</span>
    <span class="hljs-meta">@Mock</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> repository:ArtRepositoryInterface

    <span class="hljs-comment">/**
     * The UseCase we’re testing, instantiated with the mocked repository.
     */</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> searchImageUseCase: SearchImageUseCase

    <span class="hljs-comment">/**
     * ### <span class="hljs-doctag">@Before</span>
     * Called before each test.
     *
     * 1. Initializes the `<span class="hljs-doctag">@Mock</span>` fields (so that Mockito can inject fake objects).
     * 2. Instantiates the UseCase with the mock repository.
     */</span>
    <span class="hljs-meta">@Before</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setup</span><span class="hljs-params">()</span></span>{
        MockitoAnnotations.openMocks(<span class="hljs-keyword">this</span>)
        searchImageUseCase = SearchImageUseCase(repository)
    }

    <span class="hljs-comment">/**
     * ## Test 1: Success Case
     *
     * Simulates a successful API call returning a list of images.
     *
     * Flow emission pattern:
     * 
     * LOADING → SUCCESS
     * 
     */</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `should emit loading and success <span class="hljs-keyword">when</span> repository returns <span class="hljs-keyword">data</span>`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-comment">// Arrange: Create fake image data</span>
        <span class="hljs-keyword">val</span> mockImages = listOf(
            ImageResult(id = <span class="hljs-number">1</span>, previewURL = <span class="hljs-string">"url1"</span>, largeImageURL = <span class="hljs-string">"urlLarge"</span>)
        )

        <span class="hljs-comment">// Mock repository to emit loading → success sequence</span>
        `<span class="hljs-keyword">when</span>`(repository.searchImage(<span class="hljs-string">"cats"</span>)).thenReturn(
            flow {
                emit(Resource.loading())
                emit(Resource.success(mockImages))
            }
        )

        <span class="hljs-comment">// Act: Execute the use case</span>
        <span class="hljs-keyword">val</span> results = searchImageUseCase(<span class="hljs-string">"cats"</span>).toList()

        <span class="hljs-comment">// Assert: Verify the flow emits Loading then Success</span>
        assertThat(results[<span class="hljs-number">0</span>]).isInstanceOf(Resource.Loading::<span class="hljs-keyword">class</span>.java)
        assertThat(results[<span class="hljs-number">1</span>]).isInstanceOf(Resource.Success::<span class="hljs-keyword">class</span>.java)

        <span class="hljs-keyword">val</span> success = results[<span class="hljs-number">1</span>] <span class="hljs-keyword">as</span> Resource.Success
        assertThat(success.<span class="hljs-keyword">data</span>).isNotNull()
        assertThat(success.<span class="hljs-keyword">data</span>!!.size).isEqualTo(<span class="hljs-number">1</span>)

        <span class="hljs-comment">// Verify the repository was called correctly</span>
        verify(repository).searchImage(<span class="hljs-string">"cats"</span>)
    }


    <span class="hljs-comment">/**
     * ## Test 2: Error Case
     *
     * Simulates an API/network error scenario.
     *
     * Flow emission pattern:
     * 
     * LOADING → ERROR
     * 
     */</span>

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `should emit loading and error <span class="hljs-keyword">when</span> repository returns error`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-comment">// Arrange</span>
        <span class="hljs-keyword">val</span> errorMsg = <span class="hljs-string">"Network error"</span>

        <span class="hljs-comment">// Mock repository to emit loading → error sequence</span>
        `<span class="hljs-keyword">when</span>`(repository.searchImage(<span class="hljs-string">"dogs"</span>)).thenReturn(
            flow {
                emit(Resource.loading())
                emit(Resource.error(errorMsg))
            }
        )

        <span class="hljs-comment">// Act</span>
        <span class="hljs-keyword">val</span> results = searchImageUseCase(<span class="hljs-string">"dogs"</span>).toList()

        <span class="hljs-comment">// Assert</span>
        assertThat(results[<span class="hljs-number">0</span>]).isInstanceOf(Resource.Loading::<span class="hljs-keyword">class</span>.java)
        assertThat(results[<span class="hljs-number">1</span>]).isInstanceOf(Resource.Error::<span class="hljs-keyword">class</span>.java)
        assertThat(results[<span class="hljs-number">1</span>].message).isEqualTo(errorMsg)

        verify(repository).searchImage(<span class="hljs-string">"dogs"</span>)
    }

}

<span class="hljs-comment">/**
Test that SearchImageUseCase correctly:
-----------------------------------------

Emits Loading, Success, and Error states in sequence.
Interacts properly with the repository’s searchImage() flow.
Handles both success and failure paths gracefully.
 * */</span>
</code></pre>
<pre><code class="lang-kotlin"><span class="hljs-comment">/**
 * Tests the ArtRepositoryImpl class to verify that
 * - Data from DAO is correctly mapped to domain model.
 * - The Flow emitted matches the expected data.
 * - The repository interacts correctly with DAO and API layers.
 * - API returns expected Flow&lt;Resource&gt; for searchImage()
 */</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ArtRepositoryImplTest</span> </span>{

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> fakeDao: FakeArtDao
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> mockApi:ImageApi
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> repositoryImpl: ArtRepositoryImpl
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> dummyApiKey = <span class="hljs-string">"FAKE_KEY"</span>

    <span class="hljs-meta">@Before</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setup</span><span class="hljs-params">()</span></span>{

        fakeDao = FakeArtDao()
        mockApi = mock(ImageApi::<span class="hljs-keyword">class</span>.java)
        repositoryImpl = ArtRepositoryImpl(fakeDao,mockApi,dummyApiKey)
    }

    @Test
    <span class="hljs-keyword">fun</span> `getArt returns mapped domain objects`() = runTest {
        //arrange
        <span class="hljs-keyword">val</span> testArtEntity =
            ArtEntity(name = <span class="hljs-string">"jalwa fariq"</span>, artistName = <span class="hljs-string">"rehman malik"</span>, year = <span class="hljs-string">"1989"</span>, imageUrl = <span class="hljs-string">"url2"</span>)

        fakeDao.insertArt(testArtEntity)

        <span class="hljs-keyword">val</span> result = repositoryImpl.getArt().first()

        <span class="hljs-comment">//assert</span>
        assertThat(result.size).isEqualTo(<span class="hljs-number">1</span>)
        assertThat(result[<span class="hljs-number">0</span>].year).isEqualTo(<span class="hljs-string">"1989"</span>)
        assertThat(result[<span class="hljs-number">0</span>].name).isEqualTo(<span class="hljs-string">"jalwa fariq"</span>)
        assertThat(result[<span class="hljs-number">0</span>].artistName).isEqualTo(<span class="hljs-string">"rehman malik"</span>)
    }

    <span class="hljs-comment">// --- Test 2: searchImage returns success ---</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `searchImage emits loading and success <span class="hljs-keyword">when</span> API returns <span class="hljs-keyword">data</span>`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-comment">// Arrange fake API response</span>
        <span class="hljs-keyword">val</span> fakeImageResults = listOf(
            ImageResultDto(
                id = <span class="hljs-number">1</span>, previewURL = <span class="hljs-string">"url1"</span>, largeImageURL = <span class="hljs-string">"urlLarge"</span>,
                comments = <span class="hljs-number">0</span>,
                downloads = <span class="hljs-number">0</span>,
                favorites = <span class="hljs-number">0</span>,
                imageHeight = <span class="hljs-number">111</span>,
                imageSize = <span class="hljs-number">112</span>,
                imageWidth = <span class="hljs-number">111</span>,
                likes = <span class="hljs-number">12</span>,
                pageURL = <span class="hljs-string">"TODO()"</span>,
                previewHeight = <span class="hljs-number">12</span>,
                previewWidth = <span class="hljs-number">22</span>,
                tags = <span class="hljs-string">"TODO()"</span>,
                type = <span class="hljs-string">"TODO()"</span>,
                user = <span class="hljs-string">"TODO()"</span>,
                userId = <span class="hljs-number">0</span>,
                userImageURL = <span class="hljs-string">"TODO()"</span>,
                views = <span class="hljs-number">0</span>,
                webformatHeight = <span class="hljs-number">0</span>,
                webformatURL = <span class="hljs-string">"TODO()"</span>,
                webformatWidth = <span class="hljs-number">11</span>
            )
        )
        <span class="hljs-keyword">val</span> responseDto = ImageResponseDto(total = <span class="hljs-number">1</span>, totalHits = <span class="hljs-number">1</span>, hits = fakeImageResults)

        `<span class="hljs-keyword">when</span>`(mockApi.searchImages(dummyApiKey, <span class="hljs-string">"cats"</span>)).thenReturn(Response.success(responseDto))

        <span class="hljs-comment">// Act</span>
        <span class="hljs-keyword">val</span> results = repositoryImpl.searchImage(<span class="hljs-string">"cats"</span>).toList()

        <span class="hljs-comment">// Assert</span>
        assertThat(results[<span class="hljs-number">0</span>]).isInstanceOf(Resource.Loading::<span class="hljs-keyword">class</span>.java)
        assertThat(results[<span class="hljs-number">1</span>]).isInstanceOf(Resource.Success::<span class="hljs-keyword">class</span>.java)
        <span class="hljs-keyword">val</span> success = results[<span class="hljs-number">1</span>] <span class="hljs-keyword">as</span> Resource.Success
        assertThat(success.<span class="hljs-keyword">data</span>?.size).isEqualTo(<span class="hljs-number">1</span>)
        assertThat(success.<span class="hljs-keyword">data</span>?.<span class="hljs-keyword">get</span>(<span class="hljs-number">0</span>)?.previewURL).isEqualTo(<span class="hljs-string">"url1"</span>)
    }

    <span class="hljs-comment">// --- Test 3: searchImage emits error when API key empty ---</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `searchImage emits error <span class="hljs-keyword">when</span> api key empty`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-keyword">val</span> repo = ArtRepositoryImpl(fakeDao, mockApi, apiKey = <span class="hljs-string">""</span>)

        <span class="hljs-keyword">val</span> results = repo.searchImage(<span class="hljs-string">"cats"</span>).toList()

        assertThat(results[<span class="hljs-number">0</span>]).isInstanceOf(Resource.Loading::<span class="hljs-keyword">class</span>.java)
        assertThat(results[<span class="hljs-number">1</span>]).isInstanceOf(Resource.Error::<span class="hljs-keyword">class</span>.java)
        assertThat(results[<span class="hljs-number">1</span>].message).isEqualTo(<span class="hljs-string">"Empty api key"</span>)
    }

    <span class="hljs-comment">// --- Test 4: searchImage emits error when API fails ---</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `searchImage emits error <span class="hljs-keyword">when</span> API returns failure`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-keyword">val</span> errorResponse = Response.error&lt;ImageResponseDto&gt;(
            <span class="hljs-number">404</span>,
            ResponseBody.create(<span class="hljs-string">"application/json"</span>.toMediaTypeOrNull(), <span class="hljs-string">"Not Found"</span>)
        )

        `<span class="hljs-keyword">when</span>`(mockApi.searchImages(dummyApiKey, <span class="hljs-string">"dogs"</span>)).thenReturn(errorResponse)

        <span class="hljs-keyword">val</span> results = repositoryImpl.searchImage(<span class="hljs-string">"dogs"</span>).toList()

        assertThat(results[<span class="hljs-number">0</span>]).isInstanceOf(Resource.Loading::<span class="hljs-keyword">class</span>.java)
        assertThat(results[<span class="hljs-number">1</span>]).isInstanceOf(Resource.Error::<span class="hljs-keyword">class</span>.java)
        assertThat(results[<span class="hljs-number">1</span>].message).contains(<span class="hljs-string">"API error"</span>)
    }
}

<span class="hljs-comment">/*
A Fake is a lightweight, in-memory implementation of the real ArtDao interface.
FakeArtDao mimics the behavior of the Room DAO but uses plain Kotlin collections and Flows instead of a real database.
It's designed for fast, deterministic tests of higher layers (repository, use cases, ViewModel) without using Room or an emulator.
Fake has real behavior (stores and returns data) so it's ideal for validating interactions and data flow.
*/</span>

<span class="hljs-comment">/*
Responsibilities (what FakeArtDao must do)
Implement the ArtDao interface exactly (same function signatures).
Expose a Flow&lt;List&lt;ArtEntity&gt;&gt; for getAllArts() that behaves like Room: emits the current list and emits new lists when data changes.
Provide working insertArt() and deleteArt() semantics (mutate internal state).
Be deterministic and resettable between tests.
Be coroutine-friendly (suspend functions behave like the real DAO).
 */</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FakeArtDao</span> : <span class="hljs-type">ArtDao {</span></span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> <span class="hljs-keyword">data</span> = mutableListOf&lt;ArtEntity&gt;()

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getAllArts</span><span class="hljs-params">()</span></span>: Flow&lt;List&lt;ArtEntity&gt;&gt; = flowOf(<span class="hljs-keyword">data</span>)

    <span class="hljs-keyword">override</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">insertArt</span><span class="hljs-params">(artEntity: <span class="hljs-type">ArtEntity</span>)</span></span> {
        <span class="hljs-keyword">data</span>.add(artEntity)
    }

    <span class="hljs-keyword">override</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">deleteArt</span><span class="hljs-params">(artEntity: <span class="hljs-type">ArtEntity</span>)</span></span> {
        <span class="hljs-keyword">data</span>.remove(artEntity)
    }
}
</code></pre>
<pre><code class="lang-kotlin"><span class="hljs-comment">/**
 * ## Unit test for [ArtViewModel].
 *
 * Verifies that:
 * 1. [ArtViewModel] correctly collects from both [GetArtUseCase] and [SearchImageUseCase].
 * 2. Data from repository flows into StateFlows (`artList`, `imageList`, `errorMessage`, `isLoading`).
 * 3. Loading and error flags behave as expected for both success and failure.
 */</span>
<span class="hljs-meta">@OptIn(ExperimentalCoroutinesApi::class)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ArtViewModelRevisedTest</span> </span>{
    <span class="hljs-comment">/**
     * A fake repository that simulates both local DB and remote API behavior.
     */</span>
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FakeRepository</span> : <span class="hljs-type">ArtRepositoryInterface {</span></span>

        <span class="hljs-keyword">val</span> fakeArtList = listOf(
            Art(id = <span class="hljs-number">1</span>, name = <span class="hljs-string">"Starry Horse"</span>, artistName = <span class="hljs-string">"Viru Pandey"</span>, year = <span class="hljs-string">"1904"</span>, imageUrl = <span class="hljs-string">"url1"</span>),
            Art(id = <span class="hljs-number">2</span>, name = <span class="hljs-string">"Mano ya na Mano"</span>, artistName = <span class="hljs-string">"Satish Shah"</span>, year = <span class="hljs-string">"1998"</span>, imageUrl = <span class="hljs-string">"url2"</span>)
        )

        <span class="hljs-keyword">var</span> shouldThrowError = <span class="hljs-literal">false</span>
        <span class="hljs-keyword">var</span> shouldReturnEmptySearch = <span class="hljs-literal">false</span>

        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getArt</span><span class="hljs-params">()</span></span>: Flow&lt;List&lt;Art&gt;&gt; {
            <span class="hljs-keyword">return</span> flowOf(fakeArtList)
        }

        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">insertArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {
            <span class="hljs-comment">// No-op</span>
        }

        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">deleteArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {
            <span class="hljs-comment">// No-op</span>
        }

        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">searchImage</span><span class="hljs-params">(userQuery: <span class="hljs-type">String</span>)</span></span>: Flow&lt;Resource&lt;List&lt;ImageResult&gt;&gt;&gt; {
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> (shouldThrowError) {
                flow {
                    emit(Resource.Loading())
                    emit(Resource.Error(<span class="hljs-string">"Network error"</span>))
                }
            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (shouldReturnEmptySearch) {
                flow {
                    emit(Resource.Loading())
                    emit(Resource.Success(emptyList()))
                }
            } <span class="hljs-keyword">else</span> {
                <span class="hljs-keyword">val</span> results = listOf(
                    ImageResult(id = <span class="hljs-number">1</span>, previewURL = <span class="hljs-string">"preview1"</span>, largeImageURL = <span class="hljs-string">"large1"</span>),
                    ImageResult(id = <span class="hljs-number">2</span>, previewURL = <span class="hljs-string">"preview2"</span>, largeImageURL = <span class="hljs-string">"large2"</span>)
                )
                flow {
                    emit(Resource.Loading())
                    emit(Resource.Success(results))
                }
            }
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> fakeRepo: FakeRepository
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> getArtUseCase: GetArtUseCase
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> searchImageUseCase: SearchImageUseCase
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> viewModel: ArtViewModel
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> viewModel2: ArtViewModel

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> artUseCases:ArtUseCases

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> mockSearchImageUseCase: SearchImageUseCase
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">lateinit</span> <span class="hljs-keyword">var</span> fakeUseCases: ArtUseCases

    <span class="hljs-meta">@Before</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setup</span><span class="hljs-params">()</span></span> {
        fakeRepo = FakeRepository()
        getArtUseCase = GetArtUseCase(fakeRepo)
        searchImageUseCase = SearchImageUseCase(fakeRepo)
        artUseCases = ArtUseCases(getArtUseCase, searchImageUseCase)
        viewModel = ArtViewModel(artUseCases)

        mockSearchImageUseCase = mock()
        fakeUseCases = mock {
            on { searchImageUseCase } doReturn mockSearchImageUseCase
            on { getArtUseCase } doReturn mock() <span class="hljs-comment">// not used here</span>
        }

        viewModel2 = ArtViewModel(fakeUseCases)
    }

    <span class="hljs-comment">// --------------------------------------------------------------</span>
    <span class="hljs-comment">// TEST 1 — LOCAL ART LIST FLOW</span>
    <span class="hljs-comment">// --------------------------------------------------------------</span>

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `artList emits expected fake <span class="hljs-keyword">data</span> after initialization`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-keyword">val</span> expected = fakeRepo.fakeArtList

        viewModel.arts.test {
            <span class="hljs-keyword">val</span> firstEmission = awaitItem()
            <span class="hljs-comment">// First emission might be empty before collect</span>
            <span class="hljs-keyword">if</span> (firstEmission.isNotEmpty()) {
                assertThat(firstEmission).isEqualTo(expected)
            } <span class="hljs-keyword">else</span> {
                <span class="hljs-keyword">val</span> next = awaitItem()
                assertThat(next).isEqualTo(expected)
            }
            cancelAndIgnoreRemainingEvents()
        }
    }

    <span class="hljs-comment">// --------------------------------------------------------------</span>
    <span class="hljs-comment">// TEST 2 — SEARCH IMAGE SUCCESS</span>
    <span class="hljs-comment">// --------------------------------------------------------------</span>

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `searchImage emits loading then success updates imageList`<span class="hljs-params">()</span></span> = runTest {


        <span class="hljs-comment">// Arrange</span>
        <span class="hljs-keyword">val</span> fakeResults = listOf(
            ImageResult(
                previewURL = <span class="hljs-string">"preview1"</span>,
                id = <span class="hljs-number">0</span>,
                largeImageURL = <span class="hljs-string">"TODO()"</span>
            ),
            ImageResult(previewURL = <span class="hljs-string">"preview2"</span>,id=<span class="hljs-number">2</span>, largeImageURL = <span class="hljs-string">"uij"</span>)
        )

        <span class="hljs-comment">// Fake use case flow emitting Loading then Success</span>
        whenever(mockSearchImageUseCase.invoke(<span class="hljs-string">"tigers"</span>)).thenReturn(
            flowOf (
                Resource.Loading(),
                Resource.Success(fakeResults)
            )
        )

        <span class="hljs-comment">//collect emissions from ViewModel.searchState</span>
        viewModel.searchState.test {
            viewModel.searchImage(<span class="hljs-string">"tigers"</span>)

            <span class="hljs-comment">//1. first emission : initial state</span>
            awaitItem() <span class="hljs-comment">//initial {isLoading = false}</span>

            <span class="hljs-comment">//2. loading state</span>
            <span class="hljs-keyword">val</span> loading = awaitItem()
            assertThat(loading.isLoading).isTrue()

            <span class="hljs-comment">//success state</span>
            <span class="hljs-keyword">val</span> success = awaitItem()
            assertThat(success.isLoading).isFalse()
            assertThat(success.searchResults).isEqualTo(fakeResults.map{it.previewURL})
            assertThat(success.errorMessage).isNull()

            cancelAndIgnoreRemainingEvents()

        }
    }


    <span class="hljs-comment">// --------------------------------------------------------------</span>
    <span class="hljs-comment">// TEST 3 — SEARCH IMAGE ERROR</span>
    <span class="hljs-comment">// --------------------------------------------------------------</span>

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> `searchImage emits error updates errorMessage`<span class="hljs-params">()</span></span> = runTest {

    }

    <span class="hljs-comment">// --------------------------------------------------------------</span>
    <span class="hljs-comment">// TEST 4 — INITIAL STATES</span>
    <span class="hljs-comment">// --------------------------------------------------------------</span>

    <span class="hljs-function"><span class="hljs-keyword">fun</span> `initial state should be empty and not loading`<span class="hljs-params">()</span></span> = runTest {
        <span class="hljs-comment">// Use either one — both should behave the same initially</span>
        <span class="hljs-keyword">val</span> state = viewModel.searchState.value

        <span class="hljs-comment">// 1) Check the Search UI state</span>
        assertFalse(state.isLoading)
        assertNull(state.errorMessage)
        assertTrue(state.searchResults.isEmpty())

        <span class="hljs-comment">// 2) Check general states from ViewModel</span>
        assertTrue(viewModel.arts.value.isEmpty())
        assertTrue(viewModel.searchResults.value.<span class="hljs-keyword">data</span>?.isEmpty() == <span class="hljs-literal">true</span>)
        assertFalse(viewModel.isLoading.value)
        assertNull(viewModel.errorMessage.value)
    }
}
</code></pre>
<pre><code class="lang-kotlin"><span class="hljs-comment">/**
 * ## ImageSearchScreenTest
 *
 * UI-level tests for [ImageSearchScreen].
 *
 * These tests verify:
 * - UI reacts correctly to state (`isLoading`, `images`)
 * - User interactions trigger expected callbacks
 * - Proper UI components are shown or hidden
 *
 * Uses Jetpack Compose testing framework.
 */</span>
<span class="hljs-meta">@RunWith(AndroidJUnit4::class)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ImageSearchScreenTest</span> </span>{

    <span class="hljs-meta">@get:Rule</span>
    <span class="hljs-keyword">val</span> composeTestRule = createComposeRule()

    <span class="hljs-comment">/**
     * Test 1: Verify that the basic components
     * (text field + search icon) are displayed initially.
     */</span>

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">initialScreen_displaysSearchFieldAndButton</span><span class="hljs-params">()</span></span> {
        composeTestRule.setContent {
            ImageSearchScreen(
                isLoading = <span class="hljs-literal">false</span>,
                images = emptyList(),
                onSearch = {},
                onImageClick = {}
            )
        }

        composeTestRule.onNodeWithText(<span class="hljs-string">"Search Image"</span>).assertIsDisplayed()
        composeTestRule.onNodeWithContentDescription(<span class="hljs-string">"Search"</span>).assertIsDisplayed()
    }


    <span class="hljs-comment">/**
     * Test 2: When `isLoading` is true, the loading indicator
     * should be visible and no images should be rendered.
     */</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">whenLoading_showsCircularProgressIndicator</span><span class="hljs-params">()</span></span> {
        composeTestRule.setContent {
            ImageSearchScreen(
                isLoading = <span class="hljs-literal">true</span>,
                images = emptyList(),
                onSearch = {},
                onImageClick = {}
            )
        }

        composeTestRule.onNode(isDialog()).assertDoesNotExist() <span class="hljs-comment">// sanity</span>
        composeTestRule.onNode(isRoot()).printToLog(<span class="hljs-string">"DEBUG"</span>)
        composeTestRule.onNode(hasTestTag(<span class="hljs-string">"ProgressIndicator"</span>)).assertDoesNotExist()
        composeTestRule.onNode(hasTestTag(<span class="hljs-string">"ImageGrid"</span>)).assertDoesNotExist()

        <span class="hljs-comment">// You can optionally tag CircularProgressIndicator</span>
        <span class="hljs-comment">// in your composable if you want more robust test hooks.</span>
    }

    <span class="hljs-comment">/**
     * Test 3: When there are images, they appear in the LazyVerticalGrid.
     */</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">whenImagesAvailable_displaysGridItems</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">val</span> fakeImages = listOf(<span class="hljs-string">"url1"</span>, <span class="hljs-string">"url2"</span>, <span class="hljs-string">"url3"</span>)

        composeTestRule.setContent {
            ImageSearchScreen(
                isLoading = <span class="hljs-literal">false</span>,
                images = fakeImages,
                onSearch = {},
                onImageClick = {}
            )
        }

        <span class="hljs-comment">// Assert that multiple images are displayed</span>
        fakeImages.forEach { _ -&gt;
            composeTestRule.onAllNodes(hasContentDescription(<span class="hljs-string">"Search result image"</span>))
                .assertCountEquals(fakeImages.size)
        }
    }


    <span class="hljs-comment">/**
     * Test 4: Verify search callback is triggered when user enters valid query
     * and taps search icon.
     */</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">enteringValidQuery_triggersOnSearchCallback</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">var</span> searchTriggered = <span class="hljs-literal">false</span>
        <span class="hljs-keyword">var</span> queryReceived: String? = <span class="hljs-literal">null</span>

        composeTestRule.setContent {
            ImageSearchScreen(
                isLoading = <span class="hljs-literal">false</span>,
                images = emptyList(),
                onSearch = {
                    searchTriggered = <span class="hljs-literal">true</span>
                    queryReceived = it
                },
                onImageClick = {}
            )
        }

        <span class="hljs-comment">// Type a valid query</span>
        composeTestRule.onNodeWithText(<span class="hljs-string">"Search Image"</span>).performTextInput(<span class="hljs-string">"mountain"</span>)

        <span class="hljs-comment">// Tap search icon</span>
        composeTestRule.onNodeWithContentDescription(<span class="hljs-string">"Search"</span>).performClick()

        <span class="hljs-comment">// Assert callback invoked</span>
        assert(searchTriggered)
        assert(queryReceived == <span class="hljs-string">"mountain"</span>)
    }

    <span class="hljs-comment">/**
     * Test 5: Invalid query (&lt;3 characters) does not trigger search callback.
     */</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">enteringShortQuery_doesNotTriggerSearch</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">var</span> searchTriggered = <span class="hljs-literal">false</span>

        composeTestRule.setContent {
            ImageSearchScreen(
                isLoading = <span class="hljs-literal">false</span>,
                images = emptyList(),
                onSearch = { searchTriggered = <span class="hljs-literal">true</span> },
                onImageClick = {}
            )
        }

        <span class="hljs-comment">// Type invalid short query</span>
        composeTestRule.onNodeWithText(<span class="hljs-string">"Search Image"</span>).performTextInput(<span class="hljs-string">"ab"</span>)

        <span class="hljs-comment">// Click the search icon</span>
        composeTestRule.onNodeWithContentDescription(<span class="hljs-string">"Search"</span>).performClick()

        <span class="hljs-comment">// Callback should not be triggered</span>
        assert(!searchTriggered)
    }

    <span class="hljs-comment">/**
     * Test 6: Verify that clicking an image triggers `onImageClick` with correct URL.
     */</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">clickingImage_triggersImageClickCallback</span><span class="hljs-params">()</span></span> {
        <span class="hljs-keyword">val</span> fakeImages = listOf(<span class="hljs-string">"url1"</span>, <span class="hljs-string">"url2"</span>)
        <span class="hljs-keyword">var</span> clickedUrl: String? = <span class="hljs-literal">null</span>

        composeTestRule.setContent {
            ImageSearchScreen(
                isLoading = <span class="hljs-literal">false</span>,
                images = fakeImages,
                onSearch = {},
                onImageClick = { clickedUrl = it }
            )
        }

        <span class="hljs-comment">// Simulate image click</span>
        composeTestRule.onAllNodes(hasContentDescription(<span class="hljs-string">"Search result image"</span>))
            .onFirst()
            .performClick()

        assert(clickedUrl == <span class="hljs-string">"url1"</span>)
    }
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Chapter 18 - Coding the Composable UI for the SearchImageUseCase]]></title><description><![CDATA[Look at the LazyVerticalGrid in the snippet below which holds the images received from viewmodel.
@Composable
fun ImageSearchScreen(
    isLoading: Boolean,
    images: List<String>,
    onSearch: (String) -> Unit,
    onImageClick: (String) -> Unit
...]]></description><link>https://androidavatar.hashnode.dev/chapter-18-coding-the-composable-ui-for-the-searchimageusecase</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-18-coding-the-composable-ui-for-the-searchimageusecase</guid><category><![CDATA[Composable Architecture]]></category><category><![CDATA[composable]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Clean Architecture]]></category><category><![CDATA[MVVM+ Room + Factory + Coroutines in kotlin]]></category><category><![CDATA[MVVM]]></category><category><![CDATA[Android]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Sun, 02 Nov 2025 17:47:04 GMT</pubDate><content:encoded><![CDATA[<p>Look at the LazyVerticalGrid in the snippet below which holds the images received from viewmodel.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ImageSearchScreen</span><span class="hljs-params">(
    isLoading: <span class="hljs-type">Boolean</span>,
    images: <span class="hljs-type">List</span>&lt;<span class="hljs-type">String</span>&gt;,
    onSearch: (<span class="hljs-type">String</span>) -&gt; <span class="hljs-type">Unit</span>,
    onImageClick: (<span class="hljs-type">String</span>) -&gt; <span class="hljs-type">Unit</span>
)</span></span> {
    <span class="hljs-keyword">var</span> query <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-string">""</span>) }
    <span class="hljs-keyword">val</span> context = LocalContext.current
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(<span class="hljs-number">16</span>.dp)
    ) {
        OutlinedTextField(
            value = query,
            onValueChange = { query = it },
            label = { Text(<span class="hljs-string">"Search Image"</span>) },
            modifier = Modifier.fillMaxWidth(),
            trailingIcon = {
                IconButton(
                    onClick = {  <span class="hljs-keyword">val</span> trimmedQuery = query.trim()
                        <span class="hljs-keyword">if</span> (trimmedQuery.length &lt; <span class="hljs-number">3</span>) {
                            Toast.makeText( context, <span class="hljs-string">"Please enter at least 3 characters"</span>, Toast.LENGTH_SHORT).show()
                        } <span class="hljs-keyword">else</span> {
                            onSearch(trimmedQuery)
                        } },
                    enabled = query.isNotBlank()
                ) {
                    Icon(Icons.Default.Search, contentDescription = <span class="hljs-string">"Search"</span>)
                }
            }
        )

        Spacer(modifier = Modifier.height(<span class="hljs-number">16</span>.dp))

        <span class="hljs-keyword">if</span> (isLoading) {
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                CircularProgressIndicator()
            }
        } <span class="hljs-keyword">else</span> {
            LazyVerticalGrid(
                columns = GridCells.Fixed(<span class="hljs-number">2</span>),
                modifier = Modifier.fillMaxSize(),
                contentPadding = PaddingValues(<span class="hljs-number">8</span>.dp)
            ) {
                items(images) { url -&gt;
                    ImageItem(url = url, onClick = { onImageClick(url) })
                }
            }
        }
    }
}

<span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ImageItem</span><span class="hljs-params">(url: <span class="hljs-type">String</span>?, onClick: () -&gt; <span class="hljs-type">Unit</span>)</span></span> {
    Card(
        modifier = Modifier
            .padding(<span class="hljs-number">8</span>.dp)
            .fillMaxWidth()
            .clickable { onClick() },
        elevation = CardDefaults.cardElevation(<span class="hljs-number">4</span>.dp)
    ) {
        Image(
            painter = rememberAsyncImagePainter(
                model = url.takeIf { !it.isNullOrEmpty() } ?: R.drawable.ic_launcher_background
            ),
            contentDescription = <span class="hljs-string">"Search result image"</span>,
            contentScale = ContentScale.Crop,
            modifier = Modifier
                .height(<span class="hljs-number">150</span>.dp)
                .fillMaxWidth()
        )
    }
}
</code></pre>
<p>We populate the MainActivity directly with the ImageSearchScreen to check whether everything is okay.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@AndroidEntryPoint</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainActivity</span> : <span class="hljs-type">ComponentActivity</span></span>() {

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> viewModel: ArtViewModel <span class="hljs-keyword">by</span> viewModels()

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCreate</span><span class="hljs-params">(savedInstanceState: <span class="hljs-type">Bundle</span>?)</span></span> {
        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState)
<span class="hljs-comment">//        setContent {</span>
<span class="hljs-comment">//            val viewModel: ArtViewModel = hiltViewModel()</span>
<span class="hljs-comment">//            ArtListScreen(viewModel)</span>
<span class="hljs-comment">//        }</span>

        setContent {
            ArtLearnersAppTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    <span class="hljs-comment">// Collect UI states from the ViewModel</span>
                    <span class="hljs-keyword">val</span> searchState <span class="hljs-keyword">by</span> viewModel.searchState.collectAsState()
                    <span class="hljs-keyword">val</span> results = searchState.searchResults
                    <span class="hljs-keyword">val</span> loading = searchState.isLoading
                    ImageSearchScreen(
                        isLoading = loading,
                        images = searchState.searchResults,  <span class="hljs-comment">// this is already List&lt;String&gt;</span>
                        onSearch = { query -&gt;
                            viewModel.searchImage(query)
                        },
                        onImageClick = { selectedUrl -&gt;
                            <span class="hljs-comment">// Handle selected image click (navigate to another screen)</span>
                        }
                    )
                }
            }
        }
    }
}
</code></pre>
<p>Run the code on an android device. In the search Input field, try various queries like “rain”, “lion” etc.</p>
]]></content:encoded></item><item><title><![CDATA[Chapter 17 - Coding the ViewModel for the SearchImageUseCase]]></title><description><![CDATA[data class SearchImageUiState(
    val isLoading: Boolean = false,
    val errorMessage: String? = null,
    val searchResults: List<String> = emptyList()
)

/**
 * ## ArtViewModel (Simplified Version)
 *
 * This ViewModel is responsible for fetching...]]></description><link>https://androidavatar.hashnode.dev/chapter-17-coding-the-viewmodel-for-the-searchimageusecase</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-17-coding-the-viewmodel-for-the-searchimageusecase</guid><category><![CDATA[Usecase]]></category><category><![CDATA[Android]]></category><category><![CDATA[Clean Architecture]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[hilt ]]></category><category><![CDATA[dependency injection]]></category><category><![CDATA[ViewModel]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Sun, 02 Nov 2025 17:24:42 GMT</pubDate><content:encoded><![CDATA[<pre><code class="lang-kotlin"><span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SearchImageUiState</span></span>(
    <span class="hljs-keyword">val</span> isLoading: <span class="hljs-built_in">Boolean</span> = <span class="hljs-literal">false</span>,
    <span class="hljs-keyword">val</span> errorMessage: String? = <span class="hljs-literal">null</span>,
    <span class="hljs-keyword">val</span> searchResults: List&lt;String&gt; = emptyList()
)

<span class="hljs-comment">/**
 * ## ArtViewModel (Simplified Version)
 *
 * This ViewModel is responsible for fetching and exposing a list of artworks to the UI.
 *
 * - Uses [GetArtUseCase] from the Domain layer to retrieve data.
 * - Does **not** use Hilt yet (constructor injection only → easy testing).
 * - Exposes reactive [StateFlow]s for UI to observe in a Compose screen.
 *
 * ### Responsibilities:
 * - Request artwork list from domain layer (via use case).
 * - Maintain UI state: data, loading indicator, and error message.
 * - Provide immutable `StateFlow` properties for composables to observe.
 */</span>
<span class="hljs-meta">@HiltViewModel</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ArtViewModel</span> <span class="hljs-meta">@Inject</span> <span class="hljs-keyword">constructor</span></span>(
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> useCases: ArtUseCases
) : ViewModel() {

    <span class="hljs-comment">/**
     * A **mutable** StateFlow holding the current list of artworks.
     * This is **private** so that only the ViewModel can modify it.
     *
     * Initially empty; updated when Room emits new data through the use case.
     */</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _arts = MutableStateFlow&lt;List&lt;Art&gt;&gt;(emptyList())


    <span class="hljs-comment">/**
     * A **mutable** StateFlow holding the current list of ImageResult objects from the Search query.
     * This is **private** so that only the ViewModel can modify it.
     *
     * Initially empty; updated when API fetches new data through the use case.
     */</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _searchResults = MutableStateFlow&lt;Resource&lt;List&lt;ImageResult&gt;&gt;&gt;(Resource.success(emptyList()))



    <span class="hljs-comment">/**
     * A **read-only** version of [_arts] exposed to the UI.
     * Compose collects this to display the current artwork list.
     *
     * &gt; `StateFlow` is a hot flow — it always has a current value
     *   and re-emits it to new collectors (like Compose recompositions).
     */</span>
    <span class="hljs-keyword">val</span> arts: StateFlow&lt;List&lt;Art&gt;&gt; = _arts.asStateFlow()

    <span class="hljs-comment">/**
     * A **read-only** version of [_searchResults] exposed to the UI.
     * Compose collects this to display the current search results as a list.
     *
     * &gt; `StateFlow` is a hot flow — it always has a current value
     *   and re-emits it to new collectors (like Compose recompositions).
     */</span>
    <span class="hljs-keyword">val</span> searchResults : StateFlow&lt;Resource&lt;List&lt;ImageResult&gt;&gt;&gt; = _searchResults.asStateFlow()

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _state = MutableStateFlow(SearchImageUiState())
    <span class="hljs-keyword">val</span> searchState: StateFlow&lt;SearchImageUiState&gt; = _state.asStateFlow()

    <span class="hljs-comment">/**
     * Indicates whether the app is currently fetching data.
     *
     * Mutable version (internal to the ViewModel).
     */</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _isLoading = MutableStateFlow(<span class="hljs-literal">false</span>)

    <span class="hljs-comment">/**
     * Public, read-only version of [_isLoading] for the UI.
     *
     * The Compose layer can observe this to show/hide progress indicators.
     */</span>
    <span class="hljs-keyword">val</span> isLoading: StateFlow&lt;<span class="hljs-built_in">Boolean</span>&gt; = _isLoading.asStateFlow()

    <span class="hljs-comment">/**
     * Holds the latest error message, if any.
     *
     * `null` means no error.
     */</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _errorMessage = MutableStateFlow&lt;String?&gt;(<span class="hljs-literal">null</span>)

    <span class="hljs-comment">/**
     * Public, read-only flow exposing error messages.
     */</span>
    <span class="hljs-keyword">val</span> errorMessage: StateFlow&lt;String?&gt; = _errorMessage.asStateFlow()

    <span class="hljs-comment">/**
     * When ViewModel is created, it automatically loads artworks.
     * This is done inside [init] to trigger the use case once at startup.
     */</span>
    <span class="hljs-keyword">init</span> {
        loadArts()
    }

    <span class="hljs-comment">/**
     * Invokes [GetArtUseCase] and collects the emitted Flow&lt;List&lt;Art&gt;&gt;.
     *
     * ### Flow explanation:
     * - The `GetArtUseCase()` returns a `Flow&lt;List&lt;Art&gt;&gt;`.
     * - A **Flow** is a cold asynchronous data stream — it emits values
     *   (here, lists of Art) over time.
     * - Room emits updates via Flow whenever the DB changes.
     *
     * ### Steps:
     * 1. Set loading state to `true`.
     * 2. Collect emitted art lists from use case.
     * 3. Update `_arts` StateFlow (Compose UI reacts automatically).
     * 4. Handle exceptions by updating `_errorMessage`.
     * 5. Set loading back to `false` when done.
     */</span>
    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">loadArts</span><span class="hljs-params">()</span></span> {
        viewModelScope.launch {
            _isLoading.value = <span class="hljs-literal">true</span>
            <span class="hljs-keyword">try</span> {
                <span class="hljs-comment">// Collects flow from use case</span>
                useCases.getArtUseCase().collect { artList -&gt;
                    _arts.value = artList
                }
            } <span class="hljs-keyword">catch</span> (e: Exception) {
                _errorMessage.value = e.message
            } <span class="hljs-keyword">finally</span> {
                _isLoading.value = <span class="hljs-literal">false</span>
            }
        }
    }

    <span class="hljs-comment">/**
     * Searches for images from the Pixabay API based on the provided query string.
     *
     * <span class="hljs-doctag">@param</span> query The search keyword used to fetch matching images.
     *
     * This function calls [SearchImageUseCase] and collects a [Resource] flow:
     * - [Resource.Loading] → sets `isLoading = true`
     * - [Resource.Success] → updates `searchResults` with image URLs
     * - [Resource.Error] → updates `error` with the failure message
     */</span>

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">searchImage</span><span class="hljs-params">(query: <span class="hljs-type">String</span>)</span></span> {
        viewModelScope.launch {
            useCases.searchImageUseCase(query).collect { result -&gt;
                <span class="hljs-keyword">when</span> (result) {
                    <span class="hljs-keyword">is</span> Resource.Error -&gt; {
                        _state.update {
                            it.copy(isLoading = <span class="hljs-literal">false</span>, errorMessage = result.message)
                        }

                    }

                    <span class="hljs-keyword">is</span> Resource.Loading -&gt; {
                        _state.update { it.copy(isLoading = <span class="hljs-literal">true</span>) }
                    }

                    <span class="hljs-keyword">is</span> Resource.Success -&gt; {
                        _state.update {
                            it.copy(
                                isLoading = <span class="hljs-literal">false</span>,
                                searchResults = result.<span class="hljs-keyword">data</span>?.map { img -&gt; img.previewURL }
                                    ?: emptyList())
                        }
                    }
                }

            }
        }
    }
}
<span class="hljs-comment">/**
 * NOTES:
 * - ViewModel follows Clean Architecture by using Domain-layer UseCases.
 * - Hilt injects dependencies automatically (ArtUseCases).
 * - Room data is handled by GetArtUseCase via Flow&lt;List&lt;Art&gt;&gt;.
 * - Pixabay API data is handled by SearchImageUseCase via Flow&lt;Resource&lt;List&lt;ImageResult&gt;&gt;&gt;.
 * - Maintains separate UI state flows for Art list and Image search.
 * - Compose screens observe immutable StateFlow values to auto-recompose on state change.
 * - Future enhancement: merge all UI data into a unified UiState for cleaner testing.
 */</span>
</code></pre>
<p>We are focused on the searchImage function this for the SearchImageUseCase.</p>
<p>The SearchImageUseCase delegates the API call for search query to the Repository. It wraps the API response into Resource. SearchImageUiState has three possible states Error,Success and Loading. If the API succeeds to fetch the Images correctly then the List of Image strings is updated in the variable searchResults. This variable is used by the composable UI to populate the LazyColumn.</p>
<p>We code the composable UI for this use case in the next chapter.</p>
]]></content:encoded></item><item><title><![CDATA[Chapter 16 - Coding the SearchImageUseCase (Data Layer)]]></title><description><![CDATA[We have already discussed about the DTO classes and the mappers required for the Image Response API.
Please checkout chapter 6 to recap the code.
The ImageResultDto and ImageResponseDto data classes showcase our DTO. The DTO to Domain layer mapper fu...]]></description><link>https://androidavatar.hashnode.dev/chapter-16-coding-the-searchimageusecase-data-layer</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-16-coding-the-searchimageusecase-data-layer</guid><category><![CDATA[pixabay]]></category><category><![CDATA[remote api]]></category><category><![CDATA[data]]></category><category><![CDATA[data-layer]]></category><category><![CDATA[dto]]></category><category><![CDATA[use-cases]]></category><category><![CDATA[hilt ]]></category><category><![CDATA[Retrofit]]></category><category><![CDATA[injection]]></category><category><![CDATA[dependency injection]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Sun, 02 Nov 2025 11:48:05 GMT</pubDate><content:encoded><![CDATA[<p>We have already discussed about the DTO classes and the mappers required for the Image Response API.</p>
<p>Please checkout <a target="_blank" href="https://androidavatar.hashnode.dev/chapter-6-coding-the-first-use-case-data-layer">chapter 6</a> to recap the code.</p>
<p>The <strong>ImageResultDto and ImageResponseDto</strong> data classes showcase our DTO. The DTO to Domain layer mapper functions produces the ImageResult objects that satisfy the SearchImageUseCase.</p>
<p>Now we come to the important part - API Service.</p>
<h3 id="heading-imageapi"><strong>ImageAPI</strong></h3>
<pre><code class="lang-kotlin"><span class="hljs-keyword">package</span> com.learning.artsnapapp.<span class="hljs-keyword">data</span>.remote.api

<span class="hljs-keyword">import</span> com.learning.artsnapapp.<span class="hljs-keyword">data</span>.remote.dto.ImageResponseDto
<span class="hljs-keyword">import</span> retrofit2.Response
<span class="hljs-keyword">import</span> retrofit2.http.GET
<span class="hljs-keyword">import</span> retrofit2.http.Query

<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ImageApi</span> </span>{

    <span class="hljs-meta">@GET(<span class="hljs-meta-string">"."</span>)</span>
    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">searchImages</span><span class="hljs-params">(
        <span class="hljs-meta">@Query(<span class="hljs-meta-string">"key"</span>)</span> apiKey:<span class="hljs-type">String</span>,
        <span class="hljs-meta">@Query(<span class="hljs-meta-string">"q"</span>)</span> searchQuery: <span class="hljs-type">String</span>,
    )</span></span> : Response&lt;ImageResponseDto&gt;
}
</code></pre>
<p>The ImageAPI interface defines the searchImages function which takes the user Query as the parameter. The second parameter is the API key used for Pixabay. The reader is encouraged to login to the Pixabay website and obtain the API Key.</p>
<p>The searchImages() function will now be implemented in the ArtRepositoryImpl class. We already have the code below from <a target="_blank" href="https://androidavatar.hashnode.dev/chapter-7-coding-the-repository-implementation-class">chapter 7</a>. This class used the ArtDao object to fetch records from the Room DB. This class had three more functions that were not implemented. Now is the time to jump in and implement the searchImage() function. To implement the searchImage function we need to inject the ImageApi and the API key.</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">/**
 * Implementation of [ArtRepositoryInterface].
 *
 * This class acts as a single source of truth for both local (Room) and
 * remote (Pixabay API) data. It maps entities and DTOs to domain models.
 *
 * Dependencies are injected using Hilt:
 * - [ArtDao] for local persistence.
 * - [ImageApi] for remote API access.
 * - `<span class="hljs-doctag">@Named</span>("apiKey")` String for API authentication.
 *
 * The [searchImage] function emits a [Resource] stream, which allows the
 * UI layer to react to loading, success, and error states.
 */</span>

<span class="hljs-meta">@Singleton</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ArtRepositoryImpl</span> <span class="hljs-meta">@Inject</span> <span class="hljs-keyword">constructor</span></span>(
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> artDao: ArtDao,
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> imageApi: ImageApi,
    <span class="hljs-meta">@Named(<span class="hljs-meta-string">"apiKey"</span>)</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> apiKey: String
):ArtRepositoryInterface {
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getArt</span><span class="hljs-params">()</span></span>: Flow&lt;List&lt;Art&gt;&gt; {

        <span class="hljs-comment">//returns a stream (Flow) that emits a list of ArtEntity objects every time the ROOM DB table changes.</span>
        <span class="hljs-keyword">val</span> artStream = artDao.getAllArts()

       <span class="hljs-keyword">val</span> artObjs = artStream.map { entityList -&gt; entityList.map { it.toDomainModel() } }
        <span class="hljs-keyword">return</span> artObjs
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">insertArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {
        TODO(<span class="hljs-string">"Not yet implemented"</span>)
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">deleteArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {
        TODO(<span class="hljs-string">"Not yet implemented"</span>)
    }

     <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">searchImage</span><span class="hljs-params">(imageString: <span class="hljs-type">String</span>)</span></span>: Flow&lt;Resource&lt;List&lt;ImageResult&gt;&gt;&gt; = flow {
        emit(Resource.loading())

        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">val</span> response = imageApi.searchImages(apiKey, imageString)

            <span class="hljs-keyword">if</span> (response.isSuccessful) {
                <span class="hljs-keyword">val</span> body = response.body()
                <span class="hljs-keyword">if</span> (body != <span class="hljs-literal">null</span>) {
                    <span class="hljs-comment">// Map the list of DTOs to domain models</span>
                    <span class="hljs-keyword">val</span> results = body.hits.map {it.toDomainModel()}
                    emit(Resource.success(results))
                } <span class="hljs-keyword">else</span> {
                    emit(Resource.error(<span class="hljs-string">"Empty response body"</span>))
                }
            } <span class="hljs-keyword">else</span> {
                emit(Resource.error(<span class="hljs-string">"API error: <span class="hljs-subst">${response.message()}</span>"</span>))
            }

        } <span class="hljs-keyword">catch</span> (e: Exception) {
            emit(Resource.error(<span class="hljs-string">"Network error: <span class="hljs-subst">${e.message}</span>"</span>))
        }
    }

}
</code></pre>
<p>The ImageApi will be provided by Hilt. So we update the AppModule class present in the “di” package</p>
<p>Also The ArtUseCases will be updated with the SearchImageUseCase.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Module</span>
<span class="hljs-meta">@InstallIn(SingletonComponent::class)</span>
<span class="hljs-keyword">object</span> AppModule {

    <span class="hljs-meta">@Provides</span>
    <span class="hljs-meta">@Singleton</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">provideDatabase</span><span class="hljs-params">(<span class="hljs-meta">@ApplicationContext</span> context: <span class="hljs-type">Context</span>)</span></span>: ArtDatabase = Room.databaseBuilder(
        context,
        ArtDatabase::<span class="hljs-keyword">class</span>.java,
        <span class="hljs-string">"art_db"</span>
    ).build()

    <span class="hljs-meta">@Provides</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">provideArtDao</span><span class="hljs-params">(db: <span class="hljs-type">ArtDatabase</span>)</span></span> : ArtDao = db.artDao()

    <span class="hljs-meta">@Provides</span>
    <span class="hljs-meta">@Singleton</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">provideRetrofitApi</span><span class="hljs-params">()</span></span>:ImageApi = Retrofit.Builder()
        .baseUrl(BuildConfig.BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build().create(ImageApi::<span class="hljs-keyword">class</span>.java)

    @Provides
    @Singleton
    @Named(<span class="hljs-string">"apiKey"</span>)
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">provideImageApiKey</span><span class="hljs-params">()</span></span> : String = BuildConfig.PIXABAY_API_KEY


    <span class="hljs-comment">// --- USE CASES ----------------------------------------------------------</span>
    <span class="hljs-meta">@Provides</span>
    <span class="hljs-meta">@Singleton</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">provideArtUseCases</span><span class="hljs-params">(repository: <span class="hljs-type">ArtRepositoryInterface</span>)</span></span>: ArtUseCases =
        ArtUseCases(
            getArtUseCase = GetArtUseCase(repository),
            searchImageUseCase = SearchImageUseCase(repository)
        )
}

<span class="hljs-comment">/**
 *
 * - The Pixabay API key is stored locally (in local.properties) for security.
 * - The key is injected into BuildConfig at compile-time.
 * - Access it anywhere using BuildConfig.PIXABAY_API_KEY.
 * - Never commit local.properties to version control.
 */</span>
</code></pre>
<p>The app level build.gradle.kts will be updated for the PIXABAY_API_KEY and BASE_URL. The actual PIXABAY_API_KEY will reside in the local.properties.</p>
<p>Ensure you cannot commit this <strong>local.properties</strong> to your GIT remote repository.</p>
<pre><code class="lang-kotlin">PIXABAY_API_KEY=<span class="hljs-number">12345678</span>-abcdefg
</code></pre>
<pre><code class="lang-kotlin"> defaultConfig {
        applicationId = <span class="hljs-string">"com.learning.artsnapapp"</span>
        minSdk = <span class="hljs-number">29</span>
        targetSdk = <span class="hljs-number">36</span>
        versionCode = <span class="hljs-number">1</span>
        versionName = <span class="hljs-string">"1.0"</span>

        testInstrumentationRunner = <span class="hljs-string">"androidx.test.runner.AndroidJUnitRunner"</span>

            buildConfigField(
                <span class="hljs-string">"String"</span>,
                <span class="hljs-string">"PIXABAY_API_KEY"</span>,
                <span class="hljs-string">"\"<span class="hljs-subst">${project.findProperty(<span class="hljs-string">"PIXABAY_API_KEY"</span>) ?: <span class="hljs-string">""</span>}</span>\""</span>
            )
        buildConfigField(
            <span class="hljs-string">"String"</span>,
            <span class="hljs-string">"BASE_URL"</span>,
            <span class="hljs-string">"\"https://pixabay.com/api/\""</span>
        )

    }
</code></pre>
<p>The data layer code is now complete for the SearchImageUseCase</p>
<p>In the next chapter we will update the ArtViewModel class followed by the new Composable for the SearchImageUseCase.</p>
]]></content:encoded></item><item><title><![CDATA[Chapter 15 - Coding the SearchImageUseCase (Domain layer)]]></title><description><![CDATA[Before we start working on the same blueprint we used earlier for the GetArtUseCase, let us pause for a moment and depict the data flow in the form of a flowchart. This will help gauge the progress of the second use case - SearchImageUseCase. This us...]]></description><link>https://androidavatar.hashnode.dev/chapter-15-coding-the-searchimageusecase-domain-layer</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-15-coding-the-searchimageusecase-domain-layer</guid><category><![CDATA[resource]]></category><category><![CDATA[sealed class]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Clean Architecture]]></category><category><![CDATA[domain]]></category><category><![CDATA[Retrofit]]></category><category><![CDATA[business logic]]></category><category><![CDATA[data flow ]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Sun, 02 Nov 2025 09:07:02 GMT</pubDate><content:encoded><![CDATA[<p>Before we start working on the same blueprint we used earlier for the GetArtUseCase, let us pause for a moment and depict the data flow in the form of a flowchart. This will help gauge the progress of the second use case - SearchImageUseCase. This use case revolves around fetching images from the Remote API after user types in a “search” query string inside the search window.</p>
<h3 id="heading-data-flow-diagram">DATA FLOW DIAGRAM</h3>
<pre><code class="lang-mermaid">flowchart TD

%% ===== Domain Layer =====
subgraph Domain["Domain Layer"]
    A1[ArtRepositoryInterface]:::interface
    A2[SearchImageUseCase]:::usecase
    A2 --&gt; A1
end

%% ===== Data Layer =====
subgraph Data["Data Layer"]
    B1[ArtRepositoryImpl]:::repo
    B2[ImageApi Retrofit]:::api
    B3[ImageResultDto / ImageResponse]:::dto
    B4[Mapper: toImageResult]:::mapper

    B1 --&gt; B2
    B2 --&gt; B3
    B3 --&gt; B4
end

%% ===== Hilt =====
subgraph Hilt["Hilt Dependency Injection"]
    C1[AppModule]:::module
    C1 --&gt; B2
    C1 --&gt; B1
end

%% ===== Presentation Layer =====
subgraph Presentation["Presentation Layer"]
    D1[ImageSearchViewModel]:::vm
    D2[ImageSearchScreen Compose]:::ui
    D2 --&gt; D1
    D1 --&gt; A2
end

%% ===== Relationships =====
A1 -.-&gt; B1
C1 --&gt; D1

%% ===== Styling =====
classDef interface fill:#f6d6ad,stroke:#dba45e,stroke-width:1px,color:#000
classDef usecase fill:#d6f5e1,stroke:#2a9d8f,stroke-width:1px,color:#000
classDef repo fill:#ffd6a5,stroke:#f4a261,stroke-width:1px,color:#000
classDef api fill:#f7ede2,stroke:#e76f51,stroke-width:1px,color:#000
classDef dto fill:#e9ecef,stroke:#adb5bd,stroke-width:1px,color:#000
classDef mapper fill:#dee2e6,stroke:#6c757d,stroke-width:1px,color:#000
classDef module fill:#bde0fe,stroke:#4361ee,stroke-width:1px,color:#000
classDef vm fill:#d4a5ff,stroke:#7209b7,stroke-width:1px,color:#000
classDef ui fill:#ffc8dd,stroke:#b5179e,stroke-width:1px,color:#000
</code></pre>
<ul>
<li><p>Domain layer defines business logic and repository contracts.</p>
</li>
<li><p>Data layer implements actual API calls (Retrofit) and maps DTOs → Domain Models.</p>
</li>
<li><p>Hilt provides singletons of API, Repository, and UseCases for dependency injection.</p>
</li>
<li><p>ViewModel consumes UseCases and exposes state to UI via StateFlow.</p>
</li>
<li><p>Composables observe ViewModel state and render UI accordingly.</p>
</li>
<li><p><strong>Clean flow: UI → ViewModel → UseCase → Repository → API → Mapper → Domain Model.</strong></p>
</li>
</ul>
<h3 id="heading-coding-the-domain-model"><strong>Coding the Domain Model</strong></h3>
<p>The ImageResult class</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">package</span> com.learning.artsnapapp.domain.model

<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ImageResult</span> </span>(
    <span class="hljs-keyword">val</span> id: <span class="hljs-built_in">Int</span>,
    <span class="hljs-keyword">val</span> previewURL: String,
    <span class="hljs-keyword">val</span> largeImageURL: String
)
</code></pre>
<h3 id="heading-the-repository-interface">The Repository Interface</h3>
<p>Let us check the repository interface that we used for our first use case.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">package</span> com.learning.artsnapapp.domain.repository

<span class="hljs-keyword">import</span> com.learning.artsnapapp.domain.model.Art
<span class="hljs-keyword">import</span> com.learning.artsnapapp.domain.model.ImageResult
<span class="hljs-keyword">import</span> com.learning.artsnapapp.util.Resource
<span class="hljs-keyword">import</span> kotlinx.coroutines.flow.Flow

<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ArtRepositoryInterface</span> </span>{

    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">insertArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span>
    <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">deleteArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getArt</span><span class="hljs-params">()</span></span>: Flow&lt;List&lt;Art&gt;&gt;
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">searchImage</span><span class="hljs-params">(imageString: <span class="hljs-type">String</span>)</span></span>: Flow&lt;Resource&lt;List&lt;ImageResult&gt;&gt;&gt;
}

<span class="hljs-comment">/**
 The domain layer defines what data is needed, not how it is fetched.
 The data layer will implement this interface.
 * */</span>
</code></pre>
<p>We already defined the <strong><em>searchImage</em></strong> function that will return Flow of the List of ImageResult objects. This is similar to the <strong><em>getArt</em></strong> function returning the List of Art objects that we have already worked on in the previous chapters.</p>
<p>We are walking on the already trodden path and things will be simpler to understand this time around. The power of clarity that the Clean Architecture provides will be seen here. We have already worked on the Hilt dependency injection and hence this time we will re-use what is already done to build this SearchImageUseCase functionality quickly.</p>
<p>Here is the use case code</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">package</span> com.learning.artsnapapp.domain.usecase

<span class="hljs-keyword">import</span> com.learning.artsnapapp.domain.model.ImageResult
<span class="hljs-keyword">import</span> com.learning.artsnapapp.domain.repository.ArtRepositoryInterface
<span class="hljs-keyword">import</span> com.learning.artsnapapp.util.Resource
<span class="hljs-keyword">import</span> kotlinx.coroutines.flow.Flow

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SearchImageUseCase</span></span>(<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> imageRepository: ArtRepositoryInterface)
{

    <span class="hljs-keyword">operator</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">invoke</span><span class="hljs-params">(searchQuery:<span class="hljs-type">String</span>)</span></span> : Flow&lt;Resource&lt;List&lt;ImageResult&gt;&gt;&gt; {
        <span class="hljs-keyword">return</span> imageRepository.searchImage(searchQuery)
    }

}

<span class="hljs-comment">//Purpose: Search for artwork images online using the external remote API.</span>
<span class="hljs-comment">//Domain logic: Wrap API responses into Resource and map DTO → domain model.</span>
</code></pre>
<p>We need to understand the Resource utility class. There is a separate package “util” to handle utility classes.</p>
<h3 id="heading-the-resource-class">The RESOURCE class</h3>
<pre><code class="lang-kotlin"><span class="hljs-keyword">package</span> com.learning.artsnapapp.util

<span class="hljs-keyword">sealed</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Resource</span>&lt;<span class="hljs-type">T</span>&gt;</span>(
    <span class="hljs-keyword">val</span> <span class="hljs-keyword">data</span>: T? = <span class="hljs-literal">null</span>,
    <span class="hljs-keyword">val</span> message: String? = <span class="hljs-literal">null</span>
) {
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Success</span>&lt;<span class="hljs-type">T</span>&gt;</span>(<span class="hljs-keyword">data</span>: T) : Resource&lt;T&gt;(<span class="hljs-keyword">data</span>)
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Error</span>&lt;<span class="hljs-type">T</span>&gt;</span>(message: String, <span class="hljs-keyword">data</span>: T? = <span class="hljs-literal">null</span>) : Resource&lt;T&gt;(<span class="hljs-keyword">data</span>, message)
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Loading</span>&lt;<span class="hljs-type">T</span>&gt;</span>(<span class="hljs-keyword">data</span>: T? = <span class="hljs-literal">null</span>) : Resource&lt;T&gt;(<span class="hljs-keyword">data</span>)

    <span class="hljs-keyword">companion</span> <span class="hljs-keyword">object</span> {
        <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-type">&lt;T&gt;</span> <span class="hljs-title">success</span><span class="hljs-params">(<span class="hljs-keyword">data</span>: <span class="hljs-type">T</span>)</span></span>: Resource&lt;T&gt; = Success(<span class="hljs-keyword">data</span>)
        <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-type">&lt;T&gt;</span> <span class="hljs-title">error</span><span class="hljs-params">(msg: <span class="hljs-type">String</span>, <span class="hljs-keyword">data</span>: <span class="hljs-type">T</span>? = <span class="hljs-literal">null</span>)</span></span>: Resource&lt;T&gt; = Error(msg, <span class="hljs-keyword">data</span>)
        <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-type">&lt;T&gt;</span> <span class="hljs-title">loading</span><span class="hljs-params">(<span class="hljs-keyword">data</span>: <span class="hljs-type">T</span>? = <span class="hljs-literal">null</span>)</span></span>: Resource&lt;T&gt; = Loading(<span class="hljs-keyword">data</span>)
    }
}
</code></pre>
<p><strong>Resource - A wrapper class that represents Loading, Success, or Error for any data type. To make UI and ViewModel communication cleaner and handle async states elegantly.</strong></p>
<ul>
<li><p>Resource is not about data, it’s about data state.</p>
</li>
<li><p>It helps ViewModels and Composables talk in the same structured language</p>
</li>
<li><p>Benefits - Predictable state management, no null-check hell, smoother Compose UI updates.</p>
</li>
</ul>
<h3 id="heading-kotlin-sealed-class-and-benefits"><strong>KOTLIN SEALED CLASS AND BENEFITS</strong></h3>
<p><strong><em>A sealed class in Kotlin is a restricted class hierarchy — it allows you to define a fixed set of subclasses that represent different variants of a concept.</em></strong></p>
<p><strong><em>You can think of it like a closed family of related classes. Only the types you declare inside it are allowed to exist — nothing else can extend it.</em></strong></p>
<p>Why Use Sealed Classes?</p>
<ul>
<li><p>Type-safe state management You can handle different outcomes (like Success, Error, Loading) clearly.</p>
</li>
<li><p>No magic numbers or enums Instead of using strings or codes (“STATUS=200”), each state is an actual class.</p>
</li>
<li><p>Smart compiler help If you forget to handle a state in a when block, the compiler warns you</p>
</li>
</ul>
<p>In the next chapter, we will code the data layer for the SearchImageUseCase.</p>
]]></content:encoded></item><item><title><![CDATA[Chapter 14 - Adding Hilt Dependency Injection]]></title><description><![CDATA[Now we will remove the fake repository that we used in the ViewModel and the composable. The MainActivity also took support of the fake repository to show us that the Domain and Data layers are healthy.
Let us now introduce Hilt Dependency Injection....]]></description><link>https://androidavatar.hashnode.dev/chapter-14-adding-hilt-dependency-injection</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-14-adding-hilt-dependency-injection</guid><category><![CDATA[hilt ]]></category><category><![CDATA[dependency injection]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Android]]></category><category><![CDATA[Clean Architecture]]></category><category><![CDATA[MVVMArchitecture]]></category><category><![CDATA[MVVM+ Room + Factory + Coroutines in kotlin]]></category><category><![CDATA[room database]]></category><category><![CDATA[injection]]></category><category><![CDATA[AndroidManifest]]></category><category><![CDATA[UseCase Android]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Sat, 01 Nov 2025 20:37:34 GMT</pubDate><content:encoded><![CDATA[<p>Now we will remove the fake repository that we used in the ViewModel and the composable. The MainActivity also took support of the fake repository to show us that the Domain and Data layers are healthy.</p>
<p>Let us now introduce Hilt Dependency Injection. Now, if something breaks, then we know that the Hilt code has something erroneous.</p>
<h3 id="heading-steps-to-inject-hilt">STEPS TO INJECT HILT</h3>
<ul>
<li><p>@HiltAndroidApp → Application class</p>
</li>
<li><p>@Module @InstallIn(SingletonComponent::class) → Provide repository + use cases</p>
</li>
<li><p>@HiltViewModel → Automatically inject ArtUseCases into your ViewModel</p>
</li>
</ul>
<p>The code snippets are shown below</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">package</span> com.learning.artsnapapp

<span class="hljs-keyword">import</span> android.app.Application
<span class="hljs-keyword">import</span> dagger.hilt.android.HiltAndroidApp

<span class="hljs-comment">/**
 * ## ArtApplication
 *
 * This is the root of the dependency graph.
 * Hilt automatically generates the components for the app.
 */</span>
<span class="hljs-meta">@HiltAndroidApp</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ArtApplication</span> : <span class="hljs-type">Application</span></span>() {
}

<span class="hljs-comment">/**

 * • Must be declared in `AndroidManifest.xml` under `&lt;application android:name="..."&gt;`.  
 * • Required once — this bootstraps Hilt globally.  
 * • All other injections depend on this root setup.  
 */</span>
</code></pre>
<p>This application class should be named in the AndroidManifest.xml</p>
<pre><code class="lang-kotlin">&lt;?xml version=<span class="hljs-string">"1.0"</span> encoding=<span class="hljs-string">"utf-8"</span>?&gt;
&lt;manifest xmlns:android=<span class="hljs-string">"http://schemas.android.com/apk/res/android"</span>
    xmlns:tools=<span class="hljs-string">"http://schemas.android.com/tools"</span>&gt;

    &lt;application
        android:name=<span class="hljs-string">"com.learning.artsnapapp.ArtApplication"</span>
        android:allowBackup=<span class="hljs-string">"true"</span>
        android:dataExtractionRules=<span class="hljs-string">"@xml/data_extraction_rules"</span>
        android:fullBackupContent=<span class="hljs-string">"@xml/backup_rules"</span>
        android:icon=<span class="hljs-string">"@mipmap/ic_launcher"</span>
        android:label=<span class="hljs-string">"@string/app_name"</span>
        android:roundIcon=<span class="hljs-string">"@mipmap/ic_launcher_round"</span>
        android:supportsRtl=<span class="hljs-string">"true"</span>
        android:theme=<span class="hljs-string">"@style/Theme.ArtLearnersApp"</span>
        &gt;
        &lt;activity
            android:name=<span class="hljs-string">"com.learning.artsnapapp.MainActivity"</span>
            android:exported=<span class="hljs-string">"true"</span>
            android:label=<span class="hljs-string">"@string/app_name"</span>
            android:theme=<span class="hljs-string">"@style/Theme.ArtLearnersApp"</span>
           &gt;
            &lt;intent-filter&gt;
                &lt;action android:name=<span class="hljs-string">"android.intent.action.MAIN"</span> /&gt;
                &lt;category android:name=<span class="hljs-string">"android.intent.category.LAUNCHER"</span> /&gt;
            &lt;/intent-filter&gt;
        &lt;/activity&gt;
    &lt;/application&gt;

&lt;/manifest&gt;
</code></pre>
<p>The MainActivity is without fake repository now. Real dependencies wired via Hilt.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">import</span> android.os.Bundle
<span class="hljs-keyword">import</span> androidx.activity.ComponentActivity
<span class="hljs-keyword">import</span> androidx.activity.compose.setContent
<span class="hljs-keyword">import</span> androidx.activity.viewModels

<span class="hljs-keyword">import</span> com.learning.artsnapapp.presentation.artlist.ArtListScreen
<span class="hljs-keyword">import</span> com.learning.artsnapapp.presentation.viewmodel.ArtViewModel
<span class="hljs-keyword">import</span> com.learning.artsnapapp.ui.theme.ArtLearnersAppTheme
<span class="hljs-keyword">import</span> androidx.compose.runtime.collectAsState
<span class="hljs-keyword">import</span> dagger.hilt.android.AndroidEntryPoint

<span class="hljs-comment">/**
 * ## MainActivity
 *
 * Entry activity that hosts composables and enables Hilt injection.
 */</span>
<span class="hljs-meta">@AndroidEntryPoint</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainActivity</span> : <span class="hljs-type">ComponentActivity</span></span>() {

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> viewModel: ArtViewModel <span class="hljs-keyword">by</span> viewModels()

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCreate</span><span class="hljs-params">(savedInstanceState: <span class="hljs-type">Bundle</span>?)</span></span> {
        <span class="hljs-keyword">super</span>.onCreate(savedInstanceState)
         setContent {
            <span class="hljs-keyword">val</span> viewModel: ArtViewModel = hiltViewModel()
            ArtListScreen(viewModel)
        }
    }
}
<span class="hljs-comment">/**

 * • `<span class="hljs-doctag">@AndroidEntryPoint</span>` is required on any Activity, Fragment, or Composable host 
 *   that requests injected dependencies.  
 * • You can now retrieve your Hilt ViewModel using `hiltViewModel()`.  
 * • Make sure all parent components are also annotated if nesting Fragments.  
 */</span>
</code></pre>
<p>We have a new package at the same hierarchy as the data folder.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">package</span> com.learning.artsnapapp.di

<span class="hljs-keyword">import</span> com.learning.artsnapapp.<span class="hljs-keyword">data</span>.repo.ArtRepositoryImpl
<span class="hljs-keyword">import</span> com.learning.artsnapapp.domain.repository.ArtRepositoryInterface
<span class="hljs-keyword">import</span> dagger.Binds
<span class="hljs-keyword">import</span> dagger.Module
<span class="hljs-keyword">import</span> dagger.hilt.InstallIn
<span class="hljs-keyword">import</span> dagger.hilt.components.SingletonComponent
<span class="hljs-keyword">import</span> javax.inject.Singleton

<span class="hljs-comment">/**
 * Binds the repository implementation to its interface
 * so that Hilt can provide it wherever the interface is required.
 */</span>
<span class="hljs-comment">/**
 * ## RepositoryModule
 *
 * Binds the concrete [ArtRepositoryImpl] to its interface [ArtRepositoryInterface].
 *
 * This allows ViewModels and UseCases to depend on the *interface*,
 * while Hilt injects the *implementation* automatically.
 */</span>
<span class="hljs-meta">@Module</span>
<span class="hljs-meta">@InstallIn(SingletonComponent::class)</span>
<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RepositoryModule</span> </span>{

   <span class="hljs-comment">/**
     * Binds [ArtRepositoryImpl] to [ArtRepositoryInterface].
     *
     * <span class="hljs-doctag">@return</span> The implementation that Hilt should provide whenever
     * [ArtRepositoryInterface] is requested.
     */</span>
    <span class="hljs-meta">@Binds</span>
    <span class="hljs-meta">@Singleton</span>
    <span class="hljs-keyword">abstract</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">bindArtRepository</span><span class="hljs-params">(
        impl: <span class="hljs-type">ArtRepositoryImpl</span>
    )</span></span>: ArtRepositoryInterface
}

<span class="hljs-comment">/**

 * • <span class="hljs-doctag">@Binds</span> → Used when you already have an implementation class (no need to `<span class="hljs-doctag">@Provides</span>`).
 * • Promotes **dependency inversion** — ViewModels use the interface, not the concrete class.
 * • Keeps your architecture clean and testable.
 * • This pairs perfectly with AppModule — together, they build your DI graph.
 */</span>
</code></pre>
<pre><code class="lang-kotlin"><span class="hljs-keyword">package</span> com.learning.artsnapapp.di

<span class="hljs-keyword">import</span> android.content.Context
<span class="hljs-keyword">import</span> androidx.room.Database
<span class="hljs-keyword">import</span> androidx.room.Room
<span class="hljs-keyword">import</span> com.learning.artsnapapp.BuildConfig
<span class="hljs-keyword">import</span> com.learning.artsnapapp.<span class="hljs-keyword">data</span>.local.dao.ArtDao
<span class="hljs-keyword">import</span> com.learning.artsnapapp.<span class="hljs-keyword">data</span>.local.db.ArtDatabase
<span class="hljs-keyword">import</span> com.learning.artsnapapp.<span class="hljs-keyword">data</span>.remote.api.ImageApi
<span class="hljs-keyword">import</span> com.learning.artsnapapp.domain.repository.ArtRepositoryInterface
<span class="hljs-keyword">import</span> com.learning.artsnapapp.domain.usecase.ArtUseCases
<span class="hljs-keyword">import</span> com.learning.artsnapapp.domain.usecase.DeleteArtUseCase
<span class="hljs-keyword">import</span> com.learning.artsnapapp.domain.usecase.GetArtUseCase
<span class="hljs-keyword">import</span> com.learning.artsnapapp.domain.usecase.InsertArtUseCase
<span class="hljs-keyword">import</span> com.learning.artsnapapp.domain.usecase.SearchImageUseCase
<span class="hljs-keyword">import</span> dagger.Module
<span class="hljs-keyword">import</span> dagger.Provides
<span class="hljs-keyword">import</span> dagger.hilt.InstallIn
<span class="hljs-keyword">import</span> dagger.hilt.android.qualifiers.ApplicationContext
<span class="hljs-keyword">import</span> dagger.hilt.components.SingletonComponent
<span class="hljs-keyword">import</span> retrofit2.Retrofit
<span class="hljs-keyword">import</span> retrofit2.converter.gson.GsonConverterFactory
<span class="hljs-keyword">import</span> retrofit2.create
<span class="hljs-keyword">import</span> javax.inject.Named
<span class="hljs-keyword">import</span> javax.inject.Singleton

<span class="hljs-comment">/**
 * ## AppModule
 *
 * Provides **singleton dependencies** such as:
 * - Room database
 * - DAO
 * - Use cases (grouped in [ArtUseCases])
 *
 * Hilt uses this module to construct objects that are needed app-wide.
 */</span>

<span class="hljs-meta">@Module</span>
<span class="hljs-meta">@InstallIn(SingletonComponent::class)</span>
<span class="hljs-keyword">object</span> AppModule {

   <span class="hljs-comment">/**
     * Provides a single instance of the Room Database.
     *
     * <span class="hljs-doctag">@param</span> context Application context automatically injected by Hilt.
     */</span>
    <span class="hljs-meta">@Provides</span>
    <span class="hljs-meta">@Singleton</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">provideDatabase</span><span class="hljs-params">(<span class="hljs-meta">@ApplicationContext</span> context: <span class="hljs-type">Context</span>)</span></span>: ArtDatabase = Room.databaseBuilder(
        context,
        ArtDatabase::<span class="hljs-keyword">class</span>.java,
        <span class="hljs-string">"art_db"</span>
    ).build()

   <span class="hljs-comment">/**
     * Provides the DAO object to access the `Art` table.
     */</span>
    <span class="hljs-meta">@Provides</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">provideArtDao</span><span class="hljs-params">(db: <span class="hljs-type">ArtDatabase</span>)</span></span> : ArtDao = db.artDao()

    <span class="hljs-comment">/**
     * Provides all use cases (business logic) bundled in [ArtUseCases].
     *
     * <span class="hljs-doctag">@param</span> repository Injected automatically from [RepositoryModule].
     */</span>
    <span class="hljs-comment">// --- USE CASES ----------------------------------------------------------</span>
    <span class="hljs-meta">@Provides</span>
    <span class="hljs-meta">@Singleton</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">provideArtUseCases</span><span class="hljs-params">(repository: <span class="hljs-type">ArtRepositoryInterface</span>)</span></span>: ArtUseCases =
        ArtUseCases(
            getArtUseCase = GetArtUseCase(repository),
        )
}

<span class="hljs-comment">/*
     @Module → tells Hilt this class provides dependencies.

 * • @InstallIn(SingletonComponent::class) → dependencies live for the app’s lifetime.  
 * • @Provides → defines *how to create* each dependency.  
 * • Hilt automatically injects these wherever needed (e.g., ViewModel, Repository).  
*/</span>
</code></pre>
<p>We have a special data class in the <strong>domain/usecase</strong> package - ArtUseCases</p>
<pre><code class="lang-kotlin">
<span class="hljs-comment">//aggregating all use cases</span>
<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ArtUseCases</span></span>(
   <span class="hljs-keyword">val</span> getArtUseCase: GetArtUseCase,
)

<span class="hljs-comment">//Each use case represents one piece of</span>
<span class="hljs-comment">// business logic and depends only on the repository interface, </span>
<span class="hljs-comment">//not on Room or Retrofit directly.</span>
<span class="hljs-comment">//They make the domain layer pure, testable, and independent of framework details.</span>
</code></pre>
<p>The comments added at the end of each class as comments explain the code in an easy language.</p>
<p>We will now modify the ArtViewModel to accept these Hilt dependencies.</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">/**
 * ## ArtViewModel
 *
 * ViewModel that fetches art data from repository through use cases.
 */</span>

<span class="hljs-meta">@HiltViewModel</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ArtViewModel</span> <span class="hljs-meta">@Inject</span> <span class="hljs-keyword">constructor</span></span>(
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> useCases: ArtUseCases
) : ViewModel() {

    <span class="hljs-comment">/**
     * A **mutable** StateFlow holding the current list of artworks.
     * This is **private** so that only the ViewModel can modify it.
     *
     * Initially empty; updated when Room emits new data through the use case.
     */</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _arts = MutableStateFlow&lt;List&lt;Art&gt;&gt;(emptyList())

    <span class="hljs-comment">/**
     * A **read-only** version of [_arts] exposed to the UI.
     * Compose collects this to display the current artwork list.
     *
     * &gt; `StateFlow` is a hot flow — it always has a current value
     *   and re-emits it to new collectors (like Compose recompositions).
     */</span>
    <span class="hljs-keyword">val</span> arts: StateFlow&lt;List&lt;Art&gt;&gt; = _arts.asStateFlow()

    <span class="hljs-comment">/**
     * Indicates whether the app is currently fetching data.
     *
     * Mutable version (internal to the ViewModel).
     */</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _isLoading = MutableStateFlow(<span class="hljs-literal">false</span>)

    <span class="hljs-comment">/**
     * Public, read-only version of [_isLoading] for the UI.
     *
     * The Compose layer can observe this to show/hide progress indicators.
     */</span>
    <span class="hljs-keyword">val</span> isLoading: StateFlow&lt;<span class="hljs-built_in">Boolean</span>&gt; = _isLoading.asStateFlow()

    <span class="hljs-comment">/**
     * Holds the latest error message, if any.
     *
     * `null` means no error.
     */</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _errorMessage = MutableStateFlow&lt;String?&gt;(<span class="hljs-literal">null</span>)

    <span class="hljs-comment">/**
     * Public, read-only flow exposing error messages.
     */</span>
    <span class="hljs-keyword">val</span> errorMessage: StateFlow&lt;String?&gt; = _errorMessage.asStateFlow()

    <span class="hljs-comment">/**
     * When ViewModel is created, it automatically loads artworks.
     * This is done inside [init] to trigger the use case once at startup.
     */</span>
    <span class="hljs-keyword">init</span> {
        loadArts()
    }

    <span class="hljs-comment">/**
     * Invokes [GetArtUseCase] and collects the emitted Flow&lt;List&lt;Art&gt;&gt;.
     *
     * ### Flow explanation:
     * - The `GetArtUseCase()` returns a `Flow&lt;List&lt;Art&gt;&gt;`.
     * - A **Flow** is a cold asynchronous data stream — it emits values
     *   (here, lists of Art) over time.
     * - Room emits updates via Flow whenever the DB changes.
     *
     * ### Steps:
     * 1. Set loading state to `true`.
     * 2. Collect emitted art lists from use case.
     * 3. Update `_arts` StateFlow (Compose UI reacts automatically).
     * 4. Handle exceptions by updating `_errorMessage`.
     * 5. Set loading back to `false` when done.
     */</span>
    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">loadArts</span><span class="hljs-params">()</span></span> {
        viewModelScope.launch {
            _isLoading.value = <span class="hljs-literal">true</span>
            <span class="hljs-keyword">try</span> {
                <span class="hljs-comment">// Collects flow from use case</span>
                useCases.getArtUseCase().collect { artList -&gt;
                    _arts.value = artList
                }
            } <span class="hljs-keyword">catch</span> (e: Exception) {
                _errorMessage.value = e.message
            } <span class="hljs-keyword">finally</span> {
                _isLoading.value = <span class="hljs-literal">false</span>
            }
        }
    }
}
<span class="hljs-comment">/**
 * • `<span class="hljs-doctag">@HiltViewModel</span>` integrates Hilt into Android's ViewModel system.  
 * • The constructor params (like `ArtUseCases`) are auto-provided by Hilt.  
 * • `<span class="hljs-doctag">@Inject</span> constructor(...)` tells Hilt how to build this ViewModel.  
 * • No need for a manual ViewModelFactory — Hilt handles it.  
 */</span>
</code></pre>
<p>Change the ArtListScreen to accept Hilt.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ArtListScreen</span><span class="hljs-params">(
    viewModel: <span class="hljs-type">ArtViewModel</span> = hiltViewModel()</span></span>,
    isLoading: <span class="hljs-built_in">Boolean</span> = <span class="hljs-literal">false</span>,
    onFabClick: () -&gt; <span class="hljs-built_in">Unit</span> = {},
    onDeleteClick: (Art) -&gt; <span class="hljs-built_in">Unit</span> = {},
    errorMessage: String? = <span class="hljs-literal">null</span>
) {

    <span class="hljs-keyword">val</span> artList <span class="hljs-keyword">by</span> viewModel.arts.collectAsState()

    Scaffold(
        floatingActionButton = {
            FloatingActionButton(onClick = onFabClick) {
                Icon(Icons.Default.AddBox, contentDescription = <span class="hljs-string">"Add Art"</span>)
            }
        }
    ) { paddingValues -&gt;
        Box(modifier = Modifier
            .fillMaxSize()
            .padding(paddingValues)
        ) {

            <span class="hljs-keyword">when</span> {
                <span class="hljs-comment">// ✅ Loading State</span>
                isLoading -&gt; {
                    CircularProgressIndicator(
                        modifier = Modifier
                            .align(Alignment.Center)
                            .testTag(<span class="hljs-string">"Progress"</span>)
                    )
                }

                <span class="hljs-comment">// ✅ Error State</span>
                errorMessage != <span class="hljs-literal">null</span> -&gt; {
                    Text(
                        text = errorMessage,
                        color = Color.Red,
                        modifier = Modifier
                            .align(Alignment.Center)
                            .testTag(<span class="hljs-string">"ErrorMessage"</span>)
                    )
                }

                <span class="hljs-comment">// ✅ Empty List State</span>
                artList.isEmpty() -&gt; {
                    Text(
                        text = <span class="hljs-string">"No artworks found!"</span>,
                        modifier = Modifier.align(Alignment.Center)
                    )
                }

                <span class="hljs-comment">// ✅ Normal List</span>
                <span class="hljs-keyword">else</span> -&gt; {
                    LazyColumn(
                        modifier = Modifier.fillMaxSize()
                    ) {
                        items(artList) { art -&gt;
                            ListItem(
                                headlineContent = { Text(art.name) },
                                supportingContent = { Text(<span class="hljs-string">"<span class="hljs-subst">${art.artistName}</span> (<span class="hljs-subst">${art.year}</span>)"</span>) },
                                trailingContent = {
                                    IconButton(onClick = { onDeleteClick(art) }) {
                                        Icon(
                                            Icons.Default.Delete,
                                            contentDescription = <span class="hljs-string">"Delete"</span>
                                        )
                                    }
                                }
                            )
                            HorizontalDivider()
                        }
                    }
                }
            }
        }
    }
}
</code></pre>
<p>Change the ArtRepositoryImpl class as well.</p>
<pre><code class="lang-kotlin">
<span class="hljs-meta">@Singleton</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ArtRepositoryImpl</span> <span class="hljs-meta">@Inject</span> <span class="hljs-keyword">constructor</span></span>(<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> artDao: ArtDao):ArtRepositoryInterface {
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getArt</span><span class="hljs-params">()</span></span>: Flow&lt;List&lt;Art&gt;&gt; {

        <span class="hljs-comment">//returns a stream (Flow) that emits a list of ArtEntity objects every time the ROOM DB table changes.</span>
        <span class="hljs-keyword">val</span> artStream = artDao.getAllArts()

       <span class="hljs-keyword">val</span> artObjs = artStream.map { entityList -&gt; entityList.map { it.toDomainModel() } }
        <span class="hljs-keyword">return</span> artObjs
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">insertArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {
        TODO(<span class="hljs-string">"Not yet implemented"</span>)
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">deleteArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {
        TODO(<span class="hljs-string">"Not yet implemented"</span>)
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">searchImage</span><span class="hljs-params">(userQuery: <span class="hljs-type">String</span>)</span></span>: Flow&lt;Resource&lt;List&lt;ImageResult&gt;&gt;&gt; {
        TODO(<span class="hljs-string">"Not yet implemented"</span>)
    }
}
</code></pre>
<p>Now run the app and we have successfully implemented the Hilt Dependency Injection.</p>
]]></content:encoded></item><item><title><![CDATA[Chapter 13 - Testing the Composable UI ArtListScreen]]></title><description><![CDATA[@RunWith(AndroidJUnit4::class)
class ArtListScreenTest {

    @get:Rule
    val composableRule = createComposeRule()

    //fake repository
    private val fakeRepo = object : ArtRepositoryInterface {
        override fun getArt(): Flow<List<Art>> = ...]]></description><link>https://androidavatar.hashnode.dev/chapter-13-testing-the-composable-ui-artlistscreen</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-13-testing-the-composable-ui-artlistscreen</guid><category><![CDATA[Composable Architecture]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[lazycolumn]]></category><category><![CDATA[flow]]></category><category><![CDATA[flowcharts]]></category><category><![CDATA[presentation]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Sat, 01 Nov 2025 18:19:45 GMT</pubDate><content:encoded><![CDATA[<pre><code class="lang-kotlin"><span class="hljs-meta">@RunWith(AndroidJUnit4::class)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ArtListScreenTest</span> </span>{

    <span class="hljs-meta">@get:Rule</span>
    <span class="hljs-keyword">val</span> composableRule = createComposeRule()

    <span class="hljs-comment">//fake repository</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> fakeRepo = <span class="hljs-keyword">object</span> : ArtRepositoryInterface {
        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getArt</span><span class="hljs-params">()</span></span>: Flow&lt;List&lt;Art&gt;&gt; = flowOf(
            listOf(
                Art(id = <span class="hljs-number">1</span>, name = <span class="hljs-string">"Starry Horse"</span>, artistName = <span class="hljs-string">"Viru Pandey"</span>, year = <span class="hljs-string">"1904"</span>, imageUrl = <span class="hljs-string">"url1"</span>),
                Art(id = <span class="hljs-number">2</span>, name = <span class="hljs-string">"Mano ya na mano"</span>, artistName = <span class="hljs-string">"Satish Shah"</span>, year = <span class="hljs-string">"1998"</span>, imageUrl = <span class="hljs-string">"url2"</span>)
            )
        )

        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">insertArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {}
        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">deleteArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {}
        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">searchImage</span><span class="hljs-params">(userQuery: <span class="hljs-type">String</span>)</span></span>: Flow&lt;Resource&lt;List&lt;ImageResult&gt;&gt;&gt; =
            flow { emit(Resource.Success(emptyList())) }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> getArtUseCase = GetArtUseCase(fakeRepo)
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> viewModel = ArtViewModel(getArtUseCase)
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> lists = listOf(Art(<span class="hljs-number">1</span>, <span class="hljs-string">"Starry Horse"</span>, <span class="hljs-string">"Viru Pandey"</span>, <span class="hljs-string">"1904"</span>, <span class="hljs-string">"url1"</span>),
        Art(<span class="hljs-number">2</span>, <span class="hljs-string">"Mano ya na mano"</span>, <span class="hljs-string">"Satish Shah"</span>, <span class="hljs-string">"1998"</span>, <span class="hljs-string">"url2"</span>))

    <span class="hljs-comment">// 1. Normal data case</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">artListScreen_displaysArtNamesFromViewModel</span><span class="hljs-params">()</span></span> {
        composableRule.setContent {
            ArtListScreen(
                artList = lists
            )
        }


        composableRule.onNodeWithText(<span class="hljs-string">"Starry Horse"</span>).assertIsDisplayed()
        composableRule.onNodeWithText(<span class="hljs-string">"Mano ya na mano"</span>).assertIsDisplayed()
    }


    <span class="hljs-comment">// 2. Loading state test</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">artListScreen_showsLoadingIndicatorWhenLoading</span><span class="hljs-params">()</span></span> {
        composableRule.setContent {
            ArtListScreen(
                artList = emptyList(),
                isLoading = <span class="hljs-literal">true</span>
            )
        }
        composableRule.onNodeWithTag(<span class="hljs-string">"Progress"</span>).assertExists()
    }

    <span class="hljs-comment">// 3. Error state test</span>
    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">artListScreen_displaysErrorMessageWhenErrorOccurs</span><span class="hljs-params">()</span></span> {
        <span class="hljs-comment">// Option 1: Pass error directly (simplest)</span>
        composableRule.setContent {
            ArtListScreen(
                artList = emptyList(),
                errorMessage = <span class="hljs-string">"Something went wrong"</span>
            )
        }
        composableRule.onNodeWithText(<span class="hljs-string">"Something went wrong"</span>).assertIsDisplayed()
    }

}
</code></pre>
<p>The above code is an instrumentation test and will run on an android device or emulator.</p>
<p>We test the following</p>
<ul>
<li><p>The fake data appears on screen</p>
</li>
<li><p>Loading &amp; error states behave as expected.</p>
</li>
</ul>
<h3 id="heading-test-1-artlistscreendisplaysartlistcorrectly"><strong>Test 1 : artListScreen_displaysArtListCorrectly</strong></h3>
<p><strong>Purpose</strong></p>
<p>Verifies that the <strong>UI displays the list of artworks correctly</strong> when provided with sample data.</p>
<p><strong>Setup</strong></p>
<ul>
<li><p>Creates a list of two Art objects (Starry Horse and Mano ya na mano).</p>
</li>
<li><p>Renders the ArtListScreen composable with this list.</p>
</li>
</ul>
<p><strong>Assertions</strong></p>
<ul>
<li><p>Ensures that both artwork titles are <strong>visible on the screen</strong> using onNodeWithText().assertIsDisplayed().</p>
</li>
<li><p>Confirms that LazyColumn correctly renders multiple items.</p>
</li>
</ul>
<p> <strong>Summary</strong></p>
<ul>
<li><p>This is a <strong>sanity UI test</strong> — checks that Compose correctly binds data to the visible layout.</p>
</li>
<li><p>It doesn’t depend on any repository or ViewModel — pure composable test.</p>
</li>
</ul>
<h3 id="heading-test-2-artlistscreenshowsloadingindicatorwhenloading"><strong>Test 2 : artListScreen_showsLoadingIndicatorWhenLoading</strong></h3>
<p> <strong>Purpose</strong></p>
<p>Validates that the <strong>progress indicator</strong> (loading spinner) is displayed when the screen is in a loading state.</p>
<p><strong>Setup</strong></p>
<ul>
<li><p>Passes isLoading = true to ArtListScreen.</p>
</li>
<li><p>Uses a Modifier.testTag("Progress") for the loading indicator in the composable.</p>
</li>
</ul>
<p> <strong>Assertions</strong></p>
<ul>
<li>Checks that a node with tag "Progress" exists on the screen (assertExists()).</li>
</ul>
<p> <strong>Summary</strong></p>
<ul>
<li><p>Helps verify that <strong>UI reacts properly to loading state changes</strong>.</p>
</li>
<li><p>In the real app, this state will be controlled by viewModel.isLoading.</p>
</li>
<li><p>This is important for user feedback during data fetching.</p>
</li>
</ul>
<h3 id="heading-test-3-artlistscreendisplayserrormessagewhenerroroccurs"><strong>Test 3 : artListScreen_displaysErrorMessageWhenErrorOccurs</strong></h3>
<p><strong>Purpose</strong></p>
<p>Checks that an <strong>error message</strong> is displayed if something goes wrong.</p>
<p><strong>Setup</strong></p>
<ul>
<li><p>Renders ArtListScreen with errorMessage = "Something went wrong".</p>
</li>
<li><p>Passes an empty artList (since we’re only testing error display).</p>
</li>
</ul>
<p><strong>Assertions</strong></p>
<ul>
<li>Verifies that the error text is <strong>visible</strong> on the screen using onNodeWithText("Something went wrong").assertIsDisplayed().</li>
</ul>
<p> <strong>Summary</strong></p>
<ul>
<li><p>Simulates the scenario where a Room query fails.</p>
</li>
<li><p>Confirms that the user is <strong>not left with a blank screen</strong> but sees a clear error message.</p>
</li>
<li><p>This test ensures that your UI reacts correctly to error state updates from the ViewModel.</p>
</li>
</ul>
<h3 id="heading-final-summary">FINAL SUMMARY</h3>
<p>These three tests together verify <strong>UI reactivity</strong> — the most important principle of Jetpack Compose.<br />They prove that when the <strong>ViewModel updates the StateFlow</strong> (data, loading, error), the UI layer <strong>reflects those changes instantly and correctly</strong>.</p>
<pre><code class="lang-mermaid">flowchart LR

    A[Repository - Fake or Real] --&gt; B[UseCase - GetArtUseCase]
    B --&gt; C[ArtViewModel - holds StateFlows]
    C --&gt; D[ArtListScreen - observes data]
    D --&gt; E[Compose UI Tests]
    E --&gt; E1[Test: Show List]
    E --&gt; E2[Test: Show Loading]
    E --&gt; E3[Test: Show Error]

    subgraph Domain [Domain Layer]
        A
        B
    end

    subgraph Presentation [Presentation Layer]
        C
        D
    end

    subgraph Testing [Testing Layer]
        E
        E1
        E2
        E3
    end
</code></pre>
<p>The flowchart sums up the work that we have done so far.</p>
]]></content:encoded></item><item><title><![CDATA[Chapter 12 - Developing the Composable UI ArtListScreen]]></title><description><![CDATA[Let us now code the composable for the ArtListScreen and complete the flow.
package androidavatar.learning.artlearnersapp.presentation.artlist

import androidavatar.learning.artlearnersapp.domain.model.Art
import androidx.compose.foundation.layout.Bo...]]></description><link>https://androidavatar.hashnode.dev/chapter-12-developing-the-composable-ui-artlistscreen</link><guid isPermaLink="true">https://androidavatar.hashnode.dev/chapter-12-developing-the-composable-ui-artlistscreen</guid><category><![CDATA[Composable Architecture]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Clean Architecture]]></category><category><![CDATA[android app development]]></category><category><![CDATA[scaffolding]]></category><dc:creator><![CDATA[Ganesh Rama Hegde]]></dc:creator><pubDate>Sat, 01 Nov 2025 17:35:20 GMT</pubDate><content:encoded><![CDATA[<p>Let us now code the composable for the ArtListScreen and complete the flow.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">package</span> androidavatar.learning.artlearnersapp.presentation.artlist

<span class="hljs-keyword">import</span> androidavatar.learning.artlearnersapp.domain.model.Art
<span class="hljs-keyword">import</span> androidx.compose.foundation.layout.Box
<span class="hljs-keyword">import</span> androidx.compose.foundation.layout.fillMaxSize
<span class="hljs-keyword">import</span> androidx.compose.foundation.layout.padding
<span class="hljs-keyword">import</span> androidx.compose.foundation.lazy.LazyColumn
<span class="hljs-keyword">import</span> androidx.compose.foundation.lazy.items
<span class="hljs-keyword">import</span> androidx.compose.material.icons.Icons
<span class="hljs-keyword">import</span> androidx.compose.material.icons.filled.AddBox
<span class="hljs-keyword">import</span> androidx.compose.material.icons.filled.Delete
<span class="hljs-keyword">import</span> androidx.compose.material3.CircularProgressIndicator
<span class="hljs-keyword">import</span> androidx.compose.material3.Divider
<span class="hljs-keyword">import</span> androidx.compose.material3.FloatingActionButton
<span class="hljs-keyword">import</span> androidx.compose.material3.Icon
<span class="hljs-keyword">import</span> androidx.compose.material3.IconButton
<span class="hljs-keyword">import</span> androidx.compose.material3.ListItem
<span class="hljs-keyword">import</span> androidx.compose.material3.Scaffold
<span class="hljs-keyword">import</span> androidx.compose.material3.Text
<span class="hljs-keyword">import</span> androidx.compose.runtime.Composable
<span class="hljs-keyword">import</span> androidx.compose.ui.Alignment
<span class="hljs-keyword">import</span> androidx.compose.ui.Modifier
<span class="hljs-keyword">import</span> androidx.compose.ui.graphics.Color
<span class="hljs-keyword">import</span> androidx.compose.ui.platform.testTag

<span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ArtListScreen</span><span class="hljs-params">(
    artList: <span class="hljs-type">List</span>&lt;<span class="hljs-type">Art</span>&gt;,
    isLoading: <span class="hljs-type">Boolean</span> = <span class="hljs-literal">false</span>,
    onFabClick: () -&gt; <span class="hljs-type">Unit</span> = {},
    onDeleteClick: (<span class="hljs-type">Art</span>) -&gt; <span class="hljs-type">Unit</span> = {},
    errorMessage: <span class="hljs-type">String</span>? = <span class="hljs-literal">null</span>
)</span></span> {
    Scaffold(
        floatingActionButton = {
            FloatingActionButton(onClick = onFabClick) {
                Icon(Icons.Default.AddBox, contentDescription = <span class="hljs-string">"Add Art"</span>)
            }
        }
    ) { paddingValues -&gt;
        Box(modifier = Modifier
            .fillMaxSize()
            .padding(paddingValues)
        ) {

            <span class="hljs-keyword">when</span> {
                <span class="hljs-comment">// ✅ Loading State</span>
                isLoading -&gt; {
                    CircularProgressIndicator(
                        modifier = Modifier
                            .align(Alignment.Center)
                            .testTag(<span class="hljs-string">"Progress"</span>)
                    )
                }

                <span class="hljs-comment">// ✅ Error State</span>
                errorMessage != <span class="hljs-literal">null</span> -&gt; {
                    Text(
                        text = errorMessage,
                        color = Color.Red,
                        modifier = Modifier
                            .align(Alignment.Center)
                            .testTag(<span class="hljs-string">"ErrorMessage"</span>)
                    )
                }

                <span class="hljs-comment">// ✅ Empty List State</span>
                artList.isEmpty() -&gt; {
                    Text(
                        text = <span class="hljs-string">"No artworks found!"</span>,
                        modifier = Modifier.align(Alignment.Center)
                    )
                }

                <span class="hljs-comment">// ✅ Normal List</span>
                <span class="hljs-keyword">else</span> -&gt; {
                    LazyColumn(
                        modifier = Modifier.fillMaxSize()
                    ) {
                        items(artList) { art -&gt;
                            ListItem(
                                headlineContent = { Text(art.name) },
                                supportingContent = { Text(<span class="hljs-string">"<span class="hljs-subst">${art.artistName}</span> (<span class="hljs-subst">${art.year}</span>)"</span>) },
                                trailingContent = {
                                    IconButton(onClick = { onDeleteClick(art) }) {
                                        Icon(
                                            Icons.Default.Delete,
                                            contentDescription = <span class="hljs-string">"Delete"</span>
                                        )
                                    }
                                }
                            )
                            Divider()
                        }
                    }
                }
            }
        }
    }
}
</code></pre>
<p>The layout has a “fab” button at the right-end bottom corner. The list of image records are displayed from the Room DB using Lazy Column. We have already discussed this process in depth in the earlier chapters.</p>
<h3 id="heading-modifying-the-mainactivity-to-visualise-the-screen">Modifying the MainActivity to visualise the screen</h3>
<p>Now modify the MainActivity (this is the entry point) of the app.</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainActivity</span> : <span class="hljs-type">ComponentActivity</span></span>() {

    <span class="hljs-comment">// Fake repository for now</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> fakeRepository = <span class="hljs-keyword">object</span> : ArtRepositoryInterface {
        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getArt</span><span class="hljs-params">()</span></span>: Flow&lt;List&lt;Art&gt;&gt; = flow {
            emit(
                listOf(
                    Art(<span class="hljs-number">1</span>, <span class="hljs-string">"Starry Horse"</span>, <span class="hljs-string">"Viru Pandey"</span>, <span class="hljs-string">"1904"</span>, <span class="hljs-string">"url1"</span>),
                    Art(<span class="hljs-number">2</span>, <span class="hljs-string">"Mano ya na mano"</span>, <span class="hljs-string">"Satish Shah"</span>, <span class="hljs-string">"1998"</span>, <span class="hljs-string">"url2"</span>)
                )
            )
        }

        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">insertArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {}
        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">deleteArt</span><span class="hljs-params">(art: <span class="hljs-type">Art</span>)</span></span> {}
        <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">searchImage</span><span class="hljs-params">(userQuery: <span class="hljs-type">String</span>)</span></span>: Flow&lt;Resource&lt;List&lt;ImageResult&gt;&gt;&gt; =
            flow { emit(Resource.Success(emptyList())) }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> getArtUseCase = GetArtUseCase(fakeRepository)
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> viewModel <span class="hljs-keyword">by</span> viewModels&lt;ArtViewModel&gt; {
        <span class="hljs-keyword">object</span> :  ViewModelProvider.Factory {
            <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-type">&lt;T : ViewModel&gt;</span> <span class="hljs-title">create</span><span class="hljs-params">(modelClass: <span class="hljs-type">Class</span>&lt;<span class="hljs-type">T</span>&gt;)</span></span>: T {
                <span class="hljs-keyword">return</span> ArtViewModel(getArtUseCase) <span class="hljs-keyword">as</span> T
            }
        }
    }

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

            ArtListScreen(
                artList = arts,
                onFabClick = { <span class="hljs-comment">/* handle add */</span> },
                onDeleteClick = { }
            )
        }
    }
}
</code></pre>
<p>Now we have introduced the fake repository and wired all the connections. Later on we will remove the fake dependencies and introduce Hilt to upgrade our code that will be production ready.</p>
<p>You will see the two Art objects as two list elements.</p>
<p>Kudos for coming thus far and you have the first composable screen with end-to-end code.</p>
<p>In the next chapter we will add the tests for the composable ArtListScreen and wrap up the code!</p>
]]></content:encoded></item></channel></rss>