Compose for Desktop: Get Your Climate!

0
47


Compose for Desktop is a UI framework that simplifies growing consumer interfaces for desktop apps. Google launched it in 2021. This contemporary toolkit makes use of Kotlin for creating quick reactive UIs with no XMLs or templating language. Additionally, through the use of it, you’re capable of share UI code between desktop and Android apps.

JetBrain’s builders created Compose for Desktop primarily based on Jetpack Compose for Android, however there are some issues that differ. For instance, you received’t work with the Android lifecycle, Android ViewModels and even Android Studio.

For the mission, you’ll use Kotlin and IntelliJ IDEA to create an app that permits you to question climate information for a selected metropolis on the earth.

On this tutorial, you’ll learn to:

  • Mannequin your desktop app
  • Use the Loading, Content material, Error (LCE) mannequin
  • Publish your app

Getting Began

Obtain the starter mission by clicking Obtain Supplies on the prime or backside of the tutorial.

Import the starter mission into IntelliJ IDEA and run MainKt.

Then, you’ll see a easy Compose introduction display with one check Button.

The Compose app with a button on it

On this particular mission, you have already got an information supply — a repository that fetches free meteorological information for a selected metropolis from WeatherAPI.com.

First, it’s essential register an account for WeatherAPI and generate a key. Upon getting the important thing, add it inside Predominant.kt as the worth of API_KEY:


personal const val API_KEY = "your_api_key_goes_here"

Subsequent, open Repository.kt, and also you’ll see the category is utilizing Ktor to make a community request to the endpoint, remodel the information and return the outcomes — all in a handy suspending operate. The outcomes are saved in a category, which you’ll must populate the UI.

It’s time to lastly dive into UI modeling.

Be aware: When you aren’t conversant in this method otherwise you need to dive extra into Ktor, take a look at the hyperlink to Ktor: REST API for Cellular within the “The place to Go From Right here?” part.

Getting Person Enter

As step one, it’s essential get enter from the consumer. You’ll want a TextField to obtain the enter, and a Button to submit it and carry out the community name.

Create a brand new file within the SunnyDesk.predominant bundle and set its title to WeatherScreen.kt. In that file, add the next code:


@Composable
enjoyable WeatherScreen(repository: Repository) {
    
}

Right here, you’re making a Composable operate and passing in repository. You’ll be querying WeatherAPI for the outcomes, so it is smart to have your information supply helpful.

Import the runtime bundle, which holds Composable, by including this line above the category declaration:


import androidx.compose.runtime.*

The subsequent step is establishing the textual content enter. Add the next line inside WeatherScreen():


var queriedCity by bear in mind { mutableStateOf("") }

With this code, you create a variable that holds the TextField‘s state. For this line to work, it’s essential have the import for the runtime bundle talked about above.

Now, you may declare TextField itself with the state you simply created:


TextField(
    worth = queriedCity,
    onValueChange = { queriedCity = it },
    modifier = Modifier.padding(finish = 16.dp),
    placeholder = { Textual content("Any metropolis, actually...") },
    label = { Textual content(textual content = "Seek for a metropolis") },
    leadingIcon = { Icon(Icons.Stuffed.LocationOn, "Location") },
)

Within the code above, you create an enter area that holds its worth in queriedCity. Additionally, you show a floating label, a placeholder and even an icon on the aspect!

Then, add all needed imports on the prime of the file:


import androidx.compose.basis.format.*
import androidx.compose.materials.*
import androidx.compose.materials.icons.Icons
import androidx.compose.materials.icons.stuffed.LocationOn
import androidx.compose.ui.*
import androidx.compose.ui.unit.dp

Now, you need to create a Button that sits subsequent to TextField. To try this, it’s essential wrap the enter area in a Row, which helps you to have extra Composables on the identical horizontal line. Add this code to the category, and transfer the TextField declaration as follows:


Row(
  modifier = Modifier
    .fillMaxWidth()
    .padding(horizontal = 16.dp, vertical = 16.dp),
  verticalAlignment = Alignment.CenterVertically,
  horizontalArrangement = Association.Heart
) {
  TextField(...)
  // Button will go right here
}

Proper now, you’ve a Row that can take up the entire display, and it’ll heart its kids each vertically and horizontally.

Nonetheless, you continue to want the enter textual content to develop and take all of the area out there. Add a weight worth to the already declared modifier in TextField. The modifier worth will seem like this:


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

This fashion, the enter area will take all of the out there area on the road. How cool is that?!

Now, it’s essential create a Button with a significant search icon contained in the Row, proper beneath the TextField:


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

As earlier than, add the following import on the prime of the file:


import androidx.compose.materials.icons.outlined.Search

Lastly, you added Button! You now want to point out this display inside predominant(). Open Predominant.kt and substitute predominant() with the next code:


enjoyable predominant() = Window(
  title = "Sunny Desk",
  measurement = IntSize(800, 700),
) {
  val repository = Repository(API_KEY)

  MaterialTheme {
    WeatherScreen(repository)
  }
}

You simply gave a brand new title to your window and set a brand new measurement for it that can accommodate the UI you’ll construct later.

Construct and run to preview the change.
The input UI

Subsequent, you’ll study a little bit of principle concerning the LCE mannequin.

Loading, Content material, Error

Loading, Content material, Error, often known as LCE, is a paradigm that can enable you to obtain a unidirectional stream of information in a easy approach. Each phrase in its title represents a state your UI could be in. You begin with Loading, which is all the time the primary state that your logic emits. Then, you run your operation and also you both transfer to a Content material state or an Error state, primarily based on the results of the operation.

Really feel like refreshing the information? Restart the cycle by going again to Loading after which both Content material or Error once more. The picture beneath illustrates this stream.

How LCE works

To implement this in Kotlin, signify the out there states with a sealed class. Create a brand new Lce.kt file within the SunnyDesk.predominant bundle and add the next code to it:


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

Right here’s a breakdown of this code:

1. Loading: Marks the beginning of the loading cycle. This case is dealt with with an object, because it doesn’t want to carry any extra info.
2. Content material: Comprises a chunk of information with a generic sort T you could show on the UI.
3. Error: Comprises the exception that occurred so that you could determine tips on how to recuperate from it.

With this new paradigm, it’ll be tremendous straightforward to implement a pleasant UI on your customers!

Reworking the Community Knowledge

Earlier than you may dive into the UI, it’s essential get some information. You’re already conversant in the Repository that fetches climate updates from the backend, however these fashions aren’t appropriate on your UI simply but. It is advisable remodel them into one thing that extra intently matches what your UI will signify.

As a primary step, as you already did earlier than, create a WeatherUIModels.kt file and add the next code in it:


information class WeatherCard(
  val situation: String,
  val iconUrl: String,
  val temperature: Double,
  val feelsLike: Double,
  val chanceOfRain: Double? = null,
)

information class WeatherResults(
  val currentWeather: WeatherCard,
  val forecast: Checklist<WeatherCard>,
)

WeatherCard represents a single forecast: You’ve the anticipated climate situation with its icon for a visible illustration, the temperature and what the climate truly feels wish to individuals, and eventually, the prospect of rain.

WeatherResults incorporates all the varied climate stories on your UI: You’ll have a big card with the present climate, and a carousel of smaller playing cards that signify the forecast for the upcoming days.

Subsequent, you’ll remodel the fashions you get from the community into these new fashions which are simpler to show in your UI. Create a brand new Kotlin class and title it WeatherTransformer.

Then, write code to extract the present climate situation from the response. Add this operate inside WeatherTransformer:


personal enjoyable extractCurrentWeatherFrom(response: WeatherResponse): WeatherCard {
  return WeatherCard(
    situation = response.present.situation.textual content,
    iconUrl = "https:" + response.present.situation.icon.substitute("64x64", "128x128"),
    temperature = response.present.tempC,
    feelsLike = response.present.feelslikeC,
  )
}

With these strains, you’re mapping the fields in several objects of the response to a easy object that can have the information precisely how your UI expects it. As an alternative of studying nested values, you’ll have easy properties!

Sadly, the icon URL returned by the climate API isn’t an precise URL. One in every of these values appears to be like one thing like this:


//cdn.weatherapi.com/climate/64x64/day/116.png

To repair this, you prepend the HTTPS protocol and improve the dimensions of the icon, from 64×64 to 128×128. In any case, you’ll show the present climate on a bigger card!

Now, it’s essential extract the forecast information from the response, which can take a bit extra work. Under extractCurrentWeatherFrom(), add the next capabilities:


// 1
personal enjoyable extractForecastWeatherFrom(response: WeatherResponse): Checklist<WeatherCard> {
  return response.forecast.forecastday.map { forecastDay ->
    WeatherCard(
      situation = forecastDay.day.situation.textual content,
      iconUrl = "https:" + forecastDay.day.situation.icon,
      temperature = forecastDay.day.avgtempC,
      feelsLike = avgFeelsLike(forecastDay),
      chanceOfRain = avgChanceOfRain(forecastDay),
    )
  }
}

// 2
personal enjoyable avgFeelsLike(forecastDay: Forecastday): Double =
  forecastDay.hour.map(Hour::feelslikeC).common()
personal enjoyable avgChanceOfRain(forecastDay: Forecastday): Double =
  forecastDay.hour.map(Hour::chanceOfRain).common()

Right here’s a step-by-step breakdown of this code:

  1. The very first thing it’s essential do is loop by means of every of the nested forecast objects, so that you could map them every to a WeatherCard, much like what you probably did for the present climate mannequin. This time, the response represents each the sensation of the climate and the prospect of rain as arrays, containing the hourly forecasts for these values.
  2. For every hour, take the information you want (both the felt temperature or the prospect of rain) and calculate the typical throughout the entire day. This provides you an approximation you may present on the UI.

With these capabilities ready, now you can create a operate that returns the correct mannequin anticipated by your UI. On the finish of WeatherTransformer, add this operate:


enjoyable remodel(response: WeatherResponse): WeatherResults {
  val present = extractCurrentWeatherFrom(response)
  val forecast = extractForecastWeatherFrom(response)

  return WeatherResults(
    currentWeather = present,
    forecast = forecast,
  )
}

Your information transformation code is prepared! Time to place it into motion.

Updating the Repository

Open Repository.kt and alter the visibility of getWeatherForCity() to personal:


personal droop enjoyable getWeatherForCity(metropolis: String) : WeatherResponse = ...

As an alternative of calling this methodology instantly, you’re going to wrap it in a brand new one in order that it returns your new fashions.

Inside Repository, create a property that incorporates a WeatherTransformer:


personal val transformer = WeatherTransformer()

Now, add this new operate beneath the property:


droop enjoyable weatherForCity(metropolis: String): Lce<WeatherResults> {
  return strive {
    val end result = getWeatherForCity(metropolis)
    val content material = transformer.remodel(end result)
    Lce.Content material(content material)
  } catch (e: Exception) {
    e.printStackTrace()
    Lce.Error(e)
  }
}

On this methodology, you get the climate, and you utilize the transformer to transform it right into a WeatherResult and wrap it inside Lce.Content material. In case one thing goes terribly incorrect in the course of the community name, you wrap the exception into Lce.Error.

If you’d like an summary of how you would check a repository like this one, written with Ktor, have a look at RepositoryTest.kt within the last mission. It makes use of Ktor’s MockEngine to drive an offline check.

Exhibiting the Loading State

Now you understand the whole lot concerning the LCE sample, and also you’re prepared to use these ideas in a real-world software, aren’t you? Good!

Open WeatherScreen.kt, and beneath WeatherScreen(), add this operate:


@Composable
enjoyable LoadingUI() {
  Field(modifier = Modifier.fillMaxSize()) {
    CircularProgressIndicator(
      modifier = Modifier
        .align(alignment = Alignment.Heart)
        .defaultMinSize(minWidth = 96.dp, minHeight = 96.dp)
    )
  }
}

What occurs right here is the illustration of the loading UI — nothing extra, nothing much less.

Now, you need to show this loading UI beneath the enter elements. In WeatherScreen(), wrap the prevailing Row right into a vertical Column and name LoadingUI() beneath it within the following approach:


Column(horizontalAlignment = Alignment.CenterHorizontally) {
  Row(...) { ... } // Your current enter code
  LoadingUI()
}

Construct and run, and also you’ll see a spinner.

The app displaying the loading state

You’ve obtained the loading UI up and working, however you additionally want to point out the outcomes, which you’ll do subsequent.

Displaying the Outcomes

The very first thing it’s essential do is declare a UI for the content material as a operate inside WeatherScreen:


@Composable
enjoyable ContentUI(information: WeatherResults) {
}

You’ll deal with the actual UI later, however for the second, you want a placeholder. :]

Subsequent, on the prime of WeatherScreen(), it’s essential declare a few values beneath the prevailing queriedCity:


// 1
var weatherState by bear in mind { mutableStateOf<Lce<WeatherResults>?>(null) } 
// 2
val scope = rememberCoroutineScope() 

Within the code above:

  1. weatherState will maintain the present state to show. Each time the LCE modifications, Compose will recompose your UI so that you could react to this transformation.
  2. You want the scope to launch a coroutine from a Composable.

Now, it’s essential implement the button’s onClick() (the one marked with the /* We'll cope with this later */ remark), like so:


onClick = {
    weatherState = Lce.Loading
    scope.launch {
      weatherState = repository.weatherForCity(queriedCity)
    }
  }

Each time you click on, weatherState modifications to Loading, inflicting a recomposition. On the identical time, you’ll launch a request to get the up to date climate. When the end result arrives, this can change weatherState once more, inflicting one other recomposition.

Then, add the mandatory import:


import kotlinx.coroutines.launch

At this level, it’s essential deal with the recomposition, and it’s essential draw one thing completely different for every state. Go to the place you invoked LoadingUI on the finish of WeatherScreen(), and substitute that invocation with the next code:


when (val state = weatherState) {
 is Lce.Loading -> LoadingUI()
 is Lce.Error -> Unit
 is Lce.Content material -> ContentUI(state.information)
}

With this code, each time a recomposition happens, you’ll be capable to draw a distinct UI primarily based on the state.

The next move is downloading the picture for the climate situations. Sadly, there isn’t an API in Compose for Desktop for doing that simply but. Nonetheless, you may implement your individual resolution! Create a brand new file and title it ImageDownloader.kt. Inside, add this code:


import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import io.ktor.consumer.*
import io.ktor.consumer.engine.cio.*
import io.ktor.consumer.request.*
import org.jetbrains.skija.Picture

object ImageDownloader {
  personal val imageClient = HttpClient(CIO) // 1

  droop enjoyable downloadImage(url: String): ImageBitmap { // 2
    val picture = imageClient.get<ByteArray>(url)
    return Picture.makeFromEncoded(picture).asImageBitmap()
  }
}

Right here’s an summary of what this class does:

  1. The very first thing you may discover is that you simply’re creating a brand new HttpClient: It’s because you don’t want all of the JSON-related configuration from the repository, and you actually solely want one consumer for all the photographs.
  2. downloadImage() downloads a useful resource from a URL and saves it as an array of bytes. Then, it makes use of a few helper capabilities to transform the array right into a bitmap, which is able to use in your Compose UI.

Now, return to WeatherScreen.kt, discover ContentUI() and add this code to it:


var imageState by bear in mind { mutableStateOf<ImageBitmap?>(null) }

LaunchedEffect(information.currentWeather.iconUrl) {
  imageState = ImageDownloader.downloadImage(information.currentWeather.iconUrl)
}

These strains will save the picture you downloaded right into a state in order that it survives recompositions. LaunchedEffect() will run the obtain of the picture solely when the primary recomposition happens. When you didn’t use this, each time one thing else modifications, your picture obtain would run once more, downloading unneeded information and inflicting glitches within the UI.

Then, add the mandatory import:


import androidx.compose.ui.graphics.ImageBitmap

On the finish of ContentUI(), add a title for the present climate:


Textual content(
  textual content = "Present climate",
  modifier = Modifier.padding(all = 16.dp),
  model = MaterialTheme.typography.h6,
)

Subsequent, you’ll create a Card that can host the information concerning the present climate. Add this beneath the beforehand added Textual content:


Card(
  modifier = Modifier
    .fillMaxWidth()
    .padding(horizontal = 72.dp)
) {
  Column(
    modifier = Modifier.fillMaxWidth().padding(16.dp),
    horizontalAlignment = Alignment.CenterHorizontally,
  ) {
    Textual content(
      textual content = information.currentWeather.situation,
      model = MaterialTheme.typography.h6,
    )

    imageState?.let { bitmap ->
      Picture(
        bitmap = bitmap,
        contentDescription = null,
        modifier = Modifier
          .defaultMinSize(minWidth = 128.dp, minHeight = 128.dp)
          .padding(prime = 8.dp)
      )
    }

    Textual content(
      textual content = "Temperature in °C: ${information.currentWeather.temperature}",
      modifier = Modifier.padding(all = 8.dp),
    )
    Textual content(
      textual content = "Seems like: ${information.currentWeather.feelsLike}",
      model = MaterialTheme.typography.caption,
    )
  }
}

Right here, you utilize a few Textual content elements to point out the completely different values, and an Picture to point out the icon, if that’s already out there.
To make use of the code above, it’s essential import androidx.compose.basis.Picture.

Subsequent, add this code beneath Card:


Divider(
  coloration = MaterialTheme.colours.main,
  modifier = Modifier.padding(all = 16.dp),
)

This provides a easy divider between the present climate and the forecast you’ll implement within the subsequent step.

The final piece of content material you need to show is the forecast climate. Right here, you’ll use yet one more title and a LazyRow to show the carousel of things, as you don’t know what number of of them will come again from the community request, and also you need it to be scrollable.

Add this code beneath the Divider:


Textual content(
  textual content = "Forecast",
  modifier = Modifier.padding(all = 16.dp),
  model = MaterialTheme.typography.h6,
)
LazyRow {
  objects(information.forecast) { weatherCard ->
    ForecastUI(weatherCard)
  }
}

Add the lacking imports as nicely:


import androidx.compose.basis.lazy.LazyRow
import androidx.compose.basis.lazy.objects

At this level, you’ll discover the IDE complaining, however that’s anticipated, as you didn’t create ForecastUI() but. Go forward add this beneath ContentUI():


@Composable
enjoyable ForecastUI(weatherCard: WeatherCard) {
}

Right here, you declare the lacking operate. Inside, you should utilize the identical picture loading sample you used for the present climate’s icon:


var imageState by bear in mind { mutableStateOf<ImageBitmap?>(null) }

LaunchedEffect(weatherCard.iconUrl) {
  imageState = ImageDownloader.downloadImage(weatherCard.iconUrl)
}

As soon as once more, you’re downloading a picture, and it’s now time to point out the UI for the remainder of the information inside your fashions. On the backside of ForecaseUI(), add the next:


Card(modifier = Modifier.padding(all = 4.dp)) {
  Column(
    modifier = Modifier.padding(8.dp),
    horizontalAlignment = Alignment.CenterHorizontally,
  ) {
    Textual content(
      textual content = weatherCard.situation,
      model = MaterialTheme.typography.h6
    )

    imageState?.let { bitmap ->
      Picture(
        bitmap = bitmap,
        contentDescription = null,
        modifier = Modifier
          .defaultMinSize(minWidth = 64.dp, minHeight = 64.dp)
          .padding(prime = 8.dp)
      )
    }

    val chanceOfRainText = String.format(
      "Likelihood of rain: %.2f%%", weatherCard.chanceOfRain
    )

    Textual content(
      textual content = chanceOfRainText,
      model = MaterialTheme.typography.caption,
    )
  }
}

That is once more much like displaying the present climate, however this time, you’ll additionally show the prospect of rain.

Construct and run. When you seek for a legitimate metropolis title, you’ll obtain a end result like within the following picture.

The app displaying the Content state

Thus far, so good!

Exhibiting the Error State

The final part it’s essential implement is the UI for when the whole lot goes south. You’ll show an error message on this case. The app performs the search when a consumer presses the search button, so that you don’t really want a retry choice.

Add this import on the prime of WeatherScreen.kt:


<code>androidx.compose.ui.textual content.model.TextAlign</code> 

Now, add operate on the finish of WeatherScreen.kt:


@Composable
enjoyable ErrorUI() {
  Field(modifier = Modifier.fillMaxSize()) {
    Textual content(
      textual content = "One thing went incorrect, strive once more in a couple of minutes. ¯_(ツ)_/¯",
      modifier = Modifier
        .fillMaxSize()
        .padding(horizontal = 72.dp, vertical = 72.dp),
      textAlign = TextAlign.Heart,
      model = MaterialTheme.typography.h6,
      coloration = MaterialTheme.colours.error,
    )
  }
}

This code is including a Textual content that shows an error message when an error happens.

Now, it’s essential hyperlink this operate to the selection in WeatherScreen. Scroll as much as WeatherScreen() and discover the when assertion that handles the completely different states. Replace Error to point out your newly added UI:


is Lce.Error -> ErrorUI()

You’re accomplished! Construct and run. Then, seek for a non-existent metropolis. You’ll see your error message popping up.

The app displaying the error state

Be aware: The Climate API returns your native climate if the textual content you entered is legitimate. For instance, once you enter “incorrect metropolis”, it’ll show your locale, however in case you use “wrongcity”, you’ll get the error message. So, when testing exhibiting the error, attempt to use some textual content that doesn’t make any sense. :].

Lastly, you’ll learn to publish your app.

Publishing Your App

Creating an app that leverages Compose for Desktop means you additionally get out-of-the-box Gradle duties to create packages of the app, primarily based on the working system. You may run packageDmg to create a macOS installer, or run packageMsi to create an installer that runs on Home windows. You may even create a .deb bundle with packageDeb.

This course of, although, has a bit caveat connected. Because the packaging course of makes use of jpackage, it’s essential be working a minimal JDK model of 15. In any other case, the duties will fail.

The place to Go From Right here?

Obtain the finished mission recordsdata by tapping Obtain Supplies on the prime or backside of the tutorial.

Now you understand how to get began on Compose for Desktop, and you bought a glimpse of among the core components of constructing an app, like making community calls. On this tutorial, you used Ktor, which you’ll be able to learn to use on Android within the Ktor: REST API for Cellular tutorial.

Be certain that to additionally take a look at the Android Networking: Fundamentals video course for info on tips on how to get began with Android networking, or observe together with Android Networking With Kotlin Tutorial: Getting Began, which targets Kotlin particularly.

To study extra about coroutines, you may seize the Kotlin Coroutines by Tutorials e-book, or learn Kotlin Coroutines Tutorial for Android: Getting Began.

Hopefully, you loved this tutorial. When you have any questions or feedback, please be part of the discussion board dialogue beneath!