将现有应用迁移到 Room KMP

1. 前言

前提条件

所需条件

学习内容

  • 如何在 Android 应用和 iOS 应用之间共享 Room 数据库。

2. 进行设置

要开始,请执行以下步骤:

  1. 使用以下终端命令克隆 GitHub 代码库:
$ git clone https://github.com/android/codelab-android-kmp.git

或者,您也能以 Zip 文件的形式下载该代码库:

  1. 在 Android Studio 中,打开 migrate-room 项目,其中包含以下分支:
  • main:包含该项目的起始代码,您将在其中做出更改来完成此 Codelab。
  • end:包含此 Codelab 的解决方案代码。

我们建议您从 main 分支开始,按照自己的节奏逐步完成此 Codelab。

  1. 如果您想查看解决方案代码,请运行以下命令:
$ git clone -b end https://github.com/android/codelab-android-kmp.git

或者,您也可以下载解决方案代码:

3. 了解示例应用

本教程包含使用原生框架构建的 Fruitties 示例应用,该应用在 Android 上采用 Jetpack Compose 框架,在 iOS 上则采用 SwiftUi 框架。

Fruitties 应用提供两项主要功能:

  • 一个包含多个 Fruit 项目的列表,每个项目旁边都设有一个按钮以用于将该项目添加至 Cart。
  • Cart 呈现在应用顶部,显示添加的水果种类及对应数量。

4a7f262b015d7f78.png

Android 应用架构

Android 应用遵循官方 Android 架构准则,以保持清晰的模块化结构。

在 KMP 集成之前的 Android 应用架构图

iOS 应用架构

在 KMP 集成之前的 iOS 应用架构图

KMP 共享模块

此项目已设置 KMP 共享模块,但目前为空。如果您的项目尚未设置共享模块,请先完成Kotlin Multiplatform 使用入门 Codelab。

4. 为 KMP 集成准备 Room 数据库

在将 Room 数据库代码从 Fruitties Android 应用移至 shared 模块之前,您需要确保该应用与 Kotlin Multiplatform (KMP) Room API 兼容。本部分将引导您完成该过程。

一项关键更新是使用与 Android 和 iOS 均兼容的 SQLite 驱动程序。如需在多个平台上支持 Room 数据库功能,您可以使用 BundledSQLiteDriver。此驱动程序会将 SQLite 直接捆绑到应用中,使其适用于 Kotlin 的多平台使用。如需获得详细指导,请参阅 Room KMP 迁移指南。

更新依赖项

首先,将 room-runtime 和 sqlite-bundled 依赖项添加到 libs.versions.toml 文件中:

# Add libraries
[libraries]
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" }

androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "androidx-sqlite" }

接下来,更新 :androidApp 模块的 build.gradle.kts 以使用这些依赖项,并移除对 libs.androidx.room.ktx 的使用:

// Add
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.sqlite.bundled)
// Remove
implementation(libs.androidx.room.ktx)

现在,在 Android Studio 中同步项目。

为 BundledSQLiteDriver 修改数据库模块

接下来,修改 Android 应用中的数据库创建逻辑以使用 BundledSQLiteDriver,让其与 KMP 兼容,同时在 Android 上保持功能。

  1. 打开位于 androidApp/src/main/kotlin/com/example/fruitties/kmptutorial/android/di/DatabaseModule.kt 的 DatabaseModule.kt 文件
  2. 更新 providesAppDatabase 方法,如以下代码段所示:
import androidx.sqlite.driver.bundled.BundledSQLiteDriver

@Module
@InstallIn(SingletonComponent::class)
internal object DatabaseModule {

...

@Provides
@Singleton
fun providesAppDatabase(@ApplicationContext context: Context): AppDatabase {
    val dbFile = context.getDatabasePath("sharedfruits.db")
    return Room.databaseBuilder<AppDatabase>(context, dbFile.absolutePath)
        .setDriver(BundledSQLiteDriver())
        .build()
}

构建并运行 Android 应用

现在,您已将原生 SQLite 驱动程序切换为捆绑驱动程序,接下来,请验证应用 build 以及所有内容是否正常运行,然后再将数据库迁移到 :shared 模块。

5. 将数据库代码移至 :shared 模块

在此步骤中,我们会将 Room 数据库设置从 Android 应用转移到 :shared 模块,以便 Android 和 iOS 都能访问该数据库。

更新 :shared 模块的 build.gradle.kts 配置

首先,更新 :shared 模块的 build.gradle.kts,以使用 Room 多平台依赖项。

  1. 添加 KSP 和 Room 插件:
plugins {
   ...
   // TODO add KSP + ROOM plugins
   alias(libs.plugins.ksp)
   alias(libs.plugins.room)
}
  1. 将 room-runtime 和 sqlite-bundled 依赖项添加到 commonMain 代码块中:
sourceSets {
    commonMain {
        // TODO Add KMP dependencies here
        implementation(libs.androidx.room.runtime)
        implementation(libs.androidx.sqlite.bundled)
    }
}
  1. 通过添加新的顶级 dependencies 代码块,为每个平台目标添加 KSP 配置。为方便起见,您可以将其添加到文件底部:
// Should be its own top level block. For convenience, add at the bottom of the file
dependencies {
   add("kspAndroid", libs.androidx.room.compiler)
   add("kspIosSimulatorArm64", libs.androidx.room.compiler)
   add("kspIosX64", libs.androidx.room.compiler)
   add("kspIosArm64", libs.androidx.room.compiler)
}
  1. 同样在顶级,添加一个新代码块以设置 Room 架构位置:
// Should be its own top level block. For convenience, add at the bottom of the file
room {
   schemaDirectory("$projectDir/schemas")
}
  1. Gradle 会同步该项目

将 Room 架构移至 :shared 模块

将 androidApp/schemas 目录移至 src/ 文件夹旁边的 :shared 模块根文件夹:

自:e1ee37a3f3a10b35.png

至:ba3c9eb617828bac.png

移动 DAO 和实体

现在,您已向 KMP 共享模块添加了必要的 Gradle 依赖项。接下来,需要将 DAO 和实体从 :androidApp 模块移至 :shared 模块。

这一过程涉及将相关文件移至 :shared 模块中 commonMain 源代码集内的相应位置。

移动 Fruittie 模型

您可利用 Refactor → Move功能来切换模块,而不破坏导入:

  1. 找到 androidApp/src/main/kotlin/.../model/Fruittie.kt 文件,右键点击该文件,然后依次选择重构→移动(或按 F6):c893e12b8bf683ae.png
  2. 在 Move 对话框中,选择 Destination directory 字段旁边的 ... 图标。1d51c3a410e8f2c3.png
  3. 在 commonMain 对话框中选择 commonMain 源代码集,然后点击“OK”。您可能需要停用 Show only existing source roots 复选框。f61561feb28a6445.png
  4. 点击 Refactor 按钮以移动文件。

移动 CartItem 和 CartItemWithFruittie 模型

对于文件 androidApp/.../model/CartItem.kt,您需要执行以下步骤:

  1. 打开文件,右键点击 CartItem 类,然后选择 Refactor > Move。
  2. 这会打开相同的 Move 对话框,但在本例中,您还需要选中 CartItemWithFruittie 成员对应的复选框。
  3. a25022cce5cee5e0.png

继续操作,依次选择 ... 图标和 commonMain 源代码集,就像您之前对 Fruittie.kt 文件所做的那样。

移动 DAO 和 AppDatabase

对以下文件(您可以同时选择这三个文件)执行相同的步骤:

  • androidApp/.../database/FruittieDao.kt
  • androidApp/.../database/CartDao.kt
  • androidApp/.../database/AppDatabase.kt

更新共享的 AppDatabase,以便跨平台使用

现在,您已将数据库类移至 :shared 模块,接下来需要调整这些类,以便在两个平台上生成所需的实现。

  1. 打开 /shared/src/commonMain/kotlin/com/example/fruitties/kmptutorial/android/database/AppDatabase.kt 文件。
  2. 添加以下 RoomDatabaseConstructor 实现:
import androidx.room.RoomDatabaseConstructor

// The Room compiler generates the `actual` implementations.
@Suppress("NO_ACTUAL_FOR_EXPECT")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
    override fun initialize(): AppDatabase
}
  1. 为 AppDatabase 类添加 @ConstructedBy(AppDatabaseConstructor::class) 注解:
import androidx.room.ConstructedBy

@Database(
    entities = [Fruittie::class, CartItem::class],
    version = 1,
)
// TODO Add this line
@ConstructedBy(AppDatabaseConstructor::class)
abstract class AppDatabase : RoomDatabase() {
...

410a3c0c656b6499.png

将数据库创建移至 :shared 模块

接下来,您需要将 Android 专用的 Room 设置从 :androidApp 模块移至 :shared 模块。此操作为必要操作,因为在下一步中,您将从 :androidApp 模块中移除 Room 依赖项。

  1. 找到 androidApp/.../di/DatabaseModule.kt 文件。
  2. 选择 providesAppDatabase 函数的内容,右键点击,然后依次选择 Refactor > Extract Function to Scope:da4d97319f9a0e8c.png
  3. 从菜单中选择 DatabaseModule.kt。5e540a1eec6e3493.png 这会将内容移至全局 appDatabase 函数。按 Enter 键确认函数名称。e2fb113d66704a36.png
  4. 通过移除 private 可见性修饰符,将函数设为公开。
  5. 右键点击 Refactor > Move,将该函数移至 :shared 模块中。
  6. 在 Move 对话框中,选择 Destination directory 字段旁边的 … 图标。e2101005f2ef4747.png
  7. 在 Choose Destination Directory 对话框中,选择 shared >androidMain 源代码集,然后选择 /shared/src/androidMain/ 文件夹,然后点击 OK。73d244941c68dc85.png
  8. 将 To package 字段中的后缀从 .di 更改为 .database ac5cf30d32871e2c.png
  9. 点击 Refactor。

清理 :androidApp 中不需要的代码

此时,您已将 Room 数据库移至多平台模块,并且 :androidApp 模块不需要任何 Room 依赖项,因此您可以将其移除。

  1. 打开 :androidApp 模块中的 build.gradle.kts 文件。
  2. 移除依赖项和配置,如以下代码段所示:
plugins {
  // TODO Remove 
  alias(libs.plugins.room)
}

android {
  // TODO Remove
  ksp {
      arg("room.generateKotlin", "true")
  }

dependencies {
  // TODO Keep room-runtime
  implementation(libs.androidx.room.runtime)

  // TODO Remove
  implementation(libs.androidx.sqlite.bundled)
  ksp(libs.androidx.room.compiler)
}

// TODO Remove
room {
    schemaDirectory("$projectDir/schemas")
}
  1. Gradle 会同步该项目。

构建并运行 Android 应用

运行 Fruitties Android 应用,确保应用正常运行,并且现在使用 :shared 模块中的数据库。如果您之前添加了购物车商品,那么即使 Room 数据库现在位于 :shared 模块中,此时您应该也会看到相同的商品。

6. 备好 Room 以在 iOS 上使用

为了进一步为 iOS 平台准备 Room 数据库,您需要在 :shared 模块中设置一些支持代码,以便在下一步中使用。

为 iOS 应用启用数据库创建功能

首先要做的是,添加特定于 iOS 的数据库构建器。

  1. 在 iosMain 源代码集内的 :shared 模块中添加一个名为 AppDatabase.ios.kt 的新文件:dcb46ba560298865.png
  2. 添加以下辅助函数。iOS 应用将使用这些函数来获取 Room 数据库的实例。
package com.example.fruitties.kmptutorial.shared

import androidx.room.Room
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import com.example.fruitties.kmptutorial.android.database.AppDatabase
import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.ObjCObjectVar
import kotlinx.cinterop.alloc
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.value
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSError
import platform.Foundation.NSFileManager
import platform.Foundation.NSUserDomainMask

fun getPersistentDatabase(): AppDatabase {
    val dbFilePath = documentDirectory() + "/" + "fruits.db"
    return Room.databaseBuilder<AppDatabase>(name = dbFilePath)
       .setDriver(BundledSQLiteDriver())
       .build()
}

@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
private fun documentDirectory(): String {
    memScoped {
        val errorPtr = alloc<ObjCObjectVar<NSError?>>()
        val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
            directory = NSDocumentDirectory,
            inDomain = NSUserDomainMask,
            appropriateForURL = null,
            create = false,
            error = errorPtr.ptr,
        )
        if (documentDirectory != null) {
            return requireNotNull(documentDirectory.path) {
                """Couldn't determine the document directory.
                  URL $documentDirectory does not conform to RFC 1808.
               """.trimIndent()
            }
        } else {
            val error = errorPtr.value
            val localizedDescription = error?.localizedDescription ?: "Unknown error occurred"
            error("Couldn't determine document directory. Error: $localizedDescription")
        }
    }
}

为 Room 实体添加“Entity”后缀

由于您要在 Swift 中为 Room 实体添加封装容器,因此最好让 Room 实体的名称不同于封装容器的名称。我们将通过使用 @ObjCName 注解向 Room 实体添加 Entity 后缀来确保两者名称不同。

打开 shared 模块中的 Fruittie.kt 文件,然后将 @ObjCName 注解添加到 Fruittie 实体。由于此注解处于实验阶段,因此您可能需要向文件添加 @OptIn(ExperimentalObjC::class) 注解。

Fruittie.kt

import kotlin.experimental.ExperimentalObjCName
import kotlin.native.ObjCName

@OptIn(ExperimentalObjCName::class)
@Entity(indices = [Index(value = ["id"], unique = true)])
@ObjCName("FruittieEntity")
data class Fruittie(
   ...
)

然后,对 CartItem.kt 文件中的 CartItem 实体执行相同的操作。

CartItem.kt

import kotlin.experimental.ExperimentalObjCName
import kotlin.native.ObjCName

@OptIn(ExperimentalObjCName::class)
@ObjCName("CartItemEntity")
@Entity(
   foreignKeys = [
       ForeignKey(
           entity = Fruittie::class,
           parentColumns = ["id"],
           childColumns = ["id"],
           onDelete = ForeignKey.CASCADE,
       ),
   ],
)
data class CartItem(@PrimaryKey val id: Long, val count: Int = 1)

7. 在 iOS 应用中使用 Room

iOS 应用是使用 Core Data 的既有应用。在此 Codelab 中,您不必担心迁移数据库中的任何现有数据,因为此应用只是一个原型。如果您要将正式版应用迁移到 KMP,则必须编写函数来读取当前的 Core Data 数据库,并在迁移后首次启动时将这些项插入 Room 数据库。

打开 Xcode 项目

在 Xcode 中打开 iOS 项目,方法是:前往 /iosApp/ 文件夹,然后在关联的应用中打开 Fruitties.xcodeproj。

54836291a243ebe9.png

移除 Core Data 实体类

首先,您需要移除 Core Data 实体类,以便为稍后创建的实体封装容器腾出空间。您可完全移除 Core Data 实体,或者保留这些实体以便进行数据迁移,具体取决于应用在 Core Data 中存储的数据类型。在本教程中,您可以直接将其移除,因为您无需迁移任何现有数据。

在 Xcode 中:

  1. 前往 Project Navigator。
  2. 找到“Resources”文件夹。
  3. 打开 Fruitties 文件。
  4. 点击并删除每个实体。

7ad742d991d76b1c.png

如需在代码中实现这些更改,请清理并重建项目。

这会导致构建失败并显示以下错误,这是意料之中的结果。

e3e107bf0387eeab.png

创建实体封装容器

接下来,我们将为 Fruittie 和 CartItem 实体创建实体封装容器,以便顺利处理 Room 和 Core Data 实体之间的 API 差异。

这些封装容器将有助于我们从 Core Data 过渡到 Room,因为它们可最大限度地减少需要立即更新的代码量。您应该着眼于日后直接访问 Room 实体,以替换这些封装容器。

现在,我们将为 FruittieEntity 类创建一个封装容器,并为其添加可选属性。

创建 FruittieEntity 封装容器

  1. 在 Sources/Repository 目录中创建一个新的 Swift 文件,方法是:右键点击目录名称,然后选择 New File from Template… cce140b2fb3c2da8.png 6a0d4fa4292ddd4f.png
  2. 将它命名为 Fruittie,并确保仅选择 Fruitties 目标而非测试目标。827b9019b0a32352.png
  3. 将以下代码添加到新的 Fruittie 文件中:
import sharedKit

struct Fruittie: Hashable {
   let entity: FruittieEntity

   var id: Int64 {
       entity.id
   }

   var name: String? {
       entity.name
   }

   var fullName: String? {
       entity.fullName
   }
}

Fruittie 结构体封装了 FruittieEntity 类,将属性设置为可选,并传递实体的属性。此外,我们还让 Fruittie 结构体遵循 Hashable 协议,以便在 SwiftUI 的 ForEach 视图中使用。

创建 CartItemEntity 封装容器

接下来,为 CartItemEntity 类创建类似的封装容器。

在 Sources/Repository 目录中创建一个名为 CartItem.swift 的新 Swift 文件。

import sharedKit

struct CartItem: Hashable {
   let entity: CartItemWithFruittie

   let fruittie: Fruittie?

   var id: Int64 {
       entity.cartItem.id
   }

   var count: Int64 {
       Int64(entity.cartItem.count)
   }

   init(entity: CartItemWithFruittie) {
       self.entity = entity
       self.fruittie = Fruittie(entity: entity.fruittie)
   }
}

由于原始 Core Data CartItem 类具有 Fruittie 属性,因此我们还在 CartItem 结构体中添加了 Fruittie 属性。虽然 CartItem 类没有任何可选属性,但 count 属性在 Room 实体中具有不同的类型。

更新代码库

现在,实体封装容器已就位,您需要更新 DefaultCartRepository 和 DefaultFruittieRepository 以使用 Room 而非 Core Data。

更新“DefaultCartRepository”

我们先从 DefaultCartRepository 类开始,因为它是两者中更简单的那个。

打开 Sources/Repository 目录中的 CartRepository.swift 文件。

  1. 首先,将 CoreData 导入替换为 sharedKit:
import sharedKit
  1. 然后,移除 NSManagedObjectContext 属性并将其替换为 CartDao 属性:
// Remove
private let managedObjectContext: NSManagedObjectContext

// Replace with
private let cartDao: any CartDao
  1. 更新 init 构造函数以初始化新的 cartDao 属性:
init(cartDao: any CartDao) {
    self.cartDao = cartDao
}
  1. 接下来,更新 addToCart 方法。此方法需要从 Core Data 中提取现有购物车商品,但我们的 Room 实现不需要这样做。实际上,它会插入新商品或增加现有购物车商品的数量。
func addToCart(fruittie: Fruittie) async throws {
    try await cartDao.insertOrIncreaseCount(fruittie: fruittie.entity)
}
  1. 最后,更新 getCartItems() 方法。此方法将对 CartDao 调用 getAll() 方法,并将 CartItemWithFruittie 实体映射到 CartItem 封装容器。
func getCartItems() -> AsyncStream<[CartItem]> {
    return cartDao.getAll().map { entities in
        entities.map(CartItem.init(entity:))
    }.eraseToStream()
}

更新“DefaultFruittieRepository”

为了迁移 DefaultFruittieRepository 类,我们需做出相似更改,就像更改 DefaultCartRepository 类。

将 FruittieRepository 文件更新为以下内容:

import ConcurrencyExtras
import sharedKit

protocol FruittieRepository {
    func getData() -> AsyncStream<[Fruittie]>
}

class DefaultFruittieRepository: FruittieRepository {
    private let fruittieDao: any FruittieDao
    private let api: FruittieApi

    init(fruittieDao: any FruittieDao, api: FruittieApi) {
        self.fruittieDao = fruittieDao
        self.api = api
    }

    func getData() -> AsyncStream<[Fruittie]> {
        let dao = fruittieDao
        Task {
            let isEmpty = try await dao.count() == 0
            if isEmpty {
                let response = try await api.getData(pageNumber: 0)
                let fruitties = response.feed.map {
                    FruittieEntity(
                        id: 0,
                        name: $0.name,
                        fullName: $0.fullName,
                        calories: ""
                    )
                }
                _ = try await dao.insert(fruitties: fruitties)
            }
        }
        return dao.getAll().map { entities in
            entities.map(Fruittie.init(entity:))
        }.eraseToStream()
    }
}

替换 @FetchRequest 属性封装容器

我们还需要替换 SwiftUI 视图中的 @FetchRequest 属性封装容器。@FetchRequest 属性封装容器用于从 Core Data 中提取数据并观察更改,因此我们无法将其与 Room 实体搭配使用。我们改用 UIModel 从代码库中访问数据。

  1. 打开 Sources/UI/CartView.swift 文件中的 CartView。
  2. 将实现替换为以下内容:
import SwiftUI

struct CartView : View {
    @State
    private var expanded = false

    @ObservedObject
    private(set) var uiModel: ContentViewModel

    var body: some View {
        if (uiModel.cartItems.isEmpty) {
            Text("Cart is empty, add some items").padding()
        } else {
            HStack {
                Text("Cart has \(uiModel.cartItems.count) items (\(uiModel.cartItems.reduce(0) { $0 + $1.count }))")
                    .padding()

                Spacer()

                Button {
                    expanded.toggle()
                } label: {
                    if (expanded) {
                        Text("collapse")
                    } else {
                        Text("expand")
                    }
                }
                .padding()
            }
            if (expanded) {
                VStack {
                    ForEach(uiModel.cartItems, id: \.self) { item in
                        Text("\(item.fruittie!.name!): \(item.count)")
                    }
                }
            }
        }
    }
}

更新 ContentView

更新 Sources/View/ContentView.swift 文件中的 ContentView,以将 FruittieUIModel 传递给 CartView。

CartView(uiModel: uiModel)

正在更新“DataController”

iOS 应用中的 DataController 类负责设置 Core Data 堆栈。由于我们不再使用 Core Data,因此需要更新 DataController 以初始化 Room 数据库。

  1. 打开 Sources/Database 中的 DataController.swift 文件。
  2. 添加 sharedKit 导入。
  3. 移除 CoreData 导入。
  4. 在 DataController 类中实例化 Room 数据库。
  5. 最后,从 DataController 初始化程序中移除 loadPersistentStores 方法调用。

最终类应如下所示:

import Combine
import sharedKit

class DataController: ObservableObject {
    let database = getPersistentDatabase()
    init() {}
}

更新依赖项注入

iOS 应用中的 AppContainer 类负责初始化依赖关系图。由于我们更新了代码库以使用 Room 而非 Core Data,因此需要更新 AppContainer 以将 Room DAO 传递给代码库。

  1. 打开 Sources/DI 文件夹中的 AppContainer.swift。
  2. 添加 sharedKit 导入。
  3. 从 AppContainer 类中移除 managedObjectContext 属性。
  4. 通过从 DataController 提供的 AppDatabase 实例传入 Room DAO 来更改 DefaultFruittieRepository 和 DefaultCartRepository 初始化。

完成后,该类将如下所示:

import Combine
import Foundation
import sharedKit

class AppContainer: ObservableObject {
    let dataController: DataController
    let api: FruittieApi
    let fruittieRepository: FruittieRepository
    let cartRepository: CartRepository

    init() {
        dataController = DataController()
        api = FruittieNetworkApi(
            apiUrl: URL(
                string:
                    "https://android.github.io/kotlin-multiplatform-samples/fruitties-api"
            )!)
        fruittieRepository = DefaultFruittieRepository(
            fruittieDao: dataController.database.fruittieDao(),
            api: api
        )
        cartRepository = DefaultCartRepository(
                    cartDao: dataController.database.cartDao()
        )
    }
}

最后,更新 main.swift 中的 FruittiesApp 以移除 managedObjectContext:

struct FruittiesApp: App {
    @StateObject
    private var appContainer = AppContainer()

    var body: some Scene {
        WindowGroup {
            ContentView(appContainer: appContainer)
        }
    }
}

构建并运行 iOS 应用

最后,一旦您构建完应用并按 ⌘R 运行它,应用应该会基于从 Core Data 迁移到 Room 的数据库开始。

5d2ae9438747f8f6.png

8. 恭喜

恭喜!您已成功使用 Room KMP 将独立的 Android 和 iOS 应用迁移到共享数据层。

以下是应用架构的比较,供您参考,以便了解所实现的效果:

之前

Android

iOS

在 KMP 集成之前的 Android 应用架构图

在 KMP 集成之前的 iOS 应用架构图

迁移后架构

bcd8c29b00f67c19.png

了解详情