Review a reference of basic ROOM concepts:
Lab : Create the HealthyShopper app.
Here's a breakdown of the steps involved, along with the code:
1. Project setup
Create a new Android Studio project.
Choose "Empty Activity" and Kotlin as the language.
Name the application "HealthyShopper".
Select the minimum SDK to your preference.
2. Add dependencies
Add the following dependencies to your build.gradle (Module level):
Gradle
dependencies {
// Room components
implementation("androidx.room:room-runtime:2.5.2")
annotationProcessor("androidx.room:room-compiler:2.5.2")
androidTestImplementation("androidx.room:room-testing:2.5.2")
implementation("androidx.room:room-ktx:2.5.2")
// Kotlin Extensions and Coroutines support for Room
// Lifecycle components
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") implementation("androidx.lifecycle:lifecycle-common-java8:2.6.2")
// Kotlin components
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
}
3. Create the data model (Entity)
Create a Kotlin data class named GroceryItem to represent the items in your shopping list.
package com.example.healthyshopper // Replace with your package name
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "grocery_items")
data class GroceryItem(
@PrimaryKey(autoGenerate = true) val id:
Int = 0,
@ColumnInfo(name = "item_name") val itemName: String
)
4. Create the DAO (Data Access Object)
Create an interface named GroceryItemDao to define the database operations.
package com.example.healthyshopper
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface GroceryItemDao {
@Query("SELECT * FROM grocery_items")
fun getAllGroceryItems(): Flow<List<GroceryItem>>
@Insert
suspend fun insert(groceryItem: GroceryItem)
@Update
suspend fun update(groceryItem: GroceryItem)
@Delete
suspend fun delete(groceryItem: GroceryItem)
}
5. Create the Room database
Create an abstract class that extends RoomDatabase to define your database.
Kotlin
package com.example.healthyshopper
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [GroceryItem::class], version = 1, exportSchema = false)
abstract class GroceryItemDatabase : RoomDatabase() {
abstract fun groceryItemDao(): GroceryItemDao
companion
object {
@Volatile
private var INSTANCE: GroceryItemDatabase? = null
fun getDatabase(context: Context): GroceryItemDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
GroceryItemDatabase::class.java,
"grocery_item_database"
).build()
INSTANCE = instance
instance
}
}
}
}
6. Design the layout (activity_main.xml)
XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<EditText
android:id="@+id/et_grocery_item"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="Enter grocery item"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/btn_add_item"
android:importantForAutofill="no" />
<Button
android:id="@+id/btn_add_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
<TextView
android:id="@+id/tv_grocery_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/et_grocery_item"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
7. Implement the MainActivity
Kotlin
package com.example.healthyshopper
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.example.healthyshopper.databinding.ActivityMainBinding
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private
val groceryItemViewModel: GroceryItemViewModel by viewModels {
GroceryItemViewModelFactory((application as GroceryItemApplication).repository)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Set click listener
for the Add button
binding.btnAddItem.setOnClickListener {
val itemName = binding.etGroceryItem.text.toString()
if (itemName.isNotBlank()) {
lifecycleScope.launch {
groceryItemViewModel.insert(GroceryItem(itemName = itemName))
binding.etGroceryItem.text.clear()
}
}
}
// Observe the grocery items and update the TextView
groceryItemViewModel.allGroceryItems.observe(this) { items ->
val groceryList = items.joinToString("\n") { it.itemName }
binding.tvGroceryList.text = groceryList
}
}
}
8. Create the GroceryItemViewModel
Kotlin
package com.example.healthyshopper
import androidx.lifecycle.*
import kotlinx.coroutines.launch
class GroceryItemViewModel(private val repository: GroceryItemRepository) : ViewModel() {
val allGroceryItems: LiveData<List<GroceryItem>> = repository.allGroceryItems.asLiveData()
fun insert(groceryItem: GroceryItem) = viewModelScope.launch {
repository.insert(groceryItem)
}
// Add other ViewModel functions for update and delete as needed
}
class GroceryItemViewModelFactory(private val repository: GroceryItemRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(GroceryItemViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return GroceryItemViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
9. Create the GroceryItemRepository
Kotlin
package com.example.healthyshopper
import androidx.annotation.WorkerThread
class GroceryItemRepository(private val groceryItemDao: GroceryItemDao) {
val allGroceryItems: Flow<List<GroceryItem>> = groceryItemDao.getAllGroceryItems()
@Suppress("RedundantSuspendModifier")
@WorkerThread
suspend fun insert(groceryItem: GroceryItem) {
groceryItemDao.insert(groceryItem)
}
// Add other repository functions for update and delete as needed
}
Use code with caution.
10. Create the GroceryItemApplication
Kotlin
package com.example.healthyshopper
import android.app.Application
class GroceryItemApplication : Application() {
val database by lazy { GroceryItemDatabase.getDatabase(this) }
val repository by lazy { GroceryItemRepository(database.groceryItemDao()) }
}
11. Update the AndroidManifest.xml
Add the GroceryItemApplication class to the AndroidManifest.xml`
XML
<application
...
android:name=".GroceryItemApplication"
...>
...
</application>
This setup uses View Binding, Kotlin coroutines for database operations, and follows the recommended Android architecture with a ViewModel and Repository.
Explanation:
Room Database: Provides an abstraction layer over SQLite to allow easier database access while harnessing the full power of SQLite.
Entities: Represent tables in your database. In this case, GroceryItem is your entity.
DAOs: Data Access Objects that define the methods for accessing your data (insert, delete, update, query).
Repository: A class that abstracts access to multiple data sources (in this case, just the Room database).
The ViewModel interacts with the Repository.
ViewModel: Holds and manages UI-related data in a lifecycle-conscious way. It uses the Repository to access and modify the data.
View Binding: A feature that allows you to more easily write code that interacts with views, replacing findViewById.
Coroutines: A concurrency design pattern that helps you simplify asynchronous code (like database operations).
While the code I provided is structurally sound, there are some potential issues and areas for improvement:
1. Error Handling
Database Operations: Database operations (insert, update, delete) can fail. The code should include error handling (e.g., try-catch blocks) to gracefully handle potential exceptions like database constraint violations or I/O errors. Input Validation: The code currently only checks if the input is blank. It should be more robust and handle cases like invalid characters, excessively long input, etc. This prevents unexpected behavior and database issues. 2. UI Thread Blocking
Long-running Operations: Database operations should ideally be performed off the main thread to avoid blocking the UI and causing ANRs (Application Not Responding). You can use coroutines with Dispatchers.IO for this. 3. Efficiency
Database Queries: For larger datasets, fetching all items every time the data changes might become inefficient. Consider using more specific queries or pagination if necessary. 4. Testability
ViewModel Dependency: The GroceryItemViewModel directly depends on the GroceryItemRepository. This can make unit testing difficult. Consider using dependency injection (e.g., Hilt) to provide the repository as a dependency, allowing you to easily mock it in tests.
MainActivity: The implementation looks good, but ensure you've imported the necessary libraries and that the GroceryItemApplication class is correctly referenced. AndroidManifest.xml: Ensure the GroceryItemApplication class is correctly registered.
Improved Code (with some of the suggestions above):
Improved Code (with some of the suggestions above):
Kotlin
// MainActivity.kt
binding.btnAddItem.setOnClickListener {
val itemName = binding.etGroceryItem.text.toString()
if (itemName.isNotBlank()) {
lifecycleScope.launch {
try {
groceryItemViewModel.insert(GroceryItem(itemName = itemName))
binding.etGroceryItem.text.clear()
} catch (e: Exception) {
// Handle the exception, e.g., show an error message
Toast.makeText(this@MainActivity, "Error adding item", Toast.LENGTH_SHORT).show()
Log.e("MainActivity", "Error inserting item", e)
}
}
}
}
// GroceryItemViewModel.kt
fun insert(groceryItem: GroceryItem) = viewModelScope.launch(Dispatchers.IO) {
try {
repository.insert(groceryItem)
} catch (e: Exception) {
// Handle the exception, e.g., post an error event to the UI
}
}
This improved code includes basic error handling for database inserts and moves the database operation to a background thread.