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.
Related
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
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
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.
I know that adding a fragment transaction to the backstack and then moving from that fragment to another fragment, the reference of the previous fragment's view is still available and it only gets destroyed when the back button is pressed.
And to avoid this, I have set the view to null in onDestroyView but the problem is, leakcanary still shows view is not null and the view reference is still available whereas logging the view says it is null.
Why is it so ?
Also, please correct me if I'm wrong or missing anything.
The fragment class-
private var mView: View? = null
private lateinit var btnSignUp: Button
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mView = inflater.inflate(R.layout.fragment_login, container, false)
return mView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnSignUp = view.findViewById(R.id.btnSignUp)
btnSignUp.setOnClickListener {
// calling function changeFragment()
changeFragment(SignUpFragment(), FragmentsTag.SIGNUP_FRAGMENT)
}
}
override fun onDestroyView() {
super.onDestroyView()
mView=null
}
LeakCanary Analysis logs --
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
43817 bytes retained by leaking objects
Signature: 6e77557c8a679dd41391c1c5badaac98217366ad
┬───
│ GC Root: System class
│
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ com.example.foodrunner.activities.MainActivity instance
│ Leaking: NO (LoginFragment↓ is not leaking and Activity#mDestroyed is false)
│ ↓ MainActivity.mFragments
├─ androidx.fragment.app.FragmentController instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ FragmentController.mHost
├─ androidx.fragment.app.FragmentActivity$HostCallbacks instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ FragmentActivity$HostCallbacks.mFragmentManager
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mActive
├─ java.util.HashMap instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$HashMapEntry[] array
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ HashMap$HashMapEntry[].[0]
├─ java.util.HashMap$HashMapEntry instance
│ Leaking: NO (LoginFragment↓ is not leaking)
│ ↓ HashMap$HashMapEntry.value
├─ com.example.foodrunner.fragments.LoginFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ Fragment.mTag=Login Fragment
│ ↓ LoginFragment.btnLogin
│ ~~~~~~~~
├─ com.google.android.material.button.MaterialButton instance
│ Leaking: YES (View detached and has parent)
│ mContext instance of com.example.foodrunner.activities.MainActivity with mDestroyed = false
│ View#mParent is set
│ View#mAttachInfo is null (view detached)
│ View.mID = R.id.btnLogin
│ View.mWindowAttachCount = 1
│ ↓ MaterialButton.mParent
╰→ androidx.constraintlayout.widget.ConstraintLayout instance
Leaking: YES (ObjectWatcher was watching this because com.example.foodrunner.fragments.LoginFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks))
key = b72a82a6-b9dd-46c6-afb2-0ea6c7025001
watchDurationMillis = 9582
retainedDurationMillis = 4582
key = 0554b63a-c700-4c86-a451-b0daae06607a
watchDurationMillis = 9581
retainedDurationMillis = 4580
mContext instance of com.example.foodrunner.activities.MainActivity with mDestroyed = false
View#mParent is null
View#mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
====================================
You're still holding onto a reference to btnSignUp after onDestroyView - that is what is leaking. You have to drop all reference to all Views within the view that was just destroyed.
Therefore you should either use the same approach (make it a nullable var) or not hold onto a reference to btnSignUp in your Fragment at all - at least in your code sample, it could easily be a local variable. (In fact, the same applies to your mView - you get the View as an input to onViewCreated(), there's no reason to hold onto it at the Fragment level).
As #Rafsanjani mention you can use this too :
FragmentManager manager = getActivity().getSupportFragmentManager();
FragmentTransaction trans = manager.beginTransaction();
trans.remove(myFrag);
trans.commit();
manager.popBackStack();
Use this in your onBackPressed
I used Jetpack's navigation to manage Fragment, which uses databinding in Fragment.
Did not add other code.
The memory leak is HomeFragment.databinding.root, which is a LinearLayout, and LinearLayout does not put anything.
The LeakCanary message is shown below:
I used Jetpack's navigation to manage Fragment, which uses databinding in Fragment.
Did not add other code.
The memory leak is HomeFragment.databinding.root, which is a LinearLayout, and LinearLayout does not put anything.
The LeakCanary message is shown below:
ApplicationLeak(className=android.widget.LinearLayout, leakTrace=
┬
├─ android.app.ActivityThread
│ Leaking: NO (ActivityThread↓ is not leaking and a class is never leaking)
│ GC Root: System class
│ ↓ static ActivityThread.sCurrentActivityThread
├─ android.app.ActivityThread
│ Leaking: NO (ArrayMap↓ is not leaking)
│ ↓ ActivityThread.mActivities
├─ android.util.ArrayMap
│ Leaking: NO (Object[]↓ is not leaking)
│ ↓ ArrayMap.mArray
├─ java.lang.Object[]
│ Leaking: NO (ActivityThread$ActivityClientRecord↓ is not leaking)
│ ↓ array Object[].[1]
├─ android.app.ActivityThread$ActivityClientRecord
│ Leaking: NO (MainActivity↓ is not leaking)
│ ↓ ActivityThread$ActivityClientRecord.activity
├─ com.ukex.module.index.ui.MainActivity
│ Leaking: NO (FragmentController↓ is not leaking and Activity#mDestroyed is false)
│ ↓ MainActivity.mFragments
├─ androidx.fragment.app.FragmentController
│ Leaking: NO (FragmentActivity$HostCallbacks↓ is not leaking)
│ ↓ FragmentController.mHost
├─ androidx.fragment.app.FragmentActivity$HostCallbacks
│ Leaking: NO (FragmentManagerImpl↓ is not leaking)
│ ↓ FragmentActivity$HostCallbacks.mFragmentManager
├─ androidx.fragment.app.FragmentManagerImpl
│ Leaking: NO (NavHostFragment↓ is not leaking)
│ ↓ FragmentManagerImpl.mPrimaryNav
├─ androidx.navigation.fragment.NavHostFragment
│ Leaking: NO (FragmentManagerImpl↓ is not leaking and Fragment#mFragmentManager is not null)
│ ↓ NavHostFragment.mChildFragmentManager
├─ androidx.fragment.app.FragmentManagerImpl
│ Leaking: NO (HashMap↓ is not leaking)
│ ↓ FragmentManagerImpl.mActive
├─ java.util.HashMap
│ Leaking: NO (HashMap$Node[]↓ is not leaking)
│ ↓ HashMap.table
├─ java.util.HashMap$Node[]
│ Leaking: NO (HashMap$Node↓ is not leaking)
│ ↓ array HashMap$Node[].[0]
├─ java.util.HashMap$Node
│ Leaking: NO (HomeFragment↓ is not leaking)
│ ↓ HashMap$Node.value
├─ com.ukex.module.index.ui.HomeFragment
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ ↓ HomeFragment.dataBinding
│ ~~~~~~~~~~~
├─ com.ukex.databinding.HomeFragmentBindingImpl
│ Leaking: UNKNOWN
│ ↓ HomeFragmentBindingImpl.mRoot
│ ~~~~~
╰→ android.widget.LinearLayout
Leaking: YES (ObjectWatcher was watching this)
mContext instance of com.ukex.module.index.ui.MainActivity with mDestroyed = false
View#mParent is null
View#mAttachInfo is null (view detached)
View.mWindowAttachCount = 1
key = 965e1901-f293-454b-b8c2-80b869d64f9a
watchDurationMillis = 21809
retainedDurationMillis = 16807
, retainedHeapByteSize=6255)
class HomeFragment : BaseVMFragment<HomeViewModel>() {
private lateinit var dataBinding: HomeFragmentBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
dataBinding =
DataBindingUtil.inflate(inflater, R.layout.home_fragment, container, false)
return dataBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
dataBinding.apply {
vm = mViewModel
lifecycleOwner = this#HomeFragment
}
btnLogin.setOnClickListener {
startActivity(Intent(context, LoginAct::class.java))
}
}
override fun bindObserve() {
super.bindObserve()
mViewModel?.user?.observe(this, Observer {
if (it != null)
ToastUtils.showLong(it.username)
})
}
override fun providerVMClass(): Class<HomeViewModel>? {
return HomeViewModel::class.java
}
}
A view should be released from memory after Fragment.onDestroyView() is called, even if the fragment is not destroyed yet.
You need to override HomeFragment.onDestroyView() and set dataBinding (or the view it wraps) to null