Optimization & Performance
Shipping TrailLog to the Play Store isn't the end of the work — a published app still needs to install quickly, scroll smoothly, and avoid slowly consuming more memory than it should. This lesson covers app size, the tools Android Studio gives you to actually measure performance rather than guess at it, one real, common performance mistake and its fix, and memory leaks — the quiet kind of bug that only shows up after an app has been running for a while.
Reducing App Size
| Technique | What it does |
|---|---|
| R8 code shrinking | Removes unused classes and methods, and shortens remaining names — covered in ProGuard / R8 |
| Resource shrinking | Drops unused layouts, drawables, and strings that R8 can't see but Gradle's resource shrinker can detect |
| WebP instead of PNG | TrailLog's trail thumbnails and icons compress noticeably smaller as WebP with no visible quality loss |
| App Bundle per-device delivery | Play serves only the density and CPU architecture a given device needs — see App Bundle & Play Store |
Android Studio Profiler
Guessing at what's slow rarely finds the real cause — Android Studio's Profiler gives actual measurements while TrailLog runs:
| Profiler | What to look for |
|---|---|
| CPU Profiler | Which function calls are eating time on the main thread during a janky scroll |
| Memory Profiler | A memory usage graph that keeps climbing and never drops back down after garbage collection — a leak signature |
| Network Profiler | How often TrailLog's Retrofit calls actually hit the network, and how large each response is |
A Real Mistake: Decoding Images Inside onBindViewHolder()
Here's a genuinely common way TrailLog's trail list could end up janky — decoding each trail photo directly, synchronously, inside the RecyclerView adapter's bind function:
The mistake
override fun onBindViewHolder(holder: TrailViewHolder, position: Int) {
val trail = trails[position]
// Decoding a full-size bitmap on the main thread, every single time
// this row scrolls onto screen — this is what causes the jank
val bitmap = BitmapFactory.decodeFile(trail.photoUri)
holder.thumbnail.setImageBitmap(bitmap)
}
BitmapFactory.decodeFile is a slow, synchronous, main-thread operation, and
onBindViewHolder() is called constantly as a user scrolls — every one of those calls
blocks the main thread just long enough to drop frames, which is exactly what scroll jank feels like.
The Fix: Coil
Coil is an image-loading library built around Kotlin coroutines — it loads and decodes images off the main thread automatically, caches the result, and cancels an in-flight load if its target view is recycled before the load finishes:
The fix
override fun onBindViewHolder(holder: TrailViewHolder, position: Int) {
val trail = trails[position]
holder.thumbnail.load(trail.photoUri) {
crossfade(true)
placeholder(R.drawable.trail_placeholder)
}
}
load(...) here is Coil's extension function on ImageView — the decoding
happens on a background dispatcher, the result is cached so scrolling back to a row already shown
doesn't re-decode it, and the main thread stays free to keep the scroll smooth.
Avoiding Memory Leaks
A memory leak in an Android app usually comes down to one pattern: something with a long lifetime
holding a reference to something with a short one, preventing the short-lived object from ever being
garbage collected. A Fragment holding onto its View Binding instance past the Fragment
view's own lifecycle is a classic example — which is exactly why the nullable backing-property pattern
introduced in View Binding clears that reference in
onDestroyView(). The same care applies to holding an Activity or
Context reference inside a long-lived object, like a singleton or a static field — that
reference keeps the entire Activity, and everything it holds onto, alive in memory long after the
screen itself should have been destroyed.
Baseline Profiles, Briefly
A Baseline Profile is a file, shipped inside TrailLog's App Bundle, that tells Android ahead of time which classes and methods are used most heavily during critical moments like app startup or scrolling the trail list. Android uses that information to pre-compile exactly those hot paths at install time, instead of interpreting or just-in-time compiling them the first time they run — meaningfully improving startup time and early scroll smoothness for real users, without requiring any change to TrailLog's actual logic.