Migrar a Room para o Kotlin Multiplatform

Este documento descreve como migrar uma implementação existente do Room para uma que usa o Kotlin Multiplatform (KMP).

Migração de usos do Room em uma base de código Android para um KMP compartilhado comum pode variar muito de dificuldade, dependendo das APIs do Room usadas ou se a base de código já usa corrotinas. Esta seção oferece algumas orientações e dicas ao tentar migrar os usos do Room para um módulo comum.

É importante primeiro se familiarizar com as diferenças e perder entre a versão Android do Room e a versão KMP, bem como a configuração envolvida. Basicamente, uma migração bem-sucedida envolve a refatoração usos das APIs SupportSQLite* e substituí-las pelas APIs de driver do SQLite junto com as declarações do Room (a classe com a anotação @Database, os DAOs, entidades e assim por diante) em código comum.

Revise as seguintes informações antes de continuar:

As próximas seções descrevem as várias etapas necessárias para um sucesso migração.

Migrar do suporte do SQLite para o driver do SQLite

As APIs em androidx.sqlite.db são apenas para Android, e qualquer uso precisa ser refatorado com as APIs de driver do SQLite. Para compatibilidade com versões anteriores, e desde que o RoomDatabase é configurado com um SupportSQLiteOpenHelper.Factory (ou seja, nenhuma SQLiteDriver estiver definida), o Room vai se comportar no "modo de compatibilidade". em que as APIs de suporte do SQLite e do SQLite funcionam conforme o esperado. Isso permite migrações incrementais para que não seja preciso converter todo o SQLite de suporte. de uso para o driver SQLite em uma única mudança.

Os exemplos a seguir são usos comuns do Support SQLite e do SQLite deles. Contrapartes do motorista:

Suporte a SQLite (de)

Executar uma consulta sem resultado

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

Executar uma consulta com resultado, mas sem argumentos

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

Executar uma consulta com resultado e argumentos

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

Driver SQLite (para)

Executar uma consulta sem resultado

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

Executar uma consulta com resultado, mas sem argumentos

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

Executar uma consulta com resultado e argumentos

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

As APIs de transação de banco de dados estão disponíveis diretamente no SupportSQLiteDatabase com beginTransaction(), setTransactionSuccessful() e endTransaction(). Eles também estão disponíveis no Room usando runInTransaction(). Migrar estes para APIs de driver do SQLite.

Suporte a SQLite (de)

Realizar uma transação (usando RoomDatabase)

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

Realizar uma transação (usando SupportSQLiteDatabase)

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

Driver SQLite (para)

Realizar uma transação (usando RoomDatabase)

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

Realizar uma transação (usando 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")
}

Várias substituições de callback também precisam ser migradas para os equivalentes do driver:

Suporte a SQLite (de)

Subclasses de migração

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

Subclasses de especificação de migração automática

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

Subclasses de callback do banco de dados

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

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

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

Driver SQLite (para)

Subclasses de migração

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

Subclasses de especificação de migração automática

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

Subclasses de callback do banco de dados

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

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

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

Para resumir, substitua os usos de SQLiteDatabase por SQLiteConnection quando uma RoomDatabase não está disponível, como em substituições de callback (onMigrate, onCreate etc). Se um RoomDatabase estiver disponível, acesse o conexão de banco de dados usando RoomDatabase.useReaderConnection e RoomDatabase.useWriterConnection em vez de RoomDatabase.openHelper.writtableDatabase.

Converter funções DAO de bloqueio em funções de suspensão

A versão KMP do Room depende de corrotinas para realizar E/S. operações no CoroutineContext configurado. Isso significa que você é necessário migrar as funções DAO de bloqueio para suspender funções.

Função DAO de bloqueio (de)

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

Suspendendo função DAO (para)

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

A migração de funções de bloqueio DAO existentes para funções de suspensão pode ser será complicado se a base de código já existente não incorporar corrotinas. Consulte Corrotinas no Android para começar a usar corrotinas. na sua base de código.

Converter tipos de retorno reativos em Flow

Nem todas as funções DAO precisam ser de suspensão. Funções DAO que retornam tipos reativos, como LiveData ou Flowable do RxJava, não podem ser convertidos para suspender funções. Alguns tipos, no entanto, como LiveData não são KMP compatíveis. As funções DAO com tipos de retorno reativos precisam ser migradas fluxos de corrotinas.

Tipo de KMP incompatível (de)

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

Tipo de KMP compatível (para)

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

Consulte Fluxos no Android para começar a usar fluxos no seu de código aberto.

Definir um contexto de corrotina (opcional)

Um RoomDatabase pode ser configurado com um aplicativo compartilhado executores usando RoomDatabase.Builder.setQueryExecutor() para executar o banco de dados as operações. Como os executores não são compatíveis com o KMP, o setQueryExecutor() do Room A API não está disponível para fontes comuns. Em vez disso, a RoomDatabase precisa ser configurado com um CoroutineContext. É possível definir um contexto usando RoomDatabase.Builder.setCoroutineContext(), se nenhuma for definida, o O padrão RoomDatabase vai usar Dispatchers.IO.

Definir um driver SQLite

Depois que os usos de suporte do SQLite tiverem sido migrados para as APIs de driver do SQLite, um o driver precisa ser configurado usando RoomDatabase.Builder.setDriver. A driver recomendado é BundledSQLiteDriver. Consulte Implementações de drivers para descrições de implementações de drivers disponíveis.

SupportSQLiteOpenHelper.Factory personalizado configurado usando RoomDatabase.Builder.openHelperFactory() não forem compatíveis com KMP, os atributos fornecidos pelo assistente aberto personalizado precisarão ser reimplementados Interfaces de driver do SQLite.

Mover declarações de Room

Depois que a maioria das etapas da migração for concluída, será possível mover a biblioteca Room e definições a um conjunto de origem comum. As estratégias expect / actual podem ser usado para mover de maneira incremental as definições relacionadas ao Room. Por exemplo, se não todos as funções DAO de bloqueio podem ser migradas para funções de suspensão, é possível declarar uma interface com a anotação expect @Dao que está vazia em código comum, mas contém funções de bloqueio no 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
}