Skip to main content
Compose for Modern UI

Compose UI Craft: Elevating Modern Interfaces Through Professional Design Patterns at artnest

Every Compose team eventually hits the same inflection point: the prototype works, but the real app demands consistency, performance, and maintainability. That's when you stop writing Compose and start crafting a UI architecture. This guide is for developers and design engineers who need to choose the right set of design patterns—not just for today's screen, but for a system that scales across features, teams, and releases. We'll walk through the decision you face, the landscape of options, the criteria that actually matter, and the trade-offs that separate a maintainable codebase from a tangled one. By the end, you'll have a framework you can apply to your own project, whether you're starting fresh or refactoring a View-based app into Compose.

Every Compose team eventually hits the same inflection point: the prototype works, but the real app demands consistency, performance, and maintainability. That's when you stop writing Compose and start crafting a UI architecture. This guide is for developers and design engineers who need to choose the right set of design patterns—not just for today's screen, but for a system that scales across features, teams, and releases.

We'll walk through the decision you face, the landscape of options, the criteria that actually matter, and the trade-offs that separate a maintainable codebase from a tangled one. By the end, you'll have a framework you can apply to your own project, whether you're starting fresh or refactoring a View-based app into Compose.

Who Must Choose and by When

The decision to adopt structured Compose design patterns isn't academic—it's a practical fork that every team encounters, usually within the first few months of a production Compose rollout. If you're building a new feature module or migrating an existing screen, you're already making pattern choices: where to hoist state, how to model UI events, whether to use a slot-based composable or a custom layout. The question is whether you make those choices deliberately or by accident.

You need a clear pattern strategy by the time your second developer joins the Compose effort, or when your first composable exceeds 150 lines. Before that, you can experiment. After that, inconsistencies compound. Teams often find that state hoisting decisions made in week one ripple into refactoring costs in month three. The window for intentional design is narrow—so it pays to think ahead.

Signs You're Past the Decision Point

If you see any of these symptoms, you're already in the danger zone: recomposition logs show unexpected redraws, your ViewModel holds UI state that should be local, or your custom composables accept fifteen parameters because no one defined a clear contract. These aren't code quality nitpicks—they're signals that your pattern vocabulary hasn't kept pace with your feature set.

When to Delay the Decision

Not every project needs a full pattern library from day one. If you're building a throwaway prototype or a single-screen utility app, lightweight patterns—like passing state down as parameters without a formal state holder—are fine. The decision becomes urgent when you plan to maintain the code for more than six months or extend it across multiple screens.

The Landscape of Compose Design Patterns

Three broad approaches dominate professional Compose codebases today. Each solves the same core problem—managing UI state and structure—but with different trade-offs in complexity, testability, and team scalability.

Approach 1: Reactive State Containers (ViewModels + StateFlow)

This is the most common pattern in production apps. You model UI state as a single data class in the ViewModel, expose it as a StateFlow, and collect it in the composable. Events flow up through lambdas; state flows down through parameters. It's familiar to anyone coming from the MVVM world, and it works well for screen-level state that spans multiple composables.

Approach 2: Declarative Layout Trees with Custom Layouts

Instead of relying on standard containers like Column and Row, you write your own layout composable that measures and places children directly. This pattern shines when you need pixel-perfect alignment, staggered grids, or responsive layouts that the built-in components don't handle gracefully. The trade-off is more code and a steeper learning curve for new team members.

Approach 3: Component-Driven Theming and Slot APIs

Here, you define a small set of themed composables that accept content slots (via @Composable lambda parameters) and style parameters from a central theme. This pattern excels at design system consistency: every button, card, or dialog inherits spacing, typography, and color from a single source. The risk is over-abstraction—creating a slot for every visual variation can lead to a combinatorial explosion of parameters.

Which Approach Do Teams Actually Choose?

In practice, most teams combine elements of all three. A typical project uses reactive state containers for screen state, custom layouts for a few specific components (like a dashboard grid), and slot-based composables for the design system. The art is knowing where each pattern belongs—and where it doesn't.

Criteria That Actually Matter for Pattern Selection

When you evaluate patterns, three criteria dominate the decision: testability, reusability, and performance. But these terms are often used loosely. Let's define them in concrete terms.

Testability: Can You Verify Behavior Without a Device?

A pattern is testable if you can unit-test its logic without launching an emulator. Reactive state containers win here: you can test state transformations in the ViewModel with plain JUnit tests. Custom layouts, by contrast, require Compose UI tests because they depend on measurement and placement logic. Slot-based composables fall in the middle—you can test their state logic separately, but visual slot behavior needs screenshot tests.

Reusability: How Many Screens Can Share This Code?

Reusability is not just about avoiding copy-paste. It's about whether a composable can be used in a different context without modification. Slot-based patterns are inherently reusable because they defer content decisions to the caller. Custom layouts are reusable only if you parameterize them correctly—a fixed grid layout is less reusable than one that accepts span configurations. Reactive state containers are rarely reusable across screens because each screen has unique state.

Performance: Where Does Recomposition Happen?

The key performance question is not "how fast does it render" but "what triggers recomposition." Reactive state containers can cause unnecessary recomposition if you collect the entire state object when only one field changes. Custom layouts can be more efficient because they skip the overhead of standard containers, but they can also be slower if you implement measurement incorrectly. Slot-based patterns are generally performant because they isolate recomposition to the slot content.

Trade-Offs: A Structured Comparison

To make the trade-offs concrete, here's a comparison of the three approaches across the criteria above. This isn't a scorecard—it's a map of where each pattern excels and where it struggles.

PatternTestabilityReusabilityPerformanceTeam Onboarding
Reactive State ContainersHigh (unit-testable)Low (screen-specific)Medium (risks over-recomposition)Easy (MVVM familiarity)
Declarative Custom LayoutsMedium (UI tests needed)Medium (parameter-dependent)High (optimized measure/place)Hard (layout internals)
Component-Driven SlotsMedium (mixed test types)High (content-agnostic)High (isolated recomposition)Medium (slot concept)

Notice that no pattern wins all categories. The right choice depends on which criterion matters most for the component you're building. For a shared button component, slot-based theming is ideal. For a complex form with validation logic, reactive state containers are a better fit. For a custom calendar view, a custom layout gives you the control you need.

When to Mix Patterns

Most teams end up with a hybrid: reactive state containers for screens, slot-based composables for the design system, and custom layouts for a handful of specialized components. The danger is mixing them inconsistently—using a ViewModel for a simple toggle state, for example, when a remember block would suffice. The rule of thumb: use the simplest pattern that meets your testability and reusability needs for that specific composable.

Implementation Path After the Choice

Once you've selected a pattern for a given component, the implementation follows a repeatable sequence. Skipping steps leads to the risks we'll cover next.

Step 1: Define the State Contract

Write down what state the composable needs, what events it emits, and what parameters are required versus optional. For a reactive state container, this means a data class and a sealed interface for events. For a slot-based composable, it means the slot lambda signature and any style parameters. This step is often rushed, but it's where most design decisions are made.

Step 2: Implement the Composable Signature

Write the composable function header with clear parameter names and types. Avoid Boolean flags—use enums or sealed classes instead. For example, instead of a Boolean `isPrimary` parameter, use a `ButtonStyle` enum with `PRIMARY`, `SECONDARY`, and `TERTIARY` values.

Step 3: Write Unit Tests for State Logic

Even if the composable itself requires UI tests, the state logic (ViewModel or state holder) should be unit-testable. Verify that state transitions are correct and that events produce the expected state changes. This catches bugs before you run the emulator.

Step 4: Add UI Tests for Critical Paths

For custom layouts and slot-based composables, add a few UI tests that verify the component renders correctly under different state conditions. Focus on edge cases: empty state, error state, and maximum content length.

Step 5: Document the Pattern Decision

Write a brief note in the composable's KDoc explaining why you chose this pattern and what alternatives were considered. This helps future developers understand the trade-offs without repeating the analysis.

Risks If You Choose Wrong or Skip Steps

Even experienced teams fall into predictable traps. Here are the most common risks and how to spot them early.

Over-Abstraction

Creating a slot-based composable for every visual element leads to a system where nothing is concrete. You end up with a dozen composables that each accept ten parameters, and no one knows which combination to use. The fix: limit slot-based composables to components that appear in at least three different contexts. If a component is used only once, keep it concrete.

Recomposition Storms

When a reactive state container exposes a single state object, every field change triggers recomposition of the entire screen. This is the most common performance issue in Compose apps. Mitigate it by splitting state into smaller objects or using `derivedStateOf` for computed values. If you see frames dropping on simple interactions, check your state granularity first.

Inconsistent State Hoisting

Some composables hoist state to the ViewModel, others keep it local, and the inconsistency makes the codebase hard to reason about. The rule: hoist state only when it needs to be shared across siblings or persisted across configuration changes. Local UI state (text field input before submission) should stay in the composable with `remember`.

Ignoring the Design System

Teams that skip component-driven theming often end up with inconsistent spacing, colors, and typography across screens. The cost of retrofitting a theme later is higher than building it from the start. Even a minimal theme—with primary/secondary colors, a spacing scale, and type hierarchy—saves refactoring time later.

Frequently Asked Questions About Compose Design Patterns

Should I use a ViewModel for every composable?

No. Use ViewModels for screen-level state that survives configuration changes. For local UI state (like whether a dropdown is open), use `remember` and `mutableStateOf`. Overusing ViewModels adds boilerplate and makes composables harder to reuse.

How do I test a custom layout composable?

Write Compose UI tests that verify the layout's measurement and placement. Use `onNodeWithTag` to find children and assert their positions. For complex layouts, consider snapshot testing with a library like Roborazzi.

When should I use a slot-based composable instead of a regular one?

Use slots when the composable's content is variable and you want to defer the content decision to the caller. This is common for containers like cards, dialogs, and list items. If the content is fixed (like an icon button), a regular composable with parameters is simpler.

Can I mix patterns in the same screen?

Yes, but be deliberate. A typical screen uses a ViewModel for state, a custom layout for the main container, and slot-based composables for reusable UI elements. The key is to define clear boundaries so that each composable's pattern is predictable.

Recommendation Recap Without Hype

Pattern selection in Compose is not about finding the one true way—it's about matching the pattern to the component's role in your system. Here's a summary of the practical takeaways:

  • For screen-level state: Use reactive state containers (ViewModel + StateFlow). They're testable, familiar, and handle lifecycle correctly.
  • For reusable UI components: Use slot-based composables with a central theme. They ensure consistency and isolate recomposition.
  • For specialized layouts: Use custom layout composables only when standard containers can't achieve the desired arrangement. Keep them focused and well-tested.
  • For everything else: Use the simplest pattern that works—remember blocks for local state, simple composables for one-off UI.

Your next move: audit one screen in your current codebase. Identify which pattern each composable follows, and note any mismatches between the pattern and the component's role. Then refactor the most obvious mismatch—just one—and measure the impact on readability and performance. That single change will teach you more about pattern selection than any guide can.

Share this article:

Comments (0)

No comments yet. Be the first to comment!