refactor: Rewrite search functionality using Paging (#5528)

* refactor: Rewrite search functionality using Paging

* Apply code review comments

* Update app/src/main/java/com/github/libretube/ui/adapters/SearchChannelAdapter.kt

Co-authored-by: Bnyro <82752168+Bnyro@users.noreply.github.com>

* Apply code review suggestions

---------

Co-authored-by: Bnyro <82752168+Bnyro@users.noreply.github.com>
This commit is contained in:
Isira Seneviratne 2024-01-25 15:30:40 +05:30 committed by GitHub
parent a9cfd48ffd
commit 858dbe7ede
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 305 additions and 114 deletions

View File

@ -141,4 +141,7 @@ dependencies {
/* Baseline profile generation */ /* Baseline profile generation */
implementation(libs.androidx.profileinstaller) implementation(libs.androidx.profileinstaller)
"baselineProfile"(project(":baselineprofile")) "baselineProfile"(project(":baselineprofile"))
/* AndroidX Paging */
implementation(libs.androidx.paging)
} }

View File

@ -0,0 +1,185 @@
package com.github.libretube.ui.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.R
import com.github.libretube.api.JsonHelper
import com.github.libretube.api.obj.ContentItem
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.ChannelRowBinding
import com.github.libretube.databinding.PlaylistsRowBinding
import com.github.libretube.databinding.VideoRowBinding
import com.github.libretube.enums.PlaylistType
import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.ui.adapters.callbacks.SearchCallback
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.extensions.setFormattedDuration
import com.github.libretube.ui.extensions.setWatchProgressLength
import com.github.libretube.ui.extensions.setupSubscriptionButton
import com.github.libretube.ui.sheets.ChannelOptionsBottomSheet
import com.github.libretube.ui.sheets.PlaylistOptionsBottomSheet
import com.github.libretube.ui.sheets.VideoOptionsBottomSheet
import com.github.libretube.ui.viewholders.SearchViewHolder
import com.github.libretube.util.TextUtils
import kotlinx.serialization.encodeToString
// TODO: Replace with SearchResultsAdapter when migrating the channel fragment to use Paging as well
class SearchChannelAdapter : ListAdapter<ContentItem, SearchViewHolder>(SearchCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
return when (viewType) {
0 -> SearchViewHolder(VideoRowBinding.inflate(layoutInflater, parent, false))
1 -> SearchViewHolder(ChannelRowBinding.inflate(layoutInflater, parent, false))
2 -> SearchViewHolder(PlaylistsRowBinding.inflate(layoutInflater, parent, false))
else -> throw IllegalArgumentException("Invalid type")
}
}
override fun onBindViewHolder(holder: SearchViewHolder, position: Int) {
val searchItem = currentList[position]
val videoRowBinding = holder.videoRowBinding
val channelRowBinding = holder.channelRowBinding
val playlistRowBinding = holder.playlistRowBinding
if (videoRowBinding != null) {
bindVideo(searchItem, videoRowBinding, position)
} else if (channelRowBinding != null) {
bindChannel(searchItem, channelRowBinding)
} else if (playlistRowBinding != null) {
bindPlaylist(searchItem, playlistRowBinding)
}
}
override fun getItemViewType(position: Int): Int {
return when (currentList[position].type) {
StreamItem.TYPE_STREAM -> 0
StreamItem.TYPE_CHANNEL -> 1
StreamItem.TYPE_PLAYLIST -> 2
else -> 3
}
}
private fun bindVideo(item: ContentItem, binding: VideoRowBinding, position: Int) {
binding.apply {
thumbnail.setImageDrawable(null)
ImageHelper.loadImage(item.thumbnail, thumbnail)
thumbnailDuration.setFormattedDuration(item.duration, item.isShort)
videoTitle.text = item.title
val viewsString = item.views.takeIf { it != -1L }?.formatShort().orEmpty()
val uploadDate = item.uploaded.takeIf { it > 0 }?.let {
" ${TextUtils.SEPARATOR} ${TextUtils.formatRelativeDate(root.context, it)}"
}.orEmpty()
videoInfo.text = root.context.getString(
R.string.normal_views,
viewsString,
uploadDate
)
channelContainer.isGone = true
root.setOnClickListener {
NavigationHelper.navigateVideo(root.context, item.url)
}
val videoId = item.url.toID()
val activity = (root.context as BaseActivity)
val fragmentManager = activity.supportFragmentManager
root.setOnLongClickListener {
fragmentManager.setFragmentResultListener(
VideoOptionsBottomSheet.VIDEO_OPTIONS_SHEET_REQUEST_KEY,
activity
) { _, _ ->
notifyItemChanged(position)
}
val sheet = VideoOptionsBottomSheet()
val contentItemString = JsonHelper.json.encodeToString(item)
val streamItem: StreamItem = JsonHelper.json.decodeFromString(contentItemString)
sheet.arguments = bundleOf(IntentData.streamItem to streamItem)
sheet.show(fragmentManager, SearchChannelAdapter::class.java.name)
true
}
channelContainer.setOnClickListener {
NavigationHelper.navigateChannel(root.context, item.uploaderUrl)
}
watchProgress.setWatchProgressLength(videoId, item.duration)
}
}
private fun bindChannel(item: ContentItem, binding: ChannelRowBinding) {
binding.apply {
searchChannelImage.setImageDrawable(null)
ImageHelper.loadImage(item.thumbnail, searchChannelImage, true)
searchChannelName.text = item.name
val subscribers = item.subscribers.formatShort()
searchViews.text = if (item.subscribers >= 0 && item.videos >= 0) {
root.context.getString(R.string.subscriberAndVideoCounts, subscribers, item.videos)
} else if (item.subscribers >= 0) {
root.context.getString(R.string.subscribers, subscribers)
} else if (item.videos >= 0) {
root.context.getString(R.string.videoCount, item.videos)
} else {
""
}
root.setOnClickListener {
NavigationHelper.navigateChannel(root.context, item.url)
}
var subscribed = false
binding.searchSubButton.setupSubscriptionButton(item.url.toID(), item.name?.toID()) {
subscribed = it
}
root.setOnLongClickListener {
val channelOptionsSheet = ChannelOptionsBottomSheet()
channelOptionsSheet.arguments = bundleOf(
IntentData.channelId to item.url.toID(),
IntentData.channelName to item.name,
IntentData.isSubscribed to subscribed
)
channelOptionsSheet.show((root.context as BaseActivity).supportFragmentManager)
true
}
}
}
private fun bindPlaylist(item: ContentItem, binding: PlaylistsRowBinding) {
binding.apply {
playlistThumbnail.setImageDrawable(null)
ImageHelper.loadImage(item.thumbnail, playlistThumbnail)
if (item.videos != -1L) videoCount.text = item.videos.toString()
playlistTitle.text = item.name
playlistDescription.text = item.uploaderName
root.setOnClickListener {
NavigationHelper.navigatePlaylist(root.context, item.url, PlaylistType.PUBLIC)
}
root.setOnLongClickListener {
val playlistId = item.url.toID()
val playlistName = item.name!!
val sheet = PlaylistOptionsBottomSheet()
sheet.arguments = bundleOf(
IntentData.playlistId to playlistId,
IntentData.playlistName to playlistName,
IntentData.playlistType to PlaylistType.PUBLIC
)
sheet.show(
(root.context as BaseActivity).supportFragmentManager,
PlaylistOptionsBottomSheet::class.java.name
)
true
}
}
}
}

View File

@ -3,9 +3,7 @@ package com.github.libretube.ui.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isGone import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.JsonHelper import com.github.libretube.api.JsonHelper
import com.github.libretube.api.obj.ContentItem import com.github.libretube.api.obj.ContentItem
@ -19,6 +17,7 @@ import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.toID import com.github.libretube.extensions.toID
import com.github.libretube.helpers.ImageHelper import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.ui.adapters.callbacks.SearchCallback
import com.github.libretube.ui.base.BaseActivity import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.extensions.setFormattedDuration import com.github.libretube.ui.extensions.setFormattedDuration
import com.github.libretube.ui.extensions.setWatchProgressLength import com.github.libretube.ui.extensions.setWatchProgressLength
@ -30,10 +29,9 @@ import com.github.libretube.ui.viewholders.SearchViewHolder
import com.github.libretube.util.TextUtils import com.github.libretube.util.TextUtils
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
class SearchAdapter( class SearchResultsAdapter(
private val isChannelAdapter: Boolean = false,
private val timeStamp: Long = 0 private val timeStamp: Long = 0
) : ListAdapter<ContentItem, SearchViewHolder>(SearchCallback) { ) : PagingDataAdapter<ContentItem, SearchViewHolder>(SearchCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchViewHolder {
val layoutInflater = LayoutInflater.from(parent.context) val layoutInflater = LayoutInflater.from(parent.context)
@ -55,7 +53,7 @@ class SearchAdapter(
} }
override fun onBindViewHolder(holder: SearchViewHolder, position: Int) { override fun onBindViewHolder(holder: SearchViewHolder, position: Int) {
val searchItem = currentList[position] val searchItem = getItem(position)!!
val videoRowBinding = holder.videoRowBinding val videoRowBinding = holder.videoRowBinding
val channelRowBinding = holder.channelRowBinding val channelRowBinding = holder.channelRowBinding
@ -71,7 +69,7 @@ class SearchAdapter(
} }
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return when (currentList[position].type) { return when (getItem(position)?.type) {
StreamItem.TYPE_STREAM -> 0 StreamItem.TYPE_STREAM -> 0
StreamItem.TYPE_CHANNEL -> 1 StreamItem.TYPE_CHANNEL -> 1
StreamItem.TYPE_PLAYLIST -> 2 StreamItem.TYPE_PLAYLIST -> 2
@ -95,13 +93,8 @@ class SearchAdapter(
uploadDate uploadDate
) )
// only display channel related info if not in a channel tab channelName.text = item.uploaderName
if (!isChannelAdapter) { ImageHelper.loadImage(item.uploaderAvatar, channelImage, true)
channelName.text = item.uploaderName
ImageHelper.loadImage(item.uploaderAvatar, channelImage, true)
} else {
channelContainer.isGone = true
}
root.setOnClickListener { root.setOnClickListener {
NavigationHelper.navigateVideo(root.context, item.url, timestamp = timeStamp) NavigationHelper.navigateVideo(root.context, item.url, timestamp = timeStamp)
@ -121,7 +114,7 @@ class SearchAdapter(
val contentItemString = JsonHelper.json.encodeToString(item) val contentItemString = JsonHelper.json.encodeToString(item)
val streamItem: StreamItem = JsonHelper.json.decodeFromString(contentItemString) val streamItem: StreamItem = JsonHelper.json.decodeFromString(contentItemString)
sheet.arguments = bundleOf(IntentData.streamItem to streamItem) sheet.arguments = bundleOf(IntentData.streamItem to streamItem)
sheet.show(fragmentManager, SearchAdapter::class.java.name) sheet.show(fragmentManager, SearchResultsAdapter::class.java.name)
true true
} }
channelContainer.setOnClickListener { channelContainer.setOnClickListener {
@ -196,14 +189,4 @@ class SearchAdapter(
} }
} }
} }
private object SearchCallback : DiffUtil.ItemCallback<ContentItem>() {
override fun areItemsTheSame(oldItem: ContentItem, newItem: ContentItem): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: ContentItem, newItem: ContentItem): Boolean {
return true
}
}
} }

View File

@ -0,0 +1,12 @@
package com.github.libretube.ui.adapters.callbacks
import androidx.recyclerview.widget.DiffUtil
import com.github.libretube.api.obj.ContentItem
object SearchCallback : DiffUtil.ItemCallback<ContentItem>() {
override fun areItemsTheSame(oldItem: ContentItem, newItem: ContentItem): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: ContentItem, newItem: ContentItem) = true
}

View File

@ -26,7 +26,7 @@ import com.github.libretube.helpers.ImageHelper
import com.github.libretube.helpers.NavigationHelper import com.github.libretube.helpers.NavigationHelper
import com.github.libretube.obj.ChannelTabs import com.github.libretube.obj.ChannelTabs
import com.github.libretube.obj.ShareData import com.github.libretube.obj.ShareData
import com.github.libretube.ui.adapters.SearchAdapter import com.github.libretube.ui.adapters.SearchChannelAdapter
import com.github.libretube.ui.adapters.VideosAdapter import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.base.DynamicLayoutManagerFragment import com.github.libretube.ui.base.DynamicLayoutManagerFragment
import com.github.libretube.ui.dialogs.ShareDialog import com.github.libretube.ui.dialogs.ShareDialog
@ -57,7 +57,7 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
) )
private var channelTabs: List<ChannelTab> = emptyList() private var channelTabs: List<ChannelTab> = emptyList()
private var nextPages = Array<String?>(5) { null } private var nextPages = Array<String?>(5) { null }
private var searchAdapter: SearchAdapter? = null private var searchChannelAdapter: SearchChannelAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -293,9 +293,9 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
val binding = _binding ?: return@launch val binding = _binding ?: return@launch
searchAdapter = SearchAdapter(true) searchChannelAdapter = SearchChannelAdapter()
binding.channelRecView.adapter = searchAdapter binding.channelRecView.adapter = searchChannelAdapter
searchAdapter?.submitList(response.content) searchChannelAdapter?.submitList(response.content)
binding.channelRefresh.isRefreshing = false binding.channelRefresh.isRefreshing = false
isLoading = false isLoading = false
@ -320,7 +320,7 @@ class ChannelFragment : DynamicLayoutManagerFragment() {
content = content.deArrow() content = content.deArrow()
} }
searchAdapter?.let { searchChannelAdapter?.let {
it.submitList(it.currentList + newContent.content) it.submitList(it.currentList + newContent.content)
} }

View File

@ -1,47 +1,40 @@
package com.github.libretube.ui.fragments package com.github.libretube.ui.fragments
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.SoftwareKeyboardControllerCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.paging.LoadState
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.FragmentSearchResultBinding import com.github.libretube.databinding.FragmentSearchResultBinding
import com.github.libretube.db.DatabaseHelper import com.github.libretube.db.DatabaseHelper
import com.github.libretube.db.obj.SearchHistoryItem import com.github.libretube.db.obj.SearchHistoryItem
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.ceilHalf import com.github.libretube.extensions.ceilHalf
import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.activities.MainActivity import com.github.libretube.ui.activities.MainActivity
import com.github.libretube.ui.adapters.SearchAdapter import com.github.libretube.ui.adapters.SearchResultsAdapter
import com.github.libretube.ui.base.DynamicLayoutManagerFragment import com.github.libretube.ui.base.DynamicLayoutManagerFragment
import com.github.libretube.ui.dialogs.ShareDialog import com.github.libretube.ui.models.SearchResultViewModel
import com.github.libretube.util.TextUtils
import com.github.libretube.util.TextUtils.toTimeInSeconds import com.github.libretube.util.TextUtils.toTimeInSeconds
import com.github.libretube.util.deArrow
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
class SearchResultFragment : DynamicLayoutManagerFragment() { class SearchResultFragment : DynamicLayoutManagerFragment() {
private var _binding: FragmentSearchResultBinding? = null private var _binding: FragmentSearchResultBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val args by navArgs<SearchResultFragmentArgs>() private val args by navArgs<SearchResultFragmentArgs>()
private val viewModel by viewModels<SearchResultViewModel>()
private var nextPage: String? = null
private lateinit var searchAdapter: SearchAdapter
private var searchFilter = "all"
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -68,7 +61,7 @@ class SearchResultFragment : DynamicLayoutManagerFragment() {
// filter options // filter options
binding.filterChipGroup.setOnCheckedStateChangeListener { _, _ -> binding.filterChipGroup.setOnCheckedStateChangeListener { _, _ ->
searchFilter = when ( viewModel.setFilter(when (
binding.filterChipGroup.checkedChipId binding.filterChipGroup.checkedChipId
) { ) {
R.id.chip_all -> "all" R.id.chip_all -> "all"
@ -81,80 +74,28 @@ class SearchResultFragment : DynamicLayoutManagerFragment() {
R.id.chip_music_playlists -> "music_playlists" R.id.chip_music_playlists -> "music_playlists"
R.id.chip_music_artists -> "music_artists" R.id.chip_music_artists -> "music_artists"
else -> throw IllegalArgumentException("Filter out of range") else -> throw IllegalArgumentException("Filter out of range")
} })
fetchSearch()
} }
fetchSearch() val timeStamp = args.query.toHttpUrlOrNull()?.queryParameter("t")?.toTimeInSeconds()
val searchResultsAdapter = SearchResultsAdapter(timeStamp ?: 0)
binding.searchRecycler.adapter = searchResultsAdapter
binding.searchRecycler.viewTreeObserver.addOnScrollChangedListener { viewLifecycleOwner.lifecycleScope.launch {
if (_binding?.searchRecycler?.canScrollVertically(1) == false && repeatOnLifecycle(Lifecycle.State.STARTED) {
nextPage != null searchResultsAdapter.loadStateFlow.collect {
) { val isLoading = it.source.refresh is LoadState.Loading
fetchNextSearchItems() binding.progress.isVisible = isLoading
} binding.searchResultsLayout.isGone = isLoading
}
}
private fun fetchSearch() {
_binding?.progress?.isVisible = true
_binding?.searchResultsLayout?.isGone = true
lifecycleScope.launch {
var timeStamp: Long? = null
// parse search URLs from YouTube entered in the search bar
val searchQuery = args.query.toHttpUrlOrNull()?.let {
val videoId = TextUtils.getVideoIdFromUrl(it.toString()) ?: args.query
timeStamp = it.queryParameter("t")?.toTimeInSeconds()
"${ShareDialog.YOUTUBE_FRONTEND_URL}/watch?v=$videoId"
} ?: args.query
view?.let { SoftwareKeyboardControllerCompat(it).hide() }
val response = try {
withContext(Dispatchers.IO) {
RetrofitInstance.api.getSearchResults(searchQuery, searchFilter).apply {
items = items.deArrow()
}
} }
} catch (e: Exception) {
Log.e(TAG(), e.toString())
context?.toastFromMainDispatcher(R.string.unknown_error)
return@launch
} }
val binding = _binding ?: return@launch
searchAdapter = SearchAdapter(timeStamp = timeStamp ?: 0)
binding.searchRecycler.adapter = searchAdapter
searchAdapter.submitList(response.items)
binding.searchResultsLayout.isVisible = true
binding.progress.isGone = true
binding.noSearchResult.isVisible = response.items.isEmpty()
nextPage = response.nextpage
} }
}
private fun fetchNextSearchItems() { viewLifecycleOwner.lifecycleScope.launch {
lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) {
val response = try { viewModel.searchResultsFlow.collectLatest {
withContext(Dispatchers.IO) { searchResultsAdapter.submitData(it)
RetrofitInstance.api.getSearchResultsNextPage(
args.query,
searchFilter,
nextPage!!
).apply {
items = items.deArrow()
}
} }
} catch (e: Exception) {
Log.e(TAG(), e.toString())
return@launch
}
nextPage = response.nextpage
if (response.items.isNotEmpty()) {
searchAdapter.submitList(searchAdapter.currentList + response.items)
} }
} }
} }

View File

@ -0,0 +1,39 @@
package com.github.libretube.ui.models
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.ui.fragments.SearchResultFragmentArgs
import com.github.libretube.ui.models.sources.SearchPagingSource
import com.github.libretube.util.TextUtils
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
class SearchResultViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
private val args = SearchResultFragmentArgs.fromSavedStateHandle(savedStateHandle)
// parse search URLs from YouTube entered in the search bar
private val searchQuery = args.query.toHttpUrlOrNull()?.let {
val videoId = TextUtils.getVideoIdFromUrl(it.toString()) ?: args.query
"${ShareDialog.YOUTUBE_FRONTEND_URL}/watch?v=$videoId"
} ?: args.query
private val filterMutableData = MutableStateFlow("all")
@OptIn(ExperimentalCoroutinesApi::class)
val searchResultsFlow = filterMutableData.flatMapLatest {
Pager(
PagingConfig(pageSize = 20, enablePlaceholders = false),
pagingSourceFactory = { SearchPagingSource(searchQuery, it) }
).flow
}
.cachedIn(viewModelScope)
fun setFilter(filter: String) {
filterMutableData.value = filter
}
}

View File

@ -0,0 +1,26 @@
package com.github.libretube.ui.models.sources
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.ContentItem
import com.github.libretube.util.deArrow
import retrofit2.HttpException
class SearchPagingSource(
private val searchQuery: String,
private val searchFilter: String
): PagingSource<String, ContentItem>() {
override fun getRefreshKey(state: PagingState<String, ContentItem>) = null
override suspend fun load(params: LoadParams<String>): LoadResult<String, ContentItem> {
return try {
val result = params.key?.let {
RetrofitInstance.api.getSearchResultsNextPage(searchQuery, searchFilter, it)
} ?: RetrofitInstance.api.getSearchResults(searchQuery, searchFilter)
LoadResult.Page(result.items.deArrow(), null, result.nextpage)
} catch (e: HttpException) {
LoadResult.Error(e)
}
}
}

View File

@ -29,6 +29,7 @@ uiautomator = "2.2.0"
benchmarkMacroJunit4 = "1.2.3" benchmarkMacroJunit4 = "1.2.3"
baselineprofile = "1.2.3" baselineprofile = "1.2.3"
profileinstaller = "1.3.1" profileinstaller = "1.3.1"
paging = "3.2.1"
[libraries] [libraries]
androidx-activity = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" } androidx-activity = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity" }
@ -72,6 +73,7 @@ kotlinx-serialization-retrofit = { group = "com.jakewharton.retrofit", name = "r
androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" }
androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" } androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" }
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" } androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }
androidx-paging = { group = "androidx.paging", name = "paging-runtime-ktx", version.ref = "paging" }
[plugins] [plugins]
androidTest = { id = "com.android.test", version.ref = "gradle" } androidTest = { id = "com.android.test", version.ref = "gradle" }