------------------------- | User List | |-----------------------| | 1. Alice | | 2. Bob | | 3. Carol | |-----------------------| | Migration Status: | | Not started | |-----------------------| | [Migrate Database] | -------------------------
Database migrations in Android Kotlin - Mini App: Build & Ship
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.room.* @Entity data class User( @PrimaryKey val id: Int, val name: String // TODO: Add 'age' field for version 2 ) @Dao interface UserDao { @Query("SELECT * FROM User") fun getAll(): List<User> @Insert fun insertAll(vararg users: User) } @Database(entities = [User::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao } class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // TODO: Setup Room database with migration from v1 to v2 setContent { var migrationStatus by remember { mutableStateOf("Not started") } var users by remember { mutableStateOf(listOf<User>()) } Column(modifier = Modifier.padding(16.dp)) { Text("User List", style = MaterialTheme.typography.titleLarge) users.forEach { user -> Text("${user.id}. ${user.name}") } Text("Migration Status: $migrationStatus", modifier = Modifier.padding(top = 16.dp)) Button(onClick = { // TODO: Trigger migration and update users and status }, modifier = Modifier.padding(top = 8.dp)) { Text("Migrate Database") } } } } }
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.room.* import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import androidx.room.Room @Entity data class User( @PrimaryKey val id: Int, val name: String, val age: Int = 0 // Added in version 2 with default 0 ) @Dao interface UserDao { @Query("SELECT * FROM User") fun getAll(): List<User> @Insert fun insertAll(vararg users: User) } @Database(entities = [User::class], version = 2) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao } class MainActivity : ComponentActivity() { private lateinit var db: AppDatabase override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE User ADD COLUMN age INTEGER NOT NULL DEFAULT 0") } } db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "user-db") .addMigrations(MIGRATION_1_2) .fallbackToDestructiveMigration() // For first run .build() setContent { var migrationStatus by remember { mutableStateOf("Not started") } var users by remember { mutableStateOf(listOf<User>()) } fun loadUsers() { // Load users from DB on background thread Thread { val loadedUsers = db.userDao().getAll() runOnUiThread { users = loadedUsers } }.start() } LaunchedEffect(Unit) { // Insert initial data if empty Thread { if (db.userDao().getAll().isEmpty()) { db.userDao().insertAll( User(1, "Alice"), User(2, "Bob"), User(3, "Carol") ) } loadUsers() }.start() } Column(modifier = Modifier.padding(16.dp)) { Text("User List", style = MaterialTheme.typography.titleLarge) users.forEach { user -> Text("${user.id}. ${user.name} - Age: ${user.age}") } Text("Migration Status: $migrationStatus", modifier = Modifier.padding(top = 16.dp)) Button(onClick = { migrationStatus = "Migrating..." Thread { // Close old DB and reopen with migration db.close() db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "user-db") .addMigrations(MIGRATION_1_2) .build() loadUsers() runOnUiThread { migrationStatus = "Migration completed" } }.start() }, modifier = Modifier.padding(top = 8.dp)) { Text("Migrate Database") } } } } }
We first added the new age field to the User data class with a default value of 0 to keep compatibility.
The migration MIGRATION_1_2 runs SQL to add the new age column with a default value of 0 to existing rows.
The Room database is built with this migration included.
On app start, initial users are inserted if none exist.
The UI shows the list of users with their name and age.
When the user taps the "Migrate Database" button, the database is closed and reopened with the migration applied, then the user list reloads and the migration status updates.
This demonstrates a simple Room database migration adding a new column without losing existing data.
------------------------- | User List | |-----------------------| | 1. Alice - Age: 0 | | 2. Bob - Age: 0 | | 3. Carol - Age: 0 | |-----------------------| | Migration Status: | | Migration completed | |-----------------------| | [Migrate Database] | -------------------------
Practice
What is the main purpose of a database migration in Android apps?
Solution
Step 1: Understand database migration purpose
Database migrations are used to change the structure of the database safely.Step 2: Identify data preservation goal
Migrations update tables or columns without deleting existing user data.Final Answer:
To update the database schema without losing existing data -> Option AQuick Check:
Migration = Safe schema update [OK]
- Thinking migrations delete all data
- Confusing migrations with backups
- Believing migrations only optimize queries
Which of the following is the correct way to define a migration from version 1 to 2 in Room database?
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// ?
}
}Solution
Step 1: Understand Migration class usage
Migration requires SQL commands inside migrate() to change schema.Step 2: Identify correct SQL command
ALTER TABLE adds a new column safely; execSQL runs raw SQL.Final Answer:
database.execSQL("ALTER TABLE users ADD COLUMN age INTEGER DEFAULT 0") -> Option AQuick Check:
Migration uses execSQL with ALTER TABLE [OK]
- Using update() instead of execSQL
- Trying to insert or delete rows in migration
- Not providing default value for new column
Given this migration code, what will be the result after migrating from version 1 to 2?
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS books (id INTEGER PRIMARY KEY, title TEXT)")
}
}Assuming version 1 had no books table.
Solution
Step 1: Analyze migration SQL command
CREATE TABLE IF NOT EXISTS creates the table only if it doesn't exist.Step 2: Check initial database state
Version 1 has no 'books' table, so it will be created.Final Answer:
A new table 'books' is created with columns 'id' and 'title' -> Option CQuick Check:
CREATE TABLE IF NOT EXISTS creates missing table [OK]
- Assuming migration fails if table exists
- Thinking CREATE TABLE is ignored
- Believing existing tables get deleted
What is wrong with this migration code from version 2 to 3?
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE users ADD COLUMN email TEXT")
database.execSQL("ALTER TABLE users ADD COLUMN email TEXT")
}
}Solution
Step 1: Review SQL commands in migration
The code tries to add the 'email' column twice to the same table.Step 2: Understand SQLite behavior
Adding the same column twice causes a runtime error during migration.Final Answer:
The same column 'email' is added twice causing an error -> Option BQuick Check:
Duplicate column addition causes migration failure [OK]
- Ignoring duplicate column errors
- Thinking migration version is updated automatically
- Believing ALTER TABLE syntax is wrong here
You want to rename a column username to user_name in your Room database from version 3 to 4. SQLite does not support renaming columns directly. Which migration strategy is correct?
Solution
Step 1: Understand SQLite column rename limitation
SQLite does not support direct column renaming with ALTER TABLE.Step 2: Apply recommended migration pattern
Create a new table with desired schema, copy data, drop old table, rename new table.Final Answer:
Create a new table with the new column name, copy data, drop old table, rename new table -> Option DQuick Check:
Rename column = new table + copy data [OK]
- Trying to rename column with ALTER TABLE
- Deleting old table without copying data
- Adding new column but not removing old one
Ready to prove it?
Pass the test and this pattern moves unknown
