0
0
Android Kotlinmobile~20 mins

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

Choose your learning style9 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.