Android Jetpack Compose: Getting Started Guide
Android development is undergoing its most significant shift in years. Jetpack Compose, Google’s modern toolkit for building native Android UI, brings declarative programming to Android. If you have been writing XML layouts and wrangling RecyclerViews, Compose offers a fundamentally better way to build interfaces.
Compose is currently in beta (as of March 2021), with a stable 1.0 release expected later this year. While it is not yet recommended for production apps, now is the ideal time to learn. The API has stabilised significantly, and Google has made it clear that Compose is the future of Android UI development.
What Is Jetpack Compose?
Jetpack Compose is a declarative UI toolkit for Android, written entirely in Kotlin. Instead of defining your UI in XML and manipulating it imperatively with Java or Kotlin code, you describe your UI as composable functions that emit UI elements based on your app’s state.
If you are familiar with React, SwiftUI, or Flutter, the mental model is similar. You write functions that describe what the UI should look like for a given state, and the framework handles rendering and updating.
Why Compose Matters
Less code. Compose eliminates XML layouts, view binding, and much of the boilerplate associated with RecyclerViews, Fragments, and custom views.
Intuitive state management. UI updates automatically when state changes. No more manually calling notifyDataSetChanged() or juggling LiveData observers.
Powerful tooling. Android Studio Arctic Fox (currently in preview) includes Compose previews, interactive preview mode, and a layout inspector designed for Compose.
Interoperability. Compose works alongside existing Views. You can adopt it incrementally, adding Compose screens to an existing app without rewriting everything.
Setting Up Your Proje
ct
You need Android Studio Arctic Fox (2020.3.1) or later. Download it from the Android Studio preview page. It includes the Compose compiler plugin and preview tools.
Create a new project with the Empty Compose Activity template. This sets up the necessary dependencies.
If you are adding Compose to an existing project, add these dependencies to your module-level build.gradle:
android {
compileSdk 30
defaultConfig {
minSdk 21
targetSdk 30
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.0.0-beta03'
}
}
dependencies {
implementation 'androidx.compose.ui:ui:1.0.0-beta03'
implementation 'androidx.compose.material:material:1.0.0-beta03'
implementation 'androidx.compose.ui:ui-tooling:1.0.0-beta03'
implementation 'androidx.activity:activity-compose:1.3.0-alpha05'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha03'
}
Note: Version numbers reflect the beta available at the time of writing. Check the official documentation for the latest versions.
Your First Composable
A composable function is a regular Kotlin function annotated with @Composable:
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}
Set this as your activity’s content:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Greeting(name = "Android Developer")
}
}
}
}
setContent replaces setContentView. Instead of inflating an XML layout, you call composable functions directly.
Preview Your Composable
Add a preview function to see your composable in the IDE without running the app:
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
MaterialTheme {
Greeting(name = "Preview")
}
}
The preview renders in Android Studio’s design panel. You can interact with it, inspect the layout, and even deploy it directly to a device.
Layout Composabl
es
Compose provides three primary layout composables:
Column (Vertical Layout)
@Composable
fun TaskList() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text("Task 1: Design mockups")
Spacer(modifier = Modifier.height(8.dp))
Text("Task 2: Implement UI")
Spacer(modifier = Modifier.height(8.dp))
Text("Task 3: Write tests")
}
}
Row (Horizontal Layout)
@Composable
fun TaskRow(title: String, isCompleted: Boolean) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
) {
Checkbox(
checked = isCompleted,
onCheckedChange = null
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = title,
style = MaterialTheme.typography.body1
)
}
}
Box (Stacking / Overlap)
@Composable
fun BadgeIcon(count: Int) {
Box {
Icon(
imageVector = Icons.Default.Notifications,
contentDescription = "Notifications"
)
if (count > 0) {
Badge(
modifier = Modifier.align(Alignment.TopEnd)
) {
Text(text = count.toString())
}
}
}
}
Modifiers
Modifiers are Compose’s way of decorating or augmenting composables. They handle padding, sizing, background, click handling, and much more:
Text(
text = "Important Task",
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGray)
.padding(16.dp)
.clickable { /* handle click */ }
)
Modifier order matters. padding before background adds padding outside the background. background before padding adds padding inside the background. Think of modifiers as wrapping layers applied from outer to inner.
State Management
State management is where Compose truly shines compared to the traditional Android approach.
remember and mutableStateOf
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "Count: $count",
style = MaterialTheme.typography.h4
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
remember preserves state across recompositions. mutableStateOf creates an observable state holder. When count changes, Compose automatically recomposes only the affected parts of the UI.
State Hoisting
For reusable composables, hoist state to the caller:
// Stateless composable (reusable)
@Composable
fun TaskItem(
title: String,
isCompleted: Boolean,
onToggle: (Boolean) -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(12.dp)
) {
Checkbox(
checked = isCompleted,
onCheckedChange = onToggle
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = title)
}
}
// Stateful parent
@Composable
fun TaskScreen() {
var tasks by remember {
mutableStateOf(
listOf(
Task("Design UI", false),
Task("Write code", true),
Task("Test app", false)
)
)
}
LazyColumn {
itemsIndexed(tasks) { index, task ->
TaskItem(
title = task.title,
isCompleted = task.isCompleted,
onToggle = { checked ->
tasks = tasks.toMutableList().also {
it[index] = task.copy(isCompleted = checked)
}
}
)
}
}
}
This pattern separates UI rendering from state management, making composables easier to test and reuse.
ViewModel Integration
For real apps, use ViewModel to manage state that survives configuration changes:
class TaskViewModel : ViewModel() {
private val _tasks = mutableStateListOf<Task>()
val tasks: List<Task> = _tasks
init {
// Load initial data
_tasks.addAll(
listOf(
Task("Design UI", false),
Task("Write code", true)
)
)
}
fun toggleTask(index: Int) {
_tasks[index] = _tasks[index].copy(
isCompleted = !_tasks[index].isCompleted
)
}
fun addTask(title: String) {
_tasks.add(Task(title, false))
}
}
@Composable
fun TaskScreen(viewModel: TaskViewModel = viewModel()) {
LazyColumn {
itemsIndexed(viewModel.tasks) { index, task ->
TaskItem(
title = task.title,
isCompleted = task.isCompleted,
onToggle = { viewModel.toggleTask(index) }
)
}
}
}
Lists with LazyColumn
LazyColumn is Compose’s equivalent of RecyclerView. It only renders visible items, providing excellent performance for long lists:
@Composable
fun TaskList(tasks: List<Task>, onToggle: (Int) -> Unit) {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = tasks,
key = { it.id }
) { task ->
TaskCard(task = task, onToggle = { onToggle(tasks.indexOf(task)) })
}
}
}
Providing a key helps Compose efficiently track and animate list changes.
Theming with Material Design
Compose includes a Material Design theme system:
private val LightColorPalette = lightColors(
primary = Color(0xFF1976D2),
primaryVariant = Color(0xFF004BA0),
secondary = Color(0xFF26A69A),
background = Color(0xFFFAFAFA),
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.White,
onBackground = Color(0xFF212121),
onSurface = Color(0xFF212121)
)
private val DarkColorPalette = darkColors(
primary = Color(0xFF90CAF9),
primaryVariant = Color(0xFF5D99C6),
secondary = Color(0xFF80CBC4)
)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) DarkColorPalette else LightColorPalette
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
Use your theme in the activity:
setContent {
AppTheme {
Surface(color = MaterialTheme.colors.background) {
TaskScreen()
}
}
}
Navigation
For multi-screen apps, use the Navigation Compose library:
implementation 'androidx.navigation:navigation-compose:1.0.0-alpha09'
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "tasks") {
composable("tasks") {
TaskListScreen(
onTaskClick = { taskId ->
navController.navigate("task/$taskId")
}
)
}
composable(
route = "task/{taskId}",
arguments = listOf(navArgument("taskId") { type = NavType.StringType })
) { backStackEntry ->
val taskId = backStackEntry.arguments?.getString("taskId") ?: return@composable
TaskDetailScreen(taskId = taskId)
}
}
}
When to Adopt Compose
As of March 2021, Compose is in beta. Here is our recommendation:
- New personal projects: Use Compose. It is stable enough for learning and side projects.
- New client projects launching in Q3/Q4 2021: Consider Compose, especially if the stable 1.0 release arrives as expected.
- Existing production apps: Start experimenting with Compose in isolated screens. The interoperability layer makes incremental adoption straightforward.
- Apps requiring complex custom views: Wait for the ecosystem to mature. Some advanced UI patterns still lack Compose equivalents.
Resources for Learning
- Official documentation: developer.android.com/jetpack/compose
- Compose pathway on Android Developers: A structured learning path with codelabs
- Android Compose samples on GitHub: Real-world sample apps from Google
- Kotlin Slack #compose channel: Active community discussions
Final Thoughts
Jetpack Compose represents a generational leap in Android UI development. The declarative model reduces boilerplate, simplifies state management, and makes building beautiful UIs genuinely enjoyable.
The learning curve is modest, especially if you have experience with other declarative frameworks. The hardest part is unlearning imperative patterns from the XML and View world.
At eawesome, we are already building internal projects with Compose and preparing for production adoption later this year. If you are planning an Android app in 2021, learning Compose now puts you ahead of the curve.