iT邦幫忙

2022 iThome 鐵人賽

DAY 25
0

在開發 Desktop App 時,絕對不會只有刻 UI 這麼簡單,一定還有更多複雜的邏輯在背後運作,UI 只是觸發它們運作而已。而在現在 Web Service 這麼發達的時代,從 UI 發動 HTTP Request 是再常見不過的功能。好在 Compose for Desktop 是建構在 JVM 平台之上,所以要實作什麼功能,基本上都能借重來自 JVM 生態系完整的資源。

今天的耕讀筆記,就要來研究一下,怎麼在 Compose for Desktop 專案裡,以一個 Button 觸發 HTTP Request,並將取回的 JSON 格式顯示在 TextField 裡。

挑選(Multiplatform)Library

在 JVM 平台上要實作 HTTP Request 有很多種選擇,從 Java 原生的 net Package、到幾乎成業界標準的 OkHttp,甚至是 Android 上知名的 Retrofit 都可以使用。不過,Kotlin 本身是一個 Multiplatform 的語言,考量到未來 Compose for Desktop 可能會在不同平台上運行,因此筆者在挑 Library 時,大多都會選擇支援 Multiplatform 的 Library。

在 Kotlin 的生態系裡,所謂 Multiplatform Library,意指這個 Library 有針對多個平台實作,若是選擇使用這個 Library,若未來需要將程式碼移植到其他平台時,只需要切換不同的 driver 或 engine,甚至當初建立專案時就是採 Multiplatform 架構的話,就可以無痛轉換。

Kotlin 團隊為提供開發者最好的開發體驗,針對一些很常用需求都已經做出 Multiplatform Library。比方說可以操作 HTTP Request 及 Response 的 Ktor Client、大家超愛用的 Coroutine、做格式轉換一定會用的 Serialization、處理日期時間一定會用的 Datetime…等。讀者有興趣的話可以到各專案的 Github 了解詳細的細節。

因此面對今天練習的需求,筆者將使用 Ktor Client 來操作 HTTP Request 及 Response。

設定 Dependency

要使用 Ktor Client,首先需要在專案裡增加 Dependency,首先開啟 build.gradle.kts,找到 sourceSets 底下 jvmMain 這一段,在 dependencies 裡新增 Ktor Client 相關的 Library:

kotlin {
    sourceSets {
        val jvmMain by getting {
            dependencies {
                // ...
                implementation("io.ktor:ktor-client-core:2.1.2")
                implementation("io.ktor:ktor-client-cio:2.1.2")
                implementation("io.ktor:ktor-serialization-kotlinx-json:2.1.2")
                implementation("io.ktor:ktor-client-content-negotiation:2.1.2")
            }
        }
        // ...
    }
}

Ktor Client 將 Library 依不同功能切分成數個 Module,方便開發者安裝必要的 Module 即可,可有效節省 Library 的空間。在這個範例裡,我們需要用到的 Module 有:

  • ktor-client-core :Ktor Client 的核心,使用前一定要安裝的 Module。
  • ktor-client-cio :Ktor Client 提供多個 Engine,可依平台切換不同的 Engine。
  • ktor-serialization-kotlinx-json :讓 Ktor Client 使用 Kotlin Serialization Library 處理 JSON 格式的資料。
  • ktor-client-content-negotiation :讓 Ktor Client 可以處理 HTTP Header 等資訊。

由於有用到 Kotlin Serialization Library,我們還需要在 Gradle Build Script 裡安裝 Serialization 的 Gradle Plugin。這部份的設定分成兩塊,首先開啟 build.gradle.kts,在 plugins 裡增加 Plugin 設定:

plugins {
    // ...
    kotlin("plugin.serialization")
}

範例裡的 kotlin() 函式是 Kotlin 官方做出的 Alias,若是要寫 Plugin 全名的話,也可以寫 id("org.jetbrains.kotlin.plugin.serialization")

在安裝 Gradle Plugin 時,要設定安裝的版本,以 Kotlin 官方發佈的這些 Plugin,通常版本號會跟著 Kotlin 的版本號走。所以我們可以直接在 settings.gradle.kts 裡,共用 kotlin.version 的版本號:

pluginManagement {
    // ...

    plugins {
        // ...
        kotlin("plugin.serialization").version(extra["kotlin.version"] as String)
    }
}

設計 UI

接著我們做出一個簡單的 UI,這個 UI 只需要一個 TextField 用來顯示 JSON 回傳的結果,和一個 Button 用來觸發固定的 HTTP Request:

var consoleContent by remember { mutableStateOf("") }

TextField(
    value = consoleContent,
    onValueChange = { },
    readOnly = true,
)
Button(
    onClick = { }
) {
    Text(text = "Send!")
}

由於 TextField 只是用來顯示結果用的,所以加上了 readOnly = true 的屬性,另外現階段所有的 Event Handler 都暫時是空的,等完成 HTTP 的部份後再加上動作。

實作 Data Class

後端 API 的部份,由於超過 Compose for Desktop 的範例(其實是想偷懶),所以筆者直接去網路上找了 DummyJson 這種專門提供 Fake API 的 Service 來用。讀者可以先用瀏覽器或習慣的 HTTP Client 工具發送 HTTP GET Request 到 https://dummyjson.com/products/1 ,這個 API 會回傳一個 Product 資訊,包括 idtitleprice 等各種常見的商品欄位。

一般筆者在串 API 時,會先從了解收到的 JSON 格式開始,然後將 JSON 格式轉成 Kotlin 的 Data Class,方便在取得 JSON Response 後,可以用 Kotlin Serialization Library 轉回 Data Class。因此,筆者先建立一個名為 Product 的 Data Class 如下:

@Serializable
data class Product(
    val id: Int,
    val title: String,
    val description: String,
    val price: Int,
    val discountPercentage: Double,
    val rating: Double,
    val stock: Int,
    val brand: String,
    val category: String,
    val thumbnail: String,
    val images: List<String>,
)

與一般 Data Class 不同的是,為了讓 Serialization Plugin 在編譯時自動幫我們產生對應的 serializer 及 deserializer,要記得在 Class 名稱前加上 @Serializable Annotation。若沒有加這個 Annotation,在轉換 JSON 格式成 Data Class 時,會收到 Runtime Error。而若是忘了在 Gradle Build Script 裡加入 Plugin,則 IntelliJ IDEA 會提醒妳缺漏 Plugin。

實作 Client SDK

接著我們可以幫這個 API 寫一個 SDK,將 HTTP Request 及 Response 的複雜過程封裝起來,方便我們在 UI 上呼叫。筆者在這步建立一個名為 ProductSdk 的 Class,並實作如下:

class ProductSdk {
    private val httpClient = HttpClient(CIO) {
        install(ContentNegotiation) {
            json()
        }
        defaultRequest {
            url("https://dummyjson.com")
        }
        expectSuccess = true
    }

    suspend fun getProduct(): Product {
        return httpClient.get("/products/1") {
            accept(ContentType.Application.Json)
        }.body()
    }
}

在 SDK 裡,宣告一個內部屬性存在 Ktor Client。在宣告 HttpClient 時,要把 CIO engine 傳入,同時用 install() 函式安裝 ContentNegotiation Ktor Plugin,啟動 json() 處理機制。

接著宣告一個 getProduct() 方法,裡面使用 httpClient 觸發 HTTP GET Request,並透過指定方法的回傳型別為 Product,暗示 Ktor Client 在取回 JSON 字串後,要自動轉成 Product Data Class 回傳。

從 UI 觸發 SDK 取回 API 資料

最後一步就是要將 Button 的點擊事件跟 SDK 發動 HTTP Request 的動作綁定在一起,首先先將 SDK 實例及 Coroutine 的 MainScope 在 App 最前面先宣告出來:

val sdk = ProductSdk()
val mainScope = MainScope()

接著在 ButtononClick Event Handler 裡,launch MainScope,並在裡面用 sdk 呼叫 getProduct() 方法。由於方法會回傳 Product Data Class,但 TextField 要顯示的內容是 String,所以記得加上 toString() 後,再將內容回存到 consoleContent,由此觸發 UI 的 Recomposition。

Button(
    onClick = {
        mainScope.launch {
            consoleContent = sdk.getProduct().toString()
        }
    }
) {
    // ...
}

透過今天的練習,我們了解到如何將 Compose for Desktop 專案與 Kotlin Multiplatform Library 整合,也初步嘗試使用 Ktor Client 發送 HTTP Request 並將結果顯示在 UI 上。若讀者未來有其他開發上的需求,都可以依照這樣的流程與整合方式完成。

參考資料


上一篇
第 24 天:事件處理之鍵盤互動
下一篇
第 26 天:打包應用程式
系列文
傳教士的 Compose for Desktop 耕讀筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言