手记

使用Kotlin RPC、Koin、Ktor和WASM构建多平台公司商店的尝试

你是否在寻找创新的方法来简化你的跨平台开发流程?是否好奇如何利用最前沿的技术来构建高效的网络解决方案?随着最近 Kotlin RPC 0.3.0 支持 WebAssembly (WASM) 的发布,现在正是探索使用 Kotlin 多平台、Koin、Ktor 和 WASM 的好时机。

Kotlin Multiplatform让开发者可以在一个代码库内为多个平台开发,大大节省了开发时间并确保一致性。通过Kotlin RPC进行远程调用,借助Koin进行依赖注入,并使用Ktor进行服务器和客户端开发,以创建可扩展且高性能的应用。

那么是什么让WASM成为当今Web开发最有前景的选择呢?WebAssembly为网页应用带来了接近原生的执行效率,使得像Kotlin这样的语言编写的代码可以在浏览器中高效执行。尽管它仍处于alpha阶段,WASM改变Web解决方案的潜力是显而易见的。

免责声明:该项目目前可以编译,但(WASM)部分无法运行。随着我查找许多对话以找到答案,我希望能尽快移除这部分内容(仅适用于WASM目标)。

了解 Kotlin RPC 及其对 WASM 的支持
什么是 Kotlin RPC呢?

Kotlin RPC 是什么?(如需更多了解,可参考相关技术文档。)

远程过程调用(RPC)是一种协议,它允许程序像本地调用一样在远程系统上执行代码。Kotlin RPC 把这种能力带到了 Kotlin 生态系统中,让开发者可以在公共模块中定义共享接口和数据模型,客户端和服务器都可以利用这些接口和数据模型。

Kotlin RPC 抽象了网络层的细节,处理序列化、反序列化和通信协议。这让你可以专注于业务逻辑,而不是网络编程的繁琐细节,从而简化了客户端和服务器的交互。

Kotlin RPC 0.3.0 的特点:

Kotlin RPC 0.3.0 的发布带来了一些值得关注的新功能,其中对 WebAssembly (WASM) 的支持最为重要。

  • WASM 客户端支持功能:此功能支持将 Kotlin 代码编译为 WASM,使得代码可以在网页浏览器中接近原生地运行。
  • 增强的协程支持功能:全面支持 Kotlin 协程,以实现异步编程。
  • 改进的序列化:更好地处理复杂数据类型,并支持自定义序列化器。
  • 错误传播:增强的机制,用于从服务器向客户端传播异常。
  • 可扩展性:更容易根据特定项目需求进行扩展和定制化。
Kotlin RPC 和 WASM 的好处

为网页前端开发提供了一个结合Kotlin RPC与WASM的强大方案。

  • 统一代码基础:在客户端和服务器之间共享代码,减少重复和潜在错误,从而提高效率和可靠性。
  • 性能:WASM在浏览器中提供接近原生的执行速度,在计算密集型任务中明显优于JavaScript。
  • 类型安全:强类型接口确保整个应用程序的编译时安全。
  • 简化开发:抽象网络细节,简化开发过程,使开发人员能够编写更干净、更易于维护的代码。
  • 面向未来:随着WASM的发展成熟,使用Kotlin RPC构建的应用程序能够很好地利用WASM的新功能和进步。
架构概览

理解客户端和服务器组件之间的交互过程对于你架构解决方案时非常重要。下面的图说明了Kotlin RPC和WASM如何融入应用程序架构。

搭建开发环境

使用 Kotlin 多平台 (KMP) 向导工具和 Gradle 的 libs.toml 等工具,可以简化创建一个健壯和靈活的多平台項目的流程。在這一節中,我們將使用 KMP 向导工具設置項目的步驟並在 libs.toml 文件中配置依賴項。

先决条件

在开始之前,请确保已安装以下项目。

  • 需要 JDK 11 或更高版本来支持 Kotlin 和 Gradle 的开发。
  • 推荐使用 IntelliJ IDEA(社区版或专业版)作为开发 Kotlin 的 IDE。
  • Gradle 7.0 或更高版本 : 用于构建和管理项目。
  • Kotlin 多平台向导(KMP 向导):一个用来启动多平台项目的工具。
第一步:使用 KMP 向量创建项目

KMP向导通过生成必要的项目结构和配置文件来帮助简化多平台项目的初期设置。
https://kmp.jetbrains.com
我选择了所有的目标!

下一步:步骤 2:配置 libs.toml 来管理依赖项

Gradle的版本目录功能特性允许你使用libs.toml文件集中管理依赖。

    [versions]  
    agp = "8.2.2"  
    android-compileSdk = "34"  
    android-minSdk = "24"  
    android-targetSdk = "34"  
    androidx-activityCompose = "1.9.1"  
    androidx-lifecycle = "2.8.0"  
    bcrypt = "0.10.2"  
    compose-plugin = "1.6.11"  
    junit = "4.13.2"  
    kotlin = "2.0.0"  
    kotlinx-coroutines = "1.8.1"  
    ktor = "3.0.0-rc-2"  
    logback = "1.5.7"  
    koin = "4.0.0"  
    rpc = "0.3.0"  
    kotlinx-serialization = "1.7.1"  
    firebaseCrashlyticsBuildtools = "3.0.2"  

    [libraries]  
    bcrypt = { module = "at.favre.lib:bcrypt", version.ref = "bcrypt" }  
    kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }  
    androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }  
    androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }  
    androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }  

    kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }  
    kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }  
    logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }  

    ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" }  
    ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }  
    ktor-server-auth = { module = "io.ktor:ktor-server-auth-jvm", version.ref = "ktor" }  
    ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }  
    ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }  

    kotlinx-rpc-server = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-server", version.ref = "rpc" }  
    kotlinx-rpc-server-ktor = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-server", version.ref = "rpc" }  
    kotlinx-rpc-client = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-client", version.ref = "rpc" }  
    kotlinx-rpc-client-ktor = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-client", version.ref = "rpc" }  
    kotlinx-rpc-serialization = { module = "org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json", version.ref = "rpc" }  

    koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }  
    koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }  
    koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }  
    koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }  
    koin-logger-slf4j = { module = "io.insert-koin:koin-logger-slf4j", version.ref = "koin" }  

    [plugins]  
    androidApplication = { id = "com.android.application", version.ref = "agp" }  
    androidLibrary = { id = "com.android.library", version.ref = "agp" }  
    jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }  
    compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }  
    kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }  
    ktor = { id = "io.ktor.plugin", version.ref = "ktor" }  
    kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }  
    kotlin-rpc = { id = "org.jetbrains.kotlinx.rpc.plugin", version.ref = "rpc" }  
    kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

通过集中版本,你可以轻松地在所有模块中更新依赖项,而无需在多个 build.gradle.kts 文件中查找。

配置项目的构建配置

build.gradle.kts 文件是应用插件并设置所有模块共用的配置项。

    插件 {  
        // 避免在每个子项目的类加载器中多次加载同一插件  
        使用别名(libs.plugins.androidApplication) 禁用  
        使用别名(libs.plugins.androidLibrary) 禁用  
        使用别名(libs.plugins.jetbrainsCompose) 禁用  
        使用别名(libs.plugins.compose编译器) 禁用  
        使用别名(libs.plugins.kotlinJvm) 禁用  
        使用别名(libs.plugins.kotlinMultiplatform) 禁用  
        使用别名(libs.plugins.kotlin.rpc插件) 禁用  
        使用别名(libs.plugins.kotlinSerialization) 禁用  
    }
共享模块设置

共享模块包含了客户端和服务器共用的代码。

plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.androidLibrary)
    alias(libs.plugins.kotlin.rpc)
    alias(libs.plugins.kotlinSerialization)
}

kotlin {
    wasmJs {
        browser {
            val projectDirPath = project.projectDir.path
            commonWebpackConfig {
                devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
                    static = (static ?: mutableListOf()).apply {
                        // 提供源文件用于浏览器调试
                        add(projectDirPath)
                    }
                }
            }
        }
    }

    androidTarget {
        @OptIn(ExperimentalKotlinGradlePluginApi::class)
        compilerOptions {
            jvmTarget.set(JvmTarget.JVM_11)
        }
    }

    jvm()

    sourceSets {
        commonMain {
            dependencies {
                implementation(libs.koin.core)
                implementation(libs.koin.compose)
                implementation(libs.kotlinx.coroutines.core)
                implementation(libs.ktor.client.core)
                implementation(libs.kotlinx.rpc.client)
                implementation(libs.kotlinx.rpc.client.ktor)
                implementation(libs.kotlinx.rpc.serialization)
            }
        }
    }
}

android {
    namespace = "com.example.store.shared"
    compileSdk = libs.versions.android-compileSdk.get().toInt()
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    defaultConfig {
        minSdk = libs.versions.android-minSdk.get().toInt()
    }
}
服务器模块设置

服务器模块部分使用Ktor、Koin和Kotlin RPC来搭建后端。

    插件 {  
        别名设置(libs.plugins.kotlinJvm)  
        别名设置(libs.plugins.ktor)  
        别名设置(libs.plugins.kotlin.rpc)  
        应用  
        别名设置(libs.plugins.kotlinSerialization)  
    }  

    组 = "com.example.store"  
    版本 = "1.0.0"  

    应用 {  
        主类名.set("net.tactware.store.ApplicationKt")  
        默认JVM参数 = 列表("-Dio.ktor.development=${extra["io.ktor.development"] ?: "false"}")  
    }  

    依赖关系 {  
        实现(project(":shared"))  
        实现(libs.ktor_server_core)  
        实现(libs.ktor_server_netty)  
        实现(libs.ktor_server_auth)  
        实现(libs.kotlinx_rpc_server)  
        实现(libs.kotlinx_rpc_server_ktor)  
        实现(libs.kotlinx_rpc_serialization)  
        实现(libs.koin_core)  
        实现(libs.koin_ktor)  
        实现(libs.koin_logger_slf4j)  
        实现(libs.bcrypt)  

        测试依赖(libs.kotlin_test_junit)  
    }
应用程序模块设置

该应用模块是适用于多个平台(如Android、桌面端和WASM)的客户端应用程序,涵盖了Android、桌面端以及WASM。

plugins {  
    alias(libs.plugins.kotlinMultiplatform)  
    alias(libs.plugins.androidApplication)  
    alias(libs.plugins.jetbrainsCompose)  
    alias(libs.plugins.compose.compiler)  
    alias(libs.plugins.kotlinSerialization)  
}  

kotlin {  
    wasmJs {  
        moduleName = "composeApp"  
        browser {  
            val projectDirPath = project.projectDir.path  
            commonWebpackConfig {  
                outputFileName = "composeApp.js"  
                devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {  
                    static = (static ?: mutableListOf()).apply {  
                        // 在浏览器中调试时提供源代码资源  
                        add(projectDirPath)  
                    }  
                }  
            }  
        }  
        binaries.executable()  
    }  

    androidTarget {  
        @OptIn(ExperimentalKotlinGradlePluginApi::class)  
        compilerOptions {  
            jvmTarget.set(JvmTarget.JVM_11)  
        }  
    }  

    jvm("desktop")  

    sourceSets {  
        val desktopMain by getting  

        commonMain {  
            dependencies {  
                implementation(compose.runtime)  
                implementation(compose.foundation)  
                implementation(compose.material3)  
                implementation(compose.ui)  
                implementation(compose.components.resources)  
                implementation(compose.components.uiToolingPreview)  
                implementation(libs.androidx.lifecycle.viewmodel)  
                implementation(libs.androidx.lifecycle.runtime.compose)  
                implementation(project(":shared"))  
                implementation(libs.koin.core)  
                implementation(libs.kotlinx.coroutines.core)  
                implementation(libs.koin.compose)  
                implementation(libs.ktor.client.core)  
                implementation(libs.kotlinx.rpc.client)  
                implementation(libs.kotlinx.rpc.client.ktor)  
                implementation(libs.kotlinx.rpc.serialization)  
            }  
        }  

        androidMain {  
            dependencies {  
                implementation(compose.preview)  
                implementation(libs.androidx.activity.compose)  
                implementation(libs.koin.android)  
                implementation(libs.ktor.client.cio)  
            }  
        }  

        desktopMain {  
            dependencies {  
                implementation(compose.desktop.currentOs)  
                implementation(libs.kotlinx.coroutines.swing)  
                implementation(libs.ktor.client.cio)  
            }  
        }  
    }  
}  

android {  
    namespace = "com.example.store"  
    compileSdk = libs.versions.androidCompileSdk.get().toInt()  

    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")  
    sourceSets["main"].res.srcDirs("src/androidMain/res")  
    sourceSets["main"].resources.srcDirs("src/commonMain/resources")  

    defaultConfig {  
        applicationId = "com.example.store"  
        minSdk = libs.versions.androidMinSdk.get().toInt()  
        targetSdk = libs.versions.androidTargetSdk.get().toInt()  
        versionCode = 1  
        versionName = "1.0"  
    }  

    packaging {  
        resources {  
            excludes += "/META-INF/{AL2.0,LGPL2.1}"  
        }  
    }  

    buildTypes {  
        release {  
            isMinifyEnabled = false  
        }  
    }  

    compileOptions {  
        sourceCompatibility = JavaVersion.VERSION_11  
        targetCompatibility = JavaVersion.VERSION_11  
    }  

    buildFeatures {  
        compose = true  
    }  

    dependencies {  
        debugImplementation(compose.uiTooling)  
    }  
}  

compose.desktop {  
    application {  
        mainClass = "com.example.store.MainKt"  

        nativeDistributions {  
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)  
            packageName = "com.example.store"  
            packageVersion = "1.0.0"  
        }  
    }  
}
了解项目的整体布局

该项目分为多个模块。

  • 共享模块(`:shared`:此模块包含客户端和服务器共享的代码和资源。这里主要是扩展 RPC 接口的类。
  • 服务器模块(`:server`:包含后端应用的代码。
  • 应用模块(`:app`:包含客户端的应用代码。

通过将项目模块化,我们更好地实现了职责划分,使得维护更方便。

一个用Ktor来开发后端

在这部分,我们将使用 KtorKoinKotlin RPC 开发我们公司商店应用的后端。我们将实现一个认证服务,专注于让用户登录,展示这些技术如何一起工作,从而创建一个可扩展且易于维护的后端。

定义共享接口标准和数据结构模型的方式

我们首先定义共享部分中的共享接口和数据模型。这确保客户端和服务器使用相同的定义,从而在整个应用程序中保持一致性和类型安全。

    // shared/src/commonMain/kotlin/com/company/store/api/AuthService.kt  
    package com.company.store.api  

    import kotlinx.serialization.Serializable  
    import org.jetbrains.kotlinx.rpc.RPC  

    interface AuthService : RPC {  
        suspend fun login(username: String, password: String): LoginResult  
    }  

    @Serializable  
    sealed class LoginResult {  
        @Serializable  
        data class Success(val token: String) : LoginResult()  

        @Serializable  
        data class Failure(val message: String) : LoginResult()  
    }
实现后端业务逻辑

接下来,我们将在服务器端实现AuthService接口。我们将创建一个UserRepository来管理用户数据,并用它来处理认证相关的逻辑。并创建一个UserAuthService来处理认证。

/ server/src/main/kotlin/com/company/store/repository/UserRepository.kt  
package com.company.store.repository  

import at.favre.lib.crypto.bcrypt.BCrypt  

// 定义一个User数据类,包含用户名和密码哈希值。
data class User(val username: String, val passwordHash: String)  

// 定义一个UserRepository类来管理用户数据。
class UserRepository {  
    // 初始化用户列表,包含两个用户:admin和user。
    private val users = listOf(  
        User("admin", hashPassword("testPassword")),  
        User("user", hashPassword("userPassword"))  
    )  

    // 根据用户名查找用户。
    fun findByUsername(username: String): User? {  
        return users.find { it.username == username }  
    }  

    // 将密码进行哈希处理并返回哈希字符串。
    private fun hashPassword(password: String): String {  
        return BCrypt.withDefaults().hashToString(12, password.toCharArray())  
    }  
}
    // server/src/main/kotlin/com/company/store/service/UserAuthService.kt  
    package com.company.store.service  

    import com.company.store.api.AuthService  
    import com.company.store.api.LoginResult  
    import com.company.store.repository.UserRepository  
    import at.favre.lib.crypto.bcrypt.BCrypt  
    import kotlin.coroutines.CoroutineContext  

    class UserAuthService(  
        override val coroutineContext: CoroutineContext,  
        private val userRepository: UserRepository  
    ) : AuthService {  

        private fun 验证密码(password: String, passwordHash: String): Boolean {  
            val result = BCrypt.verifyer().verify(password.toCharArray(), passwordHash)  
            return result.verified  
        }  

        override suspend fun login(username: String, password: String): LoginResult {  
            val user = userRepository.findByUsername(username)  
            return if (user != null && 验证密码(password, user.passwordHash)) {  
                LoginResult.Success(token = "TestToken")  
            } else {  
                LoginResult.Failure(message = "用户名或密码无效")  
            }  
        }  
    }

此处为空

**用户存储**

  • **users**列表:模拟了一个硬编码用户的数据库。
  • **findByUsername**函数:通过用户名查找用户。
  • **hashPassword**函数:使用BCrypt对密码进行哈希处理以确保安全。

**用户认证服务类**

  • 实现了 AuthService 接口。
  • **verifyPassword** 方法:验证密码是否与存储的哈希值匹配。
  • **login** 方法:验证用户身份并返回登录结果 LoginResult

请注意:这并不是正确的认证服务设置方法。这只是为了演示一个API。

Ktor 应用的设置

我们现在将设置Ktor服务器应用程序,集成Koin进行依赖注入并配置RPC端点功能。

    // server/src/main/kotlin/com/company/store/Application.kt  
    package com.company.store  

    import com.company.store.api.AuthService  
    import com.company.store.service.UserAuthService  
    import com.company.store.repository.UserRepository  
    import io.ktor.server.engine.*  
    import io.ktor.server.netty.*  
    import io.ktor.application.*  
    import io.ktor.routing.*  
    import org.koin.core.context.startKoin  
    import org.koin.dsl.module  
    import org.koin.ktor.plugin.Koin  
    import org.koin.ktor.ext.getKoin  
    import org.koin.core.parameter.parametersOf  
    import org.jetbrains.kotlinx.rpc.ktor.server.*  
    import io.ktor.features.ContentNegotiation  
    import io.ktor.serialization.kotlinx.json.*  
    import kotlinx.serialization.json.Json  
    import kotlin.coroutines.CoroutineContext  

    fun main() {  
                                           // 你计算机的IP地址.  
        embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)  
            .start(wait = true)  
    }  

    fun Application.module() {  
        install(Koin) {  
            modules(appModule)  
        }  

        install(ContentNegotiation) {  
            json()  
        }  

        install(RPC)  

        val koin = getKoin()  
        routing {  
            rpc("/auth") {  
                rpcConfig {  
                    serialization {  
                        json(koin.get())  
                    }  
                }  
                registerService<AuthService> { ctx ->  
                    koin.get { parametersOf(ctx) }  
                }  
            }  
        }  
    }  

    val appModule = module {  
        single { UserRepository() }  
        factory<AuthService> { param ->  
            UserAuthService(param[0], get())  
        } bind AuthService // 这一步非常重要,需要绑定接口  
        single { Json { prettyPrint = true; isLenient = true } }  
    }

Koin 配置 :

  • **appModule**:定义应用模块。
  • **single { UserRepository() }**:提供一个 UserRepository 的单例实例。
  • **factory <AuthService>**:创建 UserAuthService 的实例,带有必要的依赖项。
  • **single { Json { ... } }**:配置 JSON 的序列化设置。

Ktor应用程序

  • **安装(Koin)**:将Koin集成到Ktor中。
  • **安装(ContentNegotiation)**:设置JSON序列化支持。
  • **安装(RPC)**:启用Ktor的RPC支持。

路由配置

  • **rpc("/auth")**:将 /auth 设置为 RPC 端点。
  • **registerService <AuthService>**:注册 AuthService 服务。
了解后端流程

以下是一个后端组件之间的交互的可视化表示。

重要提示
  • Ktor : 提供了一个强大且灵活的框架来构建 Kotlin 服务器端应用程序。
  • Koin : 简化了依赖注入,使代码库更模块化和易于测试。
  • Kotlin RPC : 简化了客户端与服务器之间的通信,抽象了底层网络的细节。

安全考量

  • 密码使用BCrypt进行哈希处理以实现安全存储。
  • 应妥善生成和管理安全令牌(示例中使用了占位符"TestToken")。
实现认证过程

LoginUserUseCase 类负责通过 Kotlin RPC 调用后台的 AuthService 进行登录认证的逻辑处理。

    // app/src/commonMain/kotlin/com/company/store/domain/LoginUserUseCase.kt  

    package com.company.store.domain  

    import com.company.store.api.AuthService  
    import com.company.store.api.LoginResult  
    import io.ktor.client.HttpClient  
    import org.jetbrains.kotlinx.rpc.connectTo  
    import kotlinx.serialization.json.Json  

    class LoginUserUseCase(private val json: Json) {  

        suspend operator fun invoke(username: String, password: String): LoginResult {  
            val rpcClient = HttpClient { installRPC() }.connectTo {  
                          // 请将此处替换为实际服务器地址  
                url("ws://localhost:8080/auth")  

                rpcConfig {  
                    serialization {  
                        json(json)  
                    }  
                }  
            }  

            return rpcClient.withService<AuthService>().login(username, password)  
        }  
    }

注意:"ws://localhost:8080/auth" 替换为实际的后端服务器 URL。

创建视图模型

LoginViewModel 负责管理登录屏幕的状态和互动。

    class 登录视图模型(private val loginUserUseCase: 登录用户用例, private val dispatcher: IDispatcherProvider) :  
        ViewModel() {  

        internal var username by mutableStateOf("")  
            private set  

        internal var password by mutableStateOf("")  
            private set  

        internal val snackbarHostState = SnackbarHostState()  

        fun onInteraction(interaction: LoginInteraction) {  
            when (interaction) {  
                LoginInteraction.AttemptToLogin -> {  
                    viewModelScope.launch(dispatcher.default()) {  
                        when (val result = loginUserUseCase.invoke(username, password)) {  
                            is LoginResult.Success -> {  
                                snackbarHostState.showSnackbar("登录成功了")  
                            }  

                            is LoginResult.Failure -> {  
                                snackbarHostState.showSnackbar(result.message)  
                            }  
                        }  
                    }  

                }  

                is LoginInteraction.InputsPassword -> {  
                    password = interaction.password  
                }  

                is LoginInteraction.InputsUsername -> {  
                    username = interaction.username  
                }  
            }  
        }  
    }

设计用户界面

@Composable
@Preview
fun App() {
    MaterialTheme {
        KoinContext {
            val koin = getKoin()

            定义 vm 为 remember { koin.get<LoginViewModel>() }

            Scaffold(结构 = {
                SnackbarHost(这个.snackbarHostState) {
                    显示 Snackbar(it)
                }
            }) { padding ->
                Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(padding).fillMaxSize()) {
                    Column(Modifier.fillMaxWidth(0.5f), horizontalAlignment = Alignment.CenterHorizontally) {
                        定义 groupModifier 为 Modifier.fillMaxWidth()
                        带边框的文本框(
                            modifier = groupModifier,
                            value = 它.username,
                            onValueChange = { 它.onInteraction(登录交互.EnteringUsername(it)) },
                            label = {
                                文本("用户名")
                            }
                        )

                        带边框的文本框(
                            modifier = groupModifier,
                            value = 它.password,
                            onValueChange = { 它.onInteraction(登录交互.EnteringPassword(it)) },
                            label = {
                                文本("密码")
                            }
                        )

                        按钮(
                            modifier = groupModifier,
                            点击 = { 它.onInteraction(登录交互.AttemptToLogin) }
                        ) {
                            文本("登录")
                        }

                    }
                }
            }
        }
    }
}

启动前端应用程序

你可以通过以下步骤构建并运行前端项目:

  1. 启动WASM模块:
    ./gradlew :app:启动浏览器开发模式

为应用服务

  • 上面的命令应该会启动 webpack 开发服务器。在浏览器中打开控制台输出的 URL(通常是 http://localhost:8080)。

试试登录功能是否正常

  • 输入在后端 UserRepository 中存在的用户名和密码。
  • 点击登录按钮。
  • 观察显示成功或失败消息的提示条。
好处与挑战
优点包括:
  • 统一语言:前后端都使用Kotlin开发,简化了开发流程。
  • 代码共享:共享接口和数据模型,这减少了重复和错误。
  • 性能优势:WASM在浏览器中的执行效率很高。
面临的挑战:
  • Alpha Software : WASM 支持目前仍处于 alpha 阶段;可能存在不稳定情况。这些库似乎都还无法正常运行,表明 WASM 支持仍不稳定。
  • 调试工具 : 在浏览器中调试 WASM 应用程序的工具有限。
  • 学习曲线 : 需要具备 Kotlin 多平台和新技术的知识。
演示版(桌面软件)

顺序如下:

  1. 启动服务器(使用我的电脑IP地址)
  2. 启动桌面程序(使用我的电脑IP地址)
  3. 未输入密码登录失败
  4. 密码错误登录失败
  5. 登录成功
结束语

目前,虽然WASM还没有完全部署,但我原本期望的结果和现在有些不同,不过文章中提到的原则仍然适用。

使用 Kotlin Multiplatform、Kotlin RPC、Koin、Ktor 和 WebAssembly (WASM) 构建公司商店的例子展示了现代开发工具在创建高效、可扩展和跨平台应用程序的巨大潜力。通过使用 Kotlin Multiplatform,我们展示了一个代码库如何支持多个平台,从而减少开发时间和保持不同环境间的一致性。

集成 Kotlin RPC 简化了客户端和服务器之间的通信,使我们能够定义共享接口并像调用本地方法一样无缝地调用远程过程。使用 Koin 进行依赖注入促进了代码的模块化和可测试性,使依赖管理更加容易,并且随着应用的增长更易于扩展。

Ktor 已经证明是构建后端的良好框架,提供了灵活性和丰富的功能,非常适合服务器端开发。在前端方面。将 Kotlin 编译为 WASM,解锁了高性能 web 应用程序的潜力,这些应用因此受益于 Kotlin 的强类型系统和现代语言特性。

0人推荐
随时随地看视频
慕课网APP