MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin中的跨平台应用架构设计

2024-03-201.3k 阅读

Kotlin 跨平台基础概念

Kotlin 跨平台(Kotlin Multi - Platform,简称 KMP)是 JetBrains 推出的一项强大功能,允许开发者使用 Kotlin 语言编写可在多个平台上运行的代码。这意味着可以在 iOS、Android、Web、桌面(Windows、macOS、Linux)等不同平台上共享业务逻辑代码,大大提高了代码的复用率,减少了开发和维护成本。

在传统的移动开发中,Android 开发通常使用 Java 或 Kotlin,而 iOS 开发使用 Swift 或 Objective - C。这导致业务逻辑需要在两个不同的代码库中重复实现,不仅增加了开发工作量,还使得代码同步和维护变得困难。Kotlin 跨平台旨在解决这一问题,通过一套代码库为多个平台提供支持。

Kotlin 跨平台项目结构

一个典型的 Kotlin 跨平台项目结构通常包含以下几个部分:

  1. commonMain:这个源集包含了可以在所有目标平台上共享的代码。这里定义的类、函数和逻辑是与平台无关的,是项目核心业务逻辑的存放地。例如,数据模型、网络请求逻辑、数据处理算法等都可以放在这里。
package com.example.shared

data class User(val id: Int, val name: String)

fun getUserList(): List<User> {
    // 模拟从网络获取用户列表
    return listOf(User(1, "Alice"), User(2, "Bob"))
}
  1. platform - specific sourcesets:针对每个特定平台,都有对应的源集,如 androidMainiosMain 等。这些源集主要用于实现与平台相关的功能,例如在 androidMain 中可以处理 Android 特定的 UI 逻辑、权限管理等,而在 iosMain 中可以处理 iOS 特定的界面布局、系统交互等。

Kotlin 跨平台架构模式

  1. MVVM(Model - View - ViewModel)
    • Model:在 Kotlin 跨平台项目中,模型部分可以在 commonMain 中定义。它代表了应用程序的数据和业务逻辑。例如,上面提到的 User 数据类就是模型的一部分。
    • ViewModel:也可以在 commonMain 中实现大部分与业务逻辑相关的 ViewModel 代码。ViewModel 负责处理业务逻辑并为 View 提供数据。例如,一个用于获取用户列表并提供给 UI 的 ViewModel 可以这样写:
package com.example.shared

import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

class UserViewModel : ViewModel() {
    private val _userListFlow = MutableStateFlow<List<User>>(emptyList())
    val userListFlow: StateFlow<List<User>> = _userListFlow

    init {
        loadUserList()
    }

    private fun loadUserList() {
        val users = getUserList()
        _userListFlow.value = users
    }
}
  • View:在 Android 平台,View 部分可以在 androidMain 中通过 XML 布局文件和 Kotlin 代码实现。在 iOS 平台,则可以使用 Interface Builder 或纯代码方式实现。不同平台的 View 会绑定到相同的 ViewModel 实例,以获取数据并展示。
  1. Clean Architecture
    • Entities:在 Kotlin 跨平台中,实体类(Entities)可以放在 commonMain 中。这些实体类代表了应用程序的核心业务概念,不依赖于任何特定的框架或平台。例如,一个电商应用中的 Product 实体类:
package com.example.shared

data class Product(val id: Int, val name: String, val price: Double)
  • Use Cases:用例(Use Cases)也可以在 commonMain 中定义。它们封装了应用程序的特定业务规则。比如,一个用于计算购物车总价的用例:
package com.example.shared

class CalculateCartTotalUseCase {
    fun execute(cartItems: List<Product>): Double {
        return cartItems.sumOf { it.price }
    }
}
  • Repositories:仓库(Repositories)定义了数据来源的抽象。可以在 commonMain 中定义接口,然后在不同平台的源集中实现具体的仓库。例如,一个获取产品列表的仓库接口:
package com.example.shared

interface ProductRepository {
    fun getProductList(): List<Product>
}
  • Presenters / ViewModels:和 MVVM 中的 ViewModel 类似,在 Clean Architecture 中,Presenter 或 ViewModel 负责协调 Use Cases 和 View 之间的交互。同样可以在 commonMain 中实现大部分逻辑。
  • Frameworks and Drivers:这部分涉及到具体平台相关的实现,如 Android 或 iOS 的 UI 框架、网络框架等。在 androidMainiosMain 源集中实现与这些框架的集成。

跨平台数据持久化

  1. SQLDelight
    • SQLDelight 是一个用于 Kotlin 跨平台的数据持久化库。它允许在 commonMain 中定义 SQL 模式和查询,然后在不同平台上生成对应的数据库操作代码。
    • 首先,在 build.gradle.kts 文件中添加 SQLDelight 依赖:
plugins {
    kotlin("multiplatform")
    id("com.squareup.sqldelight") version "1.5.5"
}

kotlin {
    android()
    iosX64()
    iosArm64()
    iosSimulatorArm64()

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("com.squareup.sqldelight:runtime:1.5.5")
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("com.squareup.sqldelight:android-driver:1.5.5")
            }
        }
        val iosMain by getting {
            dependencies {
                implementation("com.squareup.sqldelight:native-driver:1.5.5")
            }
        }
    }
}

sqldelight {
    database("AppDatabase") {
        packageName = "com.example.shared.db"
    }
}
  • 然后,在 commonMain 中定义数据库模式和查询:
package com.example.shared.db

import com.squareup.sqldelight.db.SqlDriver
import com.squareup.sqldelight.runtime.coroutines.asFlow
import com.squareup.sqldelight.runtime.coroutines.mapToList
import kotlinx.coroutines.flow.Flow

interface UserQueries {
    fun getAllUsers(): Flow<List<User>>
}

class AppDatabase(driver: SqlDriver) : com.example.shared.db.AppDatabase.Schema(driver) {
    val userQueries: UserQueries = object : UserQueries {
        override fun getAllUsers(): Flow<List<User>> {
            return selectFromUser().asFlow().mapToList()
        }
    }
}
  • 在不同平台的源集中创建数据库驱动实例:
  • Android
package com.example.android

import android.content.Context
import com.example.shared.db.AppDatabase
import com.squareup.sqldelight.android.AndroidSqliteDriver

fun createDatabase(context: Context): AppDatabase {
    val driver = AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
    return AppDatabase(driver)
}
  • iOS
package com.example.ios

import com.example.shared.db.AppDatabase
import com.squareup.sqldelight.native.NativeSqliteDriver

actual fun createDatabase(): AppDatabase {
    val driver = NativeSqliteDriver(AppDatabase.Schema, "app.db")
    return AppDatabase(driver)
}
  1. Kotlin Multi - Platform Preferences
    • 对于简单的键值对存储需求,可以使用 Kotlin Multi - Platform Preferences。它提供了一种在不同平台上统一访问偏好设置的方式。
    • 首先,在 build.gradle.kts 中添加依赖:
kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("com.russhwolf:multiplatform-settings:0.7.0")
            }
        }
    }
}
  • 然后,在 commonMain 中使用偏好设置:
package com.example.shared

import com.russhwolf.settings.Settings
import com.russhwolf.settings.coroutines.FlowSettings
import kotlinx.coroutines.flow.first

class UserPreferences(private val settings: Settings) {
    private val flowSettings = FlowSettings(settings)
    private val userIdKey = "user_id"

    suspend fun getUserId(): Int? {
        return flowSettings.getOrNull(userIdKey, Int::class).first()
    }

    suspend fun setUserId(userId: Int) {
        flowSettings.put(userIdKey, userId)
    }
}
  • 在不同平台上创建 Settings 实例:
  • Android
package com.example.android

import android.content.Context
import com.example.shared.UserPreferences
import com.russhwolf.settings.AndroidSettings

fun createUserPreferences(context: Context): UserPreferences {
    val settings = AndroidSettings(context)
    return UserPreferences(settings)
}
  • iOS
package com.example.ios

import com.example.shared.UserPreferences
import com.russhwolf.settings.ExperimentalSettingsApi
import com.russhwolf.settings.IosSettings

@OptIn(ExperimentalSettingsApi::class)
actual fun createUserPreferences(): UserPreferences {
    val settings = IosSettings()
    return UserPreferences(settings)
}

跨平台网络请求

  1. Ktor
    • Ktor 是一个用于 Kotlin 开发的多功能异步 HTTP 客户端/服务器框架,非常适合 Kotlin 跨平台项目中的网络请求。
    • 首先,在 build.gradle.kts 中添加 Ktor 依赖:
kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-core:1.6.8")
                implementation("io.ktor:ktor-client-content-negotiation:1.6.8")
                implementation("io.ktor:ktor-serialization-kotlinx-json:1.6.8")
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-android:1.6.8")
            }
        }
        val iosMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-ios:1.6.8")
            }
        }
    }
}
  • commonMain 中定义网络请求逻辑:
package com.example.shared

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json

class UserApi(private val client: HttpClient) {
    suspend fun getUserList(): List<User> {
        client.config {
            install(ContentNegotiation) {
                json(Json {
                    ignoreUnknownKeys = true
                })
            }
        }
        return client.get("https://example.com/api/users").body()
    }
}
  • 在不同平台上创建 HttpClient 实例:
  • Android
package com.example.android

import android.content.Context
import com.example.shared.UserApi
import io.ktor.client.*
import io.ktor.client.engine.android.*

fun createUserApi(context: Context): UserApi {
    val client = HttpClient(Android)
    return UserApi(client)
}
  • iOS
package com.example.ios

import com.example.shared.UserApi
import io.ktor.client.*
import io.ktor.client.engine.ios.*

actual fun createUserApi(): UserApi {
    val client = HttpClient(Ios)
    return UserApi(client)
}
  1. Retrofit (Android - only with Kotlin support)
    • 虽然 Retrofit 主要用于 Android 开发,但在 Kotlin 跨平台项目中,如果只考虑 Android 平台的网络请求,也可以使用它。
    • 首先,在 androidMainbuild.gradle.kts 中添加 Retrofit 依赖:
dependencies {
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
}
  • androidMain 中定义 Retrofit 接口和服务:
package com.example.android

import com.example.shared.User
import retrofit2.http.GET

interface UserService {
    @GET("api/users")
    suspend fun getUserList(): List<User>
}

object RetrofitClient {
    private const val BASE_URL = "https://example.com/"
    private val retrofit by lazy {
        Retrofit.Builder()
           .baseUrl(BASE_URL)
           .addConverterFactory(GsonConverterFactory.create())
           .build()
    }

    val userService: UserService by lazy {
        retrofit.create(UserService::class.java)
    }
}

跨平台 UI 开发

  1. Jetpack Compose Multi - Platform (Experimental)
    • Jetpack Compose 是 Android 上的现代声明式 UI 工具包,Jetpack Compose Multi - Platform 允许将 Compose 扩展到其他平台,如 iOS、桌面等。
    • 首先,在 build.gradle.kts 中添加相关依赖:
kotlin {
    android()
    iosX64()
    iosArm64()
    iosSimulatorArm64()

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("androidx.compose.ui:ui:1.2.0 - rc01")
                implementation("androidx.compose.foundation:foundation:1.2.0 - rc01")
                implementation("androidx.compose.material:material:1.2.0 - rc01")
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("androidx.compose.ui:ui - android:1.2.0 - rc01")
            }
        }
        val iosMain by getting {
            dependencies {
                implementation("io.ktor:ktor-client-ios:1.6.8")
                implementation("org.jetbrains.compose.ios:ios - foundation:1.2.0 - rc01")
                implementation("org.jetbrains.compose.ios:ios - ui:1.2.0 - rc01")
            }
        }
    }
}
  • commonMain 中定义可复用的 Compose UI 组件:
package com.example.shared

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

@Composable
fun SharedScreen() {
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "This is a shared Compose screen")
        Button(onClick = { /* Do something */ }) {
            Text(text = "Click me")
        }
    }
}
  • 在不同平台上显示共享的 Compose 组件:
  • Android
package com.example.android

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.example.shared.SharedScreen
import androidx.compose.ui.platform.LocalContext

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SharedScreen()
        }
    }
}
  • iOS
package com.example.ios

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.ComposeUIViewController
import com.example.shared.SharedScreen
import platform.UIKit.UIViewController

actual class IosMainViewController : UIViewController() {
    init {
        val viewController = ComposeUIViewController {
            SharedScreen()
        }
        addChild(viewController)
        view.addSubview(viewController.view)
        viewController.view.frame = view.bounds
        viewController.didMove(toParent = this)
    }
}
  1. SwiftUI - Kotlin (Experimental)
    • 对于 iOS 平台,有一些实验性的项目尝试将 Kotlin 与 SwiftUI 结合,实现跨平台 UI 开发。虽然目前还处于早期阶段,但展示了一种有趣的跨平台 UI 实现思路。例如,通过编写 Kotlin 代码生成 SwiftUI 视图定义,然后在 iOS 应用中使用这些视图。这通常涉及到自定义的代码生成工具和 Kotlin 与 Swift 之间的互操作机制。

解决跨平台兼容性问题

  1. 平台特定代码隔离
    • 确保将平台特定的代码放在各自对应的源集中,如 androidMainiosMain。这样可以避免平台相关的代码污染共享代码库,使得共享代码更加纯净和易于维护。例如,Android 中的权限请求代码和 iOS 中的通知设置代码应该分别放在各自的平台源集中。
  2. 使用 Expect - Actual 机制
    • Kotlin 跨平台提供了 expectactual 关键字来处理平台特定的实现。在 commonMain 中使用 expect 声明一个函数或类,然后在不同平台的源集中使用 actual 提供具体的实现。例如,获取设备唯一标识符的功能:
    • commonMain 中:
package com.example.shared

expect fun getDeviceId(): String
  • androidMain 中:
package com.example.android

import android.content.Context
import android.provider.Settings
import com.example.shared.getDeviceId

actual fun getDeviceId(): String {
    val context = LocalContext.current
    return Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
}
  • iosMain 中:
package com.example.ios

import platform.UIKit.UIDevice
import com.example.shared.getDeviceId

actual fun getDeviceId(): String {
    return UIDevice.currentDevice.identifierForVendor?.UUIDString.orEmpty()
}
  1. 处理依赖差异
    • 不同平台可能需要不同版本或不同类型的依赖库。例如,Android 上使用 OkHttp 进行网络请求,而 iOS 上可能使用 URLSession。通过在不同平台的源集中添加特定的依赖,并在共享代码中使用抽象层来处理这些差异。例如,在共享代码中定义一个网络请求的抽象接口,然后在 Android 和 iOS 平台分别实现这个接口,使用各自适合的网络库。

性能优化

  1. 代码复用与减少重复计算
    • 充分利用 Kotlin 跨平台的代码复用特性,避免在不同平台上重复实现相同的业务逻辑。例如,数据处理算法、数据验证逻辑等都应该放在 commonMain 中,减少重复代码,从而提高性能。同时,避免在不同平台上进行重复计算,例如在共享代码中计算一次数据,然后在不同平台的视图中使用相同的计算结果。
  2. 平台特定优化
    • 在 Android 平台,可以使用 Android 提供的性能优化工具,如 Profiler 来分析和优化应用性能。对于 Kotlin 跨平台项目,要注意在平台特定代码(如 androidMain 中的代码)中遵循 Android 的最佳实践,例如优化布局、管理内存等。在 iOS 平台,使用 Instruments 工具来分析性能,确保在 iosMain 中的代码符合 iOS 的性能规范,如避免主线程阻塞、优化图像渲染等。
  3. 异步编程与并发控制
    • 在网络请求、数据处理等可能耗时的操作中,充分利用 Kotlin 的异步编程特性,如 suspend 函数和 Coroutine。通过合理使用异步编程,可以避免阻塞主线程,提高应用的响应性。同时,要注意并发控制,避免多个异步任务同时访问和修改共享资源,导致数据不一致或性能问题。例如,在处理多个网络请求时,可以使用 CoroutineScopeDispatchers 来控制并发度。

测试策略

  1. 单元测试
    • 对于共享代码(commonMain 中的代码),可以使用 Kotlin 自带的测试框架,如 kotlin - test。例如,对前面定义的 UserViewModel 进行单元测试:
package com.example.shared

import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

class UserViewModelTest {
    @Test
    fun `test getUserList`() = runTest {
        val viewModel = UserViewModel()
        val userList = viewModel.userListFlow.first()
        assertEquals(2, userList.size)
    }
}
  • 对于平台特定代码,如 androidMainiosMain 中的代码,可以使用各自平台的测试框架。在 Android 中,可以使用 JUnit 或 Espresso 进行 UI 测试和单元测试。在 iOS 中,可以使用 XCTest 进行测试。
  1. 集成测试
    • 进行集成测试时,要测试跨平台代码与平台特定代码之间的集成。例如,测试网络请求在不同平台上是否正常工作,数据持久化在不同平台上是否一致等。可以使用一些测试工具,如 MockWebServer 来模拟网络请求,在不同平台上进行集成测试。同时,要测试共享的业务逻辑与平台特定的 UI 之间的交互,确保整个应用在不同平台上的功能完整性。

通过以上对 Kotlin 跨平台应用架构设计的各个方面的探讨,开发者可以更好地构建高效、可维护的跨平台应用,充分发挥 Kotlin 跨平台的优势,为多个平台提供一致的用户体验。