Compose Replaces Manual UI Sync with a Single Principle: UI = f(state)
Positioning: Introduction / Concept Enlightenment Suitable for: Android developers who have never touched Compose, or students transitioning from the View system
I. First, a story: Two ways to renovate a room
Imagine you want to renovate a room. There are two ways to do it.
Method A: Imperative You hold a checklist and direct the workers step by step:
- Nail a hook into the wall.
- If 3 pieces of clothing are hung on the rack, replace the hook with a larger one.
- If there are more than 10 pieces of clothing, install another wardrobe.
For every new state, you must manually write "if... then..." logic. The interface and the state are two parallel lines, and you must manually keep them synchronized. This is traditional Android View + XML development: findViewById, setText, setVisibility... whenever the state changes, you have to remember to update the corresponding control.
Method B: Declarative You write just one sentence: "Number of racks = number of clothes ÷ 5, number of wardrobes = number of clothes ÷ 10". The worker (the framework) calculates it itself; you never manually move bricks.
This is Jetpack Compose. You only describe "what the interface looks like," and the framework is responsible for "how to update it after the state changes." This is a fundamental shift in mindset.
II. What pain points does Compose actually solve?
| Pain Point | View + XML Era | Compose |
|---|---|---|
| State Synchronization | Manual setText/setVisibility, easy to miss updates |
Interface automatically recomposes when state changes |
| Code Dispersion | XML manages layout, Kotlin manages logic, Adapter manages lists | One Kotlin function handles everything |
| Custom Controls | Inherit View, write onMeasure/onDraw, lots of boilerplate code |
One @Composable function |
| List Reuse | RecyclerView + Adapter + ViewHolder | LazyColumn one function |
For beginners, the biggest advantage of Compose is: a short learning path and a low mental burden for writing interfaces. For senior developers, the biggest advantage is: it eliminates the entire layer of error-prone glue code between View and data.
III. The first Compose program: Hello Compose
First, get a feel for what it looks like. Below is a complete, runnable minimal example:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.example.hellocompose.ui.theme.HelloComposeTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
HelloComposeTheme {
Surface {
Greeting("Compose")
}
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
HelloComposeTheme {
Greeting("Compose")
}
}
There are two most critical things in this code:
@Composable: Marks a function as a function that "describes the interface." It has no return value because it's not "returning an interface," but rather "drawing the interface."setContent { }: The entry point for the Activity, switching from the traditionalsetContentViewto here. Everything that follows runs within this declarative world.
IV. Declarative vs. Imperative: The same counter, two ways to write it
This is the most important case study in this article; please compare carefully.
Imperative approach (traditional View) — you must manually do three steps
class CounterActivity : AppCompatActivity() {
private var count = 0
private lateinit var textView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)
textView = findViewById(R.id.tv_count)
findViewById<Button>(R.id.btn_add).setOnClickListener {
count++
textView.text = "$count" // ① Must manually synchronize
}
}
}
Declarative approach (Compose) — only describe the relationship
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) } // State
Column {
Text("Current count: $count") // Interface
Button(onClick = { count++ }) { // Event only changes state
Text("+1")
}
}
}
Notice: In the Compose version, there is not a single line of "update text" code. You just say "the text displays count," and then the button increments count by one. The framework detects that count has changed, automatically re-executes Counter, and the text updates itself.
This is the mental model of Compose, summarized in one sentence:
UI = f(state). The interface is a function of state; when the state changes, the interface is automatically recalculated.
V. 3 terms beginners must know (just get familiar for now)
- Composable: A function annotated with
@Composableused to describe the interface. Think of it as a "Lego block." - State: The data behind the interface that can change, like
countin the counter. The interface changes only when the state changes. - Recomposition: The process where the framework automatically re-executes the relevant Composable when the state changes. This is the engine of Compose, which will be covered specifically in the next article.
VI. The "aha!" moment for senior developers
If you are coming from the View system, focus on experiencing these three points:
- No
findViewById, and nonotifyDataSetChanged. UI updates are no longer a series of commands, but a declaration of "the interface derived from the state." This eliminates the most common type of bug: the state changed but the interface forgot to refresh. - Composable functions will be called repeatedly, and out of order. Therefore, they must be "pure functions" — the same input produces the same output, and you cannot write side effects inside them (like directly making a network request). Many people don't understand this at first; the next article will go deeper.
- Compose is not "View with a different syntax sugar." It is a new system where the compiler, runtime, and UI toolchain work together. Once you understand "declarative" and "recomposition," all subsequent knowledge will fall into place logically.
VII. Common pitfalls
- Treating Compose like View: Still thinking about "getting a reference to this control to modify it." In Compose, there are no control references, only state. Change the state, and the interface changes itself.
- Performing time-consuming operations in a Composable: Composables execute repeatedly. If you make a network request inside one, it will repeat the request N times. Time-consuming operations should be placed in "side effects" (covered in article 8).
- Forgetting
remember: The next article will cover this. For now, just remember that invar count by remember { mutableStateOf(0) }, theremembercannot be omitted; otherwise, the state will be lost during recomposition.
VIII. Hands-on exercise
Modify the counter demo above: add a "-1" button, and display whether "count is even or odd." Run it yourself after the changes to experience the feeling of "only changing the state, not touching the interface."
IX. Learning resources
- Official Compose documentation: https://developer.android.com/jetpack/compose
- Official "Thinking in Compose": https://developer.android.com/develop/ui/compose/mental-model
- Official Compose getting started Codelab: https://developer.android.com/courses/pathways/compose
(Next article preview: Composable functions and the "recomposition" mechanism, understanding the engine of Compose.)