Opening

We have spent three videos building up the tools and concepts you need: where time is spent, the CPU-bound versus IO-bound distinction, and how to profile. Now let us put it all together. In this video, we are going to bust some optimization myths that have been circulating in the Android community for years, and then focus on the optimizations that actually move the needle. Because here is the thing — a lot of the "performance tips" you read online are either outdated, based on misunderstandings, or so micro-level that they make no measurable difference. Let us separate what works from what is cargo cult.

Myth: Avoid Object Creation

This is probably the most persistent myth in Android development. "Avoid creating objects because allocation and garbage collection are expensive." This was partially true in the Dalvik era, ten-plus years ago. On modern ART, it is mostly wrong. ART uses something called Thread-Local Allocation Buffers — TLABs. Each thread gets its own private chunk of heap memory. Allocating an object from a TLAB is essentially just incrementing a pointer. It is almost as cheap as allocating on the stack. It takes nanoseconds. For garbage collection, ART uses a generational, concurrent collector. Most short-lived objects are collected in the young generation, which is extremely fast — often sub-millisecond. This means that for the vast majority of your code, creating objects is cheap and you should not worry about it. The exception: allocating thousands of objects in a tight loop that runs every frame — say, during RecyclerView scroll or Canvas draw operations. In those hot paths, yes, reuse objects. Everywhere else, write clean code and let the runtime handle allocation.

Myth: Use Arrays Instead of Lists

Another classic: "Use arrays instead of ArrayList because arrays are faster." In isolation, yes, array access is slightly faster than ArrayList access because ArrayList wraps an array with bounds checking. But the difference is nanoseconds per access. On a modern phone running at two to three gigahertz, you would need millions of accesses before the difference is measurable. More importantly, ArrayList gives you type safety, cleaner APIs, and functional operators like map, filter, and fold. These make your code more readable and less error-prone. The readability cost of using raw arrays almost never justifies the nanosecond performance gain. The exception, again, is tight loops processing thousands of elements where you have profiler evidence that collection overhead matters. In those cases, consider IntArray instead of List of Int to avoid autoboxing. But do this based on measurement, not on instinct.

Myth: Inline Everything

"Inline your functions for performance." Kotlin's inline keyword does eliminate the overhead of a function call — it copies the function body into the call site at compile time. But here is the thing: the ART JIT compiler already does this. When it identifies a hot method that calls a small function repeatedly, it inlines that function automatically. You do not need to tell it. The inline keyword in Kotlin is actually designed for a different purpose: allowing non-local returns from lambdas and reifying generic type parameters. Using inline for performance is usually redundant because the runtime is already doing it for you. Where inline genuinely helps: higher-order functions where you want to avoid the lambda object allocation. Functions like let, run, apply, also — these are inline for good reason. Without inline, each lambda would create an anonymous class instance. But inlining your own regular functions for "speed"? Profile first. You will almost certainly find it makes no measurable difference.

Real Optimization: Reduce Overdraw

Now let us talk about what actually works. Overdraw is when the same pixel is drawn multiple times in a single frame. If your background is white, then you draw a card on top of it, then you draw text on top of the card — that pixel was drawn three times. The GPU did three times the work for one pixel. On complex UIs, overdraw can be the single biggest cause of dropped frames. You can visualize overdraw in Developer Options on your device: Debug GPU Overdraw. Areas drawn once are blue, twice are green, three times are red. Your goal is to minimize red areas. The fixes: remove unnecessary background colors on views that are fully covered by child views. Use clipRect in custom drawing to avoid drawing outside visible bounds. Flatten your view hierarchy so fewer overlapping layers exist. For Compose, this means being mindful of background modifiers on nested layouts.

Real Optimization: Flatten the View Hierarchy

Every view in your hierarchy has a cost during the measure and layout phases. Deeply nested layouts multiply this cost because measurement propagates up and down the tree. A LinearLayout inside a RelativeLayout inside another LinearLayout inside a ConstraintLayout — each level adds measure passes. The solution: use ConstraintLayout to flatten nested layouts into a single level. One ConstraintLayout can replace three or four levels of nested LinearLayouts while achieving the same visual result. In Compose, this problem is less severe because Compose's layout system allows single-pass measurement. But deeply nested Composables still have a cost — each layer adds function call overhead during recomposition. Keep your Composable hierarchy as shallow as practically possible.

Real Optimization: Lazy Loading and Pagination

Loading everything at once is the fastest way to create a slow app. If your API returns a thousand items, do not load all of them into memory and display them in a single list. Use pagination. Android's Paging library loads data in chunks — a page of twenty items at a time. As the user scrolls, the next page loads automatically. This keeps memory usage low, network requests small, and initial load time fast. Lazy loading applies to images too. Load and decode images only when they scroll into view. Libraries like Coil and Glide do this by default. For ViewPager fragments or Compose lazy lists, defer expensive initialization until the content is actually visible. Every millisecond you defer is a millisecond the user spends looking at content instead of waiting.

Real Optimization: Avoid Main Thread IO

This is the single most impactful optimization for most Android apps: stop doing IO on the main thread. Every file read, database query, and SharedPreferences access on the main thread blocks the UI. The user sees this as jank, frozen frames, or slow response to taps. Move all IO to a background thread or coroutine with the appropriate dispatcher. For SharedPreferences specifically, use DataStore, which is asynchronous by design. For Room, ensure you are using suspend functions or Flow, not blocking calls. For file operations, use withContext Dispatchers.IO. This is not micro-optimization. This is the difference between a smooth app and a janky app. I have seen apps go from janky to buttery smooth with nothing more than moving IO off the main thread.

Real Optimization: Image Sizing

Images are a massive performance factor on Android, and the optimization is dead simple: size your images correctly. If your ImageView is 200 by 200 pixels, do not load a 4000 by 3000 pixel image into memory. That image consumes 4000 times 3000 times 4 bytes — about 48 megabytes of memory. Scaled down to 200 by 200, it needs 160 kilobytes. That is a three hundred times reduction. Both Coil and Glide handle this automatically when you provide the target size. But custom image loading code often misses this. BitmapFactory.Options.inSampleSize lets you decode at a lower resolution. Always, always, always size your images to the display dimensions. This single optimization can reduce memory usage by an order of magnitude and eliminate OutOfMemoryError crashes.

Real Optimization: RecyclerView DiffUtil

If you are still calling notifyDataSetChanged on your RecyclerView adapter, you are forcing the entire list to rebind. Every visible item is redrawn, even if only one item changed. DiffUtil calculates the minimal set of changes between your old list and your new list: which items were added, removed, or moved. Then it dispatches only those specific changes to the adapter. The result: smooth animations and minimal rebinding. With ListAdapter — which wraps DiffUtil — this is trivially easy. You provide an ItemCallback that defines how to compare items, and the adapter handles the rest. The performance difference is dramatic, especially for lists that update frequently — like chat messages or live feeds.

Real Optimization: Compose Stability

If you are using Jetpack Compose, understanding stability is one of the most impactful optimizations. Compose skips recomposition of a Composable if its parameters have not changed. But it can only check this for stable types — types where Compose can verify equality cheaply. Data classes with only primitive and String fields are stable. Classes with var properties or mutable collections are unstable. When Compose sees an unstable parameter, it always recomposes — even if the value has not actually changed. The fix: mark classes as Immutable with the @Immutable annotation if they truly are. Use kotlinx.collections.immutable for lists and maps. Avoid passing unstable types like MutableList into Composable functions. Check the Compose compiler reports to see which Composables are skippable and which are not.

Closing

Let me leave you with the one rule that governs all optimization work. Profile first. Optimize the measured bottleneck. Measure again. That is the entire methodology. Do not optimize based on blog posts. Do not optimize based on what feels slow. Do not optimize based on myths from the Dalvik era. Measure. Fix the real bottleneck. Verify the fix. The optimizations that matter on Android today are: avoid main thread IO, size your images, reduce overdraw, flatten hierarchies, use DiffUtil, understand Compose stability, and leverage Baseline Profiles for startup. These are the things that move the needle. Everything else is noise until the profiler tells you otherwise. That wraps up our performance phase. Next, we are entering Phase 8 — Failure and Resilience. Because in the real world, things fail. And how you handle failure is what separates a good app from a great one.