I have a fragment which gets data from an api, When i navigate to any other fragment i get this memory leak and
I am using the LeakCanary library to monitor memory leaks in my app. I received this memory leak and not sure how to track down what is causing it.
LeakCanaryLog:
210 bytes retained by leaking objects
D Signature: 135947a80c7aa2aa27381bdc2deb41d0342a02cb
D ┬───
D │ GC Root: Global variable in native code
D │
D ├─ java.util.HashMap instance
D │ Leaking: NO (o↓ is not leaking)
D │ ↓ HashMap["view"]
D ├─ com.google.android.gms.ads.nonagon.ad.webview.o instance
D │ Leaking: NO (MainActivity↓ is not leaking and View attached)
D │ View is part of a window view hierarchy
D │ View.mAttachInfo is not null (view attached)
D │ View.mWindowAttachCount = 1
D │ mContext instance of xxx.xxx.xxxx.activity.MainActivity with mDestroyed = false
D │ ↓ View.mContext
D ├─ xxx.xxx.xxxx.activity.MainActivity instance
D │ Leaking: NO (GamesFragment↓ is not leaking and Activity#mDestroyed is false)
D │ mApplication instance of xxx.xxx.xxxx.BaseApplication
D │ mBase instance of androidx.appcompat.view.ContextThemeWrapper
D │ ↓ ComponentActivity.mOnConfigurationChangedListeners
D ├─ java.util.concurrent.CopyOnWriteArrayList instance
D │ Leaking: NO (GamesFragment↓ is not leaking)
D │ ↓ CopyOnWriteArrayList[4]
D ├─ androidx.fragment.app.FragmentManager$$ExternalSyntheticLambda0 instance
D │ Leaking: NO (GamesFragment↓ is not leaking)
D │ ↓ FragmentManager$$ExternalSyntheticLambda0.f$0
D ├─ androidx.fragment.app.FragmentManagerImpl instance
D │ Leaking: NO (GamesFragment↓ is not leaking)
D │ ↓ FragmentManager.mParent
D ├─ xxx.xxx.xxxx.fragment.GamesFragment instance
D │ Leaking: NO (Fragment#mFragmentManager is not null)
D │ ↓ Fragment.mSavedStateRegistryController
D │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
D ├─ androidx.savedstate.SavedStateRegistryController instance
D │ Leaking: UNKNOWN
D │ Retaining 17 B in 1 objects
D │ ↓ SavedStateRegistryController.savedStateRegistry
D │ ~~~~~~~~~~~~~~~~~~
D ├─ androidx.savedstate.SavedStateRegistry instance
D │ Leaking: UNKNOWN
D │ Retaining 494 B in 18 objects
D │ ↓ SavedStateRegistry.components
D │ ~~~~~~~~~~
D ├─ androidx.arch.core.internal.SafeIterableMap instance
D │ Leaking: UNKNOWN
D │ Retaining 471 B in 17 objects
D │ ↓ SafeIterableMap["androidx.lifecycle.internal.SavedStateHandlesProvider"]
D │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
D ├─ androidx.lifecycle.SavedStateHandlesProvider instance
D │ Leaking: UNKNOWN
D │ Retaining 251 B in 10 objects
D │ ↓ SavedStateHandlesProvider.viewModel$delegate
D │ ~~~~~~~~~~~~~~~~~~
D ├─ kotlin.SynchronizedLazyImpl instance
D │ Leaking: UNKNOWN
D │ Retaining 230 B in 9 objects
D │ ↓ SynchronizedLazyImpl._value
D │ ~~~~~~
D ╰→ androidx.lifecycle.SavedStateHandlesVM instance
D Leaking: YES (ObjectWatcher was watching this because androidx.lifecycle.SavedStateHandlesVM received
D ViewModel#onCleared() callback)
D Retaining 210 B in 8 objects
D key = 292612b7-4d49-4a5a-a898-b2eedd194531
D watchDurationMillis = 16225
D retainedDurationMillis = 11225
D ====================================
How i navigate from one fragment to another:
private fun loadFragment(fragment: Fragment){
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragmentView,fragment)
transaction.addToBackStack(null)
transaction.commit()
}
Fragment Code:
class GamesFragment : DaggerFragment() {
//Backend
@Inject
lateinit var gamesDealsViewModelFactory: GamesDealsViewModelFactory
private var _gamesDealsViewModel: GamesDealsViewModel? = null
private val gamesDealsViewModel: GamesDealsViewModel get() = _gamesDealsViewModel!!
//RecyclerView
private var gamesRecyclerArray: ArrayList<GamesItem> = ArrayList()
private var gamesRecyclerAdapter: GamesRecyclerAdapter? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_view = DataBindingUtil.inflate(inflater,R.layout.fragment_games,container,false)
init()
setUpListeners()
setUpObservers()
getStoresList()
return view.root
}
private fun init(){
_gamesDealsViewModel = ViewModelProvider(this@GamesFragment,gamesDealsViewModelFactory)[GamesDealsViewModel::class.java]
}
private fun setUpListeners(){
view.resultsRecyclerViewLayout.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
....
}
}
})
}
private fun setUpObservers(){
gamesDealsViewModel.requestStatus.observe(viewLifecycleOwner){
if (it == "SUCCESS"){
....
}else if(it != ""){
....
}
}
}
private fun setUpGamesRecycler(games: Games) {
//Data Handel
for (game in games){
gamesRecyclerArray.add(game)
}
//Adapter Setup
gamesRecyclerAdapter = GamesRecyclerAdapter(gamesRecyclerArray,requireActivity())
//Adapter OnClick listener
gamesRecyclerAdapter?.onItemClick = { game ->
....
}
}
override fun onDestroyView() {
view.gamesBannerAd.destroy()
gamesRecyclerAdapter = null
view.resultsRecyclerViewLayout.adapter = null
mInterstitialAd = null
_view = null
_gamesDealsViewModel = null
super.onDestroyView()
}
}
ViewModelCode:
class GamesDealsViewModel(private val gamesDealsRepository: GameDealsRepository): ViewModel(){
private val coroutineExceptionHandler = CoroutineExceptionHandler{ _, t ->
run {
requestStatus.postValue(t.message.toString())
}
}
fun startGamesRequest(){
viewModelScope.launch(Dispatchers.IO+coroutineExceptionHandler) {
gamesDealsRepository.getGamesDeals()
}
}
val gamesDealsData :LiveData<Games> get() = gamesDealsRepository.games
val requestStatus: MutableLiveData<String> get() = gamesDealsRepository.requestStatus
}
What i have tried:
-I have tried changing the observer lifecycle owner and it did not fix the leak
The leak trace indicates that GamesFragment is alive and attached to an alive activity, but it holds on to a SavedStateHandlesVM view model internal to jetpack which has received its onClear() callback and should be garbage collected.
This looks like a leak inside Jetpack lifecycle. You should create a sample project that reproduces this and then submit it as a bug to the Android team (don't forget to provide library version and make sure it reproduces on the latest)