Zygote
UI toolkitsintermediate

Why Compose

The View system made you the maintainer of a mutable tree. Compose replaced that with a function from state to UI — and in exchange, took away your control over when things run.

11 minAPI 21–37 · 17interactivequizverified 2026-10-27

Every UI toolkit is a bet about where complexity goes. The system puts it in the developer's hands: you hold a mutable tree and you are responsible for keeping it consistent. Compose moves it into the runtime: you describe the result, and the runtime works out how to get there.

TL;DR#

  • Compose is a declarative UI toolkit: UI is a function of state. You do not mutate views, you emit UI.
  • There is no view tree to hold. The runtime builds and maintains a composition, and recomposes the parts whose inputs changed.
  • It replaces adapters and view holders with LazyColumn and item lambdas, and XML layouts with functions.
  • Compose 1.0 shipped in July 2021.1
  • In Android 17, new platform and APIs target Compose only, and View libraries enter maintenance mode.2

Mental model#

Two ways to keep a room tidy.

Imperative: you hold a list of every object in the room and a note for each one about where it should be. When something changes, you update the note, and then you must remember to move the object.

Declarative: you write a description of the room as it should be. When something changes, you rewrite the description. Something else compares the new description to the old one and moves what needs moving.

Where the analogy breaks#

The declarative version does not rebuild the room from scratch. It re-executes only the description of the parts that changed, which requires knowing which parts read which inputs. That is the whole subtlety of Compose, and it is why "just recompose everything" is not how it works — and why unstable parameters are expensive.

The big picture#

Loading diagram…
Text description of this diagram

A filterable mapping table from View-system concepts to Compose equivalents. XML layout files become composable functions; TextView becomes Text; EditText becomes TextField; ImageView becomes Image with a painter; LinearLayout becomes Row or Column; FrameLayout becomes Box; RecyclerView with an adapter becomes LazyColumn with items(); ListView becomes LazyColumn; ConstraintLayout remains available as a composable; styles and themes become MaterialTheme and CompositionLocal; findViewById becomes reading state inside composition; Fragments become NavHost destinations; a single-screen Activity becomes a single activity hosting Compose navigation; and View.animate becomes animateAsState or Transition.

The rows that are mechanical — TextView to Text — are the easy half. The rows that are not — RecyclerView to LazyColumn, Fragment to a destination — are where the architecture changes, not just the API.

Deep dive#

The same screen, twice#

View system:

class NotesActivity : AppCompatActivity() {
    private lateinit var binding: ActivityNotesBinding
    private val adapter = NoteAdapter()
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityNotesBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.list.adapter = adapter
        binding.list.layoutManager = LinearLayoutManager(this)
 
        viewModel.notes.observe(this) { notes ->
            adapter.submitList(notes)
            binding.empty.visibility = if (notes.isEmpty()) View.VISIBLE else View.GONE
        }
 
        binding.addButton.setOnClickListener { viewModel.add() }
    }
}

Compose:

@Composable
fun NotesScreen(viewModel: NotesViewModel) {
    val notes by viewModel.notes.collectAsStateWithLifecycle()
 
    Column {
        if (notes.isEmpty()) {
            EmptyState()
        } else {
            LazyColumn {
                items(notes, key = { it.id }) { note ->
                    NoteRow(note)
                }
            }
        }
        AddButton(onClick = viewModel::add)
    }
}

Three structural differences, and each removes a class of bug:

  1. No adapter. LazyColumn takes an item lambda. There is no notifyDataSetChanged, no ViewHolder, no diff callback to forget.
  2. No visibility toggle. if (notes.isEmpty()) is ordinary Kotlin. The branch is the state transition.
  3. No observer wiring. collectAsStateWithLifecycle() is the whole subscription, and its lifetime is the composition's.

What actually improved#

Compose's benefits are usually described vaguely ("it is more modern"). The concrete ones:

State has one owner. In the View system, the current text lives in an EditText, the selection in the EditText, the scroll in the RecyclerView, and the "is loading" flag in a field on the activity. Five sources of truth for one screen. In Compose, the screen is a function of one value, and any deviation is a bug you introduced deliberately.

Reuse is by function, not by inheritance. A ViewGroup subclass can only be reused where its parent type is acceptable. A composable function composes into anything.

No inflation. A composable is a function call. There is no XML parse, no reflection, no style resolution at runtime. That removes a chunk of cold-start cost — though it adds other costs, which this module's performance page covers.

Testing is more direct. A composable can be called in a test with a fake state and asserted on. There is no activity to launch.

Fewer moving parts in navigation. A NavHost with composable destinations replaces fragments, transactions and the fragment lifecycle.

What got harder#

Honesty matters more than advocacy here.

  • Recomposition is invisible until it is not. A lambda allocated on every recomposition, or an unstable List parameter, silently widens the scope. The View system's cost model was crude but obvious; Compose's is precise but requires reasoning.
  • Debugging is different. There is no view tree in the hierarchy viewer. Layout Inspector renders the composition tree instead, and stack traces point at generated code.
  • The learning curve is a cliff. remember, derivedStateOf, rememberUpdatedState, LaunchedEffect keys, stability, CompositionLocal — each is a small concept, and they interact.
  • Interop costs. Embedding Compose in a View screen (or the reverse) is supported and works, but it is a boundary with real overhead.

The compiler is part of the toolkit#

Compose is not a library in the ordinary sense. The Compose compiler plugin transforms every @Composable function, adding parameters that let the runtime track groups and restart scopes. That is how recomposition can target a single function.

@Composable
fun Greeting(name: String) {
    Text("Hello, $name")
}

The compiler rewrites this into a function with a Composer parameter, group markers, and calls to startRestartGroup/endRestartGroup. You never write those; but the fact that they exist is why Compose requires a specific Kotlin compiler plugin version and why upgrading Kotlin without upgrading the Compose compiler used to break builds.

The ecosystem decision#

For the first few years, Compose was one option among several: you could build a new screen in Compose and leave the rest in Views. That era is closing.

In Android 17, Google states that new platform and Jetpack APIs target Compose only, and that the View libraries enter maintenance mode.2 The practical reading:

  • Existing View code continues to work, and will for years.
  • New APIs — particularly those that ship as part of a platform release — may have no View equivalent.
  • New apps should be Compose-first; existing apps should migrate screen by screen.

The mental shift, concretely#

Three habits carry over from Views and are actively wrong in Compose. Naming them is faster than learning them by debugging.

Holding a reference to a UI object. There is no TextView to keep. If you find yourself wanting one, the thing you want is state.

Updating the UI in a callback. In Views, a network callback calls setText. In Compose, it updates a state value, and the UI catches up. The callback does not know the UI exists.

Measuring to decide layout. There is no getMeasuredWidth() to consult before drawing. Layout is declared with modifiers, and the runtime resolves it. Code that needs a size uses BoxWithConstraints or onSizeChanged, not a pre-measure pass.

The reverse habits are equally common: a developer new to Compose writes state into the composition body and gets an infinite loop, because they have not yet internalised that composition runs many times.

When Views are still the right answer#

  • A single simple screen with no dynamic state. A LinearLayout and three TextViews is less code than the Compose setup.
  • Heavy custom drawing in an existing View. Canvas code ports, but there is no urgency.
  • RemoteViews: app widgets and custom notifications still use a restricted View-like system, and Glance is the Compose-shaped alternative.
  • Third-party SDK views that must be embedded. Interop exists; use it.

The migration shape#

The realistic path for an existing app is not a rewrite. It is:

  1. Add Compose to the build.
  2. Start with new screens, and leaf components inside existing screens (ComposeView in a layout).
  3. Move shared design-system components to Compose.
  4. When a screen's View code is more than half interop glue, rewrite that screen.

That ordering keeps the app shippable at every step, which is the only migration strategy that survives contact with a release schedule.

How it evolved#

Interface changes16 releases
APIVersionWhat changed
11.0
  • Pull-down notification shade
  • Home screen with widgets
31.5 Cupcake
  • Home screen widgets and folders
92.3 Gingerbread
  • New UI with dark theme and green accents
113.0 Honeycomb
  • The Holo design language arrives
123.1 Honeycomb
  • Resizable widgets
164.1 Jelly Bean
  • Project Butter: vsync-driven, smooth UI
  • Expandable notifications
204.4W KitKat (Wear)
  • Voice-driven card UI
215.0 Lollipop
  • Material Design
  • Notifications on the lock screen
247.0 Nougat
  • Quick Settings tiles
  • Direct Reply in notifications
268.0 Oreo
  • Notification channels
289 Pie
  • Dark theme groundwork
2910
  • System-wide dark theme
3011
  • Conversation notifications and bubbles
  • Media controls in Quick Settings
3112
  • Material You dynamic colour
3313 Tiramisu
  • Themed app icons
  • Runtime notification permission
3616 Baklava
  • Material 3 Expressive
  • Live Updates notifications

The dates that matter: Compose 1.0 in July 2021, Material 3 in October 2021, and the Compose-first statement in Android 17. Between them lies the period in which Compose went from "an option" to "the default", driven less by a single release than by the ecosystem moving.

Under the hood#

Curiosity corner#

  • A Compose UI is still hosted in a View. ComponentActivity.setContent creates a ComposeView and attaches it. The View system did not disappear; it became the bottom layer.
  • The Compose compiler is a Kotlin compiler plugin, which is why Compose version compatibility is tied to Kotlin version compatibility.
  • @Composable functions can return a value. They usually return Unit, but a composable that returns a value is legal and is how some layout primitives work.

Try it#

See whether an app has Compose in its APK

adb shell pm path com.example.notes # then, on the pulled APK: unzip -l base.apk | grep -c "androidx/compose"

Count composables in a codebase

grep -rn "@Composable" --include=*.kt . | wc -l

Check yourself#

Check yourself

1 / 12

What is the difference between `invalidate()` and `requestLayout()`?

0 answered · 0 correct

Sources#

Sources

Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.

  1. docsAndroidX রিলিজ | Jetpackdeveloper.android.comaccessed 2026-10-27↩
  2. blogAndroid 17 is hereAndroid Developers Blogaccessed 2026-10-27↩
  3. docsThinking in Composedeveloper.android.comaccessed 2026-10-27↩
  4. docsCompose performancedeveloper.android.comaccessed 2026-10-27↩
  5. docsCompose stabilitydeveloper.android.comaccessed 2026-10-27↩
  6. docsJetpackdeveloper.android.comaccessed 2026-10-27↩
  7. docsBehavior changes: Apps targeting Android 17developer.android.comaccessed 2026-10-27↩