Jetpack Compose has revolutionized Android UI development by introducing a declarative approach to building user interfaces. When combined with Room, Google’s official persistence library, it creates a robust architecture for managing local databases in Android applications. While Room provides a rich set of predefined queries, there are scenarios where custom queries are essential for optimizing performance or handling complex data requirements. In this post, we’ll dive deep into writing custom queries in Room and integrating them effectively with Jetpack Compose.
Why Custom Queries Matter
Room’s abstraction simplifies database interactions with annotations like @Insert
, @Update
, and @Delete
. However, predefined queries may not cover advanced use cases such as:
Filtering and sorting data dynamically.
Joining multiple tables for aggregated results.
Fetching specific data subsets based on user input or UI state.
Optimizing queries for performance in resource-intensive applications.
Custom queries empower developers to fine-tune database operations and achieve greater control over data retrieval and manipulation.
Setting Up Room in a Jetpack Compose Project
Before diving into custom queries, let’s ensure the foundational setup of Room in a Jetpack Compose project.
1. Add Room Dependencies
Update your build.gradle
file to include Room dependencies:
implementation "androidx.room:room-runtime:2.5.0"
kapt "androidx.room:room-compiler:2.5.0"
implementation "androidx.room:room-ktx:2.5.0"
For Kotlin projects, enable annotation processing:
kapt {
correctErrorTypes = true
}
2. Define a Room Entity
An entity represents a table in the database. Here’s an example:
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true) val id: Int,
val name: String,
val age: Int,
val email: String
)
3. Create a DAO Interface
Data Access Objects (DAOs) define methods for interacting with the database:
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
}
4. Initialize the Database
Create a Room database class:
@Database(entities = [User::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
Initialize it in your Application
class or using dependency injection (e.g., Hilt):
val db = Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
).build()
Writing Custom Queries
Custom queries are annotated with @Query
in the DAO. Here are advanced use cases and how to implement them.
1. Filtering Data Dynamically
Imagine you want to fetch users above a certain age. Use parameters in the query:
@Query("SELECT * FROM users WHERE age > :minAge")
fun getUsersAboveAge(minAge: Int): Flow<List<User>>
Usage:
val usersAbove30 = userDao.getUsersAboveAge(30).collectAsState(initial = emptyList())
2. Sorting Results Dynamically
Dynamic sorting requires constructing queries with runtime parameters. However, Room does not support dynamic SQL strings directly. A workaround is to prepare multiple DAO methods or use third-party libraries like SQLDelight.
@Query("SELECT * FROM users ORDER BY name ASC")
fun getUsersSortedByName(): Flow<List<User>>
@Query("SELECT * FROM users ORDER BY age DESC")
fun getUsersSortedByAge(): Flow<List<User>>
3. Aggregating Data with Joins
To fetch aggregated data across multiple tables, Room supports SQL joins. Suppose you have another entity, Orders
:
@Entity(tableName = "orders")
data class Order(
@PrimaryKey(autoGenerate = true) val orderId: Int,
val userId: Int,
val amount: Double
)
Define a query to join users
and orders
:
@Query("""
SELECT users.name, SUM(orders.amount) as totalSpent
FROM users
INNER JOIN orders ON users.id = orders.userId
GROUP BY users.id
""")
data class UserSpending(
val name: String,
val totalSpent: Double
)
@Query("SELECT * FROM userSpending")
fun getUserSpendings(): Flow<List<UserSpending>>
4. Paginating Large Datasets
For large datasets, use pagination. Room integrates seamlessly with Paging
:
Add dependencies:
implementation "androidx.paging:paging-runtime:3.2.0"
Modify the DAO:
@Query("SELECT * FROM users ORDER BY name ASC")
fun getPagedUsers(): PagingSource<Int, User>
In your ViewModel:
val pager = Pager(PagingConfig(pageSize = 20)) {
userDao.getPagedUsers()
}.flow.cachedIn(viewModelScope)
Best Practices for Custom Queries
Optimize Queries: Use indices and limit data retrieval to only necessary columns to enhance performance.
Error Handling: Wrap queries in
try-catch
blocks or use Kotlin’sResult
for better error management.Flow and Compose: Prefer
Flow
for real-time updates, seamlessly integrating with Compose’s reactive paradigm.Test Queries: Write unit tests for DAO methods to ensure accuracy and prevent regressions.
Integrating Room with Jetpack Compose
Let’s create a Compose UI that displays a list of users fetched via a custom query.
Example: Display Users Above a Certain Age
ViewModel:
class UserViewModel(private val userDao: UserDao) : ViewModel() {
fun getUsersAboveAge(minAge: Int): Flow<List<User>> {
return userDao.getUsersAboveAge(minAge)
}
}
UI:
@Composable
fun UserList(viewModel: UserViewModel, minAge: Int) {
val users by viewModel.getUsersAboveAge(minAge).collectAsState(initial = emptyList())
LazyColumn {
items(users) { user ->
Text(text = "${user.name} ( ${user.age} years old)")
}
}
}
Conclusion
Writing custom queries in Room unlocks a new level of flexibility and power in managing your app’s local database. When paired with Jetpack Compose, you can build dynamic, real-time applications with highly optimized data interactions. By following best practices and leveraging Room’s capabilities, you can ensure your app remains performant and maintainable.
Do you have other advanced Room use cases or Compose integrations? Share your thoughts and experiences in the comments below!