Kotlin实战应用

文章说明

本文内容基于Kotlin官方文档总结翻译而成,旨在为自己学习Kotlin使用。文中的代码示例、技术概念和最佳实践均参考自Kotlin官方资源和相关技术文档,如需获取最新或更详细的信息,建议查阅官方文档。


1. Kotlin与Java互操作

从Kotlin调用Java代码

Kotlin设计之初就考虑了与Java的互操作性。几乎所有Java代码都可以在Kotlin中自然地调用,无需特殊处理。

// 导入Java集合并使用
import java.util.*

fun demo(source: List<Int>) {
// 创建Java ArrayList
val list = ArrayList<Int>()

// Java集合可以使用for循环
for (item in source) {
list.add(item)
}

// 操作符约定也能正常工作
for (i in 0..source.size - 1) {
list[i] = source[i] // 调用get和set方法
}
}

Java中的Getter和Setter

遵循Java getter和setter命名规范的方法在Kotlin中会表示为属性。这些被称为合成属性。

import java.util.Calendar

fun calendarDemo() {
val calendar = Calendar.getInstance()

// 调用getFirstDayOfWeek()
if (calendar.firstDayOfWeek == Calendar.SUNDAY) {
// 调用setFirstDayOfWeek()
calendar.firstDayOfWeek = Calendar.MONDAY
}

// 调用isLenient()
if (!calendar.isLenient) {
// 调用setLenient()
calendar.isLenient = true
}
}

注意:如果Java类只有setter方法,在Kotlin中不会显示为属性,因为Kotlin不支持只有setter的属性。

Java数组处理

Kotlin中的数组是不变的(invariant),而Java中则是协变的(covariant)。这意味着Kotlin不允许将Array<String>赋值给Array<Any>,以避免潜在的运行时失败。

对于原始类型数组,Kotlin提供了特殊的类(IntArrayDoubleArrayCharArray等)以避免装箱/拆箱操作带来的性能开销。

// 假设Java方法接受int[]参数
fun passArrayToJava(javaObj: JavaArrayExample) {
val array = intArrayOf(0, 1, 2, 3)
javaObj.removeIndices(array) // 将int[]传递给方法
}

// 使用展开运算符传递可变参数
fun passVarargToJava(javaObj: JavaArrayExample) {
val array = intArrayOf(0, 1, 2, 3)
javaObj.removeIndicesVarArg(*array)
}

处理已检查异常

在Kotlin中,所有异常都是未检查的(unchecked),编译器不强制捕获任何异常。因此,当调用声明已检查异常的Java方法时,Kotlin不要求执行任何额外操作。

fun render(list: List<*>, to: Appendable) {
for (item in list) {
// Java会要求在这里捕获IOException
to.append(item.toString())
}
}

从Java调用Kotlin代码

Kotlin代码可以从Java中相当流畅地使用。本节介绍Java开发者在使用Kotlin编写的代码时应该了解的细节。

属性

Kotlin属性编译为以下Java元素:

  • 一个getter方法,名称按照JavaBean约定派生(get<PropertyName>)
  • 一个setter方法(仅针对var属性),名称按照JavaBean约定派生(set<PropertyName>)
  • 一个私有字段,与属性名称相同(仅针对具有backing field的属性)

例如,var firstName: String会被编译为以下Java声明:

private String firstName;

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

包级函数

在Kotlin文件org.example/app.kt中声明的所有函数和属性,包括扩展函数,都会编译为一个名为org.example.AppKt的Java类的静态方法。

// org.example/app.kt
package org.example

fun processData(data: String): String {
return "Processed: $data"
}

从Java中调用:

// Java
String result = org.example.AppKt.processData("raw data");

顶层属性

顶层属性会被编译为静态字段和相应的静态getter/setter方法:

// Constants.kt
package org.example

const val MAX_COUNT = 100
var debugMode = false

在Java中:

// 常量可以直接访问
int max = org.example.ConstantsKt.MAX_COUNT;

// 变量需要通过getter/setter访问
boolean debug = org.example.ConstantsKt.getDebugMode();
org.example.ConstantsKt.setDebugMode(true);

互操作性的最佳实践

  1. 注解使用:利用@JvmName@JvmStatic@JvmOverloads等注解来改善Kotlin代码在Java中的可用性。

  2. 命名文件:如果希望生成的Java类有特定名称,可使用@file:JvmName注解:

// 文件:Helper.kt
@file:JvmName("StringHelper")
package org.example

fun process(s: String): String = "Processed: $s"

在Java中调用:

// 可以使用指定的名称而非默认的HelperKt
String result = StringHelper.process("data");
  1. 处理默认参数:Kotlin的函数默认参数在Java中不可用。使用@JvmOverloads注解生成重载方法:
@JvmOverloads
fun createUser(name: String, email: String = "", active: Boolean = true) {
// 实现代码
}

在Java中可以调用以下重载版本:

// 全部参数版本
createUser("John", "john@example.com", false);
// 省略最后一个参数
createUser("John", "john@example.com");
// 只提供必需参数
createUser("John");
  1. 处理命名参数:Java不支持命名参数,因此在设计API时,注意参数顺序的逻辑性,或提供构建器模式作为替代方案。

2. Kotlin DSL构建

领域特定语言(Domain-Specific Language, DSL)是一种为特定问题领域设计的编程语言。Kotlin的特性使其非常适合创建内部DSL。

Lambda与接收者函数类型

Kotlin DSL的核心是Lambda表达式和接收者函数类型。接收者函数类型允许在Lambda内部调用接收者对象的方法而无需显式限定符。

class HTML {
fun body() { /* ... */ }
}

fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}

// 使用
val result = html {
body() // 在lambda内部调用HTML的方法
}

构建HTML DSL示例

这个完整示例展示了如何构建一个用于生成HTML的DSL:

class Tag(val name: String) {
val children = mutableListOf<Tag>()
val attributes = mutableMapOf<String, String>()

fun text(value: String) {
children.add(Text(value))
}

override fun toString(): String {
val attributesStr = attributes.entries.joinToString(" ") {
"${it.key}=\"${it.value}\""
}

val openTag = if (attributesStr.isEmpty()) name else "$name $attributesStr"

return if (children.isEmpty()) {
"<$openTag/>"
} else {
val childrenStr = children.joinToString("")
"<$openTag>$childrenStr</$name>"
}
}
}

class Text(val text: String) : Tag("") {
override fun toString() = text
}

@DslMarker
annotation class HtmlTagMarker

@HtmlTagMarker
class HTML : Tag("html") {
fun head(init: Head.() -> Unit) {
val head = Head()
head.init()
children.add(head)
}

fun body(init: Body.() -> Unit) {
val body = Body()
body.init()
children.add(body)
}
}

class Head : Tag("head") {
fun title(init: Title.() -> Unit) {
val title = Title()
title.init()
children.add(title)
}
}

class Title : Tag("title")

class Body : Tag("body") {
fun h1(init: H1.() -> Unit) {
val h1 = H1()
h1.init()
children.add(h1)
}

fun p(init: P.() -> Unit) {
val p = P()
p.init()
children.add(p)
}

fun a(href: String, init: A.() -> Unit) {
val a = A()
a.attributes["href"] = href
a.init()
children.add(a)
}
}

class H1 : Tag("h1")
class P : Tag("p")
class A : Tag("a")

fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}

使用上述DSL生成HTML:

val page = html {
head {
title { text("DSL示例") }
}
body {
h1 { text("Kotlin DSL示例") }
p { text("这是使用Kotlin DSL生成的HTML") }
a(href = "https://kotlinlang.org") {
text("Kotlin官网")
}
}
}

println(page)

输出结果:

<html><head><title>DSL示例</title></head><body><h1>Kotlin DSL示例</h1><p>这是使用Kotlin DSL生成的HTML</p><a href="https://kotlinlang.org">Kotlin官网</a></body></html>

@DslMarker注解

@DslMarker注解有助于避免DSL中的歧义问题。当在嵌套Lambda中使用时,它可以防止隐式访问外部接收者:

@DslMarker
annotation class ConfigurationDsl

@ConfigurationDsl
class ServerConfig {
var host: String = "localhost"
var port: Int = 8080
}

@ConfigurationDsl
class AuthConfig {
var username: String = ""
var password: String = ""
}

@ConfigurationDsl
class AppConfig {
lateinit var server: ServerConfig
lateinit var auth: AuthConfig

fun server(init: ServerConfig.() -> Unit) {
server = ServerConfig().apply(init)
}

fun auth(init: AuthConfig.() -> Unit) {
auth = AuthConfig().apply(init)
}
}

fun appConfig(init: AppConfig.() -> Unit): AppConfig {
return AppConfig().apply(init)
}

// 使用
val config = appConfig {
server {
host = "example.com"
port = 9000

// 错误:在有@DslMarker注解的情况下,不能隐式访问外部作用域
// auth { } // 这会产生编译错误
}
auth {
username = "admin"
password = "password"
}
}

4. Kotlin多平台开发

Kotlin多平台允许在不同平台之间共享代码,无论是移动设备、Web还是桌面应用。

多平台项目结构

多平台项目包含几个关键组件:

  1. 目标平台:代码编译到的平台,如JVM、JS、iOS等。
  2. 源集:源文件的集合,每个都有自己的依赖项和编译器选项。
  3. 通用源集:共享给所有平台的代码。
  4. 平台特定源集:只针对特定平台的代码。

基本项目结构示例:

kotlin-multiplatform-sample/
├── src/
│ ├── commonMain/ // 所有平台共享的代码
│ │ └── kotlin/
│ ├── jvmMain/ // JVM平台特定代码
│ │ └── kotlin/
│ ├── jsMain/ // JavaScript平台特定代码
│ │ └── kotlin/
│ └── iosMain/ // iOS平台特定代码
│ └── kotlin/
└── build.gradle.kts

预期声明与实际声明

Kotlin多平台使用”预期和实际”声明机制,允许在共享代码中声明预期API,但在各个平台中提供不同的实现。

// commonMain
expect class PlatformLogger() {
fun log(message: String)
}

// jvmMain
actual class PlatformLogger {
actual fun log(message: String) {
println("[JVM] $message")
}
}

// jsMain
actual class PlatformLogger {
actual fun log(message: String) {
console.log("[JS] $message")
}
}

// iosMain
actual class PlatformLogger {
actual fun log(message: String) {
NSLog("[iOS] $message")
}
}

共享业务逻辑:

// commonMain
class UserManager {
private val logger = PlatformLogger()

fun login(username: String, password: String) {
logger.log("登录尝试: $username")
// 共享业务逻辑
}
}

多平台库依赖

多平台项目可以依赖其他多平台库,Kotlin会自动解析并添加适当的平台特定部分:

// build.gradle.kts
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("io.ktor:ktor-client-core:2.2.4")
}
}

val jvmMain by getting {
dependencies {
implementation("io.ktor:ktor-client-cio:2.2.4")
}
}

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

跨平台网络请求示例

下面是一个使用Ktor客户端的跨平台网络请求实现:

// commonMain
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.*

expect fun createHttpClient(): HttpClient

class ApiService {
private val client = createHttpClient()

suspend fun fetchData(url: String): String {
val response: HttpResponse = client.get(url)
return response.bodyAsText()
}
}

// jvmMain
import io.ktor.client.engine.cio.*

actual fun createHttpClient(): HttpClient {
return HttpClient(CIO) {
// JVM特定配置
}
}

// iosMain
import io.ktor.client.engine.darwin.*

actual fun createHttpClient(): HttpClient {
return HttpClient(Darwin) {
// iOS特定配置
}
}

5. Kotlin与Android开发

Android开发自2019年Google I/O大会以来一直以Kotlin为优先语言。超过60%的专业Android开发者使用Kotlin作为主要开发语言。

Android中Kotlin的优势

  1. 简洁性:Kotlin代码比Java更简洁,减少了样板代码。
  2. 空安全:通过可空类型和非空类型,减少了NullPointerException
  3. Kotlin扩展:允许为现有类添加新功能,而无需继承或使用设计模式。
  4. 协程支持:简化异步编程和后台任务处理。
  5. 互操作性:与现有Java代码完全兼容。
  6. Android KTX:专为Android设计的Kotlin扩展库。

29.2 Android项目中使用Kotlin

在Android项目中使用Kotlin,首先需要在项目的build.gradle文件中配置Kotlin:

// 项目级build.gradle
plugins {
id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
}

// 应用级build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}

android {
// 配置
}

Android视图绑定与Kotlin

视图绑定提供了一种类型安全的方式来与XML布局交互:

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

binding.welcomeText.text = "Hello Kotlin!"
binding.actionButton.setOnClickListener {
showMessage("Button clicked")
}
}

private fun showMessage(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}

Android中的Kotlin协程

在Android中使用协程需要添加依赖:

dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
}

使用协程处理网络请求和UI更新:

class UserRepository(private val apiService: ApiService) {
suspend fun fetchUser(userId: String): User {
return withContext(Dispatchers.IO) {
apiService.getUser(userId)
}
}
}

class UserViewModel(private val repository: UserRepository) : ViewModel() {
private val _userData = MutableLiveData<User>()
val userData: LiveData<User> = _userData

fun loadUser(userId: String) {
viewModelScope.launch {
try {
val user = repository.fetchUser(userId)
_userData.value = user
} catch (e: Exception) {
// 错误处理
}
}
}
}

class UserActivity : AppCompatActivity() {
private lateinit var binding: ActivityUserBinding
private val viewModel: UserViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityUserBinding.inflate(layoutInflater)
setContentView(binding.root)

viewModel.userData.observe(this) { user ->
binding.userName.text = user.name
binding.userEmail.text = user.email
}

viewModel.loadUser("user123")
}
}

Jetpack Compose与Kotlin

Jetpack Compose是Android的现代化UI工具包,专为Kotlin设计:

class ComposeActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Kotlin")
}
}
}
}
}

@Composable
fun Greeting(name: String) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Text(
text = "Hello $name!",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { /* 处理点击 */ }) {
Text("Click Me")
}
}
}

@Preview
@Composable
fun GreetingPreview() {
MyAppTheme {
Greeting("Preview")
}
}

6. 项目实战与最佳实践

代码组织与架构

构建Kotlin项目时的架构选择:

  1. **MVVM (Model-View-ViewModel)**:
    • Model:数据层,包含业务逻辑和数据源
    • View:UI层,展示数据并报告用户操作
    • ViewModel:连接Model和View,处理UI逻辑和状态管理
// 数据层
class UserRepository(private val apiService: ApiService, private val database: UserDatabase) {
suspend fun getUser(id: String): User {
// 从网络或数据库获取用户
}
}

// ViewModel层
class UserViewModel(private val repository: UserRepository) : ViewModel() {
private val _userState = MutableStateFlow<UserState>(UserState.Loading)
val userState = _userState.asStateFlow()

fun loadUser(id: String) {
viewModelScope.launch {
_userState.value = UserState.Loading
try {
val user = repository.getUser(id)
_userState.value = UserState.Success(user)
} catch (e: Exception) {
_userState.value = UserState.Error(e.message ?: "Unknown error")
}
}
}
}

// 状态管理
sealed class UserState {
object Loading : UserState()
data class Success(val user: User) : UserState()
data class Error(val message: String) : UserState()
}

// UI层 (Android示例)
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...

lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.userState.collect { state ->
when (state) {
is UserState.Loading -> showLoading()
is UserState.Success -> showUser(state.user)
is UserState.Error -> showError(state.message)
}
}
}
}
}
}
  1. **清洁架构 (Clean Architecture)**:分离关注点,提高可测试性和可维护性
// 领域层 (Domain Layer)
interface UserRepository {
suspend fun getUser(id: String): Result<User>
}

class GetUserUseCase(private val repository: UserRepository) {
suspend operator fun invoke(id: String): Result<User> {
return repository.getUser(id)
}
}

// 数据层 (Data Layer)
class UserRepositoryImpl(
private val apiService: ApiService,
private val database: UserDatabase
) : UserRepository {
override suspend fun getUser(id: String): Result<User> {
// 实现
}
}

// 表示层 (Presentation Layer)
class UserViewModel(private val getUserUseCase: GetUserUseCase) : ViewModel() {
// 实现
}

测试策略

全面的测试策略应包括多个层次:

  1. 单元测试:测试独立组件和函数
class CalculatorTest {
@Test
fun `adding two numbers returns their sum`() {
val calculator = Calculator()
val result = calculator.add(2, 3)
assertEquals(5, result)
}
}
  1. 使用MockK进行模拟:Kotlin专用的模拟库
class UserViewModelTest {
@MockK
lateinit var getUserUseCase: GetUserUseCase

private lateinit var viewModel: UserViewModel

@Before
fun setup() {
MockKAnnotations.init(this)
viewModel = UserViewModel(getUserUseCase)
}

@Test
fun `loading user emits correct states`() = runTest {
// Arrange
val user = User("1", "Test User")
coEvery { getUserUseCase("1") } returns Result.success(user)

// Act
viewModel.loadUser("1")

// Assert
coVerify { getUserUseCase("1") }
assertEquals(UserState.Success(user), viewModel.userState.value)
}
}
  1. 集成测试:测试组件之间的交互

  2. UI测试:使用Espresso或Compose测试API

@RunWith(AndroidJUnit4::class)
class UserActivityTest {
@get:Rule
val composeRule = createComposeRule()

@Test
fun userInfoIsDisplayed() {
// Arrange
val user = User("1", "Test User")

// Act
composeRule.setContent {
UserInfoScreen(user = user)
}

// Assert
composeRule.onNodeWithText("Test User").assertIsDisplayed()
}
}

性能优化

优化Kotlin代码性能的关键技术:

  1. 适当使用懒加载:使用lazylateinit延迟初始化昂贵的资源
// 仅在首次访问时初始化
val expensiveResource by lazy {
println("Initializing expensive resource")
"Resource data"
}
  1. 使用序列处理大集合:对于大型集合的链式操作,使用sequence
// 可能导致多次中间集合分配
val result = list.filter { it > 10 }.map { it * 2 }.take(5)

// 更高效的序列处理
val efficientResult = list.asSequence()
.filter { it > 10 }
.map { it * 2 }
.take(5)
.toList()
  1. 内联函数:减少Lambda调用的开销
inline fun <T> withLock(lock: Lock, action: () -> T): T {
lock.lock()
try {
return action()
} finally {
lock.unlock()
}
}
  1. 使用值类:针对简单包装类的性能优化
@JvmInline
value class UserId(val value: String)

fun processUser(userId: UserId) {
// 在运行时,这个函数接收的实际上是一个String
}

安全编码实践

  1. 利用Kotlin的空安全系统
// 使用安全调用和Elvis操作符
fun getUserName(user: User?): String {
return user?.name ?: "Unknown"
}

// 使用let进行空值检查
user?.let {
println("User found: ${it.name}")
}
  1. 使用密封类处理有限状态
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
object Loading : Result<Nothing>()
}

fun processResult(result: Result<User>) {
when (result) {
is Result.Success -> displayUser(result.data)
is Result.Error -> showError(result.exception)
is Result.Loading -> showLoading()
// 编译器确保处理所有情况,无需else分支
}
}
  1. 避免使用非空断言(!!):除非绝对确定值不为空

  2. 使用扩展函数增强安全性

fun String?.isValidEmail(): Boolean {
if (this == null) return false
// 校验邮箱格式
return matches("[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}".toRegex())
}

多平台项目案例

创建一个简单的跨平台待办事项应用,展示Kotlin Multiplatform的实际应用:

// commonMain/kotlin/model/TodoItem.kt
package model

data class TodoItem(
val id: String,
val title: String,
val description: String = "",
val isCompleted: Boolean = false
)

// commonMain/kotlin/repository/TodoRepository.kt
package repository

import model.TodoItem
import kotlinx.coroutines.flow.Flow

expect class TodoRepository() {
suspend fun addTodo(todo: TodoItem)
suspend fun updateTodo(todo: TodoItem)
suspend fun deleteTodo(id: String)
suspend fun getTodo(id: String): TodoItem?
fun observeTodos(): Flow<List<TodoItem>>
}

// androidMain/kotlin/repository/TodoRepository.kt
package repository

import model.TodoItem
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import android.content.Context
import androidx.room.
我将继续编写第六部分:Kotlin实战应用中的多平台项目案例部分,继续保持Markdown格式输出,不使用第一人称和第三人称。

```kotlin
// androidMain/kotlin/repository/TodoRepository.kt
package repository

import model.TodoItem
import kotlinx.coroutines.flow.Flow
import android.content.Context
import androidx.room.*
import kotlinx.coroutines.flow.map

@Entity(tableName = "todos")
data class TodoEntity(
@PrimaryKey val id: String,
val title: String,
val description: String,
val isCompleted: Boolean
)

@Dao
interface TodoDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTodo(todo: TodoEntity)

@Update
suspend fun updateTodo(todo: TodoEntity)

@Query("DELETE FROM todos WHERE id = :id")
suspend fun deleteTodo(id: String)

@Query("SELECT * FROM todos WHERE id = :id LIMIT 1")
suspend fun getTodo(id: String): TodoEntity?

@Query("SELECT * FROM todos ORDER BY isCompleted, title")
fun observeTodos(): Flow<List<TodoEntity>>
}

@Database(entities = [TodoEntity::class], version = 1)
abstract class TodoDatabase : RoomDatabase() {
abstract fun todoDao(): TodoDao
}

actual class TodoRepository(private val context: Context) {
private val database: TodoDatabase by lazy {
Room.databaseBuilder(
context,
TodoDatabase::class.java,
"todo-database"
).build()
}

private val dao: TodoDao by lazy { database.todoDao() }

actual suspend fun addTodo(todo: TodoItem) {
dao.insertTodo(todo.toEntity())
}

actual suspend fun updateTodo(todo: TodoItem) {
dao.updateTodo(todo.toEntity())
}

actual suspend fun deleteTodo(id: String) {
dao.deleteTodo(id)
}

actual suspend fun getTodo(id: String): TodoItem? {
return dao.getTodo(id)?.toModel()
}

actual fun observeTodos(): Flow<List<TodoItem>> {
return dao.observeTodos().map { entities ->
entities.map { it.toModel() }
}
}

private fun TodoItem.toEntity(): TodoEntity {
return TodoEntity(id, title, description, isCompleted)
}

private fun TodoEntity.toModel(): TodoItem {
return TodoItem(id, title, description, isCompleted)
}
}

// iosMain/kotlin/repository/TodoRepository.kt
package repository

import model.TodoItem
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import platform.Foundation.*

actual class TodoRepository {
private val todos = MutableStateFlow<List<TodoItem>>(emptyList())
private val userDefaults = NSUserDefaults.standardUserDefaults

init {
loadFromStorage()
}

actual suspend fun addTodo(todo: TodoItem) {
todos.update { currentList ->
currentList + todo
}
saveToStorage()
}

actual suspend fun updateTodo(todo: TodoItem) {
todos.update { currentList ->
currentList.map {
if (it.id == todo.id) todo else it
}
}
saveToStorage()
}

actual suspend fun deleteTodo(id: String) {
todos.update { currentList ->
currentList.filter { it.id != id }
}
saveToStorage()
}

actual suspend fun getTodo(id: String): TodoItem? {
return todos.value.find { it.id == id }
}

actual fun observeTodos(): Flow<List<TodoItem>> {
return todos
}

private fun saveToStorage() {
val todoList = todos.value
val data = NSMutableArray()

todoList.forEach { todo ->
val dict = NSMutableDictionary()
dict["id"] = todo.id
dict["title"] = todo.title
dict["description"] = todo.description
dict["isCompleted"] = if (todo.isCompleted) 1 else 0
data.addObject(dict)
}

userDefaults.setObject(data, "todos")
userDefaults.synchronize()
}

private fun loadFromStorage() {
val data = userDefaults.arrayForKey("todos") ?: return
val loadedTodos = mutableListOf<TodoItem>()

for (i in 0 until data.count.toInt()) {
val dict = data.objectAtIndex(i.toULong()) as NSDictionary
val id = dict["id"] as String
val title = dict["title"] as String
val description = dict["description"] as String
val isCompleted = (dict["isCompleted"] as Int) == 1

loadedTodos.add(TodoItem(id, title, description, isCompleted))
}

todos.value = loadedTodos
}
}

// commonMain/kotlin/viewmodel/TodoViewModel.kt
package viewmodel

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import model.TodoItem
import repository.TodoRepository
import kotlin.random.Random

class TodoViewModel(private val repository: TodoRepository) {
private val scope = CoroutineScope(Dispatchers.Main)

private val _uiState = MutableStateFlow<TodoListState>(TodoListState.Loading)
val uiState: StateFlow<TodoListState> = _uiState

init {
scope.launch {
repository.observeTodos().collect { todos ->
_uiState.value = TodoListState.Success(todos)
}
}
}

fun addTodo(title: String, description: String) {
if (title.isBlank()) return

val newTodo = TodoItem(
id = generateId(),
title = title,
description = description
)

scope.launch {
repository.addTodo(newTodo)
}
}

fun toggleTodoCompleted(id: String) {
scope.launch {
val todo = repository.getTodo(id) ?: return@launch
repository.updateTodo(todo.copy(isCompleted = !todo.isCompleted))
}
}

fun deleteTodo(id: String) {
scope.launch {
repository.deleteTodo(id)
}
}

private fun generateId(): String {
return Random.nextInt(1000000).toString()
}
}

sealed class TodoListState {
object Loading : TodoListState()
data class Success(val todos: List<TodoItem>) : TodoListState()
data class Error(val message: String) : TodoListState()
}

完成上述多平台项目的实现后,可以为Android和iOS创建相应的UI。下面是Android平台的Compose UI示例:

// androidMain/kotlin/ui/TodoListScreen.kt
package ui

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.collect
import model.TodoItem
import viewmodel.TodoListState
import viewmodel.TodoViewModel

@Composable
fun TodoListScreen(viewModel: TodoViewModel) {
val uiState by viewModel.uiState.collectAsState()

Scaffold(
topBar = {
TopAppBar(
title = { Text("Todo List") }
)
},
floatingActionButton = {
FloatingActionButton(
onClick = { /* 显示添加待办事项对话框 */ }
) {
Text("+")
}
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
when (val state = uiState) {
is TodoListState.Loading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
is TodoListState.Success -> {
TodoList(
todos = state.todos,
onToggleTodo = { viewModel.toggleTodoCompleted(it) },
onDeleteTodo = { viewModel.deleteTodo(it) }
)
}
is TodoListState.Error -> {
Text(
text = state.message,
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
}

@Composable
fun TodoList(
todos: List<TodoItem>,
onToggleTodo: (String) -> Unit,
onDeleteTodo: (String) -> Unit
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp)
) {
items(todos) { todo ->
TodoItemRow(
todo = todo,
onToggle = { onToggleTodo(todo.id) },
onDelete = { onDeleteTodo(todo.id) }
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}

@Composable
fun TodoItemRow(
todo: TodoItem,
onToggle: () -> Unit,
onDelete: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = todo.isCompleted,
onCheckedChange = { onToggle() }
)

Column(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp)
) {
Text(
text = todo.title,
style = MaterialTheme.typography.titleMedium
)
if (todo.description.isNotBlank()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = todo.description,
style = MaterialTheme.typography.bodyMedium
)
}
}

IconButton(onClick = onDelete) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete"
)
}
}
}
}

下面是共享业务逻辑和特定平台UI的完整结构:

kotlin-multiplatform-todo/
├── src/
│ ├── commonMain/
│ │ └── kotlin/
│ │ ├── model/
│ │ │ └── TodoItem.kt
│ │ ├── repository/
│ │ │ └── TodoRepository.kt
│ │ └── viewmodel/
│ │ └── TodoViewModel.kt
│ ├── androidMain/
│ │ └── kotlin/
│ │ ├── repository/
│ │ │ └── TodoRepository.kt
│ │ └── ui/
│ │ └── TodoListScreen.kt
│ └── iosMain/
│ └── kotlin/
│ ├── repository/
│ │ └── TodoRepository.kt
│ └── TodoAPI.kt // 导出给Swift代码使用的API
└── build.gradle.kts

Swift端可以这样调用共享逻辑:

// iOS应用中的使用
import UIKit
import shared // 导入KMP模块

class TodoListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private let tableView = UITableView()
private var todos: [TodoItem] = []
private let viewModel: TodoViewModel
private var observer: Ktor_ioCloseable?

init() {
// 初始化共享的ViewModel和存储库
let repository = TodoRepository()
viewModel = TodoViewModel(repository: repository)
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
title = "Todo List"
setupTableView()
observeTodos()

// 添加新待办按钮
navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .add,
target: self,
action: #selector(addTodoTapped)
)
}

private func setupTableView() {
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
tableView.register(TodoCell.self, forCellReuseIdentifier: "TodoCell")
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}

private func observeTodos() {
observer = viewModel.uiState.watch { [weak self] state in
if let state = state as? TodoListState.Success {
self?.todos = state.todos
DispatchQueue.main.async {
self?.tableView.reloadData()
}
}
}
}

@objc private func addTodoTapped() {
let alert = UIAlertController(title: "Add Todo", message: nil, preferredStyle: .alert)
alert.addTextField { textField in
textField.placeholder = "Title"
}
alert.addTextField { textField in
textField.placeholder = "Description"
}

let addAction = UIAlertAction(title: "Add", style: .default) { [weak self] _ in
guard let title = alert.textFields?[0].text, !title.isEmpty else { return }
let description = alert.textFields?[1].text ?? ""
self?.viewModel.addTodo(title: title, description: description)
}

let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)

alert.addAction(addAction)
alert.addAction(cancelAction)

present(alert, animated: true)
}

// UITableViewDataSource methods
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return todos.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TodoCell", for: indexPath) as! TodoCell
let todo = todos[indexPath.row]
cell.configure(with: todo)
cell.onToggleCompletion = { [weak self] in
self?.viewModel.toggleTodoCompleted(id: todo.id)
}
return cell
}

// UITableViewDelegate methods
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let todo = todos[indexPath.row]
viewModel.deleteTodo(id: todo.id)
}
}
}

// 自定义的待办事项单元格
class TodoCell: UITableViewCell {
private let titleLabel = UILabel()
private let descriptionLabel = UILabel()
private let completionButton = UIButton()

var onToggleCompletion: (() -> Void)?

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private func setupViews() {
// 界面布局代码...

completionButton.addTarget(self, action: #selector(completionButtonTapped), for: .touchUpInside)
}

func configure(with todo: TodoItem) {
titleLabel.text = todo.title
descriptionLabel.text = todo.description

// 更新完成状态按钮
let image = todo.isCompleted ?
UIImage(systemName: "checkmark.circle.fill") :
UIImage(systemName: "circle")
completionButton.setImage(image, for: .normal)
}

@objc private func completionButtonTapped() {
onToggleCompletion?()
}
}

参考链接