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
  • ViewModel holds references to destroyed View or Context
  • 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. 1.Avoid setRetainInstance with ViewModel:
  2. 2.```kotlin
  3. 3.// WRONG: retained fragment + ViewModel creates leak risk
  4. 4.class DataFragment : Fragment() {
  5. 5.init {
  6. 6.retainInstance = true // Deprecated and problematic
  7. 7.}
  8. 8.private val viewModel: DataViewModel by viewModels()
  9. 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. 1.Use Activity-scoped ViewModel instead of Fragment-scoped:
  2. 2.```kotlin
  3. 3.// Activity-level ViewModel survives fragment recreation
  4. 4.class MainActivity : AppCompatActivity() {
  5. 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. 1.Clean up ViewModel references properly:
  2. 2.```kotlin
  3. 3.class MyViewModel(application: Application) : AndroidViewModel(application) {
  4. 4.// Use Application context, not Activity context
  5. 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. 1.Use Fragment view lifecycle for observation:
  2. 2.```kotlin
  3. 3.class MyFragment : Fragment() {
  4. 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. 1.Use SavedStateHandle for critical data instead of retainInstance:
  2. 2.```kotlin
  3. 3.class MyViewModel(
  4. 4.private val savedStateHandle: SavedStateHandle
  5. 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, not this
  • Clean up resources in ViewModel.onCleared()
  • Use LeakCanary in debug builds to catch leaks early
  • Use SavedStateHandle for data that must survive process death
  • Scope ViewModels appropriately: by viewModels() for Fragment, by activityViewModels() for shared