Compose for Desktop: Get Your Weather!

Build a desktop weather app with Compose for Desktop! You’ll get user input, fetch network data and display it all with the Compose UI toolkit. By Roberto Orgiu.

5 (2) · 1 Review

Download materials
Save for later

Compose for Desktop is a UI framework that simplifies developing user interfaces for desktop apps. Google introduced it in 2021. This modern toolkit uses Kotlin for creating fast reactive UIs with no XMLs or templating language. Also, by using it, you’re able to share UI code between desktop and Android apps.

JetBrain’s developers created Compose for Desktop based on Jetpack Compose for Android, but there are some things that differ. For example, you won’t work with the Android lifecycle, Android ViewModels or even Android Studio.

For the project, you’ll use Kotlin and IntelliJ IDEA to create an app that lets you query weather data for a specific city in the world.

In this tutorial, you’ll learn how to:

  • Model your desktop app
  • Use the Loading, Content, Error (LCE) model
  • Publish your app
Note: This tutorial assumes you’re familiar with Jetpack Compose. If you haven’t used it before, check out Jetpack Compose Tutorial for Android: Getting Started.

Getting Started

Download the starter project by clicking Download Materials at the top or bottom of the tutorial.

Import the starter project into IntelliJ IDEA and run MainKt.

The MainKt configuration to run.

Then, you’ll see a simple Compose introduction screen with one test Button.

The Compose app with a button on it

In this specific project, you already have a data source — a repository that fetches free meteorological data for a specific city from

First, you need to register an account for WeatherAPI and generate a key. Once you have the key, add it inside Main.kt as the value of API_KEY:

private const val API_KEY = "your_api_key_goes_here"

Next, open Repository.kt, and you’ll see the class is using Ktor to make a network request to the endpoint, transform the data and return the results — all in a convenient suspending function. The results are saved in a class, which you’ll need to populate the UI.

It’s time to finally dive into UI modeling.

Note: If you aren’t familiar with this approach or you want to dive more into Ktor, check out the link to Ktor: REST API for Mobile in the “Where to Go From Here?” section.

Getting User Input

As the first step, you need to get input from the user. You’ll need a TextField to receive the input, and a Button to submit it and perform the network call.

Create a new file in the SunnyDesk.main package and set its name to WeatherScreen.kt. In that file, add the following code:

fun WeatherScreen(repository: Repository) {

Here, you’re creating a Composable function and passing in repository. You’ll be querying WeatherAPI for the results, so it makes sense to have your data source handy.

Import the runtime package, which holds Composable, by adding this line above the class declaration:

import androidx.compose.runtime.*

The next step is setting up the text input. Add the following line inside WeatherScreen():

var queriedCity by remember { mutableStateOf("") }

With this code, you create a variable that holds the TextField‘s state. For this line to work, you need to have the import for the runtime package mentioned above.

Now, you can declare TextField itself with the state you just created:

    value = queriedCity,
    onValueChange = { queriedCity = it },
    modifier = Modifier.padding(end = 16.dp),
    placeholder = { Text("Any city, really...") },
    label = { Text(text = "Search for a city") },
    leadingIcon = { Icon(Icons.Filled.LocationOn, "Location") },

In the code above, you create an input field that holds its value in queriedCity. Also, you display a floating label, a placeholder and even an icon on the side!

Then, add all necessary imports at the top of the file:

import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.ui.*
import androidx.compose.ui.unit.dp

Now, you want to create a Button that sits next to TextField. To do that, you need to wrap the input field in a Row, which lets you have more Composables on the same horizontal line. Add this code to the class, and move the TextField declaration as follows:

  modifier = Modifier
    .padding(horizontal = 16.dp, vertical = 16.dp),
  verticalAlignment = Alignment.CenterVertically,
  horizontalArrangement = Arrangement.Center
) {
  // Button will go here

Right now, you have a Row that will take up the whole screen, and it’ll center its children both vertically and horizontally.

However, you still need the input text to expand and take all the space available. Add a weight value to the already declared modifier in TextField. The modifier value will look like this:

modifier = Modifier.padding(end = 16.dp).weight(1f)

This way, the input field will take all the available space on the line. How cool is that?!

Now, you need to create a Button with a meaningful search icon inside the Row, right below the TextField:

Button(onClick = { /* We'll deal with this later */}) {
    Icon(Icons.Outlined.Search, "Search")

As before, add the next import at the top of the file:

import androidx.compose.material.icons.outlined.Search

Finally, you added Button! You now need to show this screen inside main(). Open Main.kt and replace main() with the following code:

fun main() = Window(
  title = "Sunny Desk",
  size = IntSize(800, 700),
) {
  val repository = Repository(API_KEY)

  MaterialTheme {

You just gave a new title to your window and set a new size for it that will accommodate the UI you’ll build later.

Build and run to preview the change.
The input UI

Next, you’ll learn a bit of theory about the LCE model.

Loading, Content, Error

Loading, Content, Error, also known as LCE, is a paradigm that will help you achieve a unidirectional flow of data in a simple way. Every word in its name represents a state your UI can be in. You start with Loading, which is always the first state that your logic emits. Then, you run your operation and you either move to a Content state or an Error state, based on the result of the operation.

Feel like refreshing the data? Restart the cycle by going back to Loading and then either Content or Error again. The image below illustrates this flow.

How LCE works

To implement this in Kotlin, represent the available states with a sealed class. Create a new Lce.kt file in the SunnyDesk.main package and add the following code to it:

sealed class Lce<out T> {
  object Loading : Lce<Nothing>() // 1
  data class Content<T>(val data: T) : Lce<T>() // 2
  data class Error(val error: Throwable) : Lce<Nothing>() // 3

Here’s a breakdown of this code:

1. Loading: Marks the start of the loading cycle. This case is handled with an object, as it doesn’t need to hold any additional information.
2. Content: Contains a piece of data with a generic type T that you can display on the UI.
3. Error: Contains the exception that happened so that you can decide how to recover from it.

With this new paradigm, it’ll be super easy to implement a delightful UI for your users!