Table of Contents
From Views to Declarative UI
Jetpack Compose is Android’s modern toolkit for building native UI with declarative code. Instead of defining layouts in XML files and then manipulating them in activities or fragments, you describe what the UI should look like for a given state in Kotlin, and Compose updates the UI automatically when that state changes.
Traditional Android UI is usually called “view based.” You work with View and XML layouts, use findViewById, and change properties on views directly. Compose is a different model. You work with composable functions in Kotlin, there is no XML layout file for Compose screens, and you rarely call methods like setText or setVisibility. You define UI as a function of state, and the framework decides how to update what is on screen.
Compose is built on top of the existing Android toolkit. You can mix Compose with views in the same app, for example adding a Compose UI inside an existing activity, or using a traditional RecyclerView inside a screen that mostly uses Compose. This makes it possible to adopt Compose gradually instead of rewriting everything at once.
Because it is declarative, Compose changes how you think about UI. Instead of asking “how do I change this view,” you ask “what should the UI look like now given this state.” This focus on state and recomposition is the core idea behind Compose.
Composable Functions
In Compose, the basic building blocks of UI are composable functions. A composable function is just a Kotlin function that is marked with the @Composable annotation. This annotation tells the Compose compiler and runtime that this function contributes UI.
Here is a minimal example of a composable function that displays some text.
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}
Inside a composable function you call other composable functions to build more complex UI. You do not return View objects. The function body describes what to draw, and Compose handles the rest.
You normally do not create instances of views like TextView or Button in Compose. Instead you call composables like Text and Button that the Compose UI library provides. Many common UI components have equivalents in Compose, but their usage style is different. They are used as functions, often with parameters for content and behavior.
Composable functions must follow some rules. They can only be called from other composable functions or from special integration points such as setContent in an activity. They should be side effect free in terms of UI. That means a composable should not perform things like starting a network request directly in its body. Think of a composable as a description of UI given some inputs, not a place to perform long running operations.
You can give composables parameters to make them reusable. For example, a custom card composable might accept a title, description, and a click handler. This parameterization makes your UI components testable and easier to reason about, because the same inputs always produce the same visual result.
Setting Up a Compose Screen
To show a Compose UI in an activity, you use setContent instead of setting an XML layout. This method is available on ComponentActivity and some related classes. Inside the setContent block you call your root composable functions.
Here is an example of using Compose from an activity.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp()
}
}
}
@Composable
fun MyApp() {
Greeting(name = "Android")
}
The setContent block creates a Compose UI tree. Every composable you call inside it builds up the UI hierarchy. There is no XML layout file in this example. The MyApp composable becomes the entry point for your UI in this activity.
In a real project, your app may still have view based screens with XML. You can add a Compose based screen by changing one activity to use setContent, and leave the rest unchanged. This mixed approach is a common way to start exploring Compose without committing the entire app to it.
State and Recomposition
State is central to how Compose works. A composable function reads state values and draws the corresponding UI. When the state changes, Compose automatically re-executes the relevant composables to reflect the new state. This process is called recomposition.
A simple example is a counter that increments when you tap a button. The count is part of the UI state. The composable reads the count and displays it. When the user taps the button, the count changes, and Compose recomposes, updating the displayed value.
Here is a very basic pattern you will see frequently in Compose.
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column {
Text(text = "Count: $count")
Button(onClick = { count++ }) {
Text("Increase")
}
}
}
The important part is the remember { mutableStateOf(0) } call. This creates a state holder for the count and instructs Compose to observe it. When count changes, Compose schedules a recomposition of the Counter composable, and the text showing the count updates.
You do not need to call methods like notifyDataSetChanged or manually request layout. Compose reacts to state changes and redraws only the parts of the UI that depend on that state.
Over time, your app will have state coming from multiple places, including view models and data repositories. Compose can observe these state holders as long as they expose observable types. The simple state shown here is enough to demonstrate the basic idea that UI is derived from state and recomposition keeps the UI in sync.
Compose updates UI automatically when observed state changes. You must treat state as the single source of truth and avoid updating views directly.
Basic Layouts and Modifiers
User interfaces in Compose are constructed from layout composables and modifier chains. Layout composables position and size their children. Modifiers adjust appearance, behavior, and layout details for a specific element.
Common layout composables include Column, Row, and Box. A Column stacks children vertically, a Row arranges them horizontally, and a Box lets you stack and overlay elements. These are analogous to traditional layouts but appear as functions instead of XML tags.
Here is an example of a simple layout.
@Composable
fun ProfileHeader(name: String, title: String) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(text = name)
Text(text = title)
}
}
The Modifier type is a powerful concept in Compose. You chain modifier calls to gradually build up behavior. In the example, fillMaxWidth() makes the Column stretch across the screen width, and padding(16.dp) adds space around its content. You apply modifiers using the modifier parameter that most composables provide.
Modifiers do many things. They can change layout, such as padding or size. They can affect drawing, such as applying a background color or a border. They can add input handling, such as clickable. The order of modifiers in the chain can matter, for example padding before or after background changes what gets padded.
Understanding how to combine layout composables with modifiers is the key skill for building layouts with Compose. Instead of configuring layout attributes in XML, you build them with nested composables and modifier chains in Kotlin.
Simple User Interaction
Compose handles user interaction through parameters and modifiers rather than separate listener objects. For example, a Button composable takes an onClick lambda parameter. When the user taps the button, Compose calls that lambda.
Here is a basic interactive composable.
@Composable
fun LikeButton() {
var liked by remember { mutableStateOf(false) }
Button(
onClick = { liked = !liked }
) {
Text(text = if (liked) "Liked" else "Like")
}
}
The click event updates the liked state, and recomposition updates the button label. You do not attach a listener object or search for the button by id. All the interaction logic is in one place, inside the composable.
Modifiers also provide interaction hooks. For example, you can add a clickable modifier to a Row or Box to respond to taps on that area. This helps you attach behavior directly to the visual elements they affect, instead of wiring up listeners separately.
User input fields, such as text fields and check boxes, follow a similar pattern. You usually pass current state and callback lambdas for changes, and Compose handles re-rendering as the state evolves.
Theming and Material Components
Compose includes support for Material Design via MaterialTheme and a collection of Material components such as Button, Text, Scaffold, Card, and more. These components integrate with theming so that your app can have a consistent look and feel.
A theme in Compose is usually defined at the root of your UI tree. Inside the theme, you place your app content. The theme provides colors, typography, and shapes that Material components use by default.
Here is a basic structure that wraps an app in a theme.
@Composable
fun MyApp() {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
Greeting("Android")
}
}
}
The MyApplicationTheme composable is typically generated for you when you create a new Compose project in Android Studio. Inside it, you can customize color schemes and typography. This works conceptually like themes in the view system, but is expressed as composables and Kotlin objects instead of XML theme resources.
You can still use your existing color and dimension resources and reference them from Compose. Compose integrates with the resource system, so you can use functions to load those values when needed. The difference is that in Compose you usually access them from code rather than from XML attributes.
Material components in Compose are aware of the theme and adapt accordingly. For example, a Button automatically uses the primary color defined in your theme. Changing the theme colors will update buttons, surfaces, and text across the app without changing each composable individually.
Integrating Compose in Existing Apps
If you already have applications that use the classic view system, you do not need to migrate everything at once. Compose was designed to work side by side with views.
To place a Compose UI inside an existing XML layout, you can use a special view called ComposeView. You add ComposeView to your XML layout and then set its content from code with composables. This lets you replace parts of your UI with Compose while leaving the rest intact.
You can also embed traditional views inside a Compose hierarchy using interoperability APIs. That allows you to reuse existing widgets that might not have Compose equivalents yet, such as some custom or third party views. Over time, you can gradually move more of your UI to Compose as you gain familiarity and as libraries provide more composable versions of components.
The ability to integrate helps you explore Compose without committing to a complete rewrite. You can start with small features, screens, or even individual widgets, and expand as you become more comfortable with the declarative style.
When to Use Jetpack Compose
Compose is now the preferred way to build new Android UIs. For new projects, using Compose as the primary UI toolkit simplifies layout construction, reduces boilerplate, and often improves code readability. For existing projects, adopting Compose incrementally can modernize the user interface and make it easier to maintain.
Compose shines when your UI depends heavily on dynamic data and state. Its recomposition model handles complex state driven interfaces without the need to manually update views. It also integrates well with modern architectural patterns that separate UI from data and business logic.
It is still important to understand the view system, especially for legacy apps and many existing libraries that target views. However, learning Compose prepares you for the current and future direction of Android UI development, where declarative patterns and Kotlin based definitions are the norm.
As you continue through more advanced topics, you will see how Compose works together with architecture components, navigation, and other parts of the Android ecosystem, and how it can help you build responsive, maintainable user interfaces.