1. Trước khi bắt đầu
Điều kiện tiên quyết
- Có hiểu biết cơ bản về Kotlin Multiplatform.
- Có kinh nghiệm sử dụng Kotlin.
- Có hiểu biết cơ bản về cú pháp Swift.
- Đã cài đặt Xcode và trình mô phỏng iOS.
Những gì bạn cần
- Phiên bản ổn định mới nhất của Android Studio.
- Máy Mac chạy hệ điều hành macOS.
- Xcode 16.1 và trình mô phỏng iPhone chạy iOS 16.0 trở lê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:
- 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:
- 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.
- 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.
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.
Cấu trúc ứng dụng iOS
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-runtime
và sqlite-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.
- Mở tệp
DatabaseModule.kt
có trongandroidApp/src/main/kotlin/com/example/fruitties/kmptutorial/android/di/DatabaseModule.kt
- 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.
- Thêm trình bổ trợ KSP và Room:
plugins {
...
// TODO add KSP + ROOM plugins
alias(libs.plugins.ksp)
alias(libs.plugins.room)
}
- Thêm các phần phụ thuộc
room-runtime
vàsqlite-bundled
vào khốicommonMain
:
sourceSets {
commonMain {
// TODO Add KMP dependencies here
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.sqlite.bundled)
}
}
- 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)
}
- 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")
}
- 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ừ:
Đến:
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:
- 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): - 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). - 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ó).
- Nhấp vào nút Refactor (Tái cấu trúc) để di chuyển tệp.
Di chuyển mô hình CartItem
và CartItemWithFruittie
Đối với tệp androidApp/.../model/CartItem.kt
, bạn cần làm theo các bước sau:
- 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). - 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
. Tiếp tục bằng cách chọn biểu tượng
...
rồi chọn nhóm tài nguyêncommonMain
giống như bạn đã làm cho tệpFruittie.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.
- Mở tệp
/shared/src/commonMain/kotlin/com/example/fruitties/kmptutorial/android/database/AppDatabase.kt
. - 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
}
- 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() {
...
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
.
- Tìm tệp
androidApp/.../di/DatabaseModule.kt
. - 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): - Chọn
DatabaseModule.kt
trong trình đơn này.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. - Đặt hàm ở chế độ công khai bằng cách xoá đối tượng sửa đổi chế độ hiển thị
private
. - 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). - 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).
- 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.
- Thay đổi hậu tố trong trường To package (Cần đóng gói) từ
.di
thành.database
- 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 đó.
- Mở tệp
build.gradle.kts
trong mô-đun:androidApp
. - 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")
}
- 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.
- Thêm một tệp mới trong mô-đun
:shared
ở nhóm tài nguyêniosMain
có tên làAppDatabase.ios.kt
: - 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.
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:
- Chuyển đến Project Navigator (Trình điều hướng dự án).
- Chuyển đến thư mục Resources (Tài nguyên).
- Mở tệp
Fruitties
. - Nhấp vào và xoá từng thực thể.
Để á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.
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ể Fruittie
và CartItem
để 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
- 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…) - Đặ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ử. - 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 DefaultCartRepository
và DefaultFruittieRepository
để 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
.
- Trước tiên, hãy thay thế lệnh nhập
CoreData
bằngsharedKit
:
import sharedKit
- Sau đó, xoá thuộc tính
NSManagedObjectContext
và thay thế bằng thuộc tínhCartDao
:
// Remove
private let managedObjectContext: NSManagedObjectContext
// Replace with
private let cartDao: any CartDao
- Cập nhật hàm khởi tạo
init
để khởi tạo thuộc tínhcartDao
mới:
init(cartDao: any CartDao) {
self.cartDao = cartDao
}
- 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)
}
- 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ứcgetAll()
trênCartDao
và liên kết các thực thểCartItemWithFruittie
với trình bao bọcCartItem
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ữ.
- Mở
CartView
trong tệpSources/UI/CartView.swift
. - 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.
- Mở tệp
DataController.swift
trongSources/Database
. - Thêm lệnh nhập
sharedKit
. - Xoá lệnh nhập
CoreData
. - Tạo thực thể cơ sở dữ liệu Room trong lớp
DataController
. - Cuối cùng, hãy xoá lệnh gọi phương thức
loadPersistentStores
khỏi trình khởi chạyDataController
.
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ữ.
- Mở
AppContainer.swift
trong thư mụcSources/DI
. - Thêm lệnh nhập
sharedKit
. - Xoá thuộc tính
managedObjectContext
khỏi lớpAppContainer
. - Thay đổi quá trình khởi chạy
DefaultFruittieRepository
vàDefaultCartRepository
bằng cách truyền vào DAO Room từ thực thểAppDatabase
doDataController
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.
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 |
Cấu trúc sau khi di chuyển
Tìm hiểu thêm
- Tìm hiểu những thư viện Jetpack khác hỗ trợ KMP.
- Đọc tài liệu về Room KMP.
- Đọc tài liệu về SQLite KMP.
- Xem tài liệu chính thức về Kotlin Multiplatform.