Migrate feed to adapter delegates

This commit is contained in:
Koitharu
2020-11-23 19:16:02 +02:00
parent 12c8cdfd70
commit b9f35f34ad
21 changed files with 237 additions and 158 deletions

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.tracker
import org.koin.android.ext.koin.androidContext
import org.koin.android.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
@@ -10,5 +11,5 @@ val trackerModule
single { TrackingRepository(get(), get()) }
viewModel { FeedViewModel(get()) }
viewModel { FeedViewModel(androidContext(), get()) }
}

View File

@@ -1,19 +1,46 @@
package org.koitharu.kotatsu.tracker.ui
import android.view.ViewGroup
import org.koitharu.kotatsu.base.ui.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.core.model.TrackingLogItem
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.list.ui.adapter.indeterminateProgressAD
import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress
import org.koitharu.kotatsu.tracker.ui.adapter.feedItemAD
import org.koitharu.kotatsu.tracker.ui.model.FeedItem
import kotlin.jvm.internal.Intrinsics
class FeedAdapter(onItemClickListener: OnRecyclerItemClickListener<TrackingLogItem>? = null) :
BaseRecyclerAdapter<TrackingLogItem, Unit>(onItemClickListener) {
class FeedAdapter(
coil: ImageLoader,
clickListener: OnListItemClickListener<Manga>
) : AsyncListDifferDelegationAdapter<Any>(DiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup): BaseViewHolder<TrackingLogItem, Unit> {
return FeedHolder(parent)
init {
delegatesManager.addDelegate(ITEM_TYPE_FEED, feedItemAD(coil, clickListener))
.addDelegate(ITEM_TYPE_PROGRESS, indeterminateProgressAD())
}
override fun onGetItemId(item: TrackingLogItem) = item.id
private class DiffCallback : DiffUtil.ItemCallback<Any>() {
override fun getExtra(item: TrackingLogItem, position: Int) = Unit
override fun areItemsTheSame(oldItem: Any, newItem: Any) = when {
oldItem is FeedItem && newItem is FeedItem -> {
oldItem.id == newItem.id
}
oldItem == IndeterminateProgress && newItem == IndeterminateProgress -> {
true
}
else -> false
}
override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
}
companion object {
const val ITEM_TYPE_FEED = 0
const val ITEM_TYPE_PROGRESS = 1
}
}

View File

@@ -8,21 +8,21 @@ import android.view.View
import androidx.core.view.isVisible
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_tracklogs.*
import org.koin.android.ext.android.get
import org.koin.android.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.base.ui.list.PaginationScrollListener
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.tracker.work.TrackWorker
import org.koitharu.kotatsu.utils.ext.callOnScrollListeners
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.hasItems
class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), PaginationScrollListener.Callback,
OnRecyclerItemClickListener<TrackingLogItem> {
OnListItemClickListener<Manga> {
private val viewModel by viewModel<FeedViewModel>()
@@ -37,7 +37,7 @@ class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), PaginationScroll
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = FeedAdapter(this)
adapter = FeedAdapter(get(), this)
recyclerView.adapter = adapter
recyclerView.addItemDecoration(
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing))
@@ -45,12 +45,13 @@ class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), PaginationScroll
recyclerView.setHasFixedSize(true)
recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
if (savedInstanceState == null) {
onRequestMoreItems(0)
onScrolledToEnd()
}
viewModel.content.observe(viewLifecycleOwner, this::onListChanged)
viewModel.isLoading.observe(viewLifecycleOwner, this::onLoadingStateChanged)
viewModel.onError.observe(viewLifecycleOwner, this::onError)
viewModel.isEmptyState.observe(viewLifecycleOwner, this::onEmptyStateChanged)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -72,15 +73,8 @@ class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), PaginationScroll
super.onDestroyView()
}
private fun onListChanged(list: List<TrackingLogItem>) {
adapter?.replaceData(list)
if (list.isEmpty()) {
setUpEmptyListHolder()
layout_holder.isVisible = true
} else {
layout_holder.isVisible = false
}
recyclerView.callOnScrollListeners()
private fun onListChanged(list: List<Any>) {
adapter?.items = list
}
private fun onError(e: Throwable) {
@@ -102,21 +96,21 @@ class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), PaginationScroll
private fun onLoadingStateChanged(isLoading: Boolean) {
val hasItems = recyclerView.hasItems
progressBar.isVisible = isLoading && !hasItems
if (isLoading) {
layout_holder.isVisible = false
}
private fun onEmptyStateChanged(isEmpty: Boolean) {
if (isEmpty) {
setUpEmptyListHolder()
}
layout_holder.isVisible = isEmpty
}
override fun getItemsCount(): Int {
return adapter?.itemCount ?: 0
override fun onScrolledToEnd() {
viewModel.loadList(append = true)
}
override fun onRequestMoreItems(offset: Int) {
viewModel.loadList(offset)
}
override fun onItemClick(item: TrackingLogItem, position: Int, view: View) {
startActivity(DetailsActivity.newIntent(context ?: return, item.manga))
override fun onItemClick(item: Manga, view: View) {
startActivity(DetailsActivity.newIntent(context ?: return, item))
}
private fun setUpEmptyListHolder() {

View File

@@ -1,48 +0,0 @@
package org.koitharu.kotatsu.tracker.ui
import android.text.format.DateUtils
import android.view.ViewGroup
import coil.ImageLoader
import coil.request.Disposable
import kotlinx.android.synthetic.main.item_tracklog.*
import org.koin.core.component.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.formatRelative
import org.koitharu.kotatsu.utils.ext.newImageRequest
class FeedHolder(parent: ViewGroup) :
BaseViewHolder<TrackingLogItem, Unit>(parent, R.layout.item_tracklog) {
private val coil by inject<ImageLoader>()
private var imageRequest: Disposable? = null
override fun onBind(data: TrackingLogItem, extra: Unit) {
imageRequest?.dispose()
imageRequest = imageView_cover.newImageRequest(data.manga.coverUrl)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.enqueueWith(coil)
textView_title.text = data.manga.title
textView_subtitle.text = buildString {
append(data.createdAt.formatRelative(DateUtils.DAY_IN_MILLIS))
append(" ")
append(
context.resources.getQuantityString(
R.plurals.new_chapters,
data.chapters.size,
data.chapters.size
)
)
}
textView_chapters.text = data.chapters.joinToString("\n")
}
override fun onRecycled() {
imageRequest?.dispose()
imageView_cover.setImageDrawable(null)
}
}

View File

@@ -1,24 +1,60 @@
package org.koitharu.kotatsu.tracker.ui
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.list.ui.model.IndeterminateProgress
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.tracker.ui.model.toFeedItem
import org.koitharu.kotatsu.utils.ext.mapItems
class FeedViewModel(
context: Context,
private val repository: TrackingRepository
) : BaseViewModel() {
val content = MutableLiveData<List<TrackingLogItem>>()
private val logList = MutableStateFlow<List<TrackingLogItem>>(emptyList())
private val hasNextPage = MutableStateFlow(false)
private var loadingJob: Job? = null
fun loadList(offset: Int) {
launchLoadingJob {
val isEmptyState = MutableLiveData(false)
val content = combine(
logList.drop(1).onEach {
isEmptyState.postValue(it.isEmpty())
}.mapItems {
it.toFeedItem(context.resources)
},
hasNextPage
) { list, isHasNextPage ->
if (isHasNextPage && list.isNotEmpty()) list + IndeterminateProgress else list
}.asLiveData(viewModelScope.coroutineContext + Dispatchers.Default)
init {
loadList(append = false)
}
fun loadList(append: Boolean) {
if (loadingJob?.isActive == true) {
return
}
loadingJob = launchLoadingJob {
val offset = if (append) logList.value.size else 0
val list = repository.getTrackingLog(offset, 20)
if (offset == 0) {
content.value = list
} else {
content.value = content.value.orEmpty() + list
if (!append) {
logList.value = list
} else if (list.isNotEmpty()) {
logList.value += list
}
hasNextPage.value = list.isNotEmpty()
}
}
}

View File

@@ -0,0 +1,41 @@
package org.koitharu.kotatsu.tracker.ui.adapter
import coil.ImageLoader
import coil.request.Disposable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
import kotlinx.android.synthetic.main.item_tracklog.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.tracker.ui.model.FeedItem
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
fun feedItemAD(
coil: ImageLoader,
clickListener: OnListItemClickListener<Manga>
) = adapterDelegateLayoutContainer<FeedItem, Any>(R.layout.item_tracklog) {
var imageRequest: Disposable? = null
itemView.setOnClickListener {
clickListener.onItemClick(item.manga, it)
}
bind {
imageRequest?.dispose()
imageRequest = imageView_cover.newImageRequest(item.imageUrl)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_placeholder)
.enqueueWith(coil)
textView_title.text = item.title
textView_subtitle.text = item.subtitle
textView_chapters.text = item.chapters
}
onViewRecycled {
imageRequest?.dispose()
imageView_cover.setImageDrawable(null)
}
}

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.tracker.ui.model
import org.koitharu.kotatsu.core.model.Manga
data class FeedItem(
val id: Long,
val imageUrl: String,
val title: String,
val subtitle: String,
val chapters: CharSequence,
val manga: Manga
)

View File

@@ -0,0 +1,26 @@
package org.koitharu.kotatsu.tracker.ui.model
import android.content.res.Resources
import android.text.format.DateUtils
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.utils.ext.formatRelative
fun TrackingLogItem.toFeedItem(resources: Resources) = FeedItem(
id = id,
imageUrl = manga.coverUrl,
title = manga.title,
subtitle = buildString {
append(createdAt.formatRelative(DateUtils.DAY_IN_MILLIS))
append(" ")
append(
resources.getQuantityString(
R.plurals.new_chapters,
chapters.size,
chapters.size
)
)
},
chapters = chapters.joinToString("\n"),
manga = manga
)