Skip to content

Commit

Permalink
Debounce performing search depending on searchquery length (#3576)
Browse files Browse the repository at this point in the history
  • Loading branch information
LZRS authored Nov 15, 2024
1 parent 978d050 commit 4761232
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.smartregister.fhircore.quest.ui.register

import android.graphics.Bitmap
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
Expand All @@ -33,10 +34,13 @@ import com.google.android.fhir.sync.CurrentSyncJobStatus
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlin.math.ceil
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -79,10 +83,12 @@ import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
import org.smartregister.fhircore.engine.util.extension.encodeJson
import org.smartregister.fhircore.quest.data.register.RegisterPagingSource
import org.smartregister.fhircore.quest.data.register.model.RegisterPagingSourceState
import org.smartregister.fhircore.quest.ui.shared.models.SearchQuery
import org.smartregister.fhircore.quest.util.extensions.referenceToBitmap
import org.smartregister.fhircore.quest.util.extensions.toParamDataMap
import timber.log.Timber

@OptIn(FlowPreview::class)
@HiltViewModel
class RegisterViewModel
@Inject
Expand Down Expand Up @@ -115,6 +121,29 @@ constructor(
}
private val decodedImageMap = mutableStateMapOf<String, Bitmap>()

private val _searchQueryFlow: MutableSharedFlow<SearchQuery> = MutableSharedFlow()

@VisibleForTesting
val debouncedSearchQueryFlow =
_searchQueryFlow.debounce {
val searchText = it.query
when (searchText.length) {
0 -> 2.milliseconds // when search is cleared
1,
2, -> 1000.milliseconds
else -> 500.milliseconds
}
}

init {
viewModelScope.launch {
debouncedSearchQueryFlow.collect {
val registerId = registerUiState.value.registerId
performSearch(registerId, it)
}
}
}

/**
* This function paginates the register data. An optional [clearCache] resets the data in the
* cache (this is necessary after a questionnaire has been submitted to refresh the register with
Expand Down Expand Up @@ -194,26 +223,7 @@ constructor(
when (event) {
// Search using name or patient logicalId or identifier. Modify to add more search params
is RegisterEvent.SearchRegister -> {
if (event.searchQuery.isBlank()) {
val regConfig = retrieveRegisterConfiguration(registerId)
val searchByDynamicQueries = !regConfig.searchBar?.dataFilterFields.isNullOrEmpty()
if (searchByDynamicQueries) {
registerFilterState.value = RegisterFilterState() // Reset queries
}
when {
regConfig.infiniteScroll ->
registerData.value = retrieveCompleteRegisterData(registerId, searchByDynamicQueries)
else ->
retrieveRegisterUiState(
registerId = registerId,
screenTitle = registerUiState.value.screenTitle,
params = registerUiState.value.params.toTypedArray(),
clearCache = searchByDynamicQueries,
)
}
} else {
filterRegisterData(event.searchQuery.query)
}
viewModelScope.launch { _searchQueryFlow.emit(event.searchQuery) }
}
is RegisterEvent.MoveToNextPage -> {
currentPage.value = currentPage.value.plus(1)
Expand All @@ -227,6 +237,30 @@ constructor(
}
}

@VisibleForTesting
fun performSearch(registerId: String, searchQuery: SearchQuery) {
if (searchQuery.isBlank()) {
val regConfig = retrieveRegisterConfiguration(registerId)
val searchByDynamicQueries = !regConfig.searchBar?.dataFilterFields.isNullOrEmpty()
if (searchByDynamicQueries) {
registerFilterState.value = RegisterFilterState() // Reset queries
}
when {
regConfig.infiniteScroll ->
registerData.value = retrieveCompleteRegisterData(registerId, searchByDynamicQueries)
else ->
retrieveRegisterUiState(
registerId = registerId,
screenTitle = registerUiState.value.screenTitle,
params = registerUiState.value.params.toTypedArray(),
clearCache = searchByDynamicQueries,
)
}
} else {
filterRegisterData(searchQuery.query)
}
}

fun filterRegisterData(searchText: String) {
val searchBar = registerUiState.value.registerConfiguration?.searchBar
val registerId = registerUiState.value.registerId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import io.mockk.runs
import io.mockk.spyk
import io.mockk.verify
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.DateType
Expand Down Expand Up @@ -118,14 +122,18 @@ class RegisterViewModelTest : RobolectricTest() {
@Test
@kotlinx.coroutines.ExperimentalCoroutinesApi
fun testRetrieveRegisterUiState() = runTest {
every { registerViewModel.retrieveRegisterConfiguration(any()) } returns
val registerConfig =
RegisterConfiguration(
appId = "app",
id = registerId,
fhirResource =
FhirResourceConfig(baseResource = ResourceConfig(resource = ResourceType.Patient)),
pageSize = 10,
)
configurationRegistry.configCacheMap[registerId] = registerConfig
registerViewModel.registerUiState.value =
registerViewModel.registerUiState.value.copy(registerId = registerId)

every { registerViewModel.paginateRegisterData(any(), any()) } just runs
coEvery { registerRepository.countRegisterData(any()) } returns 200
registerViewModel.retrieveRegisterUiState(
Expand All @@ -145,23 +153,78 @@ class RegisterViewModelTest : RobolectricTest() {
}

@Test
fun testOnEventSearchRegister() {
every { registerViewModel.retrieveRegisterConfiguration(any()) } returns
@kotlinx.coroutines.ExperimentalCoroutinesApi
fun testDebounceSearchQueryFlow() = runTest {
val registerConfig =
RegisterConfiguration(
appId = "app",
id = registerId,
fhirResource =
FhirResourceConfig(baseResource = ResourceConfig(resource = ResourceType.Patient)),
pageSize = 10,
)
every { registerViewModel.registerUiState } returns
mutableStateOf(RegisterUiState(registerId = registerId))
configurationRegistry.configCacheMap[registerId] = registerConfig
registerViewModel.registerUiState.value =
registerViewModel.registerUiState.value.copy(registerId = registerId)
coEvery { registerRepository.countRegisterData(any()) } returns 0L

val results = mutableListOf<String>()
val debounceJob = launch {
registerViewModel.debouncedSearchQueryFlow.collect { results.add(it.query) }
}
advanceUntilIdle()

// Search with empty string should paginate the data
registerViewModel.onEvent(RegisterEvent.SearchRegister(SearchQuery.emptyText))

advanceTimeBy(3.milliseconds)
Assert.assertTrue(results.isNotEmpty())
Assert.assertTrue(results.last().isBlank())

registerViewModel.onEvent(RegisterEvent.SearchRegister(SearchQuery("K")))
registerViewModel.onEvent(RegisterEvent.SearchRegister(SearchQuery("Kh")))
registerViewModel.onEvent(RegisterEvent.SearchRegister(SearchQuery("Kha")))
registerViewModel.onEvent(RegisterEvent.SearchRegister(SearchQuery("Khan")))

advanceTimeBy(1010.milliseconds)
Assert.assertEquals(2, results.size)
Assert.assertEquals("Khan", results.last())
debounceJob.cancel()
}

@Test
fun testPerformSearchWithEmptyQuery() = runTest {
val registerConfig =
RegisterConfiguration(
appId = "app",
id = registerId,
fhirResource =
FhirResourceConfig(baseResource = ResourceConfig(resource = ResourceType.Patient)),
pageSize = 10,
)
configurationRegistry.configCacheMap[registerId] = registerConfig
coEvery { registerRepository.countRegisterData(any()) } returns 0L

// Search with empty string should paginate the data
registerViewModel.performSearch(registerId, SearchQuery.emptyText)
verify { registerViewModel.retrieveRegisterUiState(any(), any(), any(), any()) }
}

@Test
fun testPerformSearchWithNonEmptyQuery() = runTest {
val registerConfig =
RegisterConfiguration(
appId = "app",
id = registerId,
fhirResource =
FhirResourceConfig(baseResource = ResourceConfig(resource = ResourceType.Patient)),
pageSize = 10,
)
configurationRegistry.configCacheMap[registerId] = registerConfig
coEvery { registerRepository.countRegisterData(any()) } returns 0L

// Search for the word 'Khan' should call the filterRegisterData function
registerViewModel.onEvent(RegisterEvent.SearchRegister(SearchQuery("Khan")))
registerViewModel.performSearch(registerId, SearchQuery("Khan"))
verify { registerViewModel.filterRegisterData(any()) }
}

Expand Down

0 comments on commit 4761232

Please sign in to comment.