2021-04-22 11:47:18 +02:00
|
|
|
package org.pixeldroid.app.posts.feeds
|
2020-11-27 17:02:52 +01:00
|
|
|
|
|
|
|
import android.view.LayoutInflater
|
|
|
|
import android.view.ViewGroup
|
2021-03-19 17:28:13 +01:00
|
|
|
import android.widget.ProgressBar
|
|
|
|
import androidx.constraintlayout.motion.widget.MotionLayout
|
2020-11-27 17:02:52 +01:00
|
|
|
import androidx.core.view.isVisible
|
|
|
|
import androidx.core.view.size
|
2021-03-26 10:42:51 +01:00
|
|
|
import androidx.lifecycle.LifecycleCoroutineScope
|
2020-11-27 17:02:52 +01:00
|
|
|
import androidx.paging.LoadState
|
|
|
|
import androidx.paging.LoadStateAdapter
|
|
|
|
import androidx.paging.PagingDataAdapter
|
|
|
|
import androidx.recyclerview.widget.RecyclerView
|
2021-03-19 17:28:13 +01:00
|
|
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
2021-04-30 14:34:52 +02:00
|
|
|
import com.google.gson.Gson
|
2021-04-22 11:47:18 +02:00
|
|
|
import org.pixeldroid.app.R
|
|
|
|
import org.pixeldroid.app.databinding.ErrorLayoutBinding
|
|
|
|
import org.pixeldroid.app.databinding.LoadStateFooterViewItemBinding
|
|
|
|
import org.pixeldroid.app.posts.feeds.uncachedFeeds.FeedViewModel
|
|
|
|
import org.pixeldroid.app.utils.api.objects.FeedContent
|
2021-03-26 10:42:51 +01:00
|
|
|
import kotlinx.coroutines.Job
|
|
|
|
import kotlinx.coroutines.flow.collectLatest
|
|
|
|
import kotlinx.coroutines.launch
|
2021-04-30 14:34:52 +02:00
|
|
|
import retrofit2.HttpException
|
2020-11-27 17:02:52 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Shows or hides the error in the different FeedFragments
|
|
|
|
*/
|
2021-03-19 17:28:13 +01:00
|
|
|
private fun showError(
|
|
|
|
errorText: String, show: Boolean = true,
|
|
|
|
motionLayout: MotionLayout,
|
|
|
|
errorLayout: ErrorLayoutBinding){
|
|
|
|
|
|
|
|
if(show) {
|
|
|
|
motionLayout.transitionToEnd()
|
|
|
|
errorLayout.errorText.text = errorText
|
|
|
|
} else if(motionLayout.progress == 1F) {
|
|
|
|
motionLayout.transitionToStart()
|
2020-11-27 17:02:52 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialises the [RecyclerView] adapter for the different FeedFragments.
|
|
|
|
*
|
|
|
|
* Makes the UI respond to various [LoadState]s, including errors when an error message is shown.
|
|
|
|
*/
|
2021-03-19 17:28:13 +01:00
|
|
|
internal fun <T: Any> initAdapter(
|
|
|
|
progressBar: ProgressBar, swipeRefreshLayout: SwipeRefreshLayout,
|
|
|
|
recyclerView: RecyclerView, motionLayout: MotionLayout, errorLayout: ErrorLayoutBinding,
|
|
|
|
adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>) {
|
|
|
|
|
|
|
|
recyclerView.adapter = adapter.withLoadStateFooter(
|
2020-11-27 17:02:52 +01:00
|
|
|
footer = ReposLoadStateAdapter { adapter.retry() }
|
|
|
|
)
|
|
|
|
|
|
|
|
adapter.addLoadStateListener { loadState ->
|
|
|
|
|
2021-03-19 17:28:13 +01:00
|
|
|
if(!progressBar.isVisible && swipeRefreshLayout.isRefreshing) {
|
2020-11-27 17:02:52 +01:00
|
|
|
// Stop loading spinner when loading is done
|
2021-04-28 15:12:21 +02:00
|
|
|
swipeRefreshLayout.isRefreshing = loadState.refresh is LoadState.Loading
|
2020-11-27 17:02:52 +01:00
|
|
|
}
|
|
|
|
|
2021-04-30 14:34:52 +02:00
|
|
|
// ProgressBar should stop showing as soon as the source stops loading ("source"
|
|
|
|
// meaning the database, so don't wait on the network)
|
|
|
|
val sourceLoading = loadState.source.refresh is LoadState.Loading
|
|
|
|
if (!sourceLoading && adapter.itemCount > 0) {
|
|
|
|
recyclerView.isVisible = true
|
|
|
|
progressBar.isVisible = false
|
|
|
|
}
|
2020-11-27 17:02:52 +01:00
|
|
|
|
2021-04-28 15:12:21 +02:00
|
|
|
// Show any error, regardless of whether it came from RemoteMediator or PagingSource
|
2020-11-27 17:02:52 +01:00
|
|
|
val errorState = loadState.source.append as? LoadState.Error
|
|
|
|
?: loadState.source.prepend as? LoadState.Error
|
|
|
|
?: loadState.source.refresh as? LoadState.Error
|
|
|
|
?: loadState.append as? LoadState.Error
|
|
|
|
?: loadState.prepend as? LoadState.Error
|
|
|
|
?: loadState.refresh as? LoadState.Error
|
|
|
|
errorState?.let {
|
2021-04-30 14:34:52 +02:00
|
|
|
val error: String = (it.error as? HttpException)?.response()?.errorBody()?.string()?.ifEmpty { null }?.let { s ->
|
|
|
|
Gson().fromJson(s, org.pixeldroid.app.utils.api.objects.Error::class.java)?.error?.ifBlank { null }
|
|
|
|
} ?: it.error.localizedMessage.orEmpty()
|
|
|
|
showError(motionLayout = motionLayout, errorLayout = errorLayout, errorText = error)
|
2021-03-19 17:28:13 +01:00
|
|
|
}
|
2021-04-30 14:34:52 +02:00
|
|
|
|
|
|
|
// If the state is not an error, hide the error layout, or show message that the feed is empty
|
2021-03-19 17:28:13 +01:00
|
|
|
if(errorState == null) {
|
2021-04-30 14:34:52 +02:00
|
|
|
if (adapter.itemCount == 0
|
|
|
|
&& loadState.append is LoadState.NotLoading
|
|
|
|
&& loadState.append.endOfPaginationReached
|
|
|
|
) {
|
|
|
|
progressBar.isVisible = false
|
|
|
|
showError(
|
|
|
|
motionLayout = motionLayout, errorLayout = errorLayout,
|
|
|
|
errorText = errorLayout.root.context.getString(R.string.empty_feed)
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
showError(motionLayout = motionLayout, errorLayout = errorLayout, show = false, errorText = "")
|
|
|
|
}
|
2020-11-27 17:02:52 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-28 14:28:32 +02:00
|
|
|
fun <T: FeedContent> launch(
|
|
|
|
job: Job?, lifecycleScope: LifecycleCoroutineScope, viewModel: FeedViewModel<T>,
|
|
|
|
pagingDataAdapter: PagingDataAdapter<T, RecyclerView.ViewHolder>): Job {
|
2021-03-26 10:42:51 +01:00
|
|
|
// Make sure we cancel the previous job before creating a new one
|
|
|
|
job?.cancel()
|
|
|
|
return lifecycleScope.launch {
|
2021-04-28 14:28:32 +02:00
|
|
|
viewModel.flow.collectLatest {
|
2021-03-26 10:42:51 +01:00
|
|
|
pagingDataAdapter.submitData(it)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-03-19 17:28:13 +01:00
|
|
|
|
2020-11-27 17:02:52 +01:00
|
|
|
/**
|
|
|
|
* Adapter to the show the a [RecyclerView] item for a [LoadState], with a callback to retry if
|
|
|
|
* the retry button is pressed.
|
|
|
|
*/
|
|
|
|
class ReposLoadStateAdapter(
|
|
|
|
private val retry: () -> Unit
|
|
|
|
) : LoadStateAdapter<ReposLoadStateViewHolder>() {
|
|
|
|
override fun onBindViewHolder(holder: ReposLoadStateViewHolder, loadState: LoadState) {
|
|
|
|
holder.bind(loadState)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ReposLoadStateViewHolder {
|
|
|
|
return ReposLoadStateViewHolder.create(parent, retry)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* [RecyclerView.ViewHolder] that is shown at the end of the feed to indicate loading or errors
|
|
|
|
* in the loading of appending values.
|
|
|
|
*/
|
|
|
|
class ReposLoadStateViewHolder(
|
|
|
|
private val binding: LoadStateFooterViewItemBinding,
|
|
|
|
retry: () -> Unit
|
|
|
|
) : RecyclerView.ViewHolder(binding.root) {
|
|
|
|
|
|
|
|
init {
|
|
|
|
binding.retryButton.setOnClickListener { retry.invoke() }
|
|
|
|
}
|
|
|
|
|
|
|
|
fun bind(loadState: LoadState) {
|
|
|
|
if (loadState is LoadState.Error) {
|
|
|
|
binding.errorMsg.text = loadState.error.localizedMessage
|
|
|
|
}
|
|
|
|
binding.progressBar.isVisible = loadState is LoadState.Loading
|
|
|
|
binding.retryButton.isVisible = loadState !is LoadState.Loading
|
|
|
|
binding.errorMsg.isVisible = loadState !is LoadState.Loading
|
|
|
|
}
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
fun create(parent: ViewGroup, retry: () -> Unit): ReposLoadStateViewHolder {
|
|
|
|
val view = LayoutInflater.from(parent.context)
|
|
|
|
.inflate(R.layout.load_state_footer_view_item, parent, false)
|
|
|
|
val binding = LoadStateFooterViewItemBinding.bind(view)
|
|
|
|
return ReposLoadStateViewHolder(binding, retry)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|