Create an Android App With Kotlin and Jetpack Compose
As an Android developer, you‘re probably aware that Google‘s recommended UI toolkit has undergone a major shift in recent years. Jetpack Compose is the modern toolkit for building native Android UIs with declarative Kotlin code. It greatly simplifies and accelerates UI development compared to the traditional View system.
In this in-depth tutorial, we‘ll dive into Jetpack Compose by building a complete Android app from scratch. No prior Compose experience is needed – I‘ll walk you through the process step-by-step. By the end, you‘ll have a solid foundation in Compose and be ready to apply it in your own apps.
Our sample app will be a basic todo list that supports adding, completing, and deleting items. We‘ll start with the UI, explore theming and styling, add a ViewModel for business logic and state management, and wire everything up to handle user input. We‘ll see how Compose makes all of this straightforward and concise.
Let‘s jump in and start composing!
What is Jetpack Compose?
Jetpack Compose is Android‘s modern toolkit for building native UI. It lets you create your app‘s UI entirely in Kotlin, using composable functions to define what your UI should look like for a given application state.
The big idea behind Compose is that your UI is essentially a function of your app‘s state. As the state changes, the composable functions are automatically re-invoked or "recomposed" to update the UI. This declarative approach vastly simplifies UI programming compared to manually updating Views.
Some key benefits of Compose include:
- Less code: Accomplish more with concise, idiomatic Kotlin
- Intuitive: Describe your UI as a function of state
- Accelerated development: Preview your UI changes instantly
- Powerful: Build beautiful, highly customized UIs with ease
Setting Up a New Compose Project
To use Compose, you‘ll need the latest Android Studio (Arctic Fox or newer). When creating a new project, select the Empty Compose Activity template:
Make sure the Language is set to Kotlin. The template will configure your project with the Compose dependencies and set up a composable MainActivity
for you.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAwesomeAppTheme {
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
MyAwesomeAppTheme {
Greeting("Android")
}
}
The @Composable
annotation identifies composable functions – the building blocks of your UI. setContent
is called to define your UI when the activity is created, similar to setContentView
in the View system.
@Preview
annotations let you preview your UI in Android Studio without needing to run the app. As you make changes, the previews update instantly.
Building a Todo App: UI
Let‘s start building our todo list app‘s UI. We‘ll add composables for inputting new tasks, displaying the list of tasks, and handling user actions.
Composable Functions
We‘ll create separate composable functions to organize our UI code:
@Composable
fun TodoScreen(
items: List<TodoItem>,
onAddItem: (TodoItem) -> Unit,
onRemoveItem: (TodoItem) -> Unit
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
TodoItemInput(onAddItem)
LazyColumn {
items(items) { todo ->
TodoItemRow(
todo = todo,
onItemClicked = { onRemoveItem(it) }
)
}
}
}
}
@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
// ...
}
@Composable
fun TodoItemRow(todo: TodoItem, onItemClicked: (TodoItem) -> Unit) {
// ...
}
TodoScreen
is the top-level composable that takes state (the list of TodoItem
s) and event callbacks (onAddItem
and onRemoveItem
). It then delegates to TodoItemInput
and TodoItemRow
to render the pieces of the UI.
This pattern of passing state down and events up is a key aspect of Compose architecture.
Basic UI Elements
Inside the TodoItemInput
, we‘ll use a TextField
to input new todo items and a Button
to add them to the list:
@Composable
fun TodoItemInput(onItemComplete: (TodoItem) -> Unit) {
val (text, setText) = remember { mutableStateOf("") }
Row(
Modifier
.padding(16.dp)
.fillMaxWidth()
) {
TextField(
value = text,
onValueChange = setText,
modifier = Modifier.weight(1f),
placeholder = { Text("Add a todo item") }
)
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = {
onItemComplete(TodoItem(text))
setText("")
}) {
Text("Add")
}
}
}
For TodoItemRow
, we‘ll show a Text
with a Checkbox
to toggle completion status:
@Composable
fun TodoItemRow(todo: TodoItem, onItemClicked: (TodoItem) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onItemClicked(todo) }
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = todo.task)
Checkbox(
checked = todo.isComplete,
onCheckedChange = { onItemClicked(todo.copy(isComplete = it)) }
)
}
}
Lists with LazyColumn
To display a scrollable list, Compose provides LazyColumn
. It renders only the visible items on screen, making it very efficient.
In TodoScreen
, we use items()
to create a composable for each TodoItem
in the list:
LazyColumn {
items(items) { todo ->
TodoItemRow(
todo = todo,
onItemClicked = { onRemoveItem(it) }
)
}
}
LazyColumn
automatically deals with recycling, view holders, adapters, etc. It greatly simplifies list UI compared to RecyclerView
.
State and Recomposition
To make our UI dynamic and interactive, we need to introduce state. Compose uses a MutableState<T>
type to hold mutable state that should trigger recomposition.
In TodoItemInput
, we used mutableStateOf
to create state for the text field:
val (text, setText) = remember { mutableStateOf("") }
Whenever text
changes by calling setText
, the TextField
will automatically recompose with the new value.
In TodoScreen
, the state comes from a ViewModel
(which we‘ll discuss shortly). When the ViewModel‘s items
property changes, TodoScreen
and its child composables recompose to display the latest data.
Theming and Styling
Compose has a powerful theming system based on MaterialTheme
. You can define colors, typography and shapes in one place and use them throughout your composables.
To theme the entire app, we wrap our root composable in a MaterialTheme
:
setContent {
MyAwesomeAppTheme {
TodoScreen(/* ... */)
}
}
@Composable
fun MyAwesomeAppTheme(content: @Composable () -> Unit) {
MaterialTheme(
colors = LightColorPalette,
typography = Typography,
shapes = Shapes,
content = content
)
}
LightColorPalette
, Typography
, and Shapes
define your custom design system. Any composables inside MaterialTheme
will inherit these theme properties.
For example, you can style a Text
using a theme‘s color and typography:
Text(
text = "Hello world",
color = MaterialTheme.colors.primary,
style = MaterialTheme.typography.h5
)
Building a Todo App: Logic and Data
Now that we have our UI in place, let‘s add the backend brains to make it functional.
Defining a Data Model
First, we need a data class to represent an individual todo item:
data class TodoItem(
val task: String,
val isComplete: Boolean = false
)
Each TodoItem
has a task
string and an isComplete
boolean to track completion state.
Adding a ViewModel
To manage the todo list and handle business logic, we‘ll use a ViewModel
:
class TodoViewModel : ViewModel() {
private val _todoItems = mutableStateListOf<TodoItem>()
val todoItems: List<TodoItem> = _todoItems
fun addItem(item: TodoItem) {
_todoItems.add(item)
}
fun removeItem(item: TodoItem) {
_todoItems.remove(item)
}
}
The ViewModel exposes the list of items as a read-only List
to prevent direct modification. It provides methods like addItem
and removeItem
to update the list in a controlled way.
To connect the ViewModel to our composables, we use the viewModel()
function:
@Composable
fun TodoActivityScreen(todoViewModel: TodoViewModel = viewModel()) {
TodoScreen(
items = todoViewModel.todoItems,
onAddItem = { todoViewModel.addItem(it) },
onRemoveItem = { todoViewModel.removeItem(it) }
)
}
When the ViewModel‘s state changes, TodoScreen
will automatically recompose to display the latest data.
Handling User Input
Finally, let‘s wire up the UI to respond to user input.
Adding New Items
When the user types into the text field and taps the "Add" button, we call the ViewModel‘s addItem
method:
Button(onClick = {
onItemComplete(TodoItem(text))
setText("")
}) {
Text("Add")
}
The onItemComplete
callback comes from TodoScreen
and is connected to the ViewModel.
Completing and Deleting Items
When the user taps a todo item, we trigger the onItemClicked
callback to either toggle completion or delete the item:
@Composable
fun TodoItemRow(todo: TodoItem, onItemClicked: (TodoItem) -> Unit) {
Row(
modifier = Modifier.clickable { onItemClicked(todo) }
) {
/* ... */
Checkbox(
checked = todo.isComplete,
onCheckedChange = {
onItemClicked(todo.copy(isComplete = it))
}
)
}
}
Checking the checkbox calls onItemClicked
with an updated copy of the item with isComplete
toggled. The ViewModel updates the item in the backing list.
To delete an item, we pass the item itself to the ViewModel‘s removeItem
method:
TodoItemRow(
todo = todo,
onItemClicked = { onRemoveItem(it) }
)
And that‘s it! We now have a fully functional todo list app composed entirely in Kotlin.
Conclusion
In this tutorial, we explored the core concepts of Jetpack Compose by building a complete Android app. We covered:
- Setting up a new Compose project
- Building UIs with composable functions
- Displaying lists with LazyColumn
- State and reactivity with mutableStateOf
- Theming and styling with MaterialTheme
- Managing logic and data with ViewModel
I hope this gives you a solid foundation to start using Compose in your own apps. The declarative approach is a big mindset shift from the View system, but its expressiveness and reusability quickly become addictive.
Compose is still evolving rapidly, so there‘s never been a better time to get involved and start learning. For more in-depth information, check out the official Compose documentation:
You can also find the full source code for the sample app we built here:
Happy composing!