diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 6f1282659b..76ecfa0965 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/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index cd3fde7f9c..881409b706 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 14901dda24..df9ed44677 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 @@ -39,11 +40,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(getPoolKey())?.let { + Log.d(TAG, "Binding acquired from pool for ${getPoolKey()}") + _binding = it + return it.root + } + val layoutId = pickLayout() val root: View? = layoutId?.let { inflater.inflate(it, container, false) } _binding = try { @@ -66,6 +78,8 @@ private interface BaseFragmentHelper { return _binding?.root ?: root } + fun getPoolKey(): String = javaClass.name + /** * Called after the fragment's view has been created. * @@ -119,6 +133,69 @@ private interface BaseFragmentHelper { * @param view The root view to adjust. */ fun fixLayout(view: View) + + /** Called by fragments when they’re destroyed, so the binding can be recycled. */ + fun recycleBindingOnDestroy() { + _binding?.let { + BaseFragmentPool.release(getPoolKey(), it) + Log.d(TAG, "Binding released to pool for ${getPoolKey()}") + _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>() + 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 + @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) + return binding + } + + /** 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. */ + fun clearAll() { + 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( @@ -150,7 +227,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() } /** @@ -205,7 +282,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() } } @@ -234,7 +311,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() } } 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 366b455c86..bef0d4856e 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 eae07a4e5b..b4fbd56184 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 501ee0eef7..492e6e4375 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 946f7eeae7..e9311596c6 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 6c4dfc8630..f4fba2bfde 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 ca5e63ccea..426f91a2eb 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 6032af56dd..41bb08685f 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