diff --git a/app/build.gradle b/app/build.gradle index 18b8e8a0b..728dc47ed 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt index 060e748ac..26ccbf44a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/ui/uiModule.kt @@ -12,11 +12,14 @@ import org.koitharu.kotatsu.local.data.CbzFetcher val uiModule get() = module { single { - val httpClient = get().newBuilder() - .cache(CoilUtils.createDefaultCache(androidContext())) - .build() + val httpClientFactory = { + get().newBuilder() + .cache(CoilUtils.createDefaultCache(androidContext())) + .build() + } ImageLoader.Builder(androidContext()) - .okHttpClient(httpClient) + .okHttpClient(httpClientFactory) + .launchInterceptorChainOnMainThread(false) .componentRegistry( ComponentRegistry.Builder() .add(CbzFetcher()) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt index 7c05d26dd..949a4a852 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/FavouritesContainerFragment.kt @@ -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(), - FavouritesTabLongClickListener, CategoriesEditDelegate.CategoriesEditCallback, - RecycledViewPoolHolder { - - override val recycledViewPool = RecyclerView.RecycledViewPool() + FavouritesTabLongClickListener, CategoriesEditDelegate.CategoriesEditCallback { private val viewModel by viewModel() private val editDelegate by lazy(LazyThreadSafetyMode.NONE) { diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt index f3ae56470..ccc92d08c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListFragment.kt @@ -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(), PaginationScrollListener.Callback, MangaListListener, SwipeRefreshLayout.OnRefreshListener { @@ -45,6 +50,7 @@ abstract class MangaListFragment : BaseFragment(), 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(), 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(), 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(), override fun onDestroyView() { listAdapter = null paginationListener = null + asyncViewFactory?.clear() + asyncViewFactory = null spanSizeLookup.invalidateCache() super.onDestroyView() } @@ -215,18 +221,26 @@ abstract class MangaListFragment : BaseFragment(), 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(), 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(), 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 } } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt index 6ad0f2533..bd4dc2feb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/MangaListViewModel.kt @@ -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 + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/AsyncViewFactory.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/AsyncViewFactory.kt new file mode 100644 index 000000000..cbd9fb071 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/AsyncViewFactory.kt @@ -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>() + + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt index 97d451ddd..2800b9274 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt @@ -19,9 +19,16 @@ import org.koitharu.kotatsu.utils.ext.referer fun mangaGridItemAD( coil: ImageLoader, lifecycleOwner: LifecycleOwner, - clickListener: OnListItemClickListener + clickListener: OnListItemClickListener, + viewFactory: AsyncViewFactory, ) = adapterDelegateViewBinding( - { 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) } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt index 5c801760d..d2cc18795 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListAdapter.kt @@ -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(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()) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt index 8803e37e6..3824cab3d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt @@ -20,9 +20,16 @@ import org.koitharu.kotatsu.utils.ext.textAndVisible fun mangaListDetailedItemAD( coil: ImageLoader, lifecycleOwner: LifecycleOwner, - clickListener: OnListItemClickListener + clickListener: OnListItemClickListener, + viewFactory: AsyncViewFactory, ) = adapterDelegateViewBinding( - { 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) } diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt index 2df6bb7ca..c955c9f90 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt @@ -20,9 +20,16 @@ import org.koitharu.kotatsu.utils.ext.textAndVisible fun mangaListItemAD( coil: ImageLoader, lifecycleOwner: LifecycleOwner, - clickListener: OnListItemClickListener + clickListener: OnListItemClickListener, + viewFactory: AsyncViewFactory, ) = adapterDelegateViewBinding( - { 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) } diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt index 8349f1a52..bfbf4e4ae 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -63,8 +63,11 @@ private const val TAG_PRIMARY = "primary" private const val TAG_SEARCH = "search" class MainActivity : BaseActivity(), - NavigationView.OnNavigationItemSelectedListener, AppBarOwner, - View.OnClickListener, View.OnFocusChangeListener, SearchSuggestionListener { + NavigationView.OnNavigationItemSelectedListener, + AppBarOwner, + View.OnClickListener, + View.OnFocusChangeListener, + SearchSuggestionListener { private val viewModel by viewModel() private val searchSuggestionViewModel by viewModel() diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/RecycledViewPoolHolder.kt b/app/src/main/java/org/koitharu/kotatsu/utils/RecycledViewPoolHolder.kt deleted file mode 100644 index c21abefc8..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/RecycledViewPoolHolder.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.koitharu.kotatsu.utils - -import androidx.recyclerview.widget.RecyclerView - -interface RecycledViewPoolHolder { - - val recycledViewPool: RecyclerView.RecycledViewPool -} \ No newline at end of file