This exercise gives you a deliberately leaky feature and asks you to identify, diagnose, and fix each leak using LeakCanary and the Memory Profiler.
Setup
// build.gradle.kts
dependencies {
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
}
The Leaky Feature
Study the following code carefully — it has four distinct leaks:
// NewsActivity.kt — deliberately leaky
class NewsActivity : AppCompatActivity() {
// LEAK 1: static reference to Activity
companion object {
var instance: NewsActivity? = null
}
private lateinit var binding: ActivityNewsBinding
private val handler = Handler(Looper.getMainLooper())
// LEAK 2: anonymous listener never removed
private val articleManager = ArticleManager.getInstance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityNewsBinding.inflate(layoutInflater)
setContentView(binding.root)
instance = this // Leak 1: static reference
// Leak 2: anonymous listener captures 'this' (Activity)
articleManager.addListener(object : ArticleListener {
override fun onArticleUpdated(article: Article) {
binding.titleText.text = article.title // implicit 'this' capture
}
})
// LEAK 3: Handler Runnable not removed
handler.postDelayed({
refreshFeed() // lambda captures 'this'
}, 30_000)
setupViewModel()
}
private fun setupViewModel() {
// LEAK 4: ViewModel holds binding reference
val vm = ViewModelProvider(this)[NewsViewModel::class.java]
vm.binding = binding // passing View to ViewModel!
vm.loadArticles()
}
}
// Leaky ViewModel
class NewsViewModel : ViewModel() {
var binding: ActivityNewsBinding? = null // LEAK 4: ViewModel holds View
fun loadArticles() {
viewModelScope.launch {
val articles = repository.getArticles()
binding?.recyclerView?.adapter = ArticleAdapter(articles)
}
}
}
Your Task
For each leak, answer:
- Why does it leak?
- When does LeakCanary fire?
- What's the fix?
Solutions
Leak 1: Static instance
// ❌ Static holds strong reference indefinitely
companion object { var instance: NewsActivity? = null }
// ✅ Never store Activity in static fields
// If you need app-wide state, move it to a ViewModel or Repository
// If you truly need the Activity, use WeakReference (and question why)
Leak 2: Anonymous listener not removed
// ❌ Anonymous object captures Activity; never unregistered
articleManager.addListener(object : ArticleListener { ... })
// ✅ Store listener reference; remove in onDestroy
private val articleListener = ArticleListener { article ->
binding.titleText.text = article.title
}
override fun onStart() {
super.onStart()
articleManager.addListener(articleListener)
}
override fun onStop() {
super.onStop()
articleManager.removeListener(articleListener)
}
Leak 3: Handler Runnable not cancelled
// ❌ postDelayed runnable still pending when Activity destroyed
handler.postDelayed({ refreshFeed() }, 30_000)
// ✅ Store runnable; cancel in onDestroy
private val refreshRunnable = Runnable { refreshFeed() }
override fun onResume() {
super.onResume()
handler.postDelayed(refreshRunnable, 30_000)
}
override fun onPause() {
super.onPause()
handler.removeCallbacks(refreshRunnable)
}
Leak 4: ViewModel holds View/Binding
// ❌ ViewModel outlives Activity; binding holds Views
class NewsViewModel : ViewModel() { var binding: ActivityNewsBinding? = null }
// ✅ ViewModel exposes state; Activity observes and binds
class NewsViewModel : ViewModel() {
private val _articles = MutableStateFlow<List<Article>>(emptyList())
val articles: StateFlow<List<Article>> = _articles.asStateFlow()
fun loadArticles() = viewModelScope.launch {
_articles.value = repository.getArticles()
}
}
// In Activity — bind in observer, not in ViewModel
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.articles.collect { articles ->
binding.recyclerView.adapter = ArticleAdapter(articles)
}
}
}
Verify with LeakCanary
After applying each fix:
- Run the debug build
- Navigate to
NewsActivity - Press Back
- Wait 5 seconds
- LeakCanary will analyze — if no notification appears, the leak is fixed
Verify with Memory Profiler
1. Profile session → Memory tab
2. Navigate to the screen → navigate away → press "Dump Java Heap"
3. Search for "NewsActivity"
4. Count should be 0 after a GC
Key Takeaways
| Leak | Pattern | Fix |
|---|---|---|
| Static field | companion object holding Activity | Delete — never store Activity statically |
| Anonymous listener | object : Listener capturing Activity | Store field; remove in mirror lifecycle method |
| Handler runnable | postDelayed lambda | removeCallbacks in onPause/onDestroy |
| ViewModel + View | ViewModel holds binding | ViewModel → state only; Activity binds in observer |