Compose跨平臺(tái)第三彈:體驗(yàn)Compose for iOS

前言

在之前,我們已經(jīng)體驗(yàn)了Compose for Desktop 與 Compose for Web,目前Compose for iOS 已經(jīng)有尚未開放的實(shí)驗(yàn)性API,樂觀估計(jì)今年年底將會(huì)發(fā)布Compose for iOS。同時(shí)Kotlin也表示將在2023年發(fā)布KMM的穩(wěn)定版本。

屆時(shí)Compose-jb + KMM 將實(shí)現(xiàn)Kotlin全平臺(tái)。

搭建項(xiàng)目

創(chuàng)建項(xiàng)目

因?yàn)槟壳癈ompose for iOS階段還在試驗(yàn)階段,所以我們無法使用Android Studio或者IDEA直接創(chuàng)建Compose支持iOS的項(xiàng)目,這里我們采用之前的方法,先使用Android Studio創(chuàng)建一個(gè)KMM項(xiàng)目,如果你不知道如何創(chuàng)建一個(gè)KMM項(xiàng)目,可以參照之前的這篇文章KMM的初次嘗試~ ,項(xiàng)目目錄結(jié)構(gòu)如下所示。

創(chuàng)建好KMM項(xiàng)目后我們需要添加Compose跨平臺(tái)的相關(guān)配置。

添加配置

首先在settings.gradle文件中聲明compose插件,代碼如下所示:

pluginManagement {
    repositories {
        google()
        gradlePluginPortal()
        mavenCentral()
        maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
    }

    plugins {
        val composeVersion = extra["compose.version"] as String
        id("org.jetbrains.compose").version(composeVersion)
    }
}

這里compose.version的版本號(hào)是聲明在gradle.properties中的,代碼如下所示:

compose.version=1.3.0

然后我們?cè)趕hared模塊中的build文件中引用插件

plugins {
    kotlin("multiplatform")
    kotlin("native.cocoapods")
    id("com.android.library")
    id("org.jetbrains.compose")
}

并為commonMain添加compose依賴,代碼如下所示:

val commonMain by getting {
    dependencies {
        implementation(compose.ui)
        implementation(compose.foundation)
        implementation(compose.material)
        implementation(compose.runtime)
    }
}

sync之后,你會(huì)發(fā)現(xiàn)一個(gè)錯(cuò)誤警告:uikit還處于試驗(yàn)階段并且有許多bug....

uikit就是compose-jb暴露的UIKit對(duì)象。為了能夠使用,我們需要在gradle.properties文件中添加如下配置:

org.jetbrains.compose.experimental.uikit.enabled=true

添加好配置之后,我們先來運(yùn)行下iOS項(xiàng)目,確保添加的配置是無誤的。

果然,不運(yùn)行不知道,一運(yùn)行嚇一跳

這個(gè)問題困擾了我兩三天,實(shí)在是無從下手,畢竟現(xiàn)在相關(guān)的資料很少,經(jīng)過N次的搜索,最終解決的方案很簡(jiǎn)單:Kotlin版本升級(jí)至1.8.0就可以了。

kotlin("android").version("1.8.0").apply(false)

再次運(yùn)行項(xiàng)目,結(jié)果如下圖所示。

不過這是KMM的iOS項(xiàng)目,接下來我們看如何使用Compose編寫iOS頁面。

開始iOS之旅

我們替換掉iOSApp.swift中的原有代碼,替換后的代碼如下所示:

import UIKit
import shared

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        let mainViewController = Main_iosKt.MainViewController()
        window?.rootViewController = mainViewController
        window?.makeKeyAndVisible()
        return true
    }
}

上面的代碼看不懂沒關(guān)系,我們只來看獲取mainViewController的這一行

let mainViewController = Main_iosKt.MainViewController()

Main_iosKt.MainViewController是通過新建在shared模塊iOSMain目錄下的main.ios.kt文件獲取的,代碼如下所示:

fun MainViewController(): UIViewController =
    Application("Login") {
        //調(diào)用一個(gè)Compose方法
    }

接下來所有的事情就都可以交給Compose了。

實(shí)現(xiàn)一個(gè)登錄頁面

因?yàn)轫撁孢@部分是公用的,所以我們?cè)趕hared模塊下的commonMain文件夾下新建Login.kt文件,編寫一個(gè)簡(jiǎn)單的登錄頁面,代碼如下所示:

@Composable
internal fun login() {
    var userName by remember {
        mutableStateOf("")
    }
    var password by remember {
        mutableStateOf("")
    }
    Surface(modifier = Modifier.padding(30.dp)) {
        Column {
            TextField(userName, onValueChange = {
                userName = it
            }, placeholder = { Text("請(qǐng)輸入用戶名") })
            TextField(password, onValueChange = {
                password = it
            }, placeholder = { Text("請(qǐng)輸入密碼") })
            Button(onClick = {
                //登錄
            }) {
                Text("登錄")
            }
        }
    }
}

上述代碼聲明了一個(gè)用戶名輸入框、密碼輸入框和一個(gè)登錄按鈕,就是簡(jiǎn)單的Compose代碼。

然后需要在main.ios.kt中調(diào)用這個(gè)login方法:

fun MainViewController(): UIViewController =
    Application("Login") {
        login()
    }

運(yùn)行iOS程序,效果如下圖所示:

嗯~,Compose 在iOS上UI幾乎可以做到100%復(fù)用,還有不學(xué)習(xí)Compose的理由嗎?

實(shí)現(xiàn)一個(gè)雙端網(wǎng)絡(luò)請(qǐng)求功能

在之前的第1彈和第2彈中,我們分別實(shí)現(xiàn)了在Desktop、和Web端的網(wǎng)絡(luò)請(qǐng)求功能,現(xiàn)在我們對(duì)之前的功能在iOS上再次實(shí)現(xiàn)。

添加網(wǎng)絡(luò)請(qǐng)求配置

首先在shared模塊下的build文件中添加網(wǎng)絡(luò)請(qǐng)求相關(guān)的配置,這里網(wǎng)絡(luò)請(qǐng)求我們使用Ktor,具體的可參照之前的文章:KMM的初次嘗試~

配置代碼如下所示:

val commonMain by getting {
    dependencies {
        ...
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
        implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
        implementation("io.ktor:ktor-client-core:$ktorVersion")
        implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
        implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
    }
}

val iosMain by getting {
    dependencies {
        implementation("io.ktor:ktor-client-darwin:$ktorVersion")
    }
}

val androidMain by getting {
    dependencies {
        implementation("io.ktor:ktor-client-android:$ktorVersion")
    }
}

添加接口

這里我們?nèi)匀皇褂谩竪android」中的每日一問接口 :wanandroid.com/wenda/list/…

DemoReqData與之前系列的實(shí)體類是一樣的,這里就不重復(fù)展示了。

創(chuàng)建接口地址類,代碼如下所示。

object Api {
    val dataApi = "https://wanandroid.com/wenda/list/1/json"
}

創(chuàng)建HttpUtil類,用于創(chuàng)建HttpClient對(duì)象和獲取數(shù)據(jù)的方法,代碼如下所示。

class HttpUtil {
    private val httpClient = HttpClient {
        install(ContentNegotiation) {
            json(Json {
                prettyPrint = true
                isLenient = true
                ignoreUnknownKeys = true
            })
        }
    }

    /**
     * 獲取數(shù)據(jù)
     */
    suspend fun getData(): DemoReqData {
        val rockets: DemoReqData =
            httpClient.get(Api.dataApi).body()
        return rockets
    }
}

這里的代碼我們應(yīng)該都是比較熟悉的,僅僅是換了一個(gè)網(wǎng)絡(luò)請(qǐng)求框架而已?,F(xiàn)在公共的業(yè)務(wù)邏輯已經(jīng)處理好了,只需要頁面端調(diào)用方法然后解析數(shù)據(jù)并展示即可。

編寫UI層

由于Android、iOS、Desktop三端的UI都是完全復(fù)用的,所以我們將之前實(shí)現(xiàn)的UI搬過來即可。代碼如下所示:

Column() {
    val scope = rememberCoroutineScope()
    var demoReqData by remember { mutableStateOf(DemoReqData()) }
    Button(onClick = {
        scope.launch {
            try {
                demoReqData = HttpUtil().getData()
            } catch (e: Exception) {
            }
        }
    }) {
        Text(text = "請(qǐng)求數(shù)據(jù)")
    }

    LazyColumn {
        repeat(demoReqData.data?.datas?.size ?: 0) {
            item {
                Message(demoReqData.data?.datas?.get(it))
            }
        }
    }
}

獲取數(shù)據(jù)后,通過

Message方法

將數(shù)據(jù)展示出來,這里只將作者與標(biāo)題內(nèi)容顯示出來,代碼如下所示。

@Composable
fun Message(data: DemoReqData.DataBean.DatasBean?) {
    Card(
        modifier = Modifier
            .background(Color.White)
            .padding(10.dp)
            .fillMaxWidth(), elevation = 10.dp
    ) {
        Column(modifier = Modifier.padding(10.dp)) {
            Text(
                text = "作者:${data?.author}"
            )
            Text(text = "${data?.title}")
        }
    }
}

分別運(yùn)行iOS、Android程序,點(diǎn)擊請(qǐng)求數(shù)據(jù)按鈕,結(jié)果如下圖:

這樣我們就用一套代碼,實(shí)現(xiàn)了在雙端的網(wǎng)絡(luò)請(qǐng)求功能。

一個(gè)尷尬的問題

我一直認(rèn)為存在一個(gè)比較尷尬的問題,那就是像上面實(shí)現(xiàn)一個(gè)完整的雙端網(wǎng)絡(luò)請(qǐng)求功能需要用到KMM + Compose-jb,但是KMM與Compose-jb并不是一個(gè)東西,但是用的時(shí)候呢基本上都是一起用。Compose-jb很久之前已經(jīng)發(fā)了穩(wěn)定版本只是Compose-iOS目前還沒有開放出來,而KMM當(dāng)前還處于試驗(yàn)階段,不過在2023年Kotlin的RoadMap中,Kotlin已經(jīng)表示將會(huì)在23年中發(fā)布第一個(gè)穩(wěn)定版本的KMM。而Compose for iOS何時(shí)發(fā)布,我想也是指日可待的事情。

所以,這個(gè)系列我覺得改名為:Kotlin跨平臺(tái)系列更適合一些,要不然以后就會(huì)存在KMM跨平臺(tái)第n彈,Compse跨平臺(tái)第n彈....

因此,從第四彈開始,此系列將更名為:Kotin跨平臺(tái)第N彈:~

寫在最后

從自身體驗(yàn)來講,我覺得KMM+Compose-jb 對(duì)Android開發(fā)者來說是非常友好的,不需要像Flutter那樣還需要額外學(xué)習(xí)Dart語言。所以,你覺得距離Kotlin一統(tǒng)“江山”的日子還會(huì)遠(yuǎn)嗎?

作者:黃林晴
鏈接:https://juejin.cn/post/7195770699524751421

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容