Introduction
ViewModels are designed to survive configuration changes and be cleared only when their owning Activity or Fragment is permanently destroyed. However, when combined with retained fragments (setRetainInstance(true) or retainInstance = true), the fragment outlives the Activity and the ViewModel holds references to destroyed resources, causing memory leaks.
Symptoms
- LeakCanary reports Activity leaked after rotation
- Memory usage grows with each configuration change
ViewModelholds references to destroyedVieworContext- Fragment receives callbacks after Activity is destroyed
- Retained fragment with ViewBinding leaks the entire view hierarchy
Example LeakCanary report:
``
LEAKING RETAINED FRAGMENT
MainActivity received Activity#onDestroy() callback
RetainedFragment retains reference to MainActivity
Reference Key: abc123def456
Heap dump: /data/data/com.example/files/leakcanary/...
Common Causes
setRetainInstance(true)on Fragment that holds ViewModel- ViewModel stores Activity context or View references
- Fragment not cleared when parent Activity is finishing
- Navigation component retaining fragments across destinations
- Retained fragment with references to large bitmaps
Step-by-Step Fix
- 1.Avoid setRetainInstance with ViewModel:
- 2.```kotlin
- 3.// WRONG: retained fragment + ViewModel creates leak risk
- 4.class DataFragment : Fragment() {
- 5.init {
- 6.retainInstance = true // Deprecated and problematic
- 7.}
- 8.private val viewModel: DataViewModel by viewModels()
- 9.}
// CORRECT: let ViewModel survive via ViewModelStore, not fragment class DataFragment : Fragment() { // No retainInstance needed - ViewModel survives config change naturally private val viewModel: DataViewModel by viewModels() } ```
- 1.Use Activity-scoped ViewModel instead of Fragment-scoped:
- 2.```kotlin
- 3.// Activity-level ViewModel survives fragment recreation
- 4.class MainActivity : AppCompatActivity() {
- 5.private val sharedViewModel: SharedViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This ViewModel is tied to the Activity lifecycle, // not any individual Fragment } }
// Fragment accesses the same ViewModel class MyFragment : Fragment() { private val sharedViewModel: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { sharedViewModel.data.observe(viewLifecycleOwner) { data -> updateUI(data) } } } ```
- 1.Clean up ViewModel references properly:
- 2.```kotlin
- 3.class MyViewModel(application: Application) : AndroidViewModel(application) {
- 4.// Use Application context, not Activity context
- 5.private val context = getApplication<Application>()
// Store lightweight data, not Views or Activities private val _data = MutableLiveData<Data>() val data: LiveData<Data> = _data
// Clean up resources when ViewModel is cleared override fun onCleared() { super.onCleared() // Cancel coroutines, close connections, etc. _data.value = null } } ```
- 1.Use Fragment view lifecycle for observation:
- 2.```kotlin
- 3.class MyFragment : Fragment() {
- 4.private val viewModel: MyViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState)
// Observe with viewLifecycleOwner - stops when view is destroyed viewModel.data.observe(viewLifecycleOwner) { data -> binding.textView.text = data }
// NOT this (uses fragment lifecycle, leaks after view destruction): // viewModel.data.observe(this) { ... } } } ```
- 1.Use SavedStateHandle for critical data instead of retainInstance:
- 2.```kotlin
- 3.class MyViewModel(
- 4.private val savedStateHandle: SavedStateHandle
- 5.) : ViewModel() {
companion object { private const val KEY_DRAFT_TEXT = "draft_text" }
var draftText: String get() = savedStateHandle[KEY_DRAFT_TEXT] ?: "" set(value) { savedStateHandle[KEY_DRAFT_TEXT] = value }
// Data survives both config change AND process death // No need for retained fragments } ```
Prevention
- Never use
setRetainInstance(true)with ViewModel - Use Application context in ViewModel, never Activity or Fragment context
- Observe LiveData with
viewLifecycleOwner, notthis - Clean up resources in
ViewModel.onCleared() - Use LeakCanary in debug builds to catch leaks early
- Use
SavedStateHandlefor data that must survive process death - Scope ViewModels appropriately:
by viewModels()for Fragment,by activityViewModels()for shared