Pre-populating Room database with static data in Android using Hilt DI

Room database is a part of Android Architecture components and provides an abstract layer over SQLite. Room makes it easy to work with databases and provides a convenient way to pre-populate the database with static data.

Here’s how to pre-populate Room database using static data in Android

Step 1: Create a Data Class

The first step in pre-populating Room database is to create a data class that represents the table in the database. In this example, we will create a data class for a User.

Here's how the data class for a User looks like:

@Entity
data class User(
    @PrimaryKey(autoGenerate = true)
    val userId: Int = 0,
    @NonNull val username: String,
    @NonNull val isActive: Boolean,
)

Step 2: Create a DAO Class

The next step is to create a Data Access Object (DAO) class. This class contains the methods used to interact with the database. In this example, we will create a DAO for the User entity.

@Dao
interface UserDao {
    @Upsert(User::class)
    suspend fun insertOrUpdateUsers(vararg users: User)

    @Query("SELECT * FROM User LIMIT 1")
    suspend fun getUser(): User?

    @Query("SELECT * FROM User")
    fun getAllUsers(): Flow<List<User>>
}

The first method, insertOrUpdateUsers, is annotated with @Upsert. This annotation is used to insert or update records in the database. If a record with the same primary key exists, it will be updated. If it does not exist, a new record will be inserted. In this case, we are inserting or updating multiple users at once.

The second method, getUser, is annotated with @Query. This annotation is used to specify a SQL query. In this case, we are querying for a single User from the User table.

The third method, getAllUsers, is used to retrieve all Users from the User table. The result is returned as a Flow, which is a type of reactive stream that emits updates whenever the data changes.

Step 3: Create a Database Class

The next step is to create a database class that ties everything together. This class is used to create an instance of the database, and it includes all the entities and DAOs that we have defined so far.

@Database(
    version = 1,
    exportSchema = true,
    entities = [User::class]
)
abstract class AppDatabase : RoomDatabase() {
    abstract val userDao: UserDao
}

With this, we have defined the database and the structure of the tables that it will contain. Room will generate an implementation of this database that we can use in our code.

Step 4: Programmatically generate our static data

Now that we have the database class, we need to initialize it with static data when the database is created for the first time. To do this, we need to create a class that implements the RoomDatabase.Callback class and overrides the onCreate method.

class RoomDbInitializer(
    private val userProvider: Provider<UserDao>,
) : RoomDatabase.Callback() {
    private val applicationScope = CoroutineScope(SupervisorJob())

    override fun onCreate(db: SupportSQLiteDatabase) {
        super.onCreate(db)
        applicationScope.launch(Dispatchers.IO) {
            populateDatabase()
        }
    }

    private suspend fun populateDatabase() {
        populateUsers()
    }

    private suspend fun populateUsers() {
        userProvider.get().insertOrUpdateUsers(*userGenerator.take(100).toList().toTypedArray())
    }
}

/**
 * This is a [Sequence] generator to generate random users.
 */
val userGenerator = generateSequence {
    User(
        userId = 0,
        username = "user_${UUID.randomUUID().toString().replace("-", "").substring(0, 6)}",
        isActive = Random.nextBoolean()
    )
}

In the RoomDbInitializer class, we have implemented the onCreate method. This method is called whenever the database is created for the first time.

In this method, we are using the CoroutineScope to launch a coroutine on the IO Dispatcher. This coroutine is used to populate the database with static data.

The populateUsers method is used to insert a list of 100 users into the User table. To generate the list of users, we are using a sequence generator called userGenerator. This generator is used to generate random users.

With this, we have created a class that is used to initialize the database with static data when the database is created for the first time.

Step 5: Setup Hilt module

To complete the process of pre-populating Room database using static data in Android, the final step is to tell Hilt DI how to provide the necessary dependencies. To achieve this, we can create a Hilt module called DatabaseModule that will have the necessary @Provides methods to return instances of our Room database and DAO classes.

@Module
@InstallIn(SingletonComponent::class)
class DatabaseModule {
    @Provides
    @Singleton
    fun provideAppDatabase(
        @ApplicationContext context: Context,
        userProvider: Provider<UserDao>,
    ): AppDatabase {
        return Room.databaseBuilder(
            context.applicationContext,
            AppDatabase::class.java,
            "${context.getString(R.string.app_name)}.db"
        ).addCallback(
            /**
             * Attach [RoomDbInitializer] as callback to the database
             */
            RoomDbInitializer(userProvider = userProvider)
        ).build()
    }

    @Provides
    @Singleton
    fun provideUserDao(db: AppDatabase): UserDao = db.userDao
}

The addCallback method is used to attach an instance of RoomDbInitializer to the database. This will ensure that when the database is created for the first time, the onCreate method of RoomDbInitializer will be called to populate the database with static data.

Step 6: Setup the Repository and ViewModel

To trigger to initialization of a database, first, we'll need to do a fake DB read. This will create and initialize the database. Here, we're using a Flow to read the updated value every time the database change, so it will automatically trigger the DB read, initialize the database and emit the updated value but if you're not using any reactive stream, make sure to do a fake DB read before fetching the actual data.

@HiltViewModel
class HomeViewModel @Inject constructor(
    private val homeRepository: HomeRepository
) : ViewModel() {
    fun getAllUsers(): Flow<List<User>> {
        return homeRepository.getAllUsers()
    }
}
@Singleton
class HomeRepository @Inject constructor(
    private val userDao: UserDao
) {
    fun getAllUsers(): Flow<List<User>> {
        return userDao.getAllUsers()
    }
}

Step 7: Display the data in UI

In the Activity or Composable, we can observe the Flow of data from the ViewModel and display it to the user.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
    homeViewModel: HomeViewModel = hiltViewModel()
) {
    val users by remember { homeViewModel.getAllUsers() }.collectAsStateWithLifecycle(initialValue = emptyList())

    Scaffold(
        topBar = {
            TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) })
        }
    ) { innerPadding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
        ) {
            UsersView(
                modifier = Modifier.fillMaxSize(),
                users = users
            )
        }
    }
}

@Composable
private fun UsersView(
    modifier: Modifier = Modifier,
    users: List<User>
) {
    LazyColumn(
        modifier = modifier,
        contentPadding = PaddingValues(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(users) { user ->
            UserItem(
                modifier = Modifier.fillMaxWidth(),
                user = user
            )
        }
    }
}

@Composable
private fun UserItem(
    modifier: Modifier = Modifier,
    user: User
) {
    Card(modifier = modifier) {
        Column(modifier = Modifier.padding(8.dp)) {
            Text(
                text = "UserId: ${user.userId}",
                style = MaterialTheme.typography.titleMedium
            )
            Text(
                text = "Username: ${user.username}",
                style = MaterialTheme.typography.titleMedium
            )
            Text(
                text = "Active: ${user.isActive}",
                style = MaterialTheme.typography.titleSmall
            )
        }
    }
}

I hope you found this blog helpful and learned about initializing a Room database with Hilt.

If you have any questions or would like to connect, feel free to follow me or reach out on LinkedIn or GitHub. I'd love to connect with you!

GitHub LinkedIn

Source code