Introduced an endless suggestion list in search.

* With the integration of libkiwix12, we now receive full results based on the search term. Previously, we were loading the entire list, but we have now implemented pagination to enhance the search functionality.
* The libkiwix provides us with a Search object, which enables us to obtain the suggestion list using start and end indices. Consequently, we have modified our `ZimSearchResultGenerator` code to return the `Search` object instead of a list.
* To accommodate the changes, we have updated the return type of `SearchResultGenerator` to a nullable Search. This change is necessary because we initialize the `SearchState` when the search is initialized in `SearchViewModel`, and initially, we do not have the Search object available. The nullable return type allows us to pass the Search object when it becomes available.
This commit is contained in:
MohitMali 2023-08-01 17:55:01 +05:30
parent a99a988a98
commit f03830c62f
8 changed files with 89 additions and 31 deletions

View File

@ -18,6 +18,7 @@
<ID>MagicNumber:ArticleCount.kt$ArticleCount$3</ID>
<ID>MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100</ID>
<ID>MagicNumber:DownloadItem.kt$DownloadItem$1000L</ID>
<ID>MagicNumber:SearchState.kt$SearchState$100</ID>
<ID>MagicNumber:DownloaderModule.kt$DownloaderModule$5</ID>
<ID>MagicNumber:FetchDownloadNotificationManager.kt$FetchDownloadNotificationManager$100</ID>
<ID>MagicNumber:FetchDownloadRequester.kt$10</ID>

View File

@ -37,6 +37,13 @@ abstract class BaseDelegateAdapter<ITEM>(
notifyDataSetChanged()
}
// Function to add new data to the adapter
fun addData(newData: List<ITEM>) {
val startPosition = items.size
items = items.toMutableList().apply { addAll(newData) }
notifyItemRangeInserted(startPosition, newData.size)
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int

View File

@ -32,7 +32,6 @@ import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.main.UNINITIALISER_ADDRESS
import org.kiwix.kiwixmobile.core.main.UNINITIALISE_HTML
import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Companion.CONTENT_PREFIX
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.ZimSearchResultListItem
import org.kiwix.kiwixmobile.core.utils.files.FileUtils
import org.kiwix.libkiwix.JNIKiwixException
import org.kiwix.libzim.Archive
@ -143,19 +142,6 @@ class ZimFileReader constructor(
null
}
fun getSearchResultList(search: Search?): List<ZimSearchResultListItem> {
val suggestionList = mutableListOf<ZimSearchResultListItem>()
val suggestionIterator =
search?.getResults(0, search.estimatedMatches.toInt())
suggestionIterator?.let {
while (it.hasNext()) {
val entry = it.next()
suggestionList.add(ZimSearchResultListItem(entry.title))
}
}
return suggestionList
}
fun getPageUrlFrom(title: String): String? =
if (jniKiwixReader.hasEntryByTitle(title))
jniKiwixReader.getEntryByTitle(title).path

View File

@ -65,6 +65,7 @@ import org.kiwix.kiwixmobile.core.utils.SimpleTextListener
import javax.inject.Inject
const val NAV_ARG_SEARCH_STRING = "searchString"
const val LAST_VISIBLE_ITEM = 5
class SearchFragment : BaseFragment() {
@ -76,6 +77,8 @@ class SearchFragment : BaseFragment() {
private val searchViewModel by lazy { viewModel<SearchViewModel>(viewModelFactory) }
private var searchAdapter: SearchAdapter? = null
private var isLoading = false
private var searchState: SearchState? = null
override fun inject(baseActivity: BaseActivity) {
baseActivity.cachedComponent.inject(this)
@ -104,6 +107,22 @@ class SearchFragment : BaseFragment() {
adapter = searchAdapter
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
setHasFixedSize(true)
// Add scroll listener to detect when the last item is reached
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val totalItemCount = layoutManager.itemCount
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
// Check if the user is about to reach the last item
if (!isLoading && totalItemCount <= (lastVisibleItem + LAST_VISIBLE_ITEM)) {
// Load more data when the last item is almost visible
loadMoreSearchResult()
}
}
})
}
lifecycleScope.launchWhenCreated {
searchViewModel.effects.collect { it.invokeWith(this@SearchFragment.coreMainActivity) }
@ -111,6 +130,27 @@ class SearchFragment : BaseFragment() {
handleBackPress()
}
private fun loadMoreSearchResult() {
if (isLoading) {
return
}
val startIndex = searchAdapter?.itemCount ?: 0
// Set isLoading flag to true to prevent multiple load requests at once
isLoading = true
val fetchMoreSearchResults = searchState?.getVisibleResults(startIndex)
if (fetchMoreSearchResults?.isNotEmpty() == true) {
// Check if there is no duplicate entry, this is specially added for searched history items.
val nonDuplicateResults = fetchMoreSearchResults.filter { newItem ->
searchAdapter?.items?.any { it != newItem } == true
}
// Append new data (non-duplicate items) to the existing dataset
searchAdapter?.addData(nonDuplicateResults)
isLoading = false // Set isLoading to false when data is loaded successfully
}
}
private fun handleBackPress() {
activity?.onBackPressedDispatcher?.addCallback(
viewLifecycleOwner,
@ -136,6 +176,7 @@ class SearchFragment : BaseFragment() {
override fun onDestroyView() {
super.onDestroyView()
searchState = null
activity?.intent?.action = null
searchView = null
searchInTextMenuItem = null
@ -194,11 +235,13 @@ class SearchFragment : BaseFragment() {
}
private fun render(state: SearchState) {
searchState = state
searchInTextMenuItem?.isVisible = state.searchOrigin == FromWebView
searchInTextMenuItem?.isEnabled = state.searchTerm.isNotBlank()
fragmentSearchBinding?.searchLoadingIndicator?.isShowing(state.isLoading)
fragmentSearchBinding?.searchNoResults?.isVisible = state.visibleResults.isEmpty()
searchAdapter?.items = state.visibleResults
fragmentSearchBinding?.searchNoResults?.isVisible =
state.getVisibleResults(0).isEmpty()
searchAdapter?.items = state.getVisibleResults(0)
}
private fun onItemClick(it: SearchListItem) {

View File

@ -22,14 +22,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
import org.kiwix.libzim.Search
import javax.inject.Inject
interface SearchResultGenerator {
suspend fun generateSearchResults(
searchTerm: String,
zimFileReader: ZimFileReader?
): List<SearchListItem>
): Search?
}
class ZimSearchResultGenerator @Inject constructor() : SearchResultGenerator {
@ -37,7 +37,7 @@ class ZimSearchResultGenerator @Inject constructor() : SearchResultGenerator {
override suspend fun generateSearchResults(searchTerm: String, zimFileReader: ZimFileReader?) =
withContext(Dispatchers.IO) {
if (searchTerm.isNotEmpty()) readResultsFromZim(searchTerm, zimFileReader)
else emptyList()
else null
}
private suspend fun readResultsFromZim(
@ -46,8 +46,4 @@ class ZimSearchResultGenerator @Inject constructor() : SearchResultGenerator {
) =
reader.also { yield() }
?.searchSuggestions(searchTerm)
.also { yield() }
.run {
reader?.getSearchResultList(this) ?: emptyList()
}
}

View File

@ -26,9 +26,25 @@ data class SearchState(
val recentResults: List<SearchListItem.RecentSearchListItem>,
val searchOrigin: SearchOrigin
) {
val visibleResults = when {
searchTerm.isNotEmpty() -> searchResultsWithTerm.results
else -> recentResults
fun getVisibleResults(startIndex: Int): List<SearchListItem.RecentSearchListItem> =
if (searchTerm.isEmpty()) {
recentResults
} else {
searchResultsWithTerm.search?.let {
val maximumResults = it.estimatedMatches
val safeEndIndex =
if ((startIndex + 100) < maximumResults) startIndex + 100 else maximumResults
val searchIterator =
it.getResults(startIndex, safeEndIndex.toInt())
val searchResults = mutableListOf<SearchListItem.RecentSearchListItem>()
while (searchIterator.hasNext()) {
val entry = searchIterator.next()
searchResults.add(SearchListItem.RecentSearchListItem(entry.title))
}
searchResults
} ?: kotlin.run {
recentResults
}
}
val isLoading = searchTerm != searchResultsWithTerm.searchTerm

View File

@ -59,6 +59,7 @@ import org.kiwix.kiwixmobile.core.search.viewmodel.effects.SearchInPreviousScree
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.ShowDeleteSearchDialog
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.ShowToast
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.StartSpeechInput
import org.kiwix.libzim.Search
import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class)
@ -69,7 +70,15 @@ class SearchViewModel @Inject constructor(
) : ViewModel() {
private val initialState: SearchState =
SearchState("", SearchResultsWithTerm("", emptyList()), emptyList(), FromWebView)
SearchState(
"",
SearchResultsWithTerm(
"",
null
),
emptyList(),
FromWebView
)
val state: MutableStateFlow<SearchState> = MutableStateFlow(initialState)
private val _effects = Channel<SideEffect<*>>()
val effects = _effects.receiveAsFlow()
@ -165,4 +174,4 @@ class SearchViewModel @Inject constructor(
}
}
data class SearchResultsWithTerm(val searchTerm: String, val results: List<SearchListItem>)
data class SearchResultsWithTerm(val searchTerm: String, val search: Search?)

View File

@ -35,7 +35,7 @@ internal class SearchStateTest {
SearchResultsWithTerm("", results),
emptyList(),
FromWebView
).visibleResults
).getVisibleResults(0)
).isEqualTo(results)
}
@ -48,7 +48,7 @@ internal class SearchStateTest {
SearchResultsWithTerm("", emptyList()),
results,
FromWebView
).visibleResults
).getVisibleResults(0)
).isEqualTo(results)
}