Dokumen ini menjelaskan cara memigrasikan implementasi Room yang ada ke implementasi yang menggunakan Multiplatform (KMP) Kotlin.
Memigrasikan penggunaan Room di codebase Android yang ada ke modul KMP bersama umum dapat sangat bervariasi tingkat kesulitannya, bergantung pada Room API yang digunakan atau jika codebase sudah menggunakan Coroutine. Bagian ini menawarkan beberapa panduan dan tips saat mencoba memigrasikan penggunaan Room ke modul umum.
Penting untuk terlebih dahulu memahami perbedaan dan fitur yang hilang
antara Room versi Android dan versi KMP beserta
penyiapan yang digunakan. Pada intinya, migrasi yang berhasil melibatkan pemfaktoran ulang
penggunaan SupportSQLite*
API dan menggantinya dengan SQLite Driver API
serta memindahkan deklarasi Room (class beranotasi @Database
, DAO,
entity, dan sebagainya) ke dalam kode umum.
Lihat kembali informasi berikut sebelum melanjutkan:
Bagian selanjutnya menjelaskan berbagai langkah yang diperlukan agar migrasi berhasil.
Bermigrasi dari Support SQLite ke Driver SQLite
API di androidx.sqlite.db
hanya untuk Android, dan setiap penggunaan harus
difaktorkan ulang dengan SQLite Driver API. Untuk kompatibilitas mundur, dan selama
RoomDatabase
dikonfigurasi dengan SupportSQLiteOpenHelper.Factory
(yaitu
tidak ada SQLiteDriver
yang ditetapkan), Room akan berperilaku dalam 'mode kompatibilitas' dengan
Support SQLite dan SQLite Driver API berfungsi seperti yang diharapkan. Hal ini memungkinkan
migrasi inkremental sehingga Anda tidak perlu mengonversi semua penggunaan SQLite
Dukungan ke Driver SQLite dalam satu perubahan.
Contoh berikut adalah penggunaan umum Support SQLite dan penggunaan SQLite Driver:
Mendukung SQLite (dari)
Menjalankan kueri tanpa hasil
val database: SupportSQLiteDatabase = ...
database.execSQL("ALTER TABLE ...")
Menjalankan kueri dengan hasil, tetapi tanpa argumen
val database: SupportSQLiteDatabase = ...
database.query("SELECT * FROM Pet").use { cursor ->
while (cusor.moveToNext()) {
// read columns
cursor.getInt(0)
cursor.getString(1)
}
}
Menjalankan kueri dengan hasil dan argumen
database.query("SELECT * FROM Pet WHERE id = ?", id).use { cursor ->
if (cursor.moveToNext()) {
// row found, read columns
} else {
// row not found
}
}
Driver SQLite (ke)
Menjalankan kueri tanpa hasil
val connection: SQLiteConnection = ...
connection.execSQL("ALTER TABLE ...")
Menjalankan kueri dengan hasil, tetapi tanpa argumen
val connection: SQLiteConnection = ...
connection.prepare("SELECT * FROM Pet").use { statement ->
while (statement.step()) {
// read columns
statement.getInt(0)
statement.getText(1)
}
}
Menjalankan kueri dengan hasil dan argumen
connection.prepare("SELECT * FROM Pet WHERE id = ?").use { statement ->
statement.bindInt(1, id)
if (statement.step()) {
// row found, read columns
} else {
// row not found
}
}
API transaksi database tersedia langsung di SupportSQLiteDatabase
dengan beginTransaction()
, setTransactionSuccessful()
, dan endTransaction()
.
Tersedia juga melalui Room menggunakan runInTransaction()
. Migrasikan penggunaan
ini ke SQLite Driver API.
Mendukung SQLite (dari)
Melakukan transaksi (menggunakan RoomDatabase
)
val database: RoomDatabase = ...
database.runInTransaction {
// perform database operations in transaction
}
Melakukan transaksi (menggunakan SupportSQLiteDatabase
)
val database: SupportSQLiteDatabase = ...
database.beginTransaction()
try {
// perform database operations in transaction
database.setTransactionSuccessful()
} finally {
database.endTransaction()
}
Driver SQLite (ke)
Melakukan transaksi (menggunakan RoomDatabase
)
val database: RoomDatabase = ...
database.useWriterConnection { transactor ->
transactor.immediateTransaction {
// perform database operations in transaction
}
}
Melakukan transaksi (menggunakan SQLiteConnection
)
val connection: SQLiteConnection = ...
connection.execSQL("BEGIN IMMEDIATE TRANSACTION")
try {
// perform database operations in transaction
connection.execSQL("END TRANSACTION")
} catch(t: Throwable) {
connection.execSQL("ROLLBACK TRANSACTION")
}
Berbagai penggantian callback juga perlu dimigrasikan ke versi drivernya:
Mendukung SQLite (dari)
Subclass migrasi
object Migration_1_2 : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
// ...
}
}
Subclass spesifikasi migrasi otomatis
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
// ...
}
}
Subclass callback database
object MyRoomCallback : RoomDatabase.Callback {
override fun onCreate(db: SupportSQLiteDatabase) {
// ...
}
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
// ...
}
override fun onOpen(db: SupportSQLiteDatabase) {
// ...
}
}
Driver SQLite (ke)
Subclass migrasi
object Migration_1_2 : Migration(1, 2) {
override fun migrate(connection: SQLiteConnection) {
// ...
}
}
Subclass spesifikasi migrasi otomatis
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(connection: SQLiteConnection) {
// ...
}
}
Subclass callback database
object MyRoomCallback : RoomDatabase.Callback {
override fun onCreate(connection: SQLiteConnection) {
// ...
}
override fun onDestructiveMigration(connection: SQLiteConnection) {
// ...
}
override fun onOpen(connection: SQLiteConnection) {
// ...
}
}
Ringkasnya, ganti penggunaan SQLiteDatabase
, dengan SQLiteConnection
saat
RoomDatabase
tidak tersedia, seperti dalam penggantian callback (onMigrate
,
onCreate
, dll.). Jika RoomDatabase
tersedia, akses koneksi database pokok menggunakan RoomDatabase.useReaderConnection
dan RoomDatabase.useWriterConnection
, bukan RoomDatabase.openHelper.writtableDatabase
.
Mengonversi fungsi DAO pemblokir untuk menangguhkan fungsi
Room versi KMP mengandalkan coroutine untuk melakukan operasi I/O
pada CoroutineContext
yang dikonfigurasi. Ini berarti Anda
harus memigrasikan fungsi DAO yang memblokir untuk menangguhkan fungsi.
Memblokir fungsi DAO (dari)
@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>
Menangguhkan fungsi DAO (ke)
@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>
Migrasi fungsi pemblokiran DAO yang sudah ada ke fungsi penangguhan dapat menjadi rumit jika codebase yang ada belum menggabungkan coroutine. Lihat Coroutine di Android untuk mulai menggunakan coroutine di codebase Anda.
Mengonversi jenis nilai yang ditampilkan reaktif menjadi Flow
Tidak semua fungsi DAO perlu berupa fungsi penangguhan. Fungsi DAO yang menampilkan
jenis reaktif seperti LiveData
atau Flowable
RxJava tidak boleh dikonversi
untuk menangguhkan fungsi. Namun, beberapa jenis seperti LiveData
tidak kompatibel
dengan KMP. Fungsi DAO dengan jenis nilai yang ditampilkan reaktif harus dimigrasikan ke
alur coroutine.
Jenis KMP tidak kompatibel (dari)
@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>
Jenis KMP yang kompatibel (ke)
@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>
Lihat Flow di Android untuk mulai menggunakan Flow di codebase Anda.
Menetapkan konteks Coroutine (Opsional)
Secara opsional, RoomDatabase
dapat dikonfigurasi dengan eksekutor aplikasi
bersama menggunakan RoomDatabase.Builder.setQueryExecutor()
untuk menjalankan operasi
database. Karena eksekutor tidak kompatibel dengan KMP, setQueryExecutor()
API
Room tidak tersedia untuk sumber umum. Sebagai gantinya, RoomDatabase
harus
dikonfigurasi dengan CoroutineContext
. Konteks dapat disetel menggunakan RoomDatabase.Builder.setCoroutineContext()
. Jika tidak ada yang disetel, RoomDatabase
akan secara default menggunakan Dispatchers.IO
.
Menetapkan Driver SQLite
Setelah penggunaan SQLite Dukungan dimigrasikan ke SQLite Driver API,
driver harus dikonfigurasi menggunakan RoomDatabase.Builder.setDriver
. Driver
yang direkomendasikan adalah BundledSQLiteDriver
. Lihat Implementasi driver untuk
deskripsi implementasi driver yang tersedia.
SupportSQLiteOpenHelper.Factory
kustom yang dikonfigurasi menggunakan
RoomDatabase.Builder.openHelperFactory()
tidak didukung di KMP, fitur yang disediakan oleh open helper kustom harus diimplementasikan kembali dengan
antarmuka Driver SQLite.
Memindahkan pernyataan Room
Setelah sebagian besar langkah migrasi selesai, Anda dapat memindahkan definisi
Room ke set sumber umum. Perhatikan bahwa strategi expect
/ actual
dapat
digunakan untuk memindahkan definisi terkait Room secara bertahap. Misalnya, jika tidak semua
fungsi DAO pemblokir dapat dimigrasikan untuk menangguhkan fungsi, Anda dapat
mendeklarasikan antarmuka teranotasi expect
@Dao
yang kosong dalam kode umum, tetapi
berisi fungsi pemblokiran di Android.
// shared/src/commonMain/kotlin/Database.kt
@Database(entities = [TodoEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun getDao(): TodoDao
abstract fun getBlockingDao(): BlockingTodoDao
}
@Dao
interface TodoDao {
@Query("SELECT count(*) FROM TodoEntity")
suspend fun count(): Int
}
@Dao
expect interface BlockingTodoDao
// shared/src/androidMain/kotlin/BlockingTodoDao.kt
@Dao
actual interface BlockingTodoDao {
@Query("SELECT count(*) FROM TodoEntity")
fun count(): Int
}