Bird
Raised Fist0
Android Kotlinmobile~20 mins

Database migrations in Android Kotlin - Mini App: Build & Ship

Choose your learning style10 modes available
Build: User Database Migration
This screen demonstrates how to migrate an Android Room database from version 1 to version 2 by adding a new column to an existing table.
Target UI
-------------------------
| User List             |
|-----------------------|
| 1. Alice              |
| 2. Bob                |
| 3. Carol              |
|-----------------------|
| Migration Status:     |
| Not started           |
|-----------------------|
| [Migrate Database]    |
-------------------------
Create a Room database with a User entity having id and name fields in version 1.
Add a new field 'age' to User entity in version 2.
Implement a migration from version 1 to 2 that adds the 'age' column with a default value.
Display the list of users with their name and age after migration.
Add a button labeled 'Migrate Database' that triggers the migration and updates the UI.
Show migration status text that updates to 'Migration completed' after success.
Starter Code
Android Kotlin
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")
                }
            }
        }
    }
}
Task 1
Task 2
Task 3
Task 4
Task 5
Solution
Android Kotlin
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.

Final Result
Completed Screen
-------------------------
| User List             |
|-----------------------|
| 1. Alice - Age: 0     |
| 2. Bob - Age: 0       |
| 3. Carol - Age: 0     |
|-----------------------|
| Migration Status:     |
| Migration completed   |
|-----------------------|
| [Migrate Database]    |
-------------------------
User taps 'Migrate Database' button.
App runs migration adding 'age' column with default 0.
User list updates showing each user's age.
Migration status text changes to 'Migration completed'.
Stretch Goal
Add a feature to update a user's age by tapping on their name.
💡 Hint
Use a clickable composable for each user item and show a dialog with a TextField to enter new age, then update the database and refresh the list.

Practice

(1/5)
1.

What is the main purpose of a database migration in Android apps?

easy
A. To update the database schema without losing existing data
B. To delete all data and recreate the database from scratch
C. To backup the database to external storage
D. To optimize database queries for faster performance

Solution

  1. Step 1: Understand database migration purpose

    Database migrations are used to change the structure of the database safely.
  2. Step 2: Identify data preservation goal

    Migrations update tables or columns without deleting existing user data.
  3. Final Answer:

    To update the database schema without losing existing data -> Option A
  4. Quick Check:

    Migration = Safe schema update [OK]
Hint: Migrations keep data safe while changing schema [OK]
Common Mistakes:
  • Thinking migrations delete all data
  • Confusing migrations with backups
  • Believing migrations only optimize queries
2.

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) {
        // ?
    }
}
easy
A. database.execSQL("ALTER TABLE users ADD COLUMN age INTEGER DEFAULT 0")
B. database.update("users", "age", 0)
C. database.insert("users", ContentValues())
D. database.delete("users")

Solution

  1. Step 1: Understand Migration class usage

    Migration requires SQL commands inside migrate() to change schema.
  2. Step 2: Identify correct SQL command

    ALTER TABLE adds a new column safely; execSQL runs raw SQL.
  3. Final Answer:

    database.execSQL("ALTER TABLE users ADD COLUMN age INTEGER DEFAULT 0") -> Option A
  4. Quick Check:

    Migration uses execSQL with ALTER TABLE [OK]
Hint: Use execSQL with ALTER TABLE to add columns [OK]
Common Mistakes:
  • Using update() instead of execSQL
  • Trying to insert or delete rows in migration
  • Not providing default value for new column
3.

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.

medium
A. No changes happen because CREATE TABLE is ignored
B. Migration fails because table 'books' already exists
C. A new table 'books' is created with columns 'id' and 'title'
D. Existing tables are deleted and only 'books' remains

Solution

  1. Step 1: Analyze migration SQL command

    CREATE TABLE IF NOT EXISTS creates the table only if it doesn't exist.
  2. Step 2: Check initial database state

    Version 1 has no 'books' table, so it will be created.
  3. Final Answer:

    A new table 'books' is created with columns 'id' and 'title' -> Option C
  4. Quick Check:

    CREATE TABLE IF NOT EXISTS creates missing table [OK]
Hint: IF NOT EXISTS avoids errors if table exists [OK]
Common Mistakes:
  • Assuming migration fails if table exists
  • Thinking CREATE TABLE is ignored
  • Believing existing tables get deleted
4.

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")
    }
}
medium
A. The migration does not update the database version
B. The same column 'email' is added twice causing an error
C. The SQL syntax for ALTER TABLE is incorrect
D. The migration should delete the 'users' table first

Solution

  1. Step 1: Review SQL commands in migration

    The code tries to add the 'email' column twice to the same table.
  2. Step 2: Understand SQLite behavior

    Adding the same column twice causes a runtime error during migration.
  3. Final Answer:

    The same column 'email' is added twice causing an error -> Option B
  4. Quick Check:

    Duplicate column addition causes migration failure [OK]
Hint: Avoid adding same column twice in one migration [OK]
Common Mistakes:
  • Ignoring duplicate column errors
  • Thinking migration version is updated automatically
  • Believing ALTER TABLE syntax is wrong here
5.

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?

hard
A. Add a new column and leave the old column as is
B. Use ALTER TABLE to rename the column directly
C. Delete the old table and create a new one with the new column name
D. Create a new table with the new column name, copy data, drop old table, rename new table

Solution

  1. Step 1: Understand SQLite column rename limitation

    SQLite does not support direct column renaming with ALTER TABLE.
  2. Step 2: Apply recommended migration pattern

    Create a new table with desired schema, copy data, drop old table, rename new table.
  3. Final Answer:

    Create a new table with the new column name, copy data, drop old table, rename new table -> Option D
  4. Quick Check:

    Rename column = new table + copy data [OK]
Hint: Rename columns by new table + copy data [OK]
Common Mistakes:
  • Trying to rename column with ALTER TABLE
  • Deleting old table without copying data
  • Adding new column but not removing old one
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard