Room Database is part of Android’s Architecture Components and serves as an abstraction layer over SQLite. It simplifies data management by offering a type-safe way to interact with the database and enables compile-time checks of SQL statements.
Key Components of Room Database
1. Entity: Represents a table in your database.
2. DAO (Data Access Object): Contains methods for accessing the database.
3. Database Class: Acts as the main access point to the underlying SQLite database.
Step 1: Add Dependencies
To get started, include the Room dependencies in your libs.versions.toml file:
room = "2.6.1"
ksp = "2.0.21-1.0.26"
[libraries]
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
[plugins]
ksp = "2.0.21-1.0.26"
room = { id = "androidx.room", version.ref = "room" }
Include the Room dependencies in your app build.gradle. file:
plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.room)
}
dependencies {
compileSdk 35
defaultConfig {
minSdk 23
targetSdk 35
versionCode 40
versionName "2.2.0"
}
}
dependencies {
implementation(libs.room.runtime)
implementation(libs.room.ktx)
ksp(libs.room.compiler)
annotationProcessor(libs.room.compiler)
}
Include the Room dependencies in your project build.gradle. file:
plugins {
alias(libs.plugins.ksp) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.room) apply false
}
Step 2: Define an Entity
An Entity represents a table within the database. Define your entity as a data class, with each property corresponding to a column.
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName
@Entity(tableName = "movieDetail")
data class MovieDetail(
@PrimaryKey(autoGenerate = false)
@SerializedName("id")
val id: Int,
@SerializedName("adult")
val adult: Boolean,
@SerializedName("backdrop_path")
val backdrop_path: String,
@SerializedName("budget")
val budget: Int,
@SerializedName("genres")
val genres: List<Genre>,
@SerializedName("homepage")
val homepage: String,
@SerializedName("imdb_id")
val imdb_id: String,
@SerializedName("original_language")
val original_language: String,
@SerializedName("original_title")
val original_title: String,
@SerializedName("overview")
val overview: String,
@SerializedName("popularity")
val popularity: Double,
@SerializedName("poster_path")
val poster_path: String,
@SerializedName("release_date")
val release_date: String,
@SerializedName("revenue")
val revenue: Int,
@SerializedName("runtime")
val runtime: Int,
@SerializedName("status")
val status: String,
@SerializedName("tagline")
val tagline: String,
@SerializedName("title")
val title: String,
@SerializedName("video")
val video: Boolean,
@SerializedName("vote_average")
val vote_average: Double,
@SerializedName("vote_count")
val vote_count: Int,
)
In this above code:
• @Entity marks this class as a Room entity.
- @PrimaryKey denotes the primary key for this entity. Here, autoGenerate automatically assigns a unique ID for each User.
Step 3: Create a DAO
The DAO (Data Access Object) interface contains methods for accessing the database. Use SQL queries to define how data should be inserted, updated, retrieved, or deleted.
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.piashcse.hilt_mvvm_compose_movie.data.model.moviedetail.MovieDetail
@Dao
interface FavoriteMovieDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(movieDetail: MovieDetail)
@Query("Select * From movieDetail Where id = :id")
suspend fun getMovieDetailById(id: Int): MovieDetail?
@Query("DELETE FROM movieDetail WHERE id = :id")
suspend fun deleteMovieDetailById(id: Int)
@Query("SELECT * FROM movieDetail")
suspend fun getAllMovieDetails(): List<MovieDetail?>
}
Each method is annotated with a specific Room operation (@Insert, @Update, @Delete, @Query) to specify the desired action.
Step 4: Create the Database Class
The Database class should be an abstract class extending RoomDatabase. Use the @Database annotation to specify the entities and database version.
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
@TypeConverters(MovieTypeConverter::class)
@Database(version = 1, entities = [MovieDetail::class], exportSchema = false)
abstract class MovieDatabase : RoomDatabase() {
abstract fun getFavoriteMovieDetailDao(): FavoriteMovieDao
}
if you have a nested data class inside Entity you have to add TypeConverter
import androidx.room.TypeConverter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class MovieTypeConverter {
private val gson : Gson by lazy {
Gson()
}
@TypeConverter
fun fromGenreList(value: List<Genre>): String = gson.toJson(value)
@TypeConverter
fun toGenreList(value: String): List<Genre> =
gson.fromJson(value, object : TypeToken<List<Genre>>() {}.type)
}
Step 5: Build the Database
To build the database, use Room’s databaseBuilder method. This is typically done in your Application class or a dependency injection setup like Hilt.
import android.content.Context
import androidx.room.Room
class DatabaseClient(private val context: Context) {
val appDatabase: AppDatabase by lazy {
Room.databaseBuilder(
context.applicationContext,
MovieDatabase::class.java,
"movieWorld.db"
).build()
}
}
Or, with Hilt:
import android.content.Context
import androidx.room.Room
import com.piashcse.hilt_mvvm_compose_movie.data.datasource.local.MovieDatabase
import com.piashcse.hilt_mvvm_compose_movie.data.datasource.local.dao.FavoriteMovieDao
import com.piashcse.hilt_mvvm_compose_movie.data.datasource.local.dao.FavoriteTvSeriesDao
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DataBaseModule {
@Provides
@Singleton
fun provideMovieDatabase(@ApplicationContext context: Context): MovieDatabase {
return Room.databaseBuilder(
context,
MovieDatabase::class.java,
"movieWorld.db"
).build()
}
@Singleton
@Provides
fun provideMovieDetailDao(moviesDatabase: MovieDatabase): FavoriteMovieDao =
moviesDatabase.getFavoriteMovieDetailDao()
}
Step 6: Perform Database in ViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.piashcse.hilt_mvvm_compose_movie.data.datasource.local.dao.FavoriteMovieDao
import com.piashcse.hilt_mvvm_compose_movie.data.model.moviedetail.MovieDetail
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class FavoriteMovieViewModel @Inject constructor(
private val movieDetailDao: FavoriteMovieDao,
) : ViewModel() {
private val _favoriteMovies = MutableStateFlow<List<MovieDetail?>>(arrayListOf())
val favoriteMovies get() = _favoriteMovies.asStateFlow()
fun favoriteMoviesFromDB() {
viewModelScope.launch {
_favoriteMovies.value = movieDetailDao.getAllMovieDetails()
}
}
fun removeMovieFromDB(movieId: Int) {
viewModelScope.launch {
movieDetailDao.deleteMovieDetailById(movieId)
}
}
}
Step 7: Perform Database in Jetpack UI
package com.piashcse.hilt_mvvm_compose_movie.ui.screens.favorite.movie
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.piashcse.hilt_mvvm_compose_movie.R
import com.piashcse.hilt_mvvm_compose_movie.data.datasource.remote.ApiURL
import com.piashcse.hilt_mvvm_compose_movie.data.model.moviedetail.MovieDetail
import com.piashcse.hilt_mvvm_compose_movie.navigation.Screen
import com.piashcse.hilt_mvvm_compose_movie.ui.component.ExitAlertDialog
import com.piashcse.hilt_mvvm_compose_movie.ui.theme.DefaultBackgroundColor
import com.piashcse.hilt_mvvm_compose_movie.ui.theme.SecondaryFontColor
import com.piashcse.hilt_mvvm_compose_movie.ui.theme.cornerRadius
import com.skydoves.landscapist.ImageOptions
import com.skydoves.landscapist.animation.circular.CircularRevealPlugin
import com.skydoves.landscapist.coil.CoilImage
import com.skydoves.landscapist.components.rememberImageComponent
import com.skydoves.landscapist.placeholder.shimmer.Shimmer
import com.skydoves.landscapist.placeholder.shimmer.ShimmerPlugin
@Composable
fun FavoriteMovie(navController: NavController) {
val viewModel = hiltViewModel<FavoriteMovieViewModel>()
val favoriteMovies by viewModel.favoriteMovies.collectAsState()
LaunchedEffect(Unit) {
viewModel.favoriteMoviesFromDB()
}
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
LazyVerticalGrid(columns = GridCells.Fixed(1),
modifier = Modifier.padding(start = 5.dp, top = 10.dp, end = 5.dp),
content = {
items(favoriteMovies) { item ->
item?.let {
FavoriteMovieItemView(item, navController, viewModel)
}
}
})
}
}
@Composable
fun FavoriteMovieItemView(
item: MovieDetail,
navController: NavController,
viewModel: FavoriteMovieViewModel,
) {
val openDialog = remember { mutableStateOf(false) }
Column(modifier = Modifier.padding(start = 5.dp, end = 5.dp, top = 0.dp, bottom = 10.dp).cornerRadius(10)) {
Box {
CoilImage(
modifier = Modifier
.height(250.dp)
.clickable {
navController.navigate(Screen.MovieDetail.route.plus("/${item.id}"))
},
imageModel = { ApiURL.IMAGE_URL.plus(item.backdrop_path) },
imageOptions = ImageOptions(
contentScale = ContentScale.Crop,
alignment = Alignment.Center,
contentDescription = "Movie item",
colorFilter = null,
),
component = rememberImageComponent {
+CircularRevealPlugin(
duration = 800
)
+ShimmerPlugin(
shimmer = Shimmer.Flash(
baseColor = SecondaryFontColor,
highlightColor = DefaultBackgroundColor
)
)
},
)
IconButton(
onClick = {
openDialog.value = true
},
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)
.background(Color.White.copy(alpha = 0.9f), shape = CircleShape)
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
tint = Color.Gray
)
}
Box(
Modifier
.fillMaxWidth()
.align(Alignment.BottomStart)
.background(Color.Gray.copy(alpha = 0.5f))) {
Text(
text = item.title,
color = Color.White,
modifier = Modifier.padding(8.dp)
)
}
}
if (openDialog.value) {
ExitAlertDialog(
title = stringResource(R.string.remove_from_favorite),
description = stringResource(R.string.do_you_wanna_remove_this_from_favorite),
{
openDialog.value = false
},
{
viewModel.removeMovieFromDB(item.id)
viewModel.favoriteMoviesFromDB()
openDialog.value = false
})
}
}
}
Step 8: Here is the final result
Github: https://github.com/piashcse/Hilt-MVVM-Compose-Movie
Medium: https://piashcse.medium.com/room-database-in-jetpack-compose-a-step-by-step-guide-for-android-development-6c7ae419105a