mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 18:56:44 -04:00
Improved 'Search' Functionality:
* Added a loading progress bar at the end of the RecyclerView when loading more search results as the user scrolls to the bottom. This indicator informs users that additional results are being loaded. The progress bar appears if there are more results available for the search term, providing users with visibility into ongoing loading. * Enhanced the search loading process for larger ZIM files by introducing coroutines. This background threading approach prevents the UI thread from being impacted and ensures a smooth scrolling experience for users.
This commit is contained in:
parent
a1b5f36c59
commit
734885a527
@ -38,6 +38,9 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kiwix.kiwixmobile.core.R
|
||||
import org.kiwix.kiwixmobile.core.base.BaseActivity
|
||||
import org.kiwix.kiwixmobile.core.base.BaseFragment
|
||||
@ -146,20 +149,27 @@ class SearchFragment : BaseFragment() {
|
||||
if (isDataLoading) return
|
||||
val safeStartIndex = searchAdapter?.itemCount ?: 0
|
||||
isDataLoading = true
|
||||
val fetchMoreSearchResults = searchState?.getVisibleResults(safeStartIndex)
|
||||
isDataLoading = when {
|
||||
fetchMoreSearchResults == null -> true
|
||||
fetchMoreSearchResults.isEmpty() -> false
|
||||
else -> {
|
||||
val nonDuplicateResults = fetchMoreSearchResults.filter { newItem ->
|
||||
searchAdapter?.items?.none { it == newItem } ?: true
|
||||
}
|
||||
fragmentSearchBinding?.loadingMoreDataIndicator?.isShowing(true)
|
||||
|
||||
if (nonDuplicateResults.isNotEmpty()) {
|
||||
searchAdapter?.addData(nonDuplicateResults)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val fetchMoreSearchResults = searchState?.getVisibleResults(safeStartIndex)
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
fragmentSearchBinding?.loadingMoreDataIndicator?.isShowing(false)
|
||||
isDataLoading = when {
|
||||
fetchMoreSearchResults == null -> true
|
||||
fetchMoreSearchResults.isEmpty() -> false
|
||||
else -> {
|
||||
val nonDuplicateResults = fetchMoreSearchResults.filter { newItem ->
|
||||
searchAdapter?.items?.none { it == newItem } ?: true
|
||||
}
|
||||
|
||||
if (nonDuplicateResults.isNotEmpty()) {
|
||||
searchAdapter?.addData(nonDuplicateResults)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -248,15 +258,16 @@ class SearchFragment : BaseFragment() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun render(state: SearchState) {
|
||||
private suspend fun render(state: SearchState) {
|
||||
isDataLoading = false
|
||||
searchState = state
|
||||
searchInTextMenuItem?.isVisible = state.searchOrigin == FromWebView
|
||||
searchInTextMenuItem?.isEnabled = state.searchTerm.isNotBlank()
|
||||
fragmentSearchBinding?.searchLoadingIndicator?.isShowing(state.isLoading)
|
||||
val searchResult = state.getVisibleResults(0)
|
||||
fragmentSearchBinding?.searchNoResults?.isVisible =
|
||||
state.getVisibleResults(0)?.isEmpty() == true
|
||||
state.getVisibleResults(0)?.let {
|
||||
searchResult?.isEmpty() == true
|
||||
searchResult?.let {
|
||||
searchAdapter?.items = it
|
||||
}
|
||||
}
|
||||
|
@ -26,10 +26,13 @@ data class SearchState(
|
||||
val recentResults: List<SearchListItem.RecentSearchListItem>,
|
||||
val searchOrigin: SearchOrigin
|
||||
) {
|
||||
fun getVisibleResults(startIndex: Int): List<SearchListItem.RecentSearchListItem>? =
|
||||
private var isDataLoading = false
|
||||
suspend fun getVisibleResults(startIndex: Int): List<SearchListItem.RecentSearchListItem>? =
|
||||
if (searchTerm.isEmpty()) {
|
||||
isDataLoading = false
|
||||
recentResults
|
||||
} else {
|
||||
isDataLoading = true
|
||||
searchResultsWithTerm.suggestionSearch?.let {
|
||||
val maximumResults = it.estimatedMatches
|
||||
val safeEndIndex =
|
||||
@ -46,13 +49,15 @@ data class SearchState(
|
||||
* We check this in SearchFragment to avoid unnecessary data loading
|
||||
* while scrolling to the end of the list when there are no items available.
|
||||
*/
|
||||
isDataLoading = false
|
||||
searchResults.ifEmpty { null }
|
||||
} ?: kotlin.run {
|
||||
isDataLoading = false
|
||||
recentResults
|
||||
}
|
||||
}
|
||||
|
||||
val isLoading = searchTerm != searchResultsWithTerm.searchTerm
|
||||
val isLoading get() = searchTerm != searchResultsWithTerm.searchTerm || isDataLoading
|
||||
}
|
||||
|
||||
enum class SearchOrigin {
|
||||
|
@ -1,4 +1,5 @@
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@ -6,7 +7,7 @@
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
@ -15,11 +16,27 @@
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/search_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/loadingMoreDataIndicator"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginTop="?actionBarSize"
|
||||
tools:listitem="@layout/list_item_search" />
|
||||
|
||||
</LinearLayout>
|
||||
<androidx.core.widget.ContentLoadingProgressBar
|
||||
android:id="@+id/loadingMoreDataIndicator"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/searchNoResults"
|
||||
|
@ -20,6 +20,7 @@ package org.kiwix.kiwixmobile.core.search.viewmodel
|
||||
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
||||
@ -28,7 +29,7 @@ import org.kiwix.kiwixmobile.core.search.viewmodel.SearchOrigin.FromWebView
|
||||
internal class SearchStateTest {
|
||||
|
||||
@Test
|
||||
internal fun `visibleResults use searchResults when searchTerm is not empty`() {
|
||||
internal fun `visibleResults use searchResults when searchTerm is not empty`() = runTest {
|
||||
val searchTerm = "notEmpty"
|
||||
val suggestionSearchWrapper: SuggestionSearchWrapper = mockk()
|
||||
val searchIteratorWrapper: SuggestionIteratorWrapper = mockk()
|
||||
@ -58,7 +59,7 @@ internal class SearchStateTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
internal fun `visibleResults use recentResults when searchTerm is empty`() {
|
||||
internal fun `visibleResults use recentResults when searchTerm is empty`() = runTest {
|
||||
val results = listOf(RecentSearchListItem(""))
|
||||
assertThat(
|
||||
SearchState(
|
||||
|
Loading…
x
Reference in New Issue
Block a user