Improve performance

This commit is contained in:
Koitharu
2022-03-17 11:24:15 +02:00
parent f0380d7eff
commit 4851139ba5
12 changed files with 139 additions and 35 deletions

View File

@@ -83,6 +83,7 @@ dependencies {
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.asynclayoutinflater:asynclayoutinflater:1.0.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'com.google.android.material:material:1.6.0-alpha03'

View File

@@ -12,11 +12,14 @@ import org.koitharu.kotatsu.local.data.CbzFetcher
val uiModule
get() = module {
single {
val httpClient = get<OkHttpClient>().newBuilder()
.cache(CoilUtils.createDefaultCache(androidContext()))
.build()
val httpClientFactory = {
get<OkHttpClient>().newBuilder()
.cache(CoilUtils.createDefaultCache(androidContext()))
.build()
}
ImageLoader.Builder(androidContext())
.okHttpClient(httpClient)
.okHttpClient(httpClientFactory)
.launchInterceptorChainOnMainThread(false)
.componentRegistry(
ComponentRegistry.Builder()
.add(CbzFetcher())

View File

@@ -5,7 +5,6 @@ import android.view.*
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator
import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -19,17 +18,13 @@ import org.koitharu.kotatsu.favourites.ui.categories.CategoriesEditDelegate
import org.koitharu.kotatsu.favourites.ui.categories.FavouritesCategoriesViewModel
import org.koitharu.kotatsu.main.ui.AppBarOwner
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.utils.RecycledViewPoolHolder
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.measureHeight
import org.koitharu.kotatsu.utils.ext.showPopupMenu
import java.util.*
class FavouritesContainerFragment : BaseFragment<FragmentFavouritesBinding>(),
FavouritesTabLongClickListener, CategoriesEditDelegate.CategoriesEditCallback,
RecycledViewPoolHolder {
override val recycledViewPool = RecyclerView.RecycledViewPool()
FavouritesTabLongClickListener, CategoriesEditDelegate.CategoriesEditCallback {
private val viewModel by viewModel<FavouritesCategoriesViewModel>()
private val editDelegate by lazy(LazyThreadSafetyMode.NONE) {

View File

@@ -24,16 +24,21 @@ import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.databinding.FragmentListBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.AsyncViewFactory
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter.Companion.ITEM_TYPE_MANGA_GRID
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.main.ui.AppBarOwner
import org.koitharu.kotatsu.main.ui.MainActivity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.RecycledViewPoolHolder
import org.koitharu.kotatsu.utils.ext.*
private const val PREFETCH_ITEM_LIST = 10
private const val PREFETCH_ITEM_DETAILED = 8
private const val PREFETCH_ITEM_GRID = 16
abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
PaginationScrollListener.Callback, MangaListListener,
SwipeRefreshLayout.OnRefreshListener {
@@ -45,6 +50,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
private val listCommitCallback = Runnable {
spanSizeLookup.invalidateCache()
}
private var asyncViewFactory: AsyncViewFactory? = null
open val isSwipeRefreshEnabled = true
protected abstract val viewModel: MangaListViewModel
@@ -61,10 +67,12 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
asyncViewFactory = AsyncViewFactory(binding.recyclerView)
listAdapter = MangaListAdapter(
coil = get(),
lifecycleOwner = viewLifecycleOwner,
listener = this,
viewFactory = checkNotNull(asyncViewFactory),
)
paginationListener = PaginationScrollListener(4, this)
with(binding.recyclerView) {
@@ -79,10 +87,6 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
isEnabled = isSwipeRefreshEnabled
}
(parentFragment as? RecycledViewPoolHolder)?.let {
binding.recyclerView.setRecycledViewPool(it.recycledViewPool)
}
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError)
viewModel.isLoading.observe(viewLifecycleOwner, ::onLoadingStateChanged)
@@ -93,6 +97,8 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
override fun onDestroyView() {
listAdapter = null
paginationListener = null
asyncViewFactory?.clear()
asyncViewFactory = null
spanSizeLookup.invalidateCache()
super.onDestroyView()
}
@@ -215,18 +221,26 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
with(binding.recyclerView) {
clearItemDecorations()
removeOnLayoutChangeListener(spanResolver)
asyncViewFactory?.clear()
val isListPending = viewModel.isListPending()
when (mode) {
ListMode.LIST -> {
layoutManager = FitHeightLinearLayoutManager(context)
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
addItemDecoration(SpacingItemDecoration(spacing))
updatePadding(left = spacing, right = spacing)
if (isListPending) {
asyncViewFactory?.prefetch(R.layout.item_manga_list, PREFETCH_ITEM_LIST)
}
}
ListMode.DETAILED_LIST -> {
layoutManager = FitHeightLinearLayoutManager(context)
val spacing = resources.getDimensionPixelOffset(R.dimen.list_spacing)
updatePadding(left = spacing, right = spacing)
addItemDecoration(SpacingItemDecoration(spacing))
if (isListPending) {
asyncViewFactory?.prefetch(R.layout.item_manga_list_details, PREFETCH_ITEM_DETAILED)
}
}
ListMode.GRID -> {
layoutManager = FitHeightGridLayoutManager(context, spanResolver.spanCount).also {
@@ -236,6 +250,9 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
addItemDecoration(SpacingItemDecoration(spacing))
updatePadding(left = spacing, right = spacing)
addOnLayoutChangeListener(spanResolver)
if (isListPending) {
asyncViewFactory?.prefetch(R.layout.item_manga_grid, PREFETCH_ITEM_GRID)
}
}
}
}
@@ -256,7 +273,7 @@ abstract class MangaListFragment : BaseFragment<FragmentListBinding>(),
val total =
(binding.recyclerView.layoutManager as? GridLayoutManager)?.spanCount ?: return 1
return when (listAdapter?.getItemViewType(position)) {
MangaListAdapter.ITEM_TYPE_MANGA_GRID -> 1
ITEM_TYPE_MANGA_GRID -> 1
else -> total
}
}

View File

@@ -9,6 +9,9 @@ import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
import org.koitharu.kotatsu.list.ui.model.MangaListDetailedModel
import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
@@ -41,4 +44,10 @@ abstract class MangaListViewModel(
abstract fun onRefresh()
abstract fun onRetry()
fun isListPending(): Boolean {
return content.value?.any {
it is MangaListModel || it is MangaGridModel || it is MangaListDetailedModel
} != true
}
}

View File

@@ -0,0 +1,58 @@
package org.koitharu.kotatsu.list.ui.adapter
import android.util.Log
import android.util.SparseArray
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.asynclayoutinflater.view.AsyncLayoutInflater
import androidx.core.util.valueIterator
import org.koitharu.kotatsu.BuildConfig
import java.util.*
class AsyncViewFactory(private val parent: ViewGroup) : AsyncLayoutInflater.OnInflateFinishedListener {
private val asyncInflater = AsyncLayoutInflater(parent.context)
private val pool = SparseArray<LinkedList<View>>()
override fun onInflateFinished(view: View, resid: Int, parent: ViewGroup?) {
var list = pool.get(resid)
if (list != null) {
list.addLast(view)
} else {
list = LinkedList()
list.add(view)
pool.put(resid, list)
}
}
fun clear() {
if (BuildConfig.DEBUG) {
pool.valueIterator().forEach {
if (it.isNotEmpty()) {
Log.w("AsyncViewFactory", "You have ${it.size} unconsumed prefetched items")
}
}
}
pool.clear()
}
fun prefetch(@LayoutRes resId: Int, count: Int) {
if (count <= 0) return
repeat(count) {
asyncInflater.inflate(resId, parent, this)
}
}
operator fun get(@LayoutRes resId: Int): View? {
val result = pool.get(resId)?.removeFirstOrNull()
if (BuildConfig.DEBUG && result == null) {
Log.w("AsyncViewFactory", "Item requested but missing")
}
return result
}
fun getCount(@LayoutRes resId: Int): Int {
return pool[resId]?.size ?: 0
}
}

View File

@@ -19,9 +19,16 @@ import org.koitharu.kotatsu.utils.ext.referer
fun mangaGridItemAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Manga>
clickListener: OnListItemClickListener<Manga>,
viewFactory: AsyncViewFactory,
) = adapterDelegateViewBinding<MangaGridModel, ListModel, ItemMangaGridBinding>(
{ inflater, parent -> ItemMangaGridBinding.inflate(inflater, parent, false) }
{ inflater, parent ->
viewFactory[R.layout.item_manga_grid]?.let {
ItemMangaGridBinding.bind(it)
} ?: run {
ItemMangaGridBinding.inflate(inflater, parent, false)
}
}
) {
var imageRequest: Disposable? = null
@@ -52,6 +59,7 @@ fun mangaGridItemAD(
itemView.clearBadge(badge)
badge = null
imageRequest?.dispose()
imageRequest = null
CoilUtils.clear(binding.imageViewCover)
binding.imageViewCover.setImageDrawable(null)
}

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.core.ui.DateTimeAgo
@@ -12,19 +13,20 @@ class MangaListAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: MangaListListener,
viewFactory: AsyncViewFactory,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
init {
delegatesManager
.addDelegate(
ITEM_TYPE_MANGA_LIST,
mangaListItemAD(coil, lifecycleOwner, listener)
mangaListItemAD(coil, lifecycleOwner, listener, viewFactory)
)
.addDelegate(
ITEM_TYPE_MANGA_LIST_DETAILED,
mangaListDetailedItemAD(coil, lifecycleOwner, listener)
mangaListDetailedItemAD(coil, lifecycleOwner, listener, viewFactory)
)
.addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, listener))
.addDelegate(ITEM_TYPE_MANGA_GRID, mangaGridItemAD(coil, lifecycleOwner, listener, viewFactory))
.addDelegate(ITEM_TYPE_LOADING_FOOTER, loadingFooterAD())
.addDelegate(ITEM_TYPE_LOADING_STATE, loadingStateAD())
.addDelegate(ITEM_TYPE_DATE, relatedDateItemAD())

View File

@@ -20,9 +20,16 @@ import org.koitharu.kotatsu.utils.ext.textAndVisible
fun mangaListDetailedItemAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Manga>
clickListener: OnListItemClickListener<Manga>,
viewFactory: AsyncViewFactory,
) = adapterDelegateViewBinding<MangaListDetailedModel, ListModel, ItemMangaListDetailsBinding>(
{ inflater, parent -> ItemMangaListDetailsBinding.inflate(inflater, parent, false) }
{ inflater, parent ->
viewFactory[R.layout.item_manga_list_details]?.let {
ItemMangaListDetailsBinding.bind(it)
} ?: run {
ItemMangaListDetailsBinding.inflate(inflater, parent, false)
}
}
) {
var imageRequest: Disposable? = null
@@ -56,6 +63,7 @@ fun mangaListDetailedItemAD(
itemView.clearBadge(badge)
badge = null
imageRequest?.dispose()
imageRequest = null
CoilUtils.clear(binding.imageViewCover)
binding.imageViewCover.setImageDrawable(null)
}

View File

@@ -20,9 +20,16 @@ import org.koitharu.kotatsu.utils.ext.textAndVisible
fun mangaListItemAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Manga>
clickListener: OnListItemClickListener<Manga>,
viewFactory: AsyncViewFactory,
) = adapterDelegateViewBinding<MangaListModel, ListModel, ItemMangaListBinding>(
{ inflater, parent -> ItemMangaListBinding.inflate(inflater, parent, false) }
{ inflater, parent ->
viewFactory[R.layout.item_manga_list]?.let {
ItemMangaListBinding.bind(it)
} ?: run {
ItemMangaListBinding.inflate(inflater, parent, false)
}
}
) {
var imageRequest: Disposable? = null
@@ -54,6 +61,7 @@ fun mangaListItemAD(
itemView.clearBadge(badge)
badge = null
imageRequest?.dispose()
imageRequest = null
CoilUtils.clear(binding.imageViewCover)
binding.imageViewCover.setImageDrawable(null)
}

View File

@@ -63,8 +63,11 @@ private const val TAG_PRIMARY = "primary"
private const val TAG_SEARCH = "search"
class MainActivity : BaseActivity<ActivityMainBinding>(),
NavigationView.OnNavigationItemSelectedListener, AppBarOwner,
View.OnClickListener, View.OnFocusChangeListener, SearchSuggestionListener {
NavigationView.OnNavigationItemSelectedListener,
AppBarOwner,
View.OnClickListener,
View.OnFocusChangeListener,
SearchSuggestionListener {
private val viewModel by viewModel<MainViewModel>()
private val searchSuggestionViewModel by viewModel<SearchSuggestionViewModel>()

View File

@@ -1,8 +0,0 @@
package org.koitharu.kotatsu.utils
import androidx.recyclerview.widget.RecyclerView
interface RecycledViewPoolHolder {
val recycledViewPool: RecyclerView.RecycledViewPool
}