Add paging support to LibraryFragment (#124)

* Add paging support to the LibraryFragment

* Fix error handling
This commit is contained in:
Jarne Demeulemeester 2022-06-17 15:16:29 +02:00 committed by GitHub
parent 82b235d3ae
commit 16c2cd634d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 204 additions and 9 deletions

View file

@ -124,4 +124,7 @@ dependencies {
val aboutLibrariesVersion = "10.3.0"
implementation("com.mikepenz:aboutlibraries-core:$aboutLibrariesVersion")
implementation("com.mikepenz:aboutlibraries:$aboutLibrariesVersion")
val pagingVersion = "3.1.1"
implementation("androidx.paging:paging-runtime-ktx:$pagingVersion")
}

View file

@ -0,0 +1,69 @@
package dev.jdtech.jellyfin.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.databinding.BaseItemBinding
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
class ViewItemPagingAdapter(
private val onClickListener: OnClickListener,
private val fixedWidth: Boolean = false,
) : PagingDataAdapter<BaseItemDto, ViewItemPagingAdapter.ItemViewHolder>(DiffCallback) {
class ItemViewHolder(private var binding: BaseItemBinding, private val parent: ViewGroup) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: BaseItemDto, fixedWidth: Boolean) {
binding.item = item
binding.itemName.text =
if (item.type == BaseItemKind.EPISODE) item.seriesName else item.name
binding.itemCount.visibility =
if (item.userData?.unplayedItemCount != null && item.userData?.unplayedItemCount!! > 0) View.VISIBLE else View.GONE
if (fixedWidth) {
binding.itemLayout.layoutParams.width =
parent.resources.getDimension(R.dimen.overview_media_width).toInt()
(binding.itemLayout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = 0
}
binding.executePendingBindings()
}
}
companion object DiffCallback : DiffUtil.ItemCallback<BaseItemDto>() {
override fun areItemsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
return ItemViewHolder(
BaseItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
), parent
)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.itemView.setOnClickListener {
onClickListener.onClick(item)
}
holder.bind(item, fixedWidth)
}
}
class OnClickListener(val clickListener: (item: BaseItemDto) -> Unit) {
fun onClick(item: BaseItemDto) = clickListener(item)
}
}

View file

@ -11,10 +11,11 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.paging.LoadState
import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.viewmodels.LibraryViewModel
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
import dev.jdtech.jellyfin.adapters.ViewItemPagingAdapter
import dev.jdtech.jellyfin.databinding.FragmentLibraryBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.dialogs.SortDialogFragment
@ -91,10 +92,25 @@ class LibraryFragment : Fragment() {
}
binding.itemsRecyclerView.adapter =
ViewItemListAdapter(ViewItemListAdapter.OnClickListener { item ->
ViewItemPagingAdapter(ViewItemPagingAdapter.OnClickListener { item ->
navigateToMediaInfoFragment(item)
})
(binding.itemsRecyclerView.adapter as ViewItemPagingAdapter).addLoadStateListener {
when (it.refresh) {
is LoadState.Error -> {
val error = Exception((it.refresh as LoadState.Error).error)
bindUiStateError(LibraryViewModel.UiState.Error(error))
}
is LoadState.Loading -> {
bindUiStateLoading()
}
is LoadState.NotLoading -> {
binding.loadingIndicator.isVisible = false
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
@ -119,8 +135,14 @@ class LibraryFragment : Fragment() {
}
private fun bindUiStateNormal(uiState: LibraryViewModel.UiState.Normal) {
val adapter = binding.itemsRecyclerView.adapter as ViewItemListAdapter
adapter.submitList(uiState.items)
val adapter = binding.itemsRecyclerView.adapter as ViewItemPagingAdapter
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
uiState.items.collect {
adapter.submitData(it)
}
}
}
binding.loadingIndicator.isVisible = false
binding.itemsRecyclerView.isVisible = true
binding.errorLayout.errorPanel.isVisible = false

View file

@ -0,0 +1,50 @@
package dev.jdtech.jellyfin.repository
import androidx.paging.PagingSource
import androidx.paging.PagingState
import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.utils.SortBy
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.SortOrder
import timber.log.Timber
import java.util.*
class ItemsPagingSource(
private val jellyfinApi: JellyfinApi,
private val parentId: UUID?,
private val includeTypes: List<BaseItemKind>?,
private val recursive: Boolean,
private val sortBy: SortBy,
private val sortOrder: SortOrder
) : PagingSource<Int, BaseItemDto>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, BaseItemDto> {
val position = params.key ?: 0
Timber.d("Retrieving position: $position")
return try {
val response = jellyfinApi.itemsApi.getItems(
jellyfinApi.userId!!,
parentId = parentId,
includeItemTypes = includeTypes,
recursive = recursive,
sortBy = listOf(sortBy.SortString),
sortOrder = listOf(sortOrder),
startIndex = position,
limit = params.loadSize
).content.items.orEmpty()
LoadResult.Page(
data = response,
prevKey = if (position == 0) null else position - params.loadSize,
nextKey = if (response.isEmpty()) null else position + params.loadSize
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, BaseItemDto>): Int {
return 0
}
}

View file

@ -1,7 +1,9 @@
package dev.jdtech.jellyfin.repository
import androidx.paging.PagingData
import dev.jdtech.jellyfin.utils.SortBy
import kotlinx.coroutines.flow.Flow
import org.jellyfin.sdk.model.api.*
import java.util.*
@ -18,6 +20,14 @@ interface JellyfinRepository {
sortOrder: SortOrder = SortOrder.ASCENDING
): List<BaseItemDto>
suspend fun getItemsPaging(
parentId: UUID? = null,
includeTypes: List<BaseItemKind>? = null,
recursive: Boolean = false,
sortBy: SortBy = SortBy.defaultValue,
sortOrder: SortOrder = SortOrder.ASCENDING
): Flow<PagingData<BaseItemDto>>
suspend fun getPersonItems(
personIds: List<UUID>,
includeTypes: List<BaseItemKind>? = null,

View file

@ -1,8 +1,12 @@
package dev.jdtech.jellyfin.repository
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.utils.SortBy
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.*
import timber.log.Timber
@ -34,6 +38,32 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
).content.items.orEmpty()
}
override suspend fun getItemsPaging(
parentId: UUID?,
includeTypes: List<BaseItemKind>?,
recursive: Boolean,
sortBy: SortBy,
sortOrder: SortOrder
): Flow<PagingData<BaseItemDto>> {
return Pager(
config = PagingConfig(
pageSize = 10,
maxSize = 100,
enablePlaceholders = false
),
pagingSourceFactory = {
ItemsPagingSource(
jellyfinApi,
parentId,
includeTypes,
recursive,
sortBy,
sortOrder
)
}
).flow
}
override suspend fun getPersonItems(
personIds: List<UUID>,
includeTypes: List<BaseItemKind>?,
@ -51,7 +81,11 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
jellyfinApi.itemsApi.getItems(
jellyfinApi.userId!!,
filters = listOf(ItemFilter.IS_FAVORITE),
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES, BaseItemKind.EPISODE),
includeItemTypes = listOf(
BaseItemKind.MOVIE,
BaseItemKind.SERIES,
BaseItemKind.EPISODE
),
recursive = true
).content.items.orEmpty()
}
@ -61,7 +95,11 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
jellyfinApi.itemsApi.getItems(
jellyfinApi.userId!!,
searchTerm = searchQuery,
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES, BaseItemKind.EPISODE),
includeItemTypes = listOf(
BaseItemKind.MOVIE,
BaseItemKind.SERIES,
BaseItemKind.EPISODE
),
recursive = true
).content.items.orEmpty()
}

View file

@ -1,9 +1,11 @@
package dev.jdtech.jellyfin.viewmodels
import androidx.lifecycle.*
import androidx.paging.PagingData
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.repository.JellyfinRepository
import dev.jdtech.jellyfin.utils.SortBy
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.BaseItemDto
@ -22,7 +24,7 @@ constructor(
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
sealed class UiState {
data class Normal(val items: List<BaseItemDto>) : UiState()
data class Normal(val items: Flow<PagingData<BaseItemDto>>) : UiState()
object Loading : UiState()
data class Error(val error: Exception) : UiState()
}
@ -46,8 +48,9 @@ constructor(
viewModelScope.launch {
uiState.emit(UiState.Loading)
try {
val items = jellyfinRepository.getItems(
parentId,
val items = jellyfinRepository.getItemsPaging(
parentId = parentId,
includeTypes = if (itemType != null) listOf(itemType) else null,
recursive = true,
sortBy = sortBy,