Web & Mobile ¡ 5 minute read
Jetpack Compose: Patterns That Survive a Real Codebase
Compose is straightforward until recomposition gets out of hand, which happens through unstable parameters and state held too high. The patterns that survive are hoisted state with a single owner, stable types, keyed lazy lists, and a deliberate boundary with the existing View system.
Compose is straightforward for a while and then becomes confusing, almost always because recomposition is doing more than anyone intended. This guide covers the patterns that survive a real codebase, drawing on FISTA Solutions' web and mobile work.
What are the core patterns?
Five decisions that determine how a Compose codebase ages.
| Pattern | Why it matters |
|---|---|
| State hoisting | Predictable, testable composables |
| Stable parameter types | Enables recomposition skipping |
| Keys on lazy list items | Correct diffing and retained state |
| Deferred state reads | Keeps hot values out of composition |
| Remembered expensive values | Avoids repeated work |
| Explicit View boundary | Uses the right component |
How should state be organised?
Hoisted to a single owner, with composables receiving values and emitting events.
A composable that owns its own state cannot be previewed in different configurations, cannot be tested easily, and cannot be controlled by its parent. Hoisting fixes all three.
The owner is usually a state holder or view model scoped to the screen. Keep the composable layer free of business logic; it should render what it is given.
What is stability and why does it matter?
Stability is the compiler's ability to prove a parameter has not changed, and it determines whether recomposition can be skipped.
A type the compiler cannot reason about â a mutable collection, a class with variable properties, a type from a module without the compiler plugin â is treated as unstable, so every composable taking it recomposes every time.
The practical fixes are immutable data classes, immutable collection types, and lambdas that are remembered rather than recreated. See the compiler's stability reports to find the offenders rather than guessing.
How should lists be handled?
Lazy containers with keys, and item content kept cheap.
Keys let the framework match items across changes. Without them, inserting at the top rebuilds everything below and loses per-item state, which is visible as flicker and lost scroll position.
Keep item composables small and stable. A complex item recomposing during scroll is the usual source of jank, and simplifying it helps more than tuning anything else.
What are deferred reads for?
Keeping frequently changing values out of composition entirely.
Reading a scroll offset directly in a composable means every scroll frame triggers recomposition. Reading it inside a layout or draw lambda means only that phase re-runs, which is dramatically cheaper.
This pattern applies to animation values, scroll positions, and anything else changing per frame. It is the difference between smooth and janky on animation-heavy screens.
How should navigation work?
Through a navigation graph with typed destinations, and with state that survives process death.
Android terminates backgrounded processes routinely. Navigation state and screen state both need to survive that, which means saving what is required to reconstruct the screen rather than holding it only in memory.
Deep links should resolve to destinations in the same graph, so an external link and an internal navigation produce the same result. See mobile app architecture guide.
How do you interoperate with Views?
In both directions, deliberately, at a narrow boundary.
Compose can host a View and a View hierarchy can host Compose. That makes incremental adoption practical: new screens in Compose, existing screens untouched, with wrapping where a View component remains better.
Keep the boundary at the component level rather than interleaving. Mixed hierarchies several layers deep are difficult to reason about and complicate both state and performance work.
What are the common mistakes?
Composables owning their own state. Unstable parameter types. Lazy lists without keys. Reading hot values in composition. Business logic in the composable layer. And rewriting working View screens for uniformity.
How do you test it?
Measure recomposition counts on your busiest screen rather than reasoning about them. The tooling reports this directly and the numbers are frequently surprising.
Test process death and restoration, which is where Android-specific state bugs live.
What does it cost to operate?
Development is fast once the patterns are established. The cost is the learning curve around stability and recomposition, which is where teams lose time early.
Incremental adoption keeps that cost bounded rather than front-loaded.
What should you measure?
Recomposition counts per screen, frame times during scroll, cold start time, and crash-free rate by Android version.
What about AI features?
Throttle streaming updates rather than recomposing on every chunk, and keep inference off the main thread with proper cancellation when the user leaves the screen.
An uncancelled request continues running and billing after the user has moved on, which is both a cost and a correctness problem. See streaming UI patterns for AI apps.
When is this the wrong approach?
A stable View-based codebase does not need converting. Adopt Compose where you are building something new; rewriting working screens for framework consistency rarely returns the investment.
What should you do first?
Turn on recomposition counts and open your busiest screen. The numbers tell you immediately whether you have a stability problem.
How FISTA Solutions helps
FISTA Solutions builds and operates production systems through web and mobile, AI enablement, and staff augmentation: state hoisted to a single owner per screen, and stability reports used to find unnecessary recomposition rather than guessing at it, decisions documented with their reasoning, and handover that leaves your team able to maintain what was delivered. The record is 150+ projects for 50+ companies across 12+ countries.
To scope this work, message FISTA on WhatsApp, or read swiftui development guide.
Share-ready article cover
Download the generated social format.
Clear answers
Questions raised by this field note.
Straightforward guidance for evaluating scope, fit, and the next step.
01What is state hoisting?
Moving state out of a composable so it receives values and emits events instead of owning them. That makes composables predictable, previewable, and testable, and it is the foundational Compose pattern.
02Why does stability matter?
Because the compiler skips recomposition only when it can prove parameters have not changed. A type it cannot reason about forces recomposition every time, which is the usual cause of unnecessary work.
03Do lazy lists need keys?
Yes, wherever items can move, be inserted, or be removed. Without keys the framework matches by position, which rebuilds more than necessary and loses item state during changes.
04What are deferred reads?
Reading a state value inside a lambda at the layout or draw phase rather than in composition. For frequently changing values such as scroll offset, this keeps the change out of recomposition entirely.
05Can Compose and Views coexist?
Yes, in both directions, and it is the normal migration path. Adopt Compose for new screens and wrap existing Views where they are still the better component rather than rewriting everything.
Continue exploring
Related capabilities
Start with the hard problem
Need the outcome owned, not merely analyzed?
Tell us where delivery is constrained. Weâll map the fastest credible path from intent to verified production.