(The whole code can be found here (without the leakCanary dependencies): https://github.com/Dawwit0001/HiltMultiModule)
I created 2 fragments, a login fragment and a register fragment, whenever the user opens the app, the login screen is displayed if the user navigates to the register screen, creates an account and then navigates back to the login screen a leak happens. I am not sure why is that, but I discovered that when I replace the "savedInstanceState" in the login fragment with null (inside onViewCreated), it does not happen.
The whole leak:
┬───
│ GC Root: Input or output parameters in native code
│
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (InternalLeakCanary↓ is not leaking and A ClassLoader is never
│ leaking)
│ ↓ ClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (InternalLeakCanary↓ is not leaking)
│ ↓ Object[728]
├─ leakcanary.internal.InternalLeakCanary class
│ Leaking: NO (MainActivity↓ is not leaking and a class is never leaking)
│ ↓ static InternalLeakCanary.resumedActivity
├─ winged.example.hiltmultimodule.MainActivity instance
│ Leaking: NO (RegisterFragment↓ is not leaking and Activity#mDestroyed is
│ false)
│ mApplication instance of winged.example.hiltmultimodule.di.
│ HiltMultiModuleApplication
│ mBase instance of androidx.appcompat.view.ContextThemeWrapper
│ ↓ ComponentActivity.mOnConfigurationChangedListeners
├─ java.util.concurrent.CopyOnWriteArrayList instance
│ Leaking: NO (RegisterFragment↓ is not leaking)
│ ↓ CopyOnWriteArrayList[4]
├─ androidx.fragment.app.FragmentManager$$ExternalSyntheticLambda0 instance
│ Leaking: NO (RegisterFragment↓ is not leaking)
│ ↓ FragmentManager$$ExternalSyntheticLambda0.f$0
├─ androidx.fragment.app.FragmentManagerImpl instance
│ Leaking: NO (RegisterFragment↓ is not leaking)
│ ↓ FragmentManager.mParent
├─ winged.example.feature_login.register.RegisterFragment instance
│ Leaking: NO (Fragment#mFragmentManager is not null)
│ componentContext instance of dagger.hilt.android.internal.managers.
│ ViewComponentManager$FragmentContextWrapper, wrapping activity winged.
│ example.hiltmultimodule.MainActivity with mDestroyed = false
│ ↓ Fragment.mSavedViewState
│ ~~~~~~~~~~~~~~~
├─ android.util.SparseArray instance
│ Leaking: UNKNOWN
│ Retaining 417.7 kB in 4154 objects
│ ↓ SparseArray.mValues
│ ~~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 417.6 kB in 4152 objects
│ ↓ Object[9]
│ ~~~
├─ android.widget.TextView$SavedState instance
│ Leaking: UNKNOWN
│ Retaining 416.1 kB in 4113 objects
│ ↓ TextView$SavedState.text
│ ~~~~
├─ android.text.SpannableStringBuilder instance
│ Leaking: UNKNOWN
│ Retaining 416.0 kB in 4109 objects
│ ↓ SpannableStringBuilder.mSpans
│ ~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 36 B in 1 objects
│ ↓ Object[0]
│ ~~~
├─ android.text.method.PasswordTransformationMethod$Visible instance
│ Leaking: UNKNOWN
│ Retaining 415.4 kB in 4099 objects
│ ↓ PasswordTransformationMethod$Visible.mText
│ ~~~~~
├─ androidx.emoji2.text.SpannableBuilder instance
│ Leaking: UNKNOWN
│ Retaining 415.4 kB in 4098 objects
│ ↓ SpannableStringBuilder.mSpans
│ ~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ Retaining 76 B in 1 objects
│ ↓ Object[0]
│ ~~~
├─ android.widget.TextView$ChangeWatcher instance
│ Leaking: UNKNOWN
│ Retaining 16 B in 1 objects
│ ↓ TextView$ChangeWatcher.this$0
│ ~~~~~~
├─ com.google.android.material.textfield.TextInputEditText instance
│ Leaking: UNKNOWN
│ Retaining 410.0 kB in 3980 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.repeatPasswordTIET
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping
│ activity winged.example.hiltmultimodule.MainActivity with mDestroyed =
│ false
│ ↓ View.mParent
│ ~~~~~~~
├─ android.widget.FrameLayout instance
│ Leaking: UNKNOWN
│ Retaining 1.0 kB in 15 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 winged.example.hiltmultimodule.MainActivity with mDestroyed =
│ false
│ ↓ View.mParent
│ ~~~~~~~
├─ com.google.android.material.textfield.TextInputLayout instance
│ Leaking: UNKNOWN
│ Retaining 381.0 kB in 3284 objects
│ View not part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.repeatPasswordTIL
│ View.mWindowAttachCount = 1
│ mContext instance of androidx.appcompat.view.ContextThemeWrapper, wrapping
│ activity winged.example.hiltmultimodule.MainActivity with mDestroyed =
│ false
│ ↓ View.mParent
│ ~~~~~~~
╰→ androidx.constraintlayout.widget.ConstraintLayout instance
Leaking: YES (ObjectWatcher was watching this because winged.example.
feature_login.register.RegisterFragment received Fragment#onDestroyView()
callback (references to its views should be cleared to prevent leaks))
Retaining 2.5 kB in 59 objects
key = 16bf9a7e-c3de-4737-a5c2-8933c6fed9d3
watchDurationMillis = 132084
retainedDurationMillis = 127081
View not part of a window view hierarchy
View.mAttachInfo is null (view detached)
View.mID = R.id.mainCL
View.mWindowAttachCount = 1
mContext instance of dagger.hilt.android.internal.managers.
ViewComponentManager$FragmentContextWrapper, wrapping activity winged.
example.hiltmultimodule.MainActivity with mDestroyed = false
METADATA
Build.VERSION.SDK_INT: 30
Build.MANUFACTURER: unknown
LeakCanary version: 2.10
App process name: winged.example.hiltmultimodule
Class count: 18527
Instance count: 115319
Primitive array count: 86210
Object array count: 17808
Thread count: 21
Heap total bytes: 16303680
Bitmap count: 4
Bitmap total bytes: 228214
Large bitmap count: 0
Large bitmap total bytes: 0
Db 1: open /data/user/0/winged.example.
hiltmultimodule/databases/HiltMultiModuleDB
Stats: LruCache[maxSize=3000,hits=40347,misses=84973,hitRate=32%]
RandomAccess[bytes=4231371,reads=84973,travel=25038680029,range=19100784,size=25
202710]
Analysis duration: 6049 ms
I'm still learning so any info / possible reasons / solutions will be appreciated, thanks :)
Edit:
BaseFragment:
abstract class BaseFragment<T : ViewDataBinding>(@LayoutRes private val fragmentRes: Int) : Fragment() {
private var _binding: T? = null
val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = DataBindingUtil.inflate(inflater, fragmentRes, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
fun navigateTo(targetDestination: Int) {
findNavController().navigate(targetDestination)
}
fun navigateUp() {
findNavController().navigateUp()
}
}
loginFragment:
@AndroidEntryPoint
class LoginFragment : BaseFragment<FragmentLoginBinding>(R.layout.fragment_login) {
private val viewModel: LoginViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpLogInButton()
setUpTextRedirection()
observeForLoginEvents()
}
private fun setUpTextRedirection() {
binding.signUpTV.setOnClickListener {
navigateTo(R.id.registerFragment)
}
}
private fun setUpLogInButton() {
binding.logInBTN.setOnClickListener {
val email = binding.emailTIET.extractText()
val password = binding.passwordTIET.extractText()
if(email.isAValidEmail() && password.isNotBlank()) {
viewModel.logIn(LoginCredentials(mail = email, password = password))
}
}
}
private fun observeForLoginEvents() {
viewModel.loginEvent.observe(viewLifecycleOwner) { result ->
if(result.isSuccess) {
/* Adding some kind of "Main Screen" module would be an idea
but as I've stated previously, this is just a small "test" project
showing off architecture, so I hope you will forgive me <3
(PS: if you are reading this and there still isn't that module, you can make a PR
and add it)*/
Toast.makeText(requireContext(), "Success!", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(requireContext(), "No matching account", Toast.LENGTH_SHORT).show()
}
}
}
}
registerFragment:
@AndroidEntryPoint
class RegisterFragment: BaseFragment<FragmentRegisterBinding>(R.layout.fragment_register) {
private val viewModel: RegisterViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpCreateAccountButton()
setUpTextRedirection()
observeRegisterEvents()
}
private fun setUpTextRedirection() {
binding.logInTV.setOnClickListener {
navigateTo(R.id.loginFragment)
}
}
private fun setUpCreateAccountButton() {
binding.createAnAccountBTN.setOnClickListener {
val email = binding.emailTIET.extractText()
val password = binding.passwordTIET.extractText()
val repeatedPassword = binding.repeatPasswordTIET.extractText()
if(email.isAValidEmail() && (password == repeatedPassword) && password.isNotEmpty()) {
viewModel.saveUser(
LoginCredentials(mail = email, password = password)
)
}
}
}
private fun observeRegisterEvents() {
viewModel.registerEvent.observe(viewLifecycleOwner) { result ->
if(result.isSuccess) {
navigateTo(R.id.loginFragment)
} else {
Toast.makeText(requireContext(), "Something went wrong", Toast.LENGTH_SHORT).show()
}
}
}
As you may notice, the BaseFragment class has a reference to a view (binding variable), but it releases it in onDestoryView, so I think that should be working, also in the leak it isn't "complaining" about the binding itself