Migracja sali do Kotlin Multiplaform

W tym dokumencie opisujemy, jak przenieść dotychczasową implementację do obsługi sali do takiej, która korzysta z usługi Kotlin Multiplatform (KMP).

Migracja przypadków użycia sal w istniejącej bazie kodu Androida do wspólnego współdzielonego modułu KMP może się znacznie różnić w zależności od używanych interfejsów API Room lub od tego, czy baza kodu korzysta już z koroutin. W tej sekcji znajdziesz wskazówki i wskazówki ułatwiające migrację wykorzystania sali do wspólnego modułu.

Warto najpierw zapoznać się z różnicami i brakującymi funkcjami między wersją Room na Androida a wersją KMP, a także związaną z konfiguracją. Ogólnie rzecz biorąc, udana migracja obejmuje refaktoryzację użycia interfejsów API SupportSQLite* i zastąpienie ich interfejsami API SQLite Driver oraz przeniesienie deklaracji dotyczących pokoju (klasa z adnotacjami @Database, DAO, encje itd.) do kodu wspólnego.

Zanim przejdziesz dalej, sprawdź te informacje:

W następnych sekcjach opisano różne kroki wymagane do udanej migracji.

Migracja z Support SQLite do sterownika SQLite

Interfejsy API w androidx.sqlite.db są przeznaczone tylko na Androida, a wszystkie ich zastosowania należy refaktoryzować za pomocą interfejsów SQLite Driver API. Aby zapewnić zgodność wsteczną, jeśli RoomDatabase ma skonfigurowany interfejs SupportSQLiteOpenHelper.Factory (czyli nie ma ustawionego SQLiteDriver), funkcja Room działa w „trybie zgodności”, w którym zarówno interfejsy API obsługujące SQLite, jak i SQLite Driver działają zgodnie z oczekiwaniami. Umożliwia to migracje przyrostowe, dzięki czemu w ramach jednej zmiany nie trzeba konwertować całego wykorzystania SQLite służącego do obsługi pomocy na sterownik SQLite.

Oto przykłady typowych zastosowań Support SQLite i ich odpowiedników sterowników SQLite:

Obsługuj SQLite (od)

Wykonaj zapytanie bez wyniku

val database: SupportSQLiteDatabase = ...
database.execSQL("ALTER TABLE ...")

Wykonaj zapytanie z wynikiem bez argumentów

val database: SupportSQLiteDatabase = ...
database.query("SELECT * FROM Pet").use { cursor ->
  while (cusor.moveToNext()) {
    // read columns
    cursor.getInt(0)
    cursor.getString(1)
  }
}

Wykonaj zapytanie z wynikiem i argumentami

database.query("SELECT * FROM Pet WHERE id = ?", id).use { cursor ->
  if (cursor.moveToNext()) {
    // row found, read columns
  } else {
    // row not found
  }
}

Sterownik SQLite (do)

Wykonaj zapytanie bez wyniku

val connection: SQLiteConnection = ...
connection.execSQL("ALTER TABLE ...")

Wykonaj zapytanie z wynikiem bez argumentów

val connection: SQLiteConnection = ...
connection.prepare("SELECT * FROM Pet").use { statement ->
  while (statement.step()) {
    // read columns
    statement.getInt(0)
    statement.getText(1)
  }
}

Wykonaj zapytanie z wynikiem i argumentami

connection.prepare("SELECT * FROM Pet WHERE id = ?").use { statement ->
  statement.bindInt(1, id)
  if (statement.step()) {
    // row found, read columns
  } else {
    // row not found
  }
}

Interfejsy API transakcji bazy danych są dostępne bezpośrednio w SupportSQLiteDatabase za pomocą beginTransaction(), setTransactionSuccessful() i endTransaction(). Są też dostępne w pokoju, korzystając z usługi runInTransaction(). Przeprowadź migrację tych zastosowań do interfejsów API sterowników SQLite.

Obsługuj SQLite (od)

Realizacja transakcji (przy użyciu: RoomDatabase)

val database: RoomDatabase = ...
database.runInTransaction {
  // perform database operations in transaction
}

Realizacja transakcji (przy użyciu: SupportSQLiteDatabase)

val database: SupportSQLiteDatabase = ...
database.beginTransaction()
try {
  // perform database operations in transaction
  database.setTransactionSuccessful()
} finally {
  database.endTransaction()
}

Sterownik SQLite (do)

Realizacja transakcji (przy użyciu: RoomDatabase)

val database: RoomDatabase = ...
database.useWriterConnection { transactor ->
  transactor.immediateTransaction {
    // perform database operations in transaction
  }
}

Realizacja transakcji (przy użyciu: 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")
}

Różne zastąpienia wywołań zwrotnych również trzeba przenieść do ich odpowiedników:

Obsługuj SQLite (od)

Podklasy migracji

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(db: SupportSQLiteDatabase) {
    // ...
  }
}

Podklasy specyfikacji automatycznej migracji

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(db: SupportSQLiteDatabase) {
    // ...
  }
}

Podklasy wywołań zwrotnych bazy danych

object MyRoomCallback : RoomDatabase.Callback {
  override fun onCreate(db: SupportSQLiteDatabase) {
    // ...
  }

  override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
    // ...
  }

  override fun onOpen(db: SupportSQLiteDatabase) {
    // ...
  }
}

Sterownik SQLite (do)

Podklasy migracji

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(connection: SQLiteConnection) {
    // ...
  }
}

Podklasy specyfikacji automatycznej migracji

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(connection: SQLiteConnection) {
    // ...
  }
}

Podklasy wywołań zwrotnych bazy danych

object MyRoomCallback : RoomDatabase.Callback {
  override fun onCreate(connection: SQLiteConnection) {
    // ...
  }

  override fun onDestructiveMigration(connection: SQLiteConnection) {
    // ...
  }

  override fun onOpen(connection: SQLiteConnection) {
    // ...
  }
}

Podsumowując, zamień przypadki użycia SQLiteDatabase na SQLiteConnection, gdy tag RoomDatabase jest niedostępny – na przykład w zastąpieniach wywołań zwrotnych (onMigrate, onCreate itp.). Jeśli dostępny jest interfejs RoomDatabase, uzyskaj dostęp do bazowego połączenia z bazą danych za pomocą interfejsów RoomDatabase.useReaderConnection i RoomDatabase.useWriterConnection zamiast RoomDatabase.openHelper.writtableDatabase.

Konwertowanie blokujących funkcji DAO na zawieszanie funkcji

Wersja KMP sali korzysta z współprogramów do wykonywania operacji wejścia-wyjścia w skonfigurowanej CoroutineContext. Oznacza to, że musisz przenieść wszystkie blokujące funkcje DAO, aby zawiesić funkcje.

Blokowanie funkcji DAO (od)

@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>

Zawieszam funkcję DAO (to)

@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>

Migracja istniejących funkcji blokujących DAO w celu zawieszania funkcji może być skomplikowana, jeśli istniejąca baza kodu nie zawiera jeszcze współprogramów. Informacje o tym, jak używać współprogramów w bazie kodu, znajdziesz w artykule Korutyny na Androidzie.

Przekształć reaktywne typy zwrotów w przepływ

Nie wszystkie funkcje DAO muszą być funkcjami zawieszania. Funkcji DAO, które zwracają typy reaktywne, takie jak LiveData czy Flowable RxJava, nie należy konwertować w celu zawieszenia funkcji. Jednak niektóre typy, takie jak LiveData, nie są zgodne z KMP. Funkcje DAO z reaktywnymi typami zwracanych muszą zostać przeniesione do przepływów współużytkowania.

Niezgodny typ KMP (z)

@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>

Zgodny typ KMP (do)

@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>

Jeśli chcesz zacząć używać przepływów w swojej bazie kodu, zapoznaj się z dokumentacją Flows na Androidzie.

Ustaw kontekst korespondencyjny (opcjonalnie)

RoomDatabase można opcjonalnie skonfigurować z wykonaniami udostępnionych aplikacji za pomocą RoomDatabase.Builder.setQueryExecutor() do wykonywania operacji na bazie danych. Wykonawcy nie są kompatybilni z KMP, dlatego interfejs Room setQueryExecutor() API jest niedostępny w przypadku popularnych źródeł. Zamiast tego RoomDatabase należy skonfigurować za pomocą CoroutineContext. Kontekst można ustawić za pomocą parametru RoomDatabase.Builder.setCoroutineContext(). Jeśli żaden nie jest ustawiony, RoomDatabase domyślnie używa atrybutu Dispatchers.IO.

Ustaw sterownik SQLite

Po przeniesieniu przypadków użycia Support SQLite do interfejsów API sterownika SQLite trzeba skonfigurować sterownik za pomocą funkcji RoomDatabase.Builder.setDriver. Zalecany sterownik to BundledSQLiteDriver. Opis dostępnych implementacji sterownika znajdziesz w sekcji Implementacje sterowników.

Niestandardowe obiekty SupportSQLiteOpenHelper.Factory skonfigurowane za pomocą RoomDatabase.Builder.openHelperFactory() nie są obsługiwane w KMP. Funkcje udostępniane przez niestandardową otwartą aplikację pomocniczą trzeba ponownie zaimplementować za pomocą interfejsów sterownika SQLite.

Przenieś deklaracje dotyczące pokoju

Po ukończeniu większości kroków migracji można przenieść definicje sal do wspólnego zbioru źródłowego. Pamiętaj, że do stopniowego przenoszenia definicji związanych z pokojami można używać strategii expect / actual. Jeśli na przykład nie wszystkie funkcje blokujące DAO można przenieść w celu zawieszenia funkcji, można zadeklarować interfejs z adnotacjami expect @Dao, który jest pusty w kodzie wspólnym, ale zawiera funkcje blokowania w Androidzie.

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