From 124f3b57fbe84e81359e636dda03f9a39faa5daf Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:05:08 -0600 Subject: [PATCH 1/4] Add ViewBinding pool to BaseFragment --- .../lagradost/cloudstream3/CommonActivity.kt | 2 + .../lagradost/cloudstream3/ui/BaseFragment.kt | 60 ++++++++++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 6f1282659bc..76ecfa09650 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -35,6 +35,7 @@ import com.lagradost.cloudstream3.actions.VideoClickActionHolder import com.lagradost.cloudstream3.databinding.ToastBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager +import com.lagradost.cloudstream3.ui.BaseFragmentPool import com.lagradost.cloudstream3.ui.home.HomeChildItemAdapter import com.lagradost.cloudstream3.ui.home.ParentItemAdapter import com.lagradost.cloudstream3.ui.player.PlayerEventType @@ -236,6 +237,7 @@ object CommonActivity { ioSafe { Torrent.deleteAllFiles() } // Clear all pools to apply the correct theme + BaseFragmentPool.clearAll() for (pool in arrayOf( PluginAdapter.sharedPool, HomeChildItemAdapter.sharedPool, ParentItemAdapter.sharedPool, ActorAdaptor.sharedPool, EpisodeAdapter.sharedPool, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt index fc7f6f12e49..d4963e20685 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui import android.content.res.Configuration import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -40,11 +41,22 @@ private interface BaseFragmentHelper { var _binding: T? val binding: T? get() = _binding + companion object { + const val TAG = "BaseFragment" + } + fun createBinding( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { + // Try to reuse a binding from the pool first + BaseFragmentPool.acquire(javaClass.name)?.let { + Log.d(TAG, "Binding acquired from pool") + _binding = it + return it.root + } + val layoutId = pickLayout() val root: View? = layoutId?.let { inflater.inflate(it, container, false) } _binding = try { @@ -120,6 +132,48 @@ private interface BaseFragmentHelper { fun fixPadding(view: View) { fixSystemBarsPadding(view) } + + /** Called by fragments when they’re destroyed, so the binding can be recycled. */ + fun recycleBindingOnDestroy() { + _binding?.let { + BaseFragmentPool.release(javaClass.name, it) + Log.d(TAG, "Binding released to pool") + _binding = null + } + } +} + +/** + * A global pool for reusing [ViewBinding] instances across fragments to reduce + * layout inflation overhead and improve navigation performance. + * + * This pool is intended for use with fragments that extend [BaseFragment], + * [BaseDialogFragment], or [BaseBottomSheetDialogFragment] which support + * recycling of their bindings. + */ +object BaseFragmentPool { + private val pool = mutableMapOf>() + + /** Attempts to acquire a recycled binding from the pool. */ + fun acquire(key: String): T? { + val list = pool[key] ?: return null + val binding = list.removeLastOrNull() as? T ?: return null + (binding.root.parent as? ViewGroup)?.removeView(binding.root) + if (list.isEmpty()) pool.remove(key) + return binding + } + + /** Releases a binding back to the pool for later reuse. */ + fun release(key: String, binding: T) { + val list = pool.getOrPut(key) { mutableListOf() } + list.add(binding) + } + + /** Clears all cached bindings from the pool. */ + fun clearAll() { + pool.values.flatten().forEach { (it.root.parent as? ViewGroup)?.removeView(it.root) } + pool.clear() + } } abstract class BaseFragment( @@ -151,7 +205,7 @@ abstract class BaseFragment( /** Cleans up the binding reference when the view is destroyed to avoid memory leaks. */ override fun onDestroyView() { super.onDestroyView() - _binding = null + recycleBindingOnDestroy() } /** @@ -206,7 +260,7 @@ abstract class BaseDialogFragment( /** Cleans up the binding reference when the view is destroyed to avoid memory leaks. */ override fun onDestroyView() { super.onDestroyView() - _binding = null + recycleBindingOnDestroy() } } @@ -235,7 +289,7 @@ abstract class BaseBottomSheetDialogFragment( /** Cleans up the binding reference when the view is destroyed to avoid memory leaks. */ override fun onDestroyView() { super.onDestroyView() - _binding = null + recycleBindingOnDestroy() } } From 49c144107022983dfdce358a10114d1b88915263 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Sun, 2 Nov 2025 10:15:17 -0700 Subject: [PATCH 2/4] Better logging --- .../main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt index d4963e20685..87b1a941333 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt @@ -52,7 +52,7 @@ private interface BaseFragmentHelper { ): View? { // Try to reuse a binding from the pool first BaseFragmentPool.acquire(javaClass.name)?.let { - Log.d(TAG, "Binding acquired from pool") + Log.d(TAG, "Binding acquired from pool for ${javaClass.name}") _binding = it return it.root } @@ -137,7 +137,7 @@ private interface BaseFragmentHelper { fun recycleBindingOnDestroy() { _binding?.let { BaseFragmentPool.release(javaClass.name, it) - Log.d(TAG, "Binding released to pool") + Log.d(TAG, "Binding released to pool for ${javaClass.name}") _binding = null } } From 8731ab8ca13256b298bb8efd766f5508e77b8a76 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Thu, 6 Nov 2025 13:55:25 -0700 Subject: [PATCH 3/4] Add fix for ResultFragmentTv and home page issue with setup --- .../lagradost/cloudstream3/MainActivity.kt | 9 ++++-- .../lagradost/cloudstream3/ui/BaseFragment.kt | 30 ++++++++++++++++--- .../cloudstream3/ui/home/HomeFragment.kt | 8 ++++- .../ui/result/ResultFragmentTv.kt | 6 ++++ .../ui/setup/SetupFragmentExtensions.kt | 4 +++ .../ui/setup/SetupFragmentLanguage.kt | 4 +++ .../ui/setup/SetupFragmentLayout.kt | 4 +++ .../ui/setup/SetupFragmentMedia.kt | 4 +++ .../ui/setup/SetupFragmentProviderLanguage.kt | 4 +++ 9 files changed, 65 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index cd3fde7f9ce..881409b706a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -106,6 +106,7 @@ import com.lagradost.cloudstream3.ui.SyncWatchType import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO +import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.home.HomeViewModel import com.lagradost.cloudstream3.ui.library.LibraryViewModel import com.lagradost.cloudstream3.ui.player.BasicLink @@ -161,6 +162,7 @@ import com.lagradost.cloudstream3.utils.ImageLoader.loadImage import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar +import com.lagradost.cloudstream3.utils.TvChannelUtils import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -194,9 +196,6 @@ import androidx.tvprovider.media.tv.TvContractCompat import android.content.ComponentName import android.content.ContentUris -import com.lagradost.cloudstream3.ui.home.HomeFragment -import com.lagradost.cloudstream3.utils.TvChannelUtils - class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCallback { companion object { var activityResultLauncher: ActivityResultLauncher? = null @@ -2002,11 +2001,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa try { if (getKey(HAS_DONE_SETUP_KEY, false) != true) { navController.navigate(R.id.navigation_setup_language) + // Causes cache to be poisoned, so we don't use yet. + HomeFragment.useBindingPool = false // If no plugins bring up extensions screen } else if (PluginManager.getPluginsOnline().isEmpty() && PluginManager.getPluginsLocal().isEmpty() // && PREBUILT_REPOSITORIES.isNotEmpty() ) { + // Causes cache to be poisoned, so we don't use yet. + HomeFragment.useBindingPool = false navController.navigate( R.id.navigation_setup_extensions, SetupFragmentExtensions.newInstance(false) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt index 52f447a8a52..8f39bda876d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt @@ -50,8 +50,8 @@ private interface BaseFragmentHelper { savedInstanceState: Bundle? ): View? { // Try to reuse a binding from the pool first - BaseFragmentPool.acquire(javaClass.name)?.let { - Log.d(TAG, "Binding acquired from pool for ${javaClass.name}") + BaseFragmentPool.acquire(getPoolKey())?.let { + Log.d(TAG, "Binding acquired from pool for ${getPoolKey()}") _binding = it return it.root } @@ -78,6 +78,8 @@ private interface BaseFragmentHelper { return _binding?.root ?: root } + fun getPoolKey(): String = javaClass.name + /** * Called after the fragment's view has been created. * @@ -135,8 +137,8 @@ private interface BaseFragmentHelper { /** Called by fragments when they’re destroyed, so the binding can be recycled. */ fun recycleBindingOnDestroy() { _binding?.let { - BaseFragmentPool.release(javaClass.name, it) - Log.d(TAG, "Binding released to pool for ${javaClass.name}") + BaseFragmentPool.release(getPoolKey(), it) + Log.d(TAG, "Binding released to pool for ${getPoolKey()}") _binding = null } } @@ -152,9 +154,11 @@ private interface BaseFragmentHelper { */ object BaseFragmentPool { private val pool = mutableMapOf>() + private const val MAX_PER_PREFIX = 3 /** Attempts to acquire a recycled binding from the pool. */ fun acquire(key: String): T? { + if (key == "") return null val list = pool[key] ?: return null val binding = list.removeLastOrNull() as? T ?: return null (binding.root.parent as? ViewGroup)?.removeView(binding.root) @@ -164,8 +168,10 @@ object BaseFragmentPool { /** Releases a binding back to the pool for later reuse. */ fun release(key: String, binding: T) { + if (key == "") return val list = pool.getOrPut(key) { mutableListOf() } list.add(binding) + trimPrefixIfNeeded(key) } /** Clears all cached bindings from the pool. */ @@ -173,6 +179,22 @@ object BaseFragmentPool { pool.values.flatten().forEach { (it.root.parent as? ViewGroup)?.removeView(it.root) } pool.clear() } + + /** Trims bindings for a prefix if total exceeds MAX_PER_PREFIX */ + private fun trimPrefixIfNeeded(key: String) { + val prefix = key.substringBefore(":") + val prefixKeys = pool.keys.filter { it.startsWith("$prefix:") } + var total = prefixKeys.sumOf { pool[it]?.size ?: 0 } + + while (total > MAX_PER_PREFIX && prefixKeys.isNotEmpty()) { + // Remove oldest from the first key with items + val oldestKey = prefixKeys.firstOrNull { pool[it]?.isNotEmpty() == true } ?: break + val removed = pool[oldestKey]?.removeFirstOrNull() ?: break + (removed.root.parent as? ViewGroup)?.removeView(removed.root) + if (pool[oldestKey]?.isEmpty() == true) pool.remove(oldestKey) + total-- + } + } } abstract class BaseFragment( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 366b455c86c..bef0d4856e8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -83,8 +83,9 @@ class HomeFragment : BaseFragment( BaseFragment.BindingCreator.Bind(FragmentHomeBinding::bind) ) { companion object { - val configEvent = Event() var currentSpan = 1 + var useBindingPool = true + val configEvent = Event() val listHomepageItems = mutableListOf() private val errorProfilePics = listOf( @@ -553,6 +554,11 @@ class HomeFragment : BaseFragment( override fun pickLayout(): Int? = if (isLayout(PHONE)) R.layout.fragment_home else R.layout.fragment_home_tv + override fun getPoolKey(): String { + if (!useBindingPool) return "" + return "HomeFragment" + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index eae07a4e5b1..b4fbd561840 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -89,6 +89,12 @@ class ResultFragmentTv : BaseFragment( return super.onCreateView(inflater, container, savedInstanceState) } + override fun getPoolKey(): String { + // Prevent poisoned pool by using no cache if we can't get key for it + val storedData = getStoredData() ?: return "" + return "ResultFragmentTv:${storedData.name}-${storedData.apiName}" + } + private fun updateUI(id: Int?) { viewModel.reloadEpisodes() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt index 501ee0eef7b..492e6e43759 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt @@ -47,6 +47,10 @@ class SetupFragmentExtensions : BaseFragment( fixSystemBarsPadding(view) } + // No cache, it should not be shown very often, + // and it just adds to memory usage. + override fun getPoolKey(): String = "" + private fun setRepositories(success: Boolean = true) { main { val repositories = RepositoryManager.getRepositories() + PREBUILT_REPOSITORIES diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt index 946f7eeae7f..e9311596c67 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt @@ -29,6 +29,10 @@ class SetupFragmentLanguage : BaseFragment( fixSystemBarsPadding(view) } + // No cache, it should not be shown very often, + // and it just adds to memory usage. + override fun getPoolKey(): String = "" + override fun onBindingCreated(binding: FragmentSetupLanguageBinding) { // We don't want a crash for all users safe { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt index 6c4dfc86308..f4fba2bfde0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLayout.kt @@ -20,6 +20,10 @@ class SetupFragmentLayout : BaseFragment( fixSystemBarsPadding(view) } + // No cache, it should not be shown very often, + // and it just adds to memory usage. + override fun getPoolKey(): String = "" + override fun onBindingCreated(binding: FragmentSetupLayoutBinding) { safe { val ctx = context ?: return@safe diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt index ca5e63cceaa..426f91a2eb1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt @@ -22,6 +22,10 @@ class SetupFragmentMedia : BaseFragment( fixSystemBarsPadding(view) } + // No cache, it should not be shown very often, + // and it just adds to memory usage. + override fun getPoolKey(): String = "" + override fun onBindingCreated(binding: FragmentSetupMediaBinding) { safe { val ctx = context ?: return@safe diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index 6032af56dd4..41bb08685fb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -24,6 +24,10 @@ class SetupFragmentProviderLanguage : BaseFragment Date: Fri, 7 Nov 2025 10:29:47 -0700 Subject: [PATCH 4/4] Suppress UNCHECKED_CAST --- app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt index cb931246eab..df9ed446772 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseFragment.kt @@ -160,6 +160,7 @@ object BaseFragmentPool { fun acquire(key: String): T? { if (key == "") return null val list = pool[key] ?: return null + @Suppress("UNCHECKED_CAST") val binding = list.removeLastOrNull() as? T ?: return null (binding.root.parent as? ViewGroup)?.removeView(binding.root) if (list.isEmpty()) pool.remove(key)