Compose for Desktop: Get Your Climate!

0
15


Compose for Desktop is a UI framework that simplifies creating 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 in a position to 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 planet.

On this tutorial, you’ll discover ways 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 high or backside of the tutorial.

Import the starter mission into IntelliJ IDEA and run MainKt.

The MainKt configuration to run.

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 a knowledge supply — a repository that fetches free meteorological information for a selected metropolis from WeatherAPI.com.

First, it’s worthwhile to register an account for WeatherAPI and generate a key. Upon getting the important thing, add it inside Primary.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, rework the info and return the outcomes — all in a handy suspending operate. The outcomes are saved in a category, which you’ll have to populate the UI.

It’s time to lastly dive into UI modeling.

Be aware: In the event you aren’t conversant in this method otherwise you wish 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 worthwhile to 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.essential package deal and set its identify 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 useful.

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


import androidx.compose.runtime.*

The following 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 worthwhile to have the import for the runtime package deal talked about above.

Now, you’ll be able to 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.Crammed.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 facet!

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


import androidx.compose.basis.structure.*
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 wish to create a Button that sits subsequent to TextField. To try this, it’s worthwhile to 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.Middle
) {
  TextField(...)
  // Button will go right here
}

Proper now, you could have a Row that may take up the entire display, and it’ll heart its youngsters each vertically and horizontally.

Nonetheless, you continue to want the enter textual content to increase and take all of the area accessible. 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 accessible area on the road. How cool is that?!

Now, it’s worthwhile to create a Button with a significant search icon contained in the Row, proper under the TextField:


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

As earlier than, add the subsequent import on the high of the file:


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

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


enjoyable essential() = Window(
  title = "Sunny Desk",
  dimension = 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 dimension for it that may 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 in regards to the LCE mannequin.

Loading, Content material, Error

Loading, Content material, Error, also referred to as LCE, is a paradigm that may assist you obtain a unidirectional circulation of information in a easy means. Each phrase in its identify represents a state your UI may 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 info? Restart the cycle by going again to Loading after which both Content material or Error once more. The picture under illustrates this circulation.

How LCE works

To implement this in Kotlin, signify the accessible states with a sealed class. Create a brand new Lce.kt file within the SunnyDesk.essential package deal 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: Incorporates a bit of information with a generic sort T you can show on the UI.
3. Error: Incorporates the exception that occurred so to resolve methods to get well from it.

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

Reworking the Community Knowledge

Earlier than you’ll be able to dive into the UI, it’s worthwhile to get some information. You’re already conversant in the Repository that fetches climate updates from the backend, however these fashions aren’t appropriate in your UI simply but. You’ll want to rework 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: Record<WeatherCard>,
)

WeatherCard represents a single forecast: You may have the anticipated climate situation with its icon for a visible illustration, the temperature and what the climate really feels prefer to individuals, and eventually, the prospect of rain.

WeatherResults comprises all the varied climate experiences in 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 rework the fashions you get from the community into these new fashions which can be simpler to show in your UI. Create a brand new Kotlin class and identify 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.change("64x64", "128x128"),
    temperature = response.present.tempC,
    feelsLike = response.present.feelslikeC,
  )
}

With these traces, you’re mapping the fields in numerous objects of the response to a easy object that may have the info 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 among these values seems to be one thing like this:


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

To repair this, you prepend the HTTPS protocol and enhance 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 worthwhile to extract the forecast information from the response, which is able to take a bit extra work. Beneath extractCurrentWeatherFrom(), add the next capabilities:


// 1
personal enjoyable extractForecastWeatherFrom(response: WeatherResponse): Record<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 worthwhile to do is loop via every of the nested forecast objects, so to map them every to a WeatherCard, just 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 info you want (both the felt temperature or the prospect of rain) and calculate the common throughout the entire day. This provides you an approximation you’ll be able to 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 rework(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 technique straight, 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 comprises a WeatherTransformer:


personal val transformer = WeatherTransformer()

Now, add this new operate under the property:


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

On this technique, 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 mistaken through the community name, you wrap the exception into Lce.Error.

In order for you an summary of how you may 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.

Displaying the Loading State

Now you understand all the pieces in regards to 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 under WeatherScreen(), add this operate:


@Composable
enjoyable LoadingUI() {
  Field(modifier = Modifier.fillMaxSize()) {
    CircularProgressIndicator(
      modifier = Modifier
        .align(alignment = Alignment.Middle)
        .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 wish to show this loading UI under the enter parts. In WeatherScreen(), wrap the prevailing Row right into a vertical Column and name LoadingUI() under it within the following means:


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 received 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 worthwhile to 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 high of WeatherScreen(), it’s worthwhile to declare a few values under 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 adjustments, Compose will recompose your UI so to react to this alteration.
  2. You want the scope to launch a coroutine from a Composable.

Now, it’s worthwhile to implement the button’s onClick() (the one marked with the /* We'll take care of this later */ remark), like so:


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

Each time you click on, weatherState adjustments to Loading, inflicting a recomposition. On the identical time, you’ll launch a request to get the up to date climate. When the consequence arrives, it will change weatherState once more, inflicting one other recomposition.

Then, add the required import:


import kotlinx.coroutines.launch

At this level, it’s worthwhile to deal with the recomposition, and it’s worthwhile to draw one thing completely different for every state. Go to the place you invoked LoadingUI on the finish of WeatherScreen(), and change 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 special UI primarily based on the state.

The next step 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’ll be able to implement your individual resolution! Create a brand new file and identify it ImageDownloader.kt. Inside, add this code:


import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import io.ktor.shopper.*
import io.ktor.shopper.engine.cio.*
import io.ktor.shopper.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 just’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 shopper for all the pictures.
  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 traces 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. In the event you didn’t use this, each time one thing else adjustments, your picture obtain would run once more, downloading unneeded information and inflicting glitches within the UI.

Then, add the required 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),
  type = MaterialTheme.typography.h6,
)

Subsequent, you’ll create a Card that may host the info in regards to the present climate. Add this under 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,
      type = MaterialTheme.typography.h6,
    )

    imageState?.let { bitmap ->
      Picture(
        bitmap = bitmap,
        contentDescription = null,
        modifier = Modifier
          .defaultMinSize(minWidth = 128.dp, minHeight = 128.dp)
          .padding(high = 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}",
      type = MaterialTheme.typography.caption,
    )
  }
}

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

Subsequent, add this code under Card:


Divider(
  shade = 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 wish 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 under the Divider:


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

Add the lacking imports as properly:


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 under 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 info 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,
      type = MaterialTheme.typography.h6
    )

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

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

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

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

Construct and run. In the event you seek for a sound metropolis identify, you’ll obtain a consequence like within the following picture.

The app displaying the Content state

Thus far, so good!

Displaying the Error State

The final element it’s worthwhile to implement is the UI for when all the pieces 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 actually need a retry possibility.

Add this import on the high of WeatherScreen.kt:


<code>androidx.compose.ui.textual content.type.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 mistaken, strive once more in a couple of minutes. ¯_(ツ)_/¯",
      modifier = Modifier
        .fillMaxSize()
        .padding(horizontal = 72.dp, vertical = 72.dp),
      textAlign = TextAlign.Middle,
      type = MaterialTheme.typography.h6,
      shade = MaterialTheme.colours.error,
    )
  }
}

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

Now, it’s worthwhile to 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 achieved! 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, if you enter “mistaken metropolis”, it’ll show your locale, however if you happen to use “wrongcity”, you’ll get the error message. So, when testing displaying the error, attempt to use some textual content that doesn’t make any sense. :].

Lastly, you’ll discover ways 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 possibly can run packageDmg to create a macOS installer, or run packageMsi to create an installer that runs on Home windows. You possibly can even create a .deb package deal with packageDeb.

This course of, although, has a little bit caveat hooked up. For the reason that packaging course of makes use of jpackage, it’s worthwhile to 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 information by tapping Obtain Supplies on the high or backside of the tutorial.

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

Make certain to additionally take a look at the Android Networking: Fundamentals video course for info on methods 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’ll be able to seize the Kotlin Coroutines by Tutorials ebook, or learn Kotlin Coroutines Tutorial for Android: Getting Began.

Hopefully, you loved this tutorial. You probably have any questions or feedback, please be a part of the discussion board dialogue under!

LEAVE A REPLY

Please enter your comment!
Please enter your name here