avatarAndrea

Summary

The provided content outlines the process of fetching and handling weather data in a multiplatform app using Kotlin Multiplatform Mobile (KMM), with a focus on setting up the data model, API calls, and platform-specific coroutine dispatchers.

Abstract

The article details the second part of building a multiplatform weather application using Kotlin Multiplatform Mobile (KMM). It builds upon the initial setup and UI development from Part 1 by introducing the implementation of real-time data fetching from the OpenWeatherMap API. The author explains the creation of Kotlin data classes to model the JSON response, the use of Ktor for making network requests, and the handling of asynchronous code using coroutines. The article emphasizes the importance of the ApplicationDispatcher for managing coroutine contexts across different platforms, providing specific implementations for Android and iOS. The ApplicationDispatcher is defined using expect in the common code and actual implementations are provided for Android and iOS, with special consideration for iOS's lack of native coroutine support. The article concludes by indicating that the getWeatherData() function is ready for use in updating the UI on each platform, with more details to be continued in Part 3.

Opinions

  • The author suggests that organizing the weather data into classes is beneficial for handling the JSON format provided by the API.
  • The use of Ktor and coroutines is presented as a modern and efficient approach to network communication and concurrency in Kotlin applications.
  • The article conveys that defining a platform-specific ApplicationDispatcher is crucial for coroutine operations in a KMM project, ensuring compatibility and proper execution across different platforms.
  • The author implies that the complexity of handling coroutines in iOS, due to its lack of native support, necessitates a workaround using the main queue for simplicity.
  • The author's approach to using expect and actual for the ApplicationDispatcher demonstrates a commitment to maintaining platform-independent common code while still accommodating platform-specific requirements.
  • By stating "To Be cont’d to Part 3," the author expresses an intent to provide a comprehensive guide to building a multiplatform weather app in a series of installments, indicating a structured and educational approach to sharing knowledge.

Building Multiplatform Weather App using KMM : Part 2 — Fetching Data

We already had project setup & UI for each platform from Part 1. It is now time to display the real data that we fetch from our API source. The weather data will be taken from https://api.openweathermap.org/. It is a free source but we still need to register to get API Key.

The data will be return in Json format. So in order to get more organize information, it will be nice if we have Class for this model data.

@Serializable
data class WeatherAPIResponse (
    @SerialName("coord"      ) var coord      : Coord?            = Coord(),
    @SerialName("weather"    ) var weather    : ArrayList<Weather> = arrayListOf(),
    @SerialName("base"       ) var base       : String?            = null,
    @SerialName("main"       ) var main       : Main              = Main(),
    @SerialName("visibility" ) var visibility : Int?               = null,
    @SerialName("wind"       ) var wind       : Wind?              = Wind(),
    @SerialName("rain"       ) var rain       : Rain?              = Rain(),
    @SerialName("clouds"     ) var clouds     : Clouds?            = Clouds(),
    @SerialName("dt"         ) var dt         : Int?               = null,
    @SerialName("sys"        ) var sys        : Sys?               = Sys(),
    @SerialName("timezone"   ) var timezone   : Int?               = null,
    @SerialName("id"         ) var id         : Int?               = null,
    @SerialName("name"       ) var name       : String?            = null,
    @SerialName("cod"        ) var cod        : Int?               = null
)

@Serializable
data class Coord (
    @SerialName("lon" ) var lon : Double? = null,
    @SerialName("lat" ) var lat : Double? = null
)

@Serializable
data class Weather (
    @SerialName("id"          ) var id          : Int?    = null,
    @SerialName("main"        ) var main        : String? = null,
    @SerialName("description" ) var description : String? = null,
    @SerialName("icon"        ) var icon        : String? = null
)

@Serializable
data class Main (
    @SerialName("temp"       ) var temp      : Double? = null,
    @SerialName("feels_like" ) var feelsLike : Double? = null,
    @SerialName("temp_min"   ) var tempMin   : Double? = null,
    @SerialName("temp_max"   ) var tempMax   : Double? = null,
    @SerialName("pressure"   ) var pressure  : Int?    = null,
    @SerialName("humidity"   ) var humidity  : Int?    = null,
    @SerialName("sea_level"  ) var seaLevel  : Int?    = null,
    @SerialName("grnd_level" ) var grndLevel : Int?    = null
)

@Serializable
data class Wind (
    @SerialName("speed" ) var speed : Double? = null,
    @SerialName("deg"   ) var deg   : Int?    = null,
    @SerialName("gust"  ) var gust  : Double? = null
)

@Serializable
data class Rain (
    @SerialName("1h" ) var h : Double? = null
)

@Serializable
data class Clouds (
    @SerialName("all" ) var all : Int? = null
)

@Serializable
data class Sys (
    @SerialName("type"    ) var type    : Int?    = null,
    @SerialName("id"      ) var id      : Int?    = null,
    @SerialName("country" ) var country : String? = null,
    @SerialName("sunrise" ) var sunrise : Int?    = null,
    @SerialName("sunset"  ) var sunset  : Int?    = null
)

The most important step will be to create function that will retrive data from its source inside commonMain

class WeatherAPI {
    private val apiUrl =
        "https://api.openweathermap.org/data/2.5/weather?lat=45.5019&lon=-73.5674&appid=${API_KEY}"

    @OptIn(DelicateCoroutinesApi::class)
    fun getWeatherAPIData(
        successFunction: (WeatherAPIResponse) -> Unit, failureFunction: (String) -> Unit) {
        GlobalScope.launch(ApplicationDispatcher) {
            try {
                val url = apiUrl
                val json = HttpClient().get(url) {}
                Json.decodeFromString(WeatherAPIResponse.serializer(), json.bodyAsText())
                    .also(successFunction)
            } catch (ex: Exception) {
                failureFunction("${ex.message}")
            }
        }
    }
}

Since Ktor needs to be called on a coroutine, this launches a lambda using a dispatcher, ApplicationDispatcher. This needs to be defined before you can build the app without errors.

Now, we need to write ApplicationDispatcher class inside CommonMain using expect. By using expect we are stating that the application for this class will be platform specific

internal expect val ApplicationDispatcher: CoroutineDispatcher

Hence, we need to write the actual class inside shared/androidMain

internal actual val ApplicationDispatcher: CoroutineDispatcher = Dispatchers.Default

And also inside shared/iosMain, defining the expected ApplicationDispatcher variable, but this time in iOS. It’s a bit more complicated than Android. Since iOS doesn’t support coroutines, any calls to dispatch to a coroutine will be dispatched to the main queue in iOS. This is for simplicity.

// 1
internal actual val ApplicationDispatcher: CoroutineDispatcher =
    NsQueueDispatcher(dispatch_get_main_queue())

// 2
internal class NsQueueDispatcher(
    private val dispatchQueue: dispatch_queue_t
) : CoroutineDispatcher() {

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        dispatch_async(dispatchQueue) {
            block.run()
        }
    }
}

The getWeatherData() function is ready. We will be using getWeatherData function inside each platform later and present it for each UI.

To Be cont’d to Part 3

Kmm
Kmp
Kotlin Multiplatform
Recommended from ReadMedium