Integrating Room with Hilt in Jetpack Compose: A Practical Guide

Jetpack Compose has revolutionized Android UI development, offering a modern, declarative approach to building user interfaces. Combining this with Room’s robust database capabilities and Hilt’s seamless dependency injection creates a powerful trifecta for scalable, maintainable, and testable Android applications. In this guide, we’ll explore how to integrate Room with Hilt in a Jetpack Compose application, ensuring best practices and performance optimization throughout.

Prerequisites

Before diving in, ensure you have the following:

  • A basic understanding of Jetpack Compose, Room, and Hilt.

  • Android Studio Flamingo or higher.

  • Kotlin version 1.9 or later.

If you're not familiar with the basics of these technologies, consider reviewing the official documentation for Jetpack Compose, Room, and Hilt.

Step 1: Adding Dependencies

To get started, include the necessary dependencies in your build.gradle (or build.gradle.kts) file:

// Jetpack Compose dependencies
implementation "androidx.compose.ui:ui:1.5.0"
implementation "androidx.compose.material:material:1.5.0"
implementation "androidx.compose.ui:ui-tooling-preview:1.5.0"
kapt "androidx.compose.ui:ui-tooling:1.5.0"

// Room dependencies
implementation "androidx.room:room-runtime:2.5.2"
kapt "androidx.room:room-compiler:2.5.2"
implementation "androidx.room:room-ktx:2.5.2"

// Hilt dependencies
implementation "com.google.dagger:hilt-android:2.50"
kapt "com.google.dagger:hilt-android-compiler:2.50"

// Hilt Compose integration
implementation "androidx.hilt:hilt-navigation-compose:1.1.0"

Synchronize your project to ensure all dependencies are downloaded.

Step 2: Setting Up Room

Define Your Entity

Room requires entity classes to define database tables. Here’s an example:

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "notes")
data class Note(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val title: String,
    val content: String
)

Create the DAO

Define the data access object (DAO) for querying the database:

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query

@Dao
interface NoteDao {
    @Query("SELECT * FROM notes")
    suspend fun getAllNotes(): List<Note>

    @Insert
    suspend fun insert(note: Note)
}

Setup the Database

Create the Room database class:

import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [Note::class], version = 1)
abstract class NoteDatabase : RoomDatabase() {
    abstract fun noteDao(): NoteDao
}

Step 3: Configuring Hilt

Hilt simplifies dependency injection, making it easy to provide instances of Room components.

Application Class

Add the @HiltAndroidApp annotation to your Application class:

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class MyApplication : Application()

Module for Room

Create a Hilt module to provide Room dependencies:

import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.SingletonComponent
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): NoteDatabase {
        return Room.databaseBuilder(
            context,
            NoteDatabase::class.java,
            "note_database"
        ).build()
    }

    @Provides
    fun provideNoteDao(database: NoteDatabase): NoteDao {
        return database.noteDao()
    }
}

Step 4: Building the UI with Jetpack Compose

With Room and Hilt configured, let’s create a Compose-based UI to display and interact with the data.

ViewModel

Use Hilt to inject dependencies into a ViewModel:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class NoteViewModel @Inject constructor(
    private val noteDao: NoteDao
) : ViewModel() {

    val notes = mutableStateOf<List<Note>>(emptyList())

    fun fetchNotes() {
        viewModelScope.launch {
            notes.value = noteDao.getAllNotes()
        }
    }

    fun addNote(note: Note) {
        viewModelScope.launch {
            noteDao.insert(note)
            fetchNotes()
        }
    }
}

Composable Function

Build a Composable UI to display notes:

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel

@Composable
fun NoteScreen() {
    val viewModel: NoteViewModel = hiltViewModel()
    val notes by viewModel.notes.collectAsState()

    var title by remember { mutableStateOf("") }
    var content by remember { mutableStateOf("") }

    Column(modifier = Modifier.padding(16.dp)) {
        BasicTextField(
            value = title,
            onValueChange = { title = it },
            modifier = Modifier.fillMaxWidth().padding(8.dp),
            singleLine = true
        )
        BasicTextField(
            value = content,
            onValueChange = { content = it },
            modifier = Modifier.fillMaxWidth().padding(8.dp)
        )
        Button(onClick = {
            viewModel.addNote(Note(title = title, content = content))
            title = ""
            content = ""
        }) {
            Text("Add Note")
        }

        LazyColumn {
            items(notes) { note ->
                Text("${note.title}: ${note.content}")
            }
        }
    }
}

Best Practices

  • Avoid Blocking Main Thread: Always use suspend functions for Room database operations.

  • State Management: Use State or Flow with Compose to manage and observe UI state effectively.

  • Testing: Write unit tests for your DAOs and UI tests for Compose screens.

  • Dependency Injection: Leverage Hilt’s modular approach to keep code maintainable and scalable.

Conclusion

Integrating Room with Hilt in Jetpack Compose applications is a straightforward process that offers clean architecture, improved scalability, and better testability. By following the practices outlined in this guide, you’ll be able to create performant and maintainable Android apps with ease.

Remember to keep your dependencies up-to-date and continuously refine your architecture to meet the evolving needs of your projects.