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" val aboutLibrariesVersion = "10.3.0"
implementation("com.mikepenz:aboutlibraries-core:$aboutLibrariesVersion") implementation("com.mikepenz:aboutlibraries-core:$aboutLibrariesVersion")
implementation("com.mikepenz:aboutlibraries:$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.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import androidx.paging.LoadState
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import dev.jdtech.jellyfin.R import dev.jdtech.jellyfin.R
import dev.jdtech.jellyfin.viewmodels.LibraryViewModel 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.databinding.FragmentLibraryBinding
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
import dev.jdtech.jellyfin.dialogs.SortDialogFragment import dev.jdtech.jellyfin.dialogs.SortDialogFragment
@ -91,10 +92,25 @@ class LibraryFragment : Fragment() {
} }
binding.itemsRecyclerView.adapter = binding.itemsRecyclerView.adapter =
ViewItemListAdapter(ViewItemListAdapter.OnClickListener { item -> ViewItemPagingAdapter(ViewItemPagingAdapter.OnClickListener { item ->
navigateToMediaInfoFragment(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.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState -> viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
@ -119,8 +135,14 @@ class LibraryFragment : Fragment() {
} }
private fun bindUiStateNormal(uiState: LibraryViewModel.UiState.Normal) { private fun bindUiStateNormal(uiState: LibraryViewModel.UiState.Normal) {
val adapter = binding.itemsRecyclerView.adapter as ViewItemListAdapter val adapter = binding.itemsRecyclerView.adapter as ViewItemPagingAdapter
adapter.submitList(uiState.items) viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
uiState.items.collect {
adapter.submitData(it)
}
}
}
binding.loadingIndicator.isVisible = false binding.loadingIndicator.isVisible = false
binding.itemsRecyclerView.isVisible = true binding.itemsRecyclerView.isVisible = true
binding.errorLayout.errorPanel.isVisible = false 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 package dev.jdtech.jellyfin.repository
import androidx.paging.PagingData
import dev.jdtech.jellyfin.utils.SortBy import dev.jdtech.jellyfin.utils.SortBy
import kotlinx.coroutines.flow.Flow
import org.jellyfin.sdk.model.api.* import org.jellyfin.sdk.model.api.*
import java.util.* import java.util.*
@ -18,6 +20,14 @@ interface JellyfinRepository {
sortOrder: SortOrder = SortOrder.ASCENDING sortOrder: SortOrder = SortOrder.ASCENDING
): List<BaseItemDto> ): 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( suspend fun getPersonItems(
personIds: List<UUID>, personIds: List<UUID>,
includeTypes: List<BaseItemKind>? = null, includeTypes: List<BaseItemKind>? = null,

View file

@ -1,8 +1,12 @@
package dev.jdtech.jellyfin.repository 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.api.JellyfinApi
import dev.jdtech.jellyfin.utils.SortBy import dev.jdtech.jellyfin.utils.SortBy
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.model.api.* import org.jellyfin.sdk.model.api.*
import timber.log.Timber import timber.log.Timber
@ -34,6 +38,32 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
).content.items.orEmpty() ).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( override suspend fun getPersonItems(
personIds: List<UUID>, personIds: List<UUID>,
includeTypes: List<BaseItemKind>?, includeTypes: List<BaseItemKind>?,
@ -51,7 +81,11 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
jellyfinApi.itemsApi.getItems( jellyfinApi.itemsApi.getItems(
jellyfinApi.userId!!, jellyfinApi.userId!!,
filters = listOf(ItemFilter.IS_FAVORITE), filters = listOf(ItemFilter.IS_FAVORITE),
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES, BaseItemKind.EPISODE), includeItemTypes = listOf(
BaseItemKind.MOVIE,
BaseItemKind.SERIES,
BaseItemKind.EPISODE
),
recursive = true recursive = true
).content.items.orEmpty() ).content.items.orEmpty()
} }
@ -61,7 +95,11 @@ class JellyfinRepositoryImpl(private val jellyfinApi: JellyfinApi) : JellyfinRep
jellyfinApi.itemsApi.getItems( jellyfinApi.itemsApi.getItems(
jellyfinApi.userId!!, jellyfinApi.userId!!,
searchTerm = searchQuery, searchTerm = searchQuery,
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES, BaseItemKind.EPISODE), includeItemTypes = listOf(
BaseItemKind.MOVIE,
BaseItemKind.SERIES,
BaseItemKind.EPISODE
),
recursive = true recursive = true
).content.items.orEmpty() ).content.items.orEmpty()
} }

View file

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