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:

Selecting Empty Compose Activity template in Android Studio

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 TodoItems) 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:

Compose Todo App

Happy composing!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *