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 跨平台项目结构通常包含以下几个部分:
- 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"))
}
- platform - specific sourcesets:针对每个特定平台,都有对应的源集,如
androidMain
、iosMain
等。这些源集主要用于实现与平台相关的功能,例如在androidMain
中可以处理 Android 特定的 UI 逻辑、权限管理等,而在iosMain
中可以处理 iOS 特定的界面布局、系统交互等。
Kotlin 跨平台架构模式
- MVVM(Model - View - ViewModel)
- Model:在 Kotlin 跨平台项目中,模型部分可以在
commonMain
中定义。它代表了应用程序的数据和业务逻辑。例如,上面提到的User
数据类就是模型的一部分。 - ViewModel:也可以在
commonMain
中实现大部分与业务逻辑相关的 ViewModel 代码。ViewModel 负责处理业务逻辑并为 View 提供数据。例如,一个用于获取用户列表并提供给 UI 的 ViewModel 可以这样写:
- Model:在 Kotlin 跨平台项目中,模型部分可以在
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 实例,以获取数据并展示。
- Clean Architecture
- Entities:在 Kotlin 跨平台中,实体类(Entities)可以放在
commonMain
中。这些实体类代表了应用程序的核心业务概念,不依赖于任何特定的框架或平台。例如,一个电商应用中的Product
实体类:
- Entities:在 Kotlin 跨平台中,实体类(Entities)可以放在
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 框架、网络框架等。在
androidMain
和iosMain
源集中实现与这些框架的集成。
跨平台数据持久化
- SQLDelight
- SQLDelight 是一个用于 Kotlin 跨平台的数据持久化库。它允许在
commonMain
中定义 SQL 模式和查询,然后在不同平台上生成对应的数据库操作代码。 - 首先,在
build.gradle.kts
文件中添加 SQLDelight 依赖:
- SQLDelight 是一个用于 Kotlin 跨平台的数据持久化库。它允许在
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)
}
- 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)
}
跨平台网络请求
- 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)
}
- Retrofit (Android - only with Kotlin support)
- 虽然 Retrofit 主要用于 Android 开发,但在 Kotlin 跨平台项目中,如果只考虑 Android 平台的网络请求,也可以使用它。
- 首先,在
androidMain
的build.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 开发
- 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)
}
}
- SwiftUI - Kotlin (Experimental)
- 对于 iOS 平台,有一些实验性的项目尝试将 Kotlin 与 SwiftUI 结合,实现跨平台 UI 开发。虽然目前还处于早期阶段,但展示了一种有趣的跨平台 UI 实现思路。例如,通过编写 Kotlin 代码生成 SwiftUI 视图定义,然后在 iOS 应用中使用这些视图。这通常涉及到自定义的代码生成工具和 Kotlin 与 Swift 之间的互操作机制。
解决跨平台兼容性问题
- 平台特定代码隔离
- 确保将平台特定的代码放在各自对应的源集中,如
androidMain
和iosMain
。这样可以避免平台相关的代码污染共享代码库,使得共享代码更加纯净和易于维护。例如,Android 中的权限请求代码和 iOS 中的通知设置代码应该分别放在各自的平台源集中。
- 确保将平台特定的代码放在各自对应的源集中,如
- 使用 Expect - Actual 机制
- Kotlin 跨平台提供了
expect
和actual
关键字来处理平台特定的实现。在commonMain
中使用expect
声明一个函数或类,然后在不同平台的源集中使用actual
提供具体的实现。例如,获取设备唯一标识符的功能: - 在
commonMain
中:
- Kotlin 跨平台提供了
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()
}
- 处理依赖差异
- 不同平台可能需要不同版本或不同类型的依赖库。例如,Android 上使用 OkHttp 进行网络请求,而 iOS 上可能使用 URLSession。通过在不同平台的源集中添加特定的依赖,并在共享代码中使用抽象层来处理这些差异。例如,在共享代码中定义一个网络请求的抽象接口,然后在 Android 和 iOS 平台分别实现这个接口,使用各自适合的网络库。
性能优化
- 代码复用与减少重复计算
- 充分利用 Kotlin 跨平台的代码复用特性,避免在不同平台上重复实现相同的业务逻辑。例如,数据处理算法、数据验证逻辑等都应该放在
commonMain
中,减少重复代码,从而提高性能。同时,避免在不同平台上进行重复计算,例如在共享代码中计算一次数据,然后在不同平台的视图中使用相同的计算结果。
- 充分利用 Kotlin 跨平台的代码复用特性,避免在不同平台上重复实现相同的业务逻辑。例如,数据处理算法、数据验证逻辑等都应该放在
- 平台特定优化
- 在 Android 平台,可以使用 Android 提供的性能优化工具,如 Profiler 来分析和优化应用性能。对于 Kotlin 跨平台项目,要注意在平台特定代码(如
androidMain
中的代码)中遵循 Android 的最佳实践,例如优化布局、管理内存等。在 iOS 平台,使用 Instruments 工具来分析性能,确保在iosMain
中的代码符合 iOS 的性能规范,如避免主线程阻塞、优化图像渲染等。
- 在 Android 平台,可以使用 Android 提供的性能优化工具,如 Profiler 来分析和优化应用性能。对于 Kotlin 跨平台项目,要注意在平台特定代码(如
- 异步编程与并发控制
- 在网络请求、数据处理等可能耗时的操作中,充分利用 Kotlin 的异步编程特性,如
suspend
函数和Coroutine
。通过合理使用异步编程,可以避免阻塞主线程,提高应用的响应性。同时,要注意并发控制,避免多个异步任务同时访问和修改共享资源,导致数据不一致或性能问题。例如,在处理多个网络请求时,可以使用CoroutineScope
和Dispatchers
来控制并发度。
- 在网络请求、数据处理等可能耗时的操作中,充分利用 Kotlin 的异步编程特性,如
测试策略
- 单元测试
- 对于共享代码(
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)
}
}
- 对于平台特定代码,如
androidMain
和iosMain
中的代码,可以使用各自平台的测试框架。在 Android 中,可以使用 JUnit 或 Espresso 进行 UI 测试和单元测试。在 iOS 中,可以使用 XCTest 进行测试。
- 集成测试
- 进行集成测试时,要测试跨平台代码与平台特定代码之间的集成。例如,测试网络请求在不同平台上是否正常工作,数据持久化在不同平台上是否一致等。可以使用一些测试工具,如 MockWebServer 来模拟网络请求,在不同平台上进行集成测试。同时,要测试共享的业务逻辑与平台特定的 UI 之间的交互,确保整个应用在不同平台上的功能完整性。
通过以上对 Kotlin 跨平台应用架构设计的各个方面的探讨,开发者可以更好地构建高效、可维护的跨平台应用,充分发挥 Kotlin 跨平台的优势,为多个平台提供一致的用户体验。