Di chuyển các ứng dụng hiện có sang Room KMP

1. Trước khi bắt đầu

Điều kiện tiên quyết

Những gì bạn cần

Kiến thức bạn sẽ học được

  • Cách chia sẻ cơ sở dữ liệu Room giữa một ứng dụng Android và một ứng dụng iOS.

2. Bắt đầu thiết lập

Để bắt đầu, hãy làm theo các bước sau:

  1. Sao chép kho lưu trữ GitHub bằng lệnh sau trong cửa sổ dòng lệnh:
$ git clone https://github.com/android/codelab-android-kmp.git

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp ZIP:

  1. Trong Android Studio, hãy mở dự án migrate-room chứa các nhánh sau:
  • main: Chứa đoạn mã khởi đầu cho dự án này, nơi bạn thực hiện các thay đổi để hoàn tất lớp học lập trình.
  • end: Chứa đoạn mã giải pháp cho lớp học lập trình này.

Bạn nên bắt đầu bằng nhánh main và làm theo hướng dẫn từng bước trong lớp học lập trình theo tiến độ của riêng mình.

  1. Nếu bạn muốn thấy đoạn mã giải pháp, hãy chạy lệnh sau:
$ git clone -b end https://github.com/android/codelab-android-kmp.git

Ngoài ra, bạn có thể tải đoạn mã giải pháp xuống:

3. Tìm hiểu về ứng dụng mẫu

Hướng dẫn này đề cập đến ứng dụng mẫu Fruitties được tạo trong các khung gốc (Jetpack Compose trên Android, SwiftUi trên iOS).

Ứng dụng Fruitties có 2 tính năng chính:

  • Danh sách các mặt hàng Trái cây, mỗi mặt hàng có một nút để thêm mặt hàng đó vào Giỏ hàng.
  • Một Giỏ hàng hiển thị ở trên cùng, cho biết số lượng trái cây đã được thêm và số lượng của chúng.

4a7f262b015d7f78.png

Cấu trúc ứng dụng Android

Ứng dụng Android tuân theo nguyên tắc chính thức về cấu trúc Android để duy trì cấu trúc theo mô-đun rõ ràng.

Sơ đồ cấu trúc của ứng dụng Android trước khi tích hợp KMP

Cấu trúc ứng dụng iOS

Sơ đồ cấu trúc của ứng dụng iOS trước khi tích hợp KMP

Mô-đun dùng chung cho KMP

Dự án này đã được thiết lập bằng một mô-đun dùng chung cho KMP, mặc dù mô-đun này đang trống. Nếu dự án của bạn chưa được thiết lập bằng một mô-đun dùng chung, hãy bắt đầu với lớp học lập trình Làm quen với Kotlin Multiplatform.

4. Chuẩn bị cơ sở dữ liệu Room để tích hợp KMP

Trước khi di chuyển mã cơ sở dữ liệu Room từ ứng dụng Fruitties trên Android sang mô-đun shared, bạn cần đảm bảo rằng ứng dụng đó tương thích với các API Room của Kotlin Multiplatform (KMP). Phần này sẽ hướng dẫn bạn thực hiện quy trình đó.

Một điểm cập nhật quan trọng là sử dụng trình điều khiển SQLite tương thích với cả Android và iOS. Để hỗ trợ chức năng cơ sở dữ liệu Room trên nhiều nền tảng, bạn có thể sử dụng BundledSQLiteDriver. Nhờ đóng gói SQLite trực tiếp vào ứng dụng nên trình điều khiển này có thể dùng trên nhiều nền tảng trong Kotlin. Để xem hướng dẫn chi tiết, hãy tham khảo hướng dẫn di chuyển Room KMP.

Cập nhật phần phụ thuộc

Trước tiên, hãy thêm các phần phụ thuộc room-runtimesqlite-bundled vào tệp 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" }

Tiếp theo, cập nhật build.gradle.kts của mô-đun :androidApp để sử dụng các phần phụ thuộc này và ngừng sử dụng libs.androidx.room.ktx:

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

Bây giờ, hãy đồng bộ hoá dự án trong Android Studio.

Sửa đổi mô-đun cơ sở dữ liệu cho BundledSQLiteDriver

Tiếp theo, hãy sửa đổi logic tạo cơ sở dữ liệu trong ứng dụng Android để sử dụng BundledSQLiteDriver, giúp tương thích với KMP trong khi vẫn duy trì chức năng trên Android.

  1. Mở tệp DatabaseModule.kt có trong androidApp/src/main/kotlin/com/example/fruitties/kmptutorial/android/di/DatabaseModule.kt
  2. Cập nhật phương thức providesAppDatabase như trong đoạn mã sau:
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()
}

Tạo và chạy ứng dụng Android

Sau khi chuyển trình điều khiển gốc SQLite thành trình điều khiển được đóng gói, hãy xác minh rằng ứng dụng được tạo và mọi thứ hoạt động đúng cách trước khi di chuyển cơ sở dữ liệu sang mô-đun :shared.

5. Di chuyển mã cơ sở dữ liệu sang mô-đun :shared

Trong bước này, chúng ta sẽ chuyển chế độ thiết lập cơ sở dữ liệu Room từ ứng dụng Android sang mô-đun :shared để cả Android và iOS đều truy cập được vào cơ sở dữ liệu.

Cập nhật cấu hình build.gradle.kts của mô-đun :shared

Bắt đầu bằng cách cập nhật build.gradle.kts của mô-đun :shared để sử dụng các phần phụ thuộc đa nền tảng của Room.

  1. Thêm trình bổ trợ KSP và Room:
plugins {
   ...
   // TODO add KSP + ROOM plugins
   alias(libs.plugins.ksp)
   alias(libs.plugins.room)
}
  1. Thêm các phần phụ thuộc room-runtimesqlite-bundled vào khối commonMain:
sourceSets {
    commonMain {
        // TODO Add KMP dependencies here
        implementation(libs.androidx.room.runtime)
        implementation(libs.androidx.sqlite.bundled)
    }
}
  1. Thêm cấu hình KSP cho từng mục tiêu nền tảng bằng cách thêm một khối dependencies mới ở cấp cao nhất. Để đơn giản, bạn chỉ cần thêm khối này vào cuối tệp:
// 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. Cũng ở cấp cao nhất, hãy thêm một khối mới để đặt vị trí của giản đồ Room:
// Should be its own top level block. For convenience, add at the bottom of the file
room {
   schemaDirectory("$projectDir/schemas")
}
  1. Gradle đồng bộ hoá dự án

Di chuyển giản đồ Room sang mô-đun :shared

Di chuyển thư mục androidApp/schemas vào thư mục gốc của mô-đun :shared bên cạnh thư mục src/:

Từ: e1ee37a3f3a10b35.png

Đến: ba3c9eb617828bac.png

Di chuyển DAO và thực thể

Giờ đây, khi đã thêm các phần phụ thuộc Gradle cần thiết vào mô-đun dùng chung trong KMP, bạn cần di chuyển các DAO và thực thể từ mô-đun :androidApp sang mô-đun :shared.

Để làm việc này, bạn cần di chuyển các tệp đến vị trí tương ứng trong nhóm tài nguyên commonMain trong mô-đun :shared.

Di chuyển mô hình Fruittie

Bạn có thể tận dụng chức năng Refactor → Move (Tái cấu trúc → Di chuyển) để chuyển đổi các mô-đun mà không làm gián đoạn các lệnh nhập:

  1. Tìm tệp androidApp/src/main/kotlin/.../model/Fruittie.kt, nhấp chuột phải vào tệp đó rồi chọn Refactor → Move (Tái cấu trúc → Di chuyển) (hoặc nhấn phím F6):c893e12b8bf683ae.png
  2. Trong hộp thoại Move (Di chuyển), hãy chọn biểu tượng ... bên cạnh trường Destination directory (Thư mục đích). 1d51c3a410e8f2c3.png
  3. Chọn nhóm tài nguyên commonMain trong hộp thoại Choose Destination Directory (Chọn thư mục đích) và nhấp vào OK. Bạn có thể phải tắt hộp đánh dấu Show only existing source roots (Chỉ hiển thị gốc nguồn hiện có). f61561feb28a6445.png
  4. Nhấp vào nút Refactor (Tái cấu trúc) để di chuyển tệp.

Di chuyển mô hình CartItemCartItemWithFruittie

Đối với tệp androidApp/.../model/CartItem.kt, bạn cần làm theo các bước sau:

  1. Mở tệp, nhấp chuột phải vào lớp CartItem rồi chọn Refactor > Move (Tái cấu trúc > Di chuyển).
  2. Thao tác này mở cùng hộp thoại Move (Di chuyển), nhưng trong trường hợp này, bạn cũng đánh dấu vào hộp cho thành phần CartItemWithFruittie.
  3. a25022cce5cee5e0.png Tiếp tục bằng cách chọn biểu tượng ... rồi chọn nhóm tài nguyên commonMain giống như bạn đã làm cho tệp Fruittie.kt.

Di chuyển DAO và AppDatabase

Làm theo các bước tương tự cho những tệp sau (bạn có thể chọn cả 3 tệp cùng lúc):

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

Cập nhật AppDatabase dùng chung để hoạt động trên nhiều nền tảng

Giờ đây, khi đã di chuyển các lớp cơ sở dữ liệu sang mô-đun :shared, bạn cần điều chỉnh các lớp này để tạo cách triển khai bắt buộc trên cả hai nền tảng.

  1. Mở tệp /shared/src/commonMain/kotlin/com/example/fruitties/kmptutorial/android/database/AppDatabase.kt.
  2. Thêm cách triển khai RoomDatabaseConstructor như sau:
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. Chú giải lớp AppDatabase này bằng @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

Di chuyển tính năng tạo cơ sở dữ liệu sang mô-đun :shared

Tiếp theo, bạn di chuyển cách thiết lập Room dành riêng cho Android từ mô-đun :androidApp sang mô-đun :shared. Điều này là cần thiết vì trong bước tiếp theo, bạn sẽ nhận được phần phụ thuộc Room từ mô-đun :androidApp.

  1. Tìm tệp androidApp/.../di/DatabaseModule.kt.
  2. Chọn nội dung của hàm providesAppDatabase, nhấp chuột phải và chọn Refactor > Extract Function to Scope (Tái cấu trúc > Trích xuất hàm sang phạm vi): da4d97319f9a0e8c.png
  3. Chọn DatabaseModule.kt trong trình đơn này. 5e540a1eec6e3493.png Thao tác này sẽ di chuyển nội dung sang một hàm appDatabase toàn cục. Nhấn Enter để xác nhận tên hàm. e2fb113d66704a36.png
  4. Đặt hàm ở chế độ công khai bằng cách xoá đối tượng sửa đổi chế độ hiển thị private.
  5. Di chuyển hàm vào mô-đun :shared bằng cách nhấp chuột phải vào Refactor > Move (Tái cấu trúc > Di chuyển).
  6. Trong hộp thoại Move (Di chuyển), hãy chọn biểu tượng ... bên cạnh trường Destination directory (Thư mục đích). e2101005f2ef4747.png
  7. Trong hộp thoại Choose Destination Directory (Chọn thư mục đích), hãy chọn nhóm tài nguyên shared >androidMain, sau đó chọn thư mục /shared/src/androidMain/ rồi nhấp vào OK.73d244941c68dc85.png
  8. Thay đổi hậu tố trong trường To package (Cần đóng gói) từ .di thành .databaseac5cf30d32871e2c.png
  9. Nhấp vào Refactor (Tái cấu trúc).

Dọn dẹp mã không cần thiết khỏi :androidApp

Tại thời điểm này, bạn đã di chuyển cơ sở dữ liệu Room sang mô-đun đa nền tảng và không cần phần phụ thuộc Room nào trong mô-đun :androidApp. Vì vậy, bạn có thể xoá các phần phụ thuộc đó.

  1. Mở tệp build.gradle.kts trong mô-đun :androidApp.
  2. Xoá các phần phụ thuộc và cấu hình như trong đoạn mã sau:
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 sẽ đồng bộ hoá dự án này.

Tạo và chạy ứng dụng Android

Chạy ứng dụng Fruitties trên Android để đảm bảo ứng dụng này chạy đúng cách và hiện sử dụng cơ sở dữ liệu của mô-đun :shared. Nếu đã từng thêm các mặt hàng vào giỏ hàng, thì bạn cũng sẽ thấy các mặt hàng đó tại thời điểm này, mặc dù cơ sở dữ liệu Room hiện nằm trong mô-đun :shared.

6. Chuẩn bị Room để sử dụng trên iOS

Để chuẩn bị thêm cơ sở dữ liệu Room cho nền tảng iOS, bạn cần thiết lập một số mã hỗ trợ trong mô-đun :shared để sử dụng ở bước tiếp theo.

Bật tính năng tạo cơ sở dữ liệu cho ứng dụng iOS

Việc đầu tiên cần làm là thêm trình tạo cơ sở dữ liệu dành riêng cho iOS.

  1. Thêm một tệp mới trong mô-đun :shared ở nhóm tài nguyên iosMain có tên là AppDatabase.ios.kt: dcb46ba560298865.png
  2. Thêm các hàm trợ giúp sau. Ứng dụng iOS sẽ sử dụng các hàm này để lấy một thực thể của cơ sở dữ liệu 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")
        }
    }
}

Thêm hậu tố "Entity" vào các thực thể Room

Vì bạn đang thêm trình bao bọc cho các thực thể Room trong Swift, nên tốt nhất là tên của các thực thể Room phải khác với tên của trình bao bọc. Chúng ta sẽ đảm bảo điều đó bằng cách thêm hậu tố Entity vào các thực thể Room bằng nội dung chú giải @ObjCName.

Mở tệp Fruittie.kt trong mô-đun :shared rồi thêm nội dung chú giải @ObjCName vào thực thể Fruittie. Vì nội dung chú giải này đang trong giai đoạn thử nghiệm nên bạn có thể cần thêm nội dung chú giải @OptIn(ExperimentalObjC::class) vào tệp.

Fruittie.kt

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

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

Sau đó, hãy làm tương tự cho thực thể CartItem trong tệp CartItem.kt.

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. Sử dụng Room trong ứng dụng iOS

Ứng dụng iOS là một ứng dụng có sẵn sử dụng Core Data. Trong lớp học lập trình này, bạn sẽ không phải lo lắng về việc di chuyển bất kỳ dữ liệu hiện có nào trong cơ sở dữ liệu vì ứng dụng này chỉ là một nguyên mẫu. Nếu đang di chuyển một ứng dụng phát hành chính thức sang KMP, bạn sẽ phải viết các hàm để đọc cơ sở dữ liệu Core Data hiện tại và chèn các mục đó vào cơ sở dữ liệu Room trong lần chạy đầu tiên sau khi di chuyển.

Mở dự án Xcode

Mở dự án iOS trong Xcode bằng cách chuyển đến thư mục /iosApp/ và mở Fruitties.xcodeproj trong ứng dụng được liên kết.

54836291a243ebe9.png

Xoá các lớp thực thể Core Data

Trước tiên, bạn cần xoá các lớp thực thể Core Data để tạo không gian cho các trình bao bọc thực thể mà bạn sẽ tạo sau. Tuỳ thuộc vào loại dữ liệu mà ứng dụng của bạn lưu trữ trong Core Data, bạn có thể xoá hoàn toàn các thực thể Core Data hoặc giữ lại các thực thể đó cho mục đích di chuyển dữ liệu. Trong hướng dẫn này, bạn chỉ cần xoá các thực thể đó vì không cần di chuyển dữ liệu hiện có.

Trong Xcode:

  1. Chuyển đến Project Navigator (Trình điều hướng dự án).
  2. Chuyển đến thư mục Resources (Tài nguyên).
  3. Mở tệp Fruitties.
  4. Nhấp vào và xoá từng thực thể.

7ad742d991d76b1c.png

Để áp dụng các thay đổi này trong mã, hãy dọn dẹp và tạo lại dự án.

Việc này dự kiến sẽ khiến quá trình tạo không thành công với các lỗi sau.

e3e107bf0387eeab.png

Tạo trình bao bọc thực thể

Tiếp theo, chúng ta sẽ tạo trình bao bọc thực thể cho các thực thể FruittieCartItem để xử lý trơn tru sự khác biệt về API giữa các thực thể Room và Core Data.

Các trình bao bọc này sẽ giúp chúng ta chuyển đổi từ Core Data sang Room bằng cách giảm thiểu lượng mã cần cập nhật ngay lập tức. Bạn nên thay thế các trình bao bọc này bằng quyền truy cập trực tiếp vào các thực thể Room trong tương lai.

Hiện tại, chúng ta sẽ tạo một trình bao bọc cho lớp FruittieEntity để cung cấp cho lớp này các thuộc tính không bắt buộc.

Tạo trình bao bọc FruittieEntity

  1. Tạo một tệp Swift mới trong thư mục Sources/Repository bằng cách nhấp chuột phải vào tên thư mục rồi chọn New File from Template… (Tạo tệp mới từ mẫu…) cce140b2fb3c2da8.png 6a0d4fa4292ddd4f.png
  2. Đặt tên tệp là Fruittie và đảm bảo rằng bạn chỉ chọn mục tiêu Fruitties chứ không phải mục tiêu kiểm thử. 827b9019b0a32352.png
  3. Thêm mã sau vào tệp Fruittie mới:
import sharedKit

struct Fruittie: Hashable {
   let entity: FruittieEntity

   var id: Int64 {
       entity.id
   }

   var name: String? {
       entity.name
   }

   var fullName: String? {
       entity.fullName
   }
}

Cấu trúc Fruittie bao bọc lớp FruittieEntity, làm cho các thuộc tính trở thành không bắt buộc và truyền qua các thuộc tính của thực thể. Ngoài ra, chúng ta sẽ tạo cấu trúc Fruittie tuân thủ giao thức Hashable để có thể sử dụng trong khung hiển thị ForEach của SwiftUI.

Tạo trình bao bọc CartItemEntity

Tiếp theo, hãy tạo một trình bao bọc tương tự cho lớp CartItemEntity.

Tạo một tệp Swift mới có tên CartItem.swift trong thư mục Sources/Repository.

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)
   }
}

Vì lớp CartItem ban đầu cho Core Data có thuộc tính Fruittie nên chúng tôi cũng đã đưa thuộc tính Fruittie vào cấu trúc CartItem. Mặc dù lớp CartItem không có thuộc tính không bắt buộc, thuộc tính count có một loại khác trong thực thể Room.

Cập nhật kho lưu trữ

Giờ đây, khi đã có trình bao bọc thực thể, bạn cần cập nhật DefaultCartRepositoryDefaultFruittieRepository để sử dụng Room thay vì Core Data.

Cập nhật DefaultCartRepository

Hãy bắt đầu với lớp DefaultCartRepository vì lớp này đơn giản hơn so với lớp còn lại.

Mở tệp CartRepository.swift trong thư mục Sources/Repository.

  1. Trước tiên, hãy thay thế lệnh nhập CoreData bằng sharedKit:
import sharedKit
  1. Sau đó, xoá thuộc tính NSManagedObjectContext và thay thế bằng thuộc tính CartDao:
// Remove
private let managedObjectContext: NSManagedObjectContext

// Replace with
private let cartDao: any CartDao
  1. Cập nhật hàm khởi tạo init để khởi tạo thuộc tính cartDao mới:
init(cartDao: any CartDao) {
    self.cartDao = cartDao
}
  1. Tiếp theo, hãy cập nhật phương thức addToCart. Phương thức này cần phải lấy các mặt hàng hiện có trong giỏ hàng từ Core Data, nhưng cách triển khai Room của chúng ta không yêu cầu điều này. Thay vào đó, hệ thống sẽ chèn một mặt hàng mới hoặc tăng số lượng của một mặt hàng hiện có trong giỏ hàng.
func addToCart(fruittie: Fruittie) async throws {
    try await cartDao.insertOrIncreaseCount(fruittie: fruittie.entity)
}
  1. Cuối cùng, hãy cập nhật phương thức getCartItems(). Phương thức này sẽ gọi phương thức getAll() trên CartDao và liên kết các thực thể CartItemWithFruittie với trình bao bọc CartItem của chúng ta.
func getCartItems() -> AsyncStream<[CartItem]> {
    return cartDao.getAll().map { entities in
        entities.map(CartItem.init(entity:))
    }.eraseToStream()
}

Cập nhật DefaultFruittieRepository

Để di chuyển lớp DefaultFruittieRepository, chúng ta áp dụng các thay đổi tương tự như đã thực hiện cho DefaultCartRepository.

Cập nhật tệp FruittieRepository như sau:

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()
    }
}

Thay thế trình bao bọc thuộc tính @FetchRequest

Chúng ta cũng cần thay đổi trình bao bọc thuộc tính @FetchRequest trong khung hiển thị SwiftUI. Trình bao bọc thuộc tính @FetchRequest được dùng để tìm nạp dữ liệu từ Core Data và quan sát các thay đổi. Vì vậy, chúng ta không thể sử dụng trình bao bọc này với các thực thể Room. Thay vào đó, hãy sử dụng UIModel để truy cập vào dữ liệu trong các kho lưu trữ.

  1. Mở CartView trong tệp Sources/UI/CartView.swift.
  2. Thay thế cách triển khai bằng nội dung sau:
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)")
                    }
                }
            }
        }
    }
}

Cập nhật ContentView

Cập nhật ContentView trong tệp Sources/View/ContentView.swift để truyền FruittieUIModel đến CartView.

CartView(uiModel: uiModel)

Đang cập nhật DataController

Lớp DataController trong ứng dụng iOS chịu trách nhiệm thiết lập ngăn xếp Core Data. Vì sẽ không sử dụng Core Data nữa nên chúng ta cần cập nhật DataController để khởi chạy cơ sở dữ liệu Room.

  1. Mở tệp DataController.swift trong Sources/Database.
  2. Thêm lệnh nhập sharedKit.
  3. Xoá lệnh nhập CoreData.
  4. Tạo thực thể cơ sở dữ liệu Room trong lớp DataController.
  5. Cuối cùng, hãy xoá lệnh gọi phương thức loadPersistentStores khỏi trình khởi chạy DataController.

Lớp hoàn thiện sẽ có dạng như sau:

import Combine
import sharedKit

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

Cập nhật tính năng chèn phần phụ thuộc

Lớp AppContainer trong ứng dụng iOS chịu trách nhiệm khởi tạo biểu đồ phần phụ thuộc. Vì đã cập nhật kho lưu trữ để sử dụng Room thay vì Core Data, nên chúng ta cần cập nhật AppContainer để truyền DAO Room đến kho lưu trữ.

  1. Mở AppContainer.swift trong thư mục Sources/DI.
  2. Thêm lệnh nhập sharedKit.
  3. Xoá thuộc tính managedObjectContext khỏi lớp AppContainer.
  4. Thay đổi quá trình khởi chạy DefaultFruittieRepositoryDefaultCartRepository bằng cách truyền vào DAO Room từ thực thể AppDatabase do DataController cung cấp.

Khi bạn hoàn tất, lớp sẽ có dạng như sau:

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()
        )
    }
}

Cuối cùng, hãy cập nhật FruittiesApp trong main.swift để xoá managedObjectContext:

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

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

Tạo và chạy ứng dụng iOS

Cuối cùng, sau khi bạn tạo và chạy ứng dụng bằng cách nhấn tổ hợp phím ⌘R, ứng dụng sẽ bắt đầu với cơ sở dữ liệu được di chuyển từ Core Data sang Room.

5d2ae9438747f8f6.png

8. Xin chúc mừng

Xin chúc mừng! Bạn đã di chuyển thành công các ứng dụng Android và iOS độc lập sang một lớp dữ liệu dùng chung bằng cách sử dụng Room KMP.

Để tham khảo, dưới đây là bảng so sánh các cấu trúc ứng dụng để xem chúng ta đã đạt được những gì:

Trước

Android

iOS

Sơ đồ cấu trúc của ứng dụng Android trước khi tích hợp KMP

Sơ đồ cấu trúc của ứng dụng iOS trước khi tích hợp KMP

Cấu trúc sau khi di chuyển

bcd8c29b00f67c19.png

Tìm hiểu thêm