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:
- Documentação de configuração de KMP da Room
- Documentação de KMP do SQLite (link em inglês)
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
}