Android: ViewModel with paging 3 flow is leaking - android

My problem is, that my shopViewModel which holds an instance of a paging-flow is somehow leaking. I've tried to solve this problem by converting the flow into a livedata, but that changed nothing.
ViewModel
class ShopViewModel #ViewModelInject constructor(
private val shopPagingSource: ShopPagingSource,
) : ViewModel() {
val SHOP_PAGE_CONFIG: PagingConfig = PagingConfig(pageSize = 20, enablePlaceholders = false)
// As LiveData
val shopFlow = Pager(SHOP_PAGE_CONFIG) { shopPagingSource }.flow.cachedIn(viewModelScope).asLiveData()
// Before
val shopFlow = Pager(SHOP_PAGE_CONFIG) { shopPagingSource }.flow.cachedIn(viewModelScope)
}
Fragment
#AndroidEntryPoint
class ShopFragment(private val shopListAdapter: ShopAdapter) : Fragment(R.layout.fragment_shop), ShopAdapter.OnItemClickListener {
private val shopViewModel: ShopViewModel by viewModels()
private val shopBinding: FragmentShopBinding by viewBinding()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
shopBinding.adapter = shopListAdapter.withLoadStateFooter(ShopLoadAdapter(shopListAdapter::retry))
shopListAdapter.clickHandler(this)
collectShopList()
}
override fun forwardClick(product: #NotNull Product) {
val action = ShopFragmentDirections.actionShopFragmentToShopItemFragment(product)
findNavController().navigate(action)
}
private fun collectShopListWithLiveData() = lifecycleScope.launch {
shopViewModel.shopFlow.observe(viewLifecycleOwner) {
lifecycleScope.launch {
shopListAdapter.submitData(it)
}
}
}
// Before converting to livedata
private fun collectShopListWithFlow() = lifecycleScope.launch {
shopViewModel.shopFlow.collectLatest {
shopListAdapter.submitData(it)
}
}
// To avoid memory leak from injected adapter
override fun onDestroyView() {
requireView().findViewById<RecyclerView>(R.id.rv_shop).adapter = null
super.onDestroyView()
}
}
Adapter
class ShopAdapter #Inject constructor() : PagingDataAdapter<Product, ShopAdapter.ShopViewHolder>(Companion) {
private lateinit var clickListener: OnItemClickListener
companion object: DiffUtil.ItemCallback<Product>() {
override fun areItemsTheSame(oldItem: Product, newItem: Product): Boolean = oldItem.articelNumber == newItem.articelNumber
override fun areContentsTheSame(oldItem: Product, newItem: Product): Boolean = oldItem == newItem
}
inner class ShopViewHolder(val binding: ShopListItemBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShopAdapter.ShopViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ShopListItemBinding.inflate(layoutInflater, parent, false)
return ShopViewHolder(binding).also {
binding.mcvProductItem.setOnClickListener { clickListener.forwardClick(binding.product!!) }
}
}
override fun onBindViewHolder(holder: ShopAdapter.ShopViewHolder, position: Int) {
holder.binding.product = getItem(position) ?: return
holder.binding.executePendingBindings()
}
fun clickHandler(clickEventHandler: OnItemClickListener) {
clickListener = clickEventHandler
}
interface OnItemClickListener {
fun forwardClick(product: #NotNull Product)
}
}
MainFragmentFactory
class MainFragmentFactory #Inject constructor(
// .. other dependencies
private val shopAdapter: ShopAdapter,
) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment = when(className) {
// ... other fragments
ShopFragment::class.java.name -> ShopFragment(shopAdapter)
else -> super.instantiate(classLoader, className)
}
PagingSource
class ShopPagingSource #Inject constructor(
private val shopRepository: ShopFirebaseRepository,
) : PagingSource<QuerySnapshot, Product>() {
override suspend fun load(params: LoadParams<QuerySnapshot>): LoadResult<QuerySnapshot, Product> = try {
withTimeout(SHOP_MAX_LOADING_TIME) {
val currentPage = params.key ?: shopRepository.getCurrentPage()
val lastDocumentSnapShot = currentPage.documents[currentPage.size() - 1]
val nextPage = shopRepository.getNextPage(lastDocumentSnapShot)
LoadResult.Page(
data = currentPage.toObjects(),
prevKey = null,
nextKey = nextPage
)
}
} catch (e: TimeoutCancellationException) {
Timber.d("Mediator failed, No Internet Connection")
LoadResult.Error(e)
} catch (e: ArrayIndexOutOfBoundsException) {
Timber.d("Mediator failed, ArrayIndexOutOfBounds")
LoadResult.Error(e)
} catch (e: Exception) {
Timber.d("Mediator failed, Unknown Error: ${e.message.toString()}")
LoadResult.Error(e)
}
}
LeakCanary
D/LeakCanary: ====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
2618 bytes retained by leaking objects
Signature: 944313b4ecbdb77c99682dc8c1646e12e4f37d8
┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never leaking)
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (InternalLeakCanary↓ is not leaking)
│ ↓ Object[].[2142]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.example.app.framework.ui.view.MainActivity instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking and Activity#mDestroyed is false)
│ ↓ MainActivity.navController$delegate
├─ kotlin.SynchronizedLazyImpl instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking)
│ ↓ SynchronizedLazyImpl._value
├─ androidx.navigation.NavHostController instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking)
│ ↓ NavHostController.mLifecycleOwner
├─ com.example.app.framework.ui.view.utils.MainNavHostFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ ↓ MainNavHostFragment.mainFragmentFactory
│ ~~~~~~~~~~~~~~~~~~~
├─ com.example.app.framework.ui.view.utils.MainFragmentFactory instance
│ Leaking: UNKNOWN
│ ↓ MainFragmentFactory.shopAdapter
│ ~~~~~~~~~~~
├─ com.example.app.framework.ui.adapter.recyclerview.ShopAdapter instance
│ Leaking: UNKNOWN
│ ↓ ShopAdapter.differ
│ ~~~~~~
├─ androidx.paging.AsyncPagingDataDiffer instance
│ Leaking: UNKNOWN
│ ↓ AsyncPagingDataDiffer.differBase
│ ~~~~~~~~~~
├─ androidx.paging.AsyncPagingDataDiffer$differBase$1 instance
│ Leaking: UNKNOWN
│ Anonymous subclass of androidx.paging.PagingDataDiffer
│ ↓ AsyncPagingDataDiffer$differBase$1.receiver
│ ~~~~~~~~
├─ androidx.paging.PageFetcher$PagerUiReceiver instance
│ Leaking: UNKNOWN
│ ↓ PageFetcher$PagerUiReceiver.this$0
│ ~~~~~~
├─ androidx.paging.PageFetcher instance
│ Leaking: UNKNOWN
│ ↓ PageFetcher.pagingSourceFactory
│ ~~~~~~~~~~~~~~~~~~~
├─ com.example.app.framework.ui.viewmodel.ShopViewModel$shopFlow$1 instance
│ Leaking: UNKNOWN
│ Anonymous subclass of kotlin.jvm.internal.Lambda
│ ↓ ShopViewModel$shopFlow$1.this$0
│ ~~~~~~
╰→ com.example.app.framework.ui.viewmodel.ShopViewModel instance
​ Leaking: YES (ObjectWatcher was watching this because com.example.app.framework.ui.viewmodel.ShopViewModel received ViewModel#onCleared() callback)
​ key = 0e65fcab-e6dd-475a-83d4-87b2050d797b
​ watchDurationMillis = 7771
​ retainedDurationMillis = 2769
====================================
EDIT
When scoping the ShopAdapter with #FragmentScoped, I get the following leak:
┬───
│ GC Root: Local variable in native code
│
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never leaking)
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (InternalLeakCanary↓ is not leaking)
│ ↓ Object[].[409]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.example.app.framework.ui.view.MainActivity instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking and Activity#mDestroyed is false)
│ mApplication instance of com.example.app.App
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper, not wrapping known Android context
│ ↓ MainActivity.navController$delegate
├─ kotlin.SynchronizedLazyImpl instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking)
│ ↓ SynchronizedLazyImpl._value
├─ androidx.navigation.NavHostController instance
│ Leaking: NO (MainNavHostFragment↓ is not leaking)
│ mActivity instance of com.example.app.framework.ui.view.MainActivity with mDestroyed = false
│ mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping
│ activity com.example.app.framework.ui.view.MainActivity with mDestroyed = false
│ ↓ NavHostController.mLifecycleOwner
├─ com.example.app.framework.ui.view.utils.MainNavHostFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
│ wrapping activity com.example.app.framework.ui.view.MainActivity with mDestroyed = false
│ ↓ MainNavHostFragment.mainFragmentFactory
│ ~~~~~~~~~~~~~~~~~~~
D/LeakCanary: ├─ com.example.app.framework.ui.view.utils.MainFragmentFactory instance
│ Leaking: UNKNOWN
│ Retaining 212 bytes in 7 objects
│ ↓ MainFragmentFactory.shopAdapter
│ ~~~~~~~~~~~
├─ com.example.app.framework.ui.adapter.recyclerview.ShopAdapter instance
│ Leaking: UNKNOWN
│ Retaining 14461 bytes in 546 objects
│ ↓ ShopAdapter.clickListener
│ ~~~~~~~~~~~~~
╰→ com.example.app.framework.ui.view.fragments.shop.ShopFragment instance
​ Leaking: YES (ObjectWatcher was watching this because com.example.app.framework.ui.view.fragments.shop.
​ ShopFragment received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
​ Retaining 2121 bytes in 79 objects
​ key = 71ec5094-8509-47a5-9e0a-070fe642ca8a
​ watchDurationMillis = 18366
​ retainedDurationMillis = 13365
​ componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
​ wrapping activity com.example.app.framework.ui.view.MainActivity with mDestroyed = false

I know that I'm late for the party but I can see that you are using lifecycleScope.launch a lot and inside you are calling the adapter to submitData. This means this adapter will not be able to be properly garbage collected. This is probably the root of memory leak.
Try to use viewLifecycleOwner.lifecycleScope.launch instead.
This is a known mistake:
https://proandroiddev.com/5-common-mistakes-when-using-architecture-components-403e9899f4cb

Okay I've managed to solve this leak. The leak was caused, because I've injected my ShopAdapter via Constructor Injection into my Fragment. When injecting something into the fragment via constructor injection, you have to pass the dependency to the MainFragmentFactory. But because of this, the MainFragmentFactory will always hold a reference to the adapter, even when the fragment is destroyed and the fragment is not needed any more (therefore, requireView().findViewById<RecyclerView>(R.id.rv_shop).adapter = null wont't even make a change here).
To solve this problem, DON'T inject the Adapter via constructor injection and rather inject it via field injection.

Related

Android AlertDialog DecorView Memory Leak

When initiating network communication through the Executor class, I use the showProgress function. When the result is received after communication ends, hideProgess function is used. The code is as follows and I am using it in BaseActivity.
private val networkIO = Executors.newFixedThreadPool(3)
private var progressBar: AlertDialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
progressBar = AlertDialog.Builder(this)
.setView(R.layout.dialog_loading)
.setCancelable(false)
.create()
.apply { window?.setBackgroundDrawableResource(android.R.color.transparent) }
doOnCreate()
}
override fun onDestroy() {
progressBar = null
super.onDestroy()
}
fun showProgress() {
runOnUiThread {
try {
if (progressBar?.isShowing == false) {
progressBar?.show()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun hideProgress() {
runOnUiThread { progressBar?.dismiss() }
}
A memory leak occurs when moving to another activity after performing the next network communication in MainActivity.
private fun fetchCard() {
networkIO.execute {
showProgress()
networkManager.getCardInfo { _, code, message ->
Timber.d("## $code")
Timber.d("## $message")
hideProgress()
}
}
}
Leak causes provided by LeakCanary
2021-03-08 05:11:42.552 8678-8678/com.tagless D/LeakCanary: ​
┬───
│ GC Root: System class
│
├─ android.app.ActivityThread class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static ActivityThread.sCurrentActivityThread
├─ android.app.ActivityThread instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ mInitialApplication instance of com.tagless.TaglessApp
│ mSystemContext instance of android.app.ContextImpl
│ mSystemUiContext instance of android.app.ContextImpl
│ ↓ ActivityThread.mActivities
├─ android.util.ArrayMap instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ArrayMap.mArray
├─ java.lang.Object[] array
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ Object[].[3]
├─ android.app.ActivityThread$ActivityClientRecord instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ activity instance of com.tagless.ui.main.MainActivity with mDestroyed = false
│ ↓ ActivityThread$ActivityClientRecord.activity
├─ com.tagless.ui.main.MainActivity instance
│ Leaking: NO (Activity#mDestroyed is false)
│ mApplication instance of com.tagless.TaglessApp
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper
│ ↓ BaseActivity.progressBar
│ ~~~~~~~~~~~
├─ androidx.appcompat.app.AlertDialog instance
│ Leaking: UNKNOWN
│ Retaining 106.7 kB in 1647 objects
│ mContext instance of android.view.ContextThemeWrapper, wrapping activity com.tagless.ui.main.MainActivity with
│ mDestroyed = false
│ Dialog#mDecor is null
│ ↓ Dialog.mWindow
│ ~~~~~~~
├─ com.android.internal.policy.PhoneWindow instance
│ Leaking: UNKNOWN
│ Retaining 23.3 kB in 289 objects
│ mContext instance of android.view.ContextThemeWrapper, wrapping activity com.tagless.ui.main.MainActivity with
│ mDestroyed = false
│ Window#mDestroyed is false
│ ↓ PhoneWindow.mDecor
│ ~~~~~~
╰→ com.android.internal.policy.DecorView instance
​ Leaking: YES (ObjectWatcher was watching this because com.android.internal.policy.DecorView received
​ View#onDetachedFromWindow() callback)
​ Retaining 3.4 kB in 59 objects
​ key = 5f2f819a-9640-429f-afff-4489f7b039d2
​ watchDurationMillis = 5703
​ retainedDurationMillis = 702
​ View not part of a window view hierarchy
​ View.mAttachInfo is null (view detached)
​ View.mWindowAttachCount = 1
​ mContext instance of android.view.ContextThemeWrapper, wrapping activity com.tagless.ui.main.MainActivity with
​ mDestroyed = false
METADATA
Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: samsung
LeakCanary version: 2.6
App process name: com.tagless
Stats: LruCache[maxSize=3000,hits=5668,misses=70891,hitRate=7%]
RandomAccess[bytes=3887192,reads=70891,travel=27244024610,range=20775300,size=25990100]
Heap dump reason: user request
Analysis duration: 3864 ms
Thanks in advance Anyone please help!!
Add progressBar = null should solve your issue. The progressBar is referenced by the activity and it can not be recycled by GC.
fun showProgress() {
runOnUiThread {
try {
if (progressBar == null) {
progressBar = AlertDialog.Builder(this)
.setView(R.layout.dialog_loading)
.setCancelable(false)
.create()
.apply { window?.setBackgroundDrawableResource(android.R.color.transparent) }
}
if (progressBar?.isShowing == false) {
progressBar?.show()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun hideProgress() {
runOnUiThread {
progressBar?.dismiss()
progressBar = null
}
}

Fragment binding is leaking even after setting null

As per android docs I have implemented simple fragment but when I move to next fragment it started leaking in _binding even I have set binding null.
Here is my leak Stack trace
37740 bytes retained by leaking objects
Signature: 7162f0f053a181182714235f17d7e8c36154eb81
┬───
│ GC Root: Global variable in native code
│
├─ android.database.ContentObserver$Transport instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ ContentObserver$Transport.mContentObserver
├─ android.widget.Editor$HandleViewShowObserver instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ Editor$HandleViewShowObserver.mContext
├─ dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ViewComponentManager$FragmentContextWrapper wraps an Activity with Activity.mDestroyed false
│ ↓ ViewComponentManager$FragmentContextWrapper.fragment
├─ com.ics.homework.ui.auth.LoginFragment instance
│ Leaking: NO (WelcomeFragment↓ is not leaking and Fragment#mFragmentManager is not null)
│ ↓ LoginFragment.mFragmentManager
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mFragmentStore
├─ androidx.fragment.app.FragmentStore instance
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ FragmentStore.mActive
├─ java.util.HashMap instance
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$Node[] array
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ HashMap$Node[].[1]
├─ java.util.HashMap$Node instance
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ HashMap$Node.value
├─ androidx.fragment.app.FragmentStateManager instance
│ Leaking: NO (WelcomeFragment↓ is not leaking)
│ ↓ FragmentStateManager.mFragment
├─ com.ics.homework.ui.auth.WelcomeFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ ↓ WelcomeFragment._binding
│ ~~~~~~~~
├─ com.ics.homework.databinding.FragmentWelcomeBindingImpl instance
│ Leaking: UNKNOWN
│ ↓ FragmentWelcomeBindingImpl.mRoot
│ ~~~~~
╰→ androidx.constraintlayout.widget.ConstraintLayout instance
​ Leaking: YES (ObjectWatcher was watching this because com.ics.homework.ui.auth.WelcomeFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
​ key = a1bab73c-de04-4e36-bda1-b29a54da8348
​ watchDurationMillis = 22798
​ retainedDurationMillis = 17792
​ mContext instance of com.ics.homework.ui.auth.AuthActivity with mDestroyed = false
​ View#mParent is null
​ View#mAttachInfo is null (view detached)
​ View.mWindowAttachCount = 1
====================================
0 LIBRARY LEAKS
Here is my implementation
class WelcomeFragment : Fragment() {
private var _binding: FragmentWelcomeBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentWelcomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.handler = onClickListener
}
private val onClickListener = View.OnClickListener { view ->
with(binding) {
when (view.id) {
btnLoginSignup.id -> {
val action = WelcomeFragmentDirections.actionWelcomeFragmentToLoginFragment()
findNavController().navigate(action)
}
tvHelpline.id,
tvHelplineNumber.id -> {
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:0123456789")
startActivity(intent)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
binding.handler = null
_binding = null
}
}
In the example from documentation binding is set to null in OnDestroyView() method, not in OnDestroy(). As far as I know, OnDestroy() method of Fragment A is not called when you replace it with Fragment B and add Fragment A to backstack. That's why your binding is not null.

Constraint Layout Leaking in Fragment - Leak Canary - Leak in TextInputLayout

I got a memory leak in my login page and I can`t figure why. I am using Leak Canary to identify the leak and this was the leak trace I got.
D/LeakCanary: ┬───
│ GC Root: Input or output parameters in native code
│
├─ okio.AsyncTimeout class
│ Leaking: NO (PathClassLoader↓ is not leaking and a class is never leaking)
│ ↓ static AsyncTimeout.$class$classLoader
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never leaking)
│ ↓ PathClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (InternalLeakCanary↓ is not leaking)
│ ↓ Object[].[957]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.eim.rdoApplication.ui.activity.MainActivity instance
│ Leaking: NO (LoginFragment↓ is not leaking and Activity#mDestroyed is false)
│ mApplication instance of com.eim.rdoApplication.AppApplication
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper, not wrapping known Android context
│ ↓ MainActivity.mActivityResultRegistry
├─ androidx.activity.ComponentActivity$2 instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ Anonymous subclass of androidx.activity.result.ActivityResultRegistry
D/LeakCanary: │ this$0 instance of com.eim.rdoApplication.ui.activity.MainActivity with mDestroyed = false
│ ↓ ComponentActivity$2.mKeyToCallback
├─ java.util.HashMap instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$Node[] array
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ HashMap$Node[].[2]
├─ java.util.HashMap$Node instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ HashMap$Node.value
├─ androidx.activity.result.ActivityResultRegistry$CallbackAndContract instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ ActivityResultRegistry$CallbackAndContract.mCallback
├─ androidx.fragment.app.FragmentManager$11 instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ Anonymous class implementing androidx.activity.result.ActivityResultCallback
│ ↓ FragmentManager$11.this$0
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mParent
├─ com.eim.rdoApplication.ui.fragment.login.LoginFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
D/LeakCanary: │ componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
│ wrapping activity com.eim.rdoApplication.ui.activity.MainActivity with mDestroyed = false
│ ↓ LoginFragment.mAnimationInfo
│ ~~~~~~~~~~~~~~
├─ androidx.fragment.app.Fragment$AnimationInfo instance
│ Leaking: UNKNOWN
│ Retaining 319257 bytes in 3340 objects
│ ↓ Fragment$AnimationInfo.mFocusedView
│ ~~~~~~~~~~~~
├─ com.google.android.material.textfield.TextInputEditText instance
│ Leaking: UNKNOWN
│ Retaining 319171 bytes in 3339 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.inputEditTextEmailField
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping activity com.eim.rdoApplication.ui.
│ activity.MainActivity with mDestroyed = false
│ ↓ TextInputEditText.mParent
D/LeakCanary: │ ~~~~~~~
├─ android.widget.FrameLayout instance
│ Leaking: UNKNOWN
│ Retaining 2231 bytes in 14 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping activity com.eim.rdoApplication.ui.
│ activity.MainActivity with mDestroyed = false
│ ↓ FrameLayout.mParent
│ ~~~~~~~
├─ com.google.android.material.textfield.TextInputLayout instance
│ Leaking: UNKNOWN
│ Retaining 204872 bytes in 2728 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.textInputLayoutEmail
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping activity com.eim.rdoApplication.ui.
│ activity.MainActivity with mDestroyed = false
D/LeakCanary: │ ↓ TextInputLayout.mParent
│ ~~~~~~~
╰→ androidx.constraintlayout.widget.ConstraintLayout instance
​ Leaking: YES (ObjectWatcher was watching this because com.eim.rdoApplication.ui.fragment.login.LoginFragment
D/LeakCanary: ​ received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
​ Retaining 4897 bytes in 71 objects
​ key = 0d74b072-904d-4aba-9a0e-4e17260ffc96
​ watchDurationMillis = 9630
​ retainedDurationMillis = 4627
​ View not part of a window view hierarchy
​ View.mAttachInfo is null (view detached)
​ View.mWindowAttachCount = 1
​ mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping
​ activity com.eim.rdoApplication.ui.activity.MainActivity with mDestroyed = false
METADATA
Build.VERSION.SDK_INT: 27
Build.MANUFACTURER: motorola
LeakCanary version: 2.5
App process name: com.eim.aplicativo_rdo
What could be causing this leak? Any help ?
I am working with one activity and multiple fragments being handled by navigation component.
From what I could understand, the main problem is that my textInputlayoutEmail somehow is not part of a view hierarchy at some point of the lifecycle of my fragment. This is my Fragment Code :
private const val ERROR_LOGIN = "ERROR_LOGIN"
#AndroidEntryPoint
class LoginFragment : Fragment() {
private val navController by lazy {findNavController()}
private val loginViewModel: LoginViewModel by navGraphViewModels(R.id.navigation_graph){
defaultViewModelProviderFactory
}
private var _binding: FragmentLoginBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentLoginBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setLoadingView()
setGoToForgotPasswordFragmentButton()
setLoginButton()
super.onViewCreated(view, savedInstanceState)
}
private fun setLoginButton() {
binding.buttonSignIn.setOnClickListener {
if(areFieldsValid()){
loginViewModel.emailField = binding.inputEditTextEmailField.text.toString()
loginViewModel.passwordField = binding.editTextPasswordField.text.toString()
loginUser(loginViewModel.let { UserDataRequest(it.emailField, it.passwordField) })
}
}
}
private fun areFieldsValid(): Boolean {
return !(binding.editTextPasswordField.text.isNullOrBlank()||binding.inputEditTextEmailField.text.isNullOrBlank())
}
private fun setGoToForgotPasswordFragmentButton() {
binding.forgotPasswordEditText.setOnClickListener{
val action = LoginFragmentDirections.actionLoginFragmentToForgotPasswordFragment()
navController.navigate(action)
}
}
private fun setLoadingView() {
binding.apply {
loadingIconLoginFragment.visibility = View.INVISIBLE
loadingBgLoginFragment.visibility = View.INVISIBLE
}
}
private fun loginUser(loginUserRequest: UserDataRequest){
viewLifecycleOwner.lifecycleScope.launch {
loginViewModel.loginUser(loginUserRequest).collect {
when(it.status) {
Resource.Status.LOADING -> {
withContext(Dispatchers.Main) {
setLoadingInterface()
}
}
Resource.Status.ERROR -> {
withContext(Dispatchers.Main){
val errorMessage = "Não foi possível realizar seu Login"
clearLoadingInterface()
showErrorInterface(errorMessage, ERROR_LOGIN)
}
}
Resource.Status.SUCCESS -> {
withContext(Dispatchers.Main) {
clearLoadingInterface()
val action = LoginFragmentDirections.actionLoginFragmentToGeneralHomeFragment(0, "", "")
navController.navigate(action)
}
}
}
}
}
}
private fun setLoadingInterface() {
binding.apply {
loadingBgLoginFragment.visibility = View.VISIBLE
loadingIconLoginFragment.visibility = View.VISIBLE
}
rotateAnimation()
}
private fun showErrorInterface(errorMessage: String, errorType: String) {
clearLoadingInterface()
val dialog = NetworkErrorDialog.newInstance(errorMessage, errorType)
dialog.show(childFragmentManager, dialog.tag)
}
private fun clearLoadingInterface() {
binding.apply {
loadingBgLoginFragment.visibility = View.GONE
loadingIconLoginFragment.apply {
visibility = View.GONE
clearAnimation()
}
}
}
private fun rotateAnimation() {
val animation = AnimationUtils.loadAnimation(activity?.applicationContext, R.anim.rotate)
binding.loadingIconLoginFragment.startAnimation(animation)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
In my views so I don`t know what else can I do to clear this reference.
If anyone could show me in which part of the code I am holding a reference to this view, would help me a lot. Thanks

RecyclerView↑ is leaking and View detached and has parent

I have tried to set adapter null in onDestroyView also tried with addOnAttachStateChangeListener but still there is memory leak.
Here is my Stack Trace
┬───
│ GC Root: System class
│
├─ android.app.ActivityThread class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static ActivityThread.sCurrentActivityThread
├─ android.app.ActivityThread instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ActivityThread.mActivities
├─ android.util.ArrayMap instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ArrayMap.mArray
├─ java.lang.Object[] array
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ Object[].[1]
├─ android.app.ActivityThread$ActivityClientRecord instance
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ActivityThread$ActivityClientRecord.activity
D/LeakCanary: ├─ com.ics.homework.ui.MainActivity instance
│ Leaking: NO (TopicFragment↓ is not leaking and Activity#mDestroyed is false)
│ ↓ MainActivity.mActivityResultRegistry
├─ androidx.activity.ComponentActivity$2 instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ Anonymous subclass of androidx.activity.result.ActivityResultRegistry
│ ↓ ComponentActivity$2.mKeyToCallback
├─ java.util.HashMap instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$HashMapEntry[] array
│ Leaking: NO (TopicFragment↓ is not leaking)
│ ↓ HashMap$HashMapEntry[].[1]
├─ java.util.HashMap$HashMapEntry instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ ↓ HashMap$HashMapEntry.value
├─ androidx.activity.result.ActivityResultRegistry$CallbackAndContract instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ ↓ ActivityResultRegistry$CallbackAndContract.mCallback
├─ androidx.fragment.app.FragmentManager$10 instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ Anonymous class implementing androidx.activity.result.ActivityResultCallback
│ ↓ FragmentManager$10.this$0
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (TopicFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mParent
├─ com.ics.homework.ui.course.topics.TopicFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ ↓ TopicFragment.mAnimationInfo
│ ~~~~~~~~~~~~~~
├─ androidx.fragment.app.Fragment$AnimationInfo instance
│ Leaking: UNKNOWN
│ ↓ Fragment$AnimationInfo.mFocusedView
│ ~~~~~~~~~~~~
├─ androidx.recyclerview.widget.RecyclerView instance
│ Leaking: YES (View detached and has parent)
│ mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping activity com.ics.homework.ui.MainActivity with mDestroyed = false
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mID = R.id.recyclerView
│ View.mWindowAttachCount = 1
│ ↓ RecyclerView.mParent
├─ androidx.swiperefreshlayout.widget.SwipeRefreshLayout instance
│ Leaking: YES (RecyclerView↑ is leaking and View detached and has parent)
│ mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping activity com.ics.homework.ui.MainActivity with mDestroyed = false
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mID = R.id.swipeRefreshLayout
│ View.mWindowAttachCount = 1
│ ↓ SwipeRefreshLayout.mParent
D/LeakCanary: ╰→ androidx.constraintlayout.widget.ConstraintLayout instance
​ Leaking: YES (ObjectWatcher was watching this because com.ics.homework.ui.course.topics.TopicFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
​ key = b5fcd20b-86ca-432c-ab69-0e4a90881651
​ watchDurationMillis = 26087
​ retainedDurationMillis = 21084
​ mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping activity com.ics.homework.ui.MainActivity with mDestroyed = false
​ View#mParent is null
​ View#mAttachInfo is null (view detached)
​ View.mWindowAttachCount = 1
====================================
0 LIBRARY LEAKS
My Implementaion is
#AndroidEntryPoint
class TopicFragment : Fragment() {
private var courseId: String? = null
private var title: String? = null
private var _binding: FragmentTopicBinding? = null
private val binding get() = _binding!!
private val topicViewModel by viewModels<TopicViewModel>()
private lateinit var topicAdapter: TopicAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
courseId = it.getString(ARG_COURSE_ID)
title = it.getString(ARG_TITLE)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentTopicBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = topicViewModel
topicAdapter = TopicAdapter(topicItemClick)
binding.apply {
stateErrorView.apply {
handler = retryCallback
}
swipeRefreshLayout.setOnRefreshListener {
topicViewModel.retry()
}
recyclerView.apply {
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
adapter = topicAdapter
setHasFixedSize(true)
}
}
observeUI()
}
private fun observeUI() {
topicViewModel.topics.observe(viewLifecycleOwner, {
Timber.e(it.status.toString())
it.data?.let(topicAdapter::submitList)
if (it.status == Status.ERROR) topicAdapter.submitList(listOf())
if (it.status != Status.LOADING) binding.swipeRefreshLayout.isRefreshing = false
})
}
private val topicItemClick = object : TopicItemClick {
override fun onClick(topic: Topic) {
val action = TopicFragmentDirections.actionTopicFragmentToChapterFragment(
topic.postId, title!!, false, topic.id, topic.topic
)
findNavController().navigate(action)
}
}
private val retryCallback = object : RetryCallback {
override fun retry() {
topicViewModel.retry()
}
}
override fun onDestroyView() {
binding.recyclerView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {}
override fun onViewDetachedFromWindow(v: View) {
binding.recyclerView.adapter=null
}
})
super.onDestroyView()
_binding = null
}
companion object {
private const val ARG_COURSE_ID = "courseId"
private const val ARG_TITLE = "title"
}
}
TopicFragment is in a created state, Fragment.mAnimationInfo retains a Fragment$AnimationInfo and Fragment$AnimationInfo.mFocusedView retains the fragments detached view which should have been GCed.
This value is set by calls to Fragment.setFocusView(), and a quick search shows that's never ever cleared: https://cs.android.com/search?q=setFocusedView&sq=&ss=androidx%2Fplatform%2Fframeworks%2Fsupport
Looks like this change was introduced in 2020: https://cs.android.com/androidx/platform/frameworks/support/+/1052c3662c40176f7f02da9e06b989dcab21d500
The best would be to file an issue against the androidx fragments library.
Actually this was already filed, fixed in the next release: https://issuetracker.google.com/issues/179925887

NavContoller Leaking in Fragment

I am have trouble finding the cause of the leaks happening when I transition between fragments using JetPack Navigation.
See below the stack trace of the from leak canary
D/LeakCanary: ​
┬───
│ GC Root: System class
│
├─ android.view.inputmethod.InputMethodManager class
│ Leaking: NO (InputMethodManager↓ is not leaking and a class is never leaking)
│ ↓ static InputMethodManager.sInstance
├─ android.view.inputmethod.InputMethodManager instance
│ Leaking: NO (DecorView↓ is not leaking and InputMethodManager is a singleton)
│ ↓ InputMethodManager.mNextServedView
├─ com.android.internal.policy.DecorView instance
│ Leaking: NO (LinearLayout↓ is not leaking and View attached)
│ View is part of a window view hierarchy
D/LeakCanary: │ View.mAttachInfo is not null (view attached)
│ View.mWindowAttachCount = 1
│ mContext instance of com.android.internal.policy.DecorContext, wrapping activity com.carepay.flows.CarePayActivity
│ with mDestroyed = false
│ ↓ DecorView.mContentRoot
├─ android.widget.LinearLayout instance
│ Leaking: NO (CarePayActivity↓ is not leaking and View attached)
│ View is part of a window view hierarchy
│ View.mAttachInfo is not null (view attached)
│ View.mWindowAttachCount = 1
│ mContext instance of com.carepay.flows.CarePayActivity with mDestroyed = false
│ ↓ LinearLayout.mContext
├─ com.carepay.flows.CarePayActivity instance
│ Leaking: NO (NavHostFragment↓ is not leaking and Activity#mDestroyed is false)
│ mApplication instance of com.carepay.MemberApp
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper, not wrapping known Android context
│ ↓ CarePayActivity.mFragments
├─ androidx.fragment.app.FragmentController instance
│ Leaking: NO (NavHostFragment↓ is not leaking)
│ ↓ FragmentController.mHost
├─ androidx.fragment.app.FragmentActivity$HostCallbacks instance
│ Leaking: NO (NavHostFragment↓ is not leaking)
│ this$0 instance of com.carepay.flows.CarePayActivity with mDestroyed = false
│ mActivity instance of com.carepay.flows.CarePayActivity with mDestroyed = false
│ mContext instance of com.carepay.flows.CarePayActivity with mDestroyed = false
│ ↓ FragmentActivity$HostCallbacks.mFragmentManager
D/LeakCanary: ├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (NavHostFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mPrimaryNav
├─ androidx.navigation.fragment.NavHostFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ ↓ NavHostFragment.mNavController
│ ~~~~~~~~~~~~~~
├─ androidx.navigation.NavHostController instance
│ Leaking: UNKNOWN
│ Retaining 1313968 bytes in 7994 objects
│ mActivity instance of com.carepay.flows.CarePayActivity with mDestroyed = false
│ mContext instance of com.carepay.flows.CarePayActivity with mDestroyed = false
│ ↓ NavHostController.mOnDestinationChangedListeners
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
├─ java.util.concurrent.CopyOnWriteArrayList instance
│ Leaking: UNKNOWN
│ Retaining 1302920 bytes in 7681 objects
│ ↓ CopyOnWriteArrayList.elements
│ ~~~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 1302896 bytes in 7679 objects
│ ↓ Object[].[2]
D/LeakCanary: │ ~~~
├─ androidx.navigation.ui.ActionBarOnDestinationChangedListener instance
│ Leaking: UNKNOWN
│ Retaining 5425 bytes in 190 objects
│ mActivity instance of com.carepay.flows.CarePayActivity with mDestroyed = false
│ mContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper, wrapping
│ activity com.carepay.flows.CarePayActivity with mDestroyed = false
│ ↓ ActionBarOnDestinationChangedListener.mContext
│ ~~~~~~~~
├─ dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper instance
│ Leaking: UNKNOWN
│ Retaining 4365 bytes in 162 objects
│ mBase instance of com.carepay.flows.CarePayActivity with mDestroyed = false
│ ViewComponentManager$FragmentContextWrapper wraps an Activity with Activity.mDestroyed false
│ ↓ ViewComponentManager$FragmentContextWrapper.fragment
│ ~~~~~~~~
╰→ com.carepay.flows.treatmentdetail.TreatmentDetailFragment instance
​ Leaking: YES (ObjectWatcher was watching this because com.carepay.flows.treatmentdetail.TreatmentDetailFragment
​ received Fragment#onDestroy() callback and Fragment#mFragmentManager is null)
​ Retaining 3565 bytes in 129 objects
​ key = 3b650fce-861c-463b-8ccb-78255dfc535a
​ watchDurationMillis = 22434
​ retainedDurationMillis = 16751
​ componentContext instance of dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper,
​ wrapping activity com.carepay.flows.CarePayActivity with mDestroyed = false
All fragment transitions are throwing this leak so I will share this snippet from for the fragment in the selected stack trace.
TreatmentDetailFragment
class TreatmentDetailFragment : OverlayFragment(R.layout.fragment_treatment_detail) {
private val binding get() = (_binding as FragmentTreatmentDetailBinding?)!!
private val args: TreatmentDetailFragmentArgs by navArgs()
private val viewModel by viewModels<TreatmentDetailViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentTreatmentDetailBinding.inflate(inflater, container, false)
val adapter = ItemsAdapter()
binding.treatmentItems.adapter = adapter
setupObservers(adapter)
viewModel.fetchTreatment(args.treatmentId, lifecycle)
return binding.root
}
private fun setupObservers(adapter: ItemsAdapter) {
viewModel.treatment.observe(viewLifecycleOwner){ treatment ->
binding.titleView.text = getString(R.string.treatment, treatment.treatmentCode)
binding.treatment = treatment
adapter.items = treatment.getItems()
adapter.notifyDataSetChanged()
}
}
}
OverlayFragment
abstract class OverlayFragment(#LayoutRes layoutResource: Int) : BaseFragment(layoutResource) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val activity = requireActivity() as CarePayActivity
activity.showBackButton(true)
activity.showCloseIcon(true)
activity.showBottomNavigation(View.GONE)
}
}
BaseFragment
#AndroidEntryPoint
abstract class BaseFragment(#LayoutRes layoutResource: Int) : Fragment(layoutResource) {
#Inject
protected lateinit var memberSettingsRepository: MemberSettingsRepository
protected var _binding: ViewBinding? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = findNavController()
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.navigation_health,
R.id.navigation_benefits,
R.id.navigation_clinics,
R.id.navigation_account
)
)
val toolbar = view.findViewById<Toolbar>(R.id.toolbar)
val activity = requireActivity() as CarePayActivity
activity.setSupportActionBar(toolbar)
activity.setupActionBarWithNavController(navController, appBarConfiguration)
}
}
Activity
#AndroidEntryPoint
class CarePayActivity : AppCompatActivity() {
#Inject
lateinit var memberSettingsRepository: MemberSettingsRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_carepay)
val navView: BottomNavigationView = findViewById(R.id.nav_view)
val navController = findNavController(R.id.nav_host_fragment)
val inflater = navController.navInflater
val graph = inflater.inflate(R.navigation.mobile_navigation)
when {
intent.hasExtra(RESUMED) -> {
graph.startDestination = R.id.navigation_health
}
else -> {
graph.startDestination = R.id.navigation_setup
}
}
navController.graph = graph
navView.setupWithNavController(navController)
}
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.nav_host_fragment).navigateUp() || super.onSupportNavigateUp()
}
override fun onBackPressed() {
val currentDestination = findNavController(R.id.nav_host_fragment).currentDestination
when (currentDestination?.id) {
R.id.navigation_health -> {
val dialog = getDialog(
getString(R.string.exit),
getString(R.string.exit_message, getString(R.string.app_name)),
getString(R.string.yes),
getString(R.string.no),
object : DialogButtonEvents {
override fun onButtonClicked(id: Int) {
if (id == R.id.positiveButton) {
finish()
}
}
})
dialog.show()
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT));
}
else -> {
findNavController(R.id.nav_host_fragment).navigateUp()
}
}
}
}
val toolbar = view.findViewById<Toolbar>(R.id.toolbar)
val activity = requireActivity() as CarePayActivity
activity.setSupportActionBar(toolbar)
activity.setupActionBarWithNavController(navController, appBarConfiguration)
Those lines cause the the leak, because you are providing the navController that belongs to the Fragment to the Activity, so whenever Fragment is destroy it will be unable to be dereferenced because of the navController.
Note: Fragments outlive their views. Make sure you clean up any references to the binding class instance in the fragment's onDestroyView() method. link
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

Categories

Resources