Este documento descreve como migrar uma implementação do Room para uma que usa o Kotlin Multiplatform (KMP).
A migração dos usos do Room em uma base de código do Android para um módulo KMP compartilhado pode variar muito, 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 para tentar migrar os usos do Room para um módulo comum.
É importante se familiarizar primeiro com as diferenças e os recursos
que estão faltando entre as versões do Room e do KMP para Android, além da
configuração envolvida. Basicamente, uma migração bem-sucedida envolve a refatoração
do uso das APIs SupportSQLite*
e a substituição delas por APIs SQLite Driver, além da movimentação de declarações do Room (classe anotada @Database
, DAOs, entidades e assim por diante) para um código comum.
Consulte as seguintes informações antes de continuar:
As próximas seções descrevem as várias etapas necessárias para uma migração bem-sucedida.
Migrar do driver SQLite para SQLite
As APIs em androidx.sqlite.db
são apenas do Android, e qualquer uso precisa ser
refatorado com APIs de driver SQLite. Para compatibilidade com versões anteriores, e desde que
o RoomDatabase
esteja configurado com um SupportSQLiteOpenHelper.Factory
(ou seja,
nenhum SQLiteDriver
esteja definido), o Room se comporta no "modo de compatibilidade" em que
as APIs de driver SQLite e SQLite funcionam conforme o esperado. Isso permite
migrações incrementais para que você não precise converter todos os usos do Support SQLite
para o driver SQLite em uma única mudança.
Os exemplos abaixo são usos comuns do Support SQLite e dos correspondentes do driver SQLite:
Compatibilidade com SQLite (de)
Executar uma consulta sem resultado
val database: SupportSQLiteDatabase = ...
database.execSQL("ALTER TABLE ...")
Executar uma consulta com o 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 o 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 do banco de dados estão disponíveis diretamente em SupportSQLiteDatabase
com
beginTransaction()
, setTransactionSuccessful()
e endTransaction()
.
Elas também estão disponíveis no Room usando o runInTransaction()
. Migre esses
usos para as APIs de driver do SQLite.
Compatibilidade com 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")
}
Também é necessário migrar várias substituições de callback para os drivers:
Compatibilidade com 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 um
RoomDatabase
não estiver disponível, como em substituições de callback (onMigrate
,
onCreate
etc.). Se um RoomDatabase
estiver disponível, acesse a conexão
do banco de dados usando RoomDatabase.useReaderConnection
e
RoomDatabase.useWriterConnection
em vez de
RoomDatabase.openHelper.writtableDatabase
.
Converter funções DAO de bloqueio para suspender funções.
A versão KMP do Room depende de corrotinas para executar operações
de E/S no CoroutineContext
configurado. Isso significa que é
necessário migrar todas as funções DAO de bloqueio para suspender funções.
Como bloquear a função DAO (de)
@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>
Suspendendo a função DAO (para)
@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>
A migração de funções de bloqueio DAO para suspender funções poderá ser complicada se a base de código ainda 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 para fluxo
Nem todas as funções DAO precisam ser de suspensão. As funções DAO que retornam
tipos reativos, como LiveData
ou Flowable
do RxJava, não podem ser convertidas
para funções de suspensão. No entanto, alguns tipos, como LiveData
, não são compatíveis
com KMP. As funções DAO com tipos de retorno reativos precisam ser migradas para
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 na sua base de código.
Definir um contexto de corrotina (opcional)
Opcionalmente, um RoomDatabase
pode ser configurado com executores de aplicativos
compartilhados usando RoomDatabase.Builder.setQueryExecutor()
para realizar operações de
banco de dados. Como os executores não são compatíveis com KMP, a API setQueryExecutor()
do Room não está disponível para fontes comuns. Em vez disso, o RoomDatabase
precisa
ser configurado com um CoroutineContext
. Um contexto pode ser definido usando
RoomDatabase.Builder.setCoroutineContext()
. Se nenhum contexto for definido, o
RoomDatabase
usará Dispatchers.IO
por padrão.
Definir um driver SQLite
Depois que o suporte ao uso do SQLite foi migrado para as APIs de drivers SQLite, um
driver precisa ser configurado usando RoomDatabase.Builder.setDriver
. O
driver recomendado é BundledSQLiteDriver
. Consulte Implementações de driver para ver descrições das implementações de driver disponíveis.
O KMP não oferece suporte a SupportSQLiteOpenHelper.Factory
personalizados configurados usando
RoomDatabase.Builder.openHelperFactory()
. Os recursos fornecidos pelo auxiliar aberto personalizado precisarão ser reimplementados com
interfaces do driver SQLite.
Mover declarações de salas
Depois que a maioria das etapas de migração é concluída, é possível mover as definições do Room
para um conjunto de origem comum. As estratégias expect
/ actual
podem
ser usadas para mover definições relacionadas ao Room de forma incremental. Por exemplo, se nem todas
as funções DAO de bloqueio puderem ser migradas para funções de suspensão, é possível
declarar uma interface com a anotação expect
@Dao
vazia no código comum, mas
contendo 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
}