Compose MVVM RoomDB with Flow and DI

Compose MVVM RoomDB with Flow and DI

A Clean Architecture Guide for Building Modern Android Apps

This article guides you through building a basic Android application using Compose, MVVM architecture, Room database for data persistence, Flow for asynchronous data updates, and Dagger Hilt for dependency injection. It's intended for developers familiar with Android development and interested in exploring modern techniques for building efficient and maintainable applications.

Key Technologies:

  • Compose: A modern declarative UI framework for building beautiful and responsive Android applications.

  • MVVM: An architectural pattern for separating the user interface (UI) from the business logic and data access layers, promoting cleaner code and better testability.

  • Room Database: A lightweight and powerful persistence library for storing data on the device.

  • Flow: A reactive programming library for managing asynchronous data streams, offering improved data handling and UI updates.

  • Dagger Hilt: A dependency injection framework for Android, simplifying dependency management and reducing boilerplate code.

Benefits:

Combining these technologies offers various benefits, including:

  • Improved UI development experience: Compose's declarative approach makes UI development easier and more intuitive.

  • Clean and maintainable code: MVVM architecture promotes separation of concerns and testability.

  • Efficient data persistence: Room Database provides a robust and efficient solution for storing and retrieving data locally.

  • Simplified data handling: Flow enables reactive data management and simplifies UI updates for asynchronous data changes.

  • Reduced boilerplate: Dagger Hilt automates dependency injection, reducing development time and code complexity.

Here is an File Structure of Our App:

saurabhjadhav@pop-os:~/AndroidStudioProjects/composeRoomMVVMFlow/app/src/main/java/com/simplifymindfulness/composeroommvvmflow$ tree
.
├── composables
│   └── ItemListScreen.kt
├── di
│   └── AppModule.kt
├── MainActivity.kt
├── MyApplication.kt
├── repository
│   └── ItemRepository.kt
├── room
│   ├── AppDatabase.kt
│   ├── ItemDao.kt
│   └── Item.kt
├── ui
│   └── theme
│       ├── Color.kt
│       ├── Theme.kt
│       └── Type.kt
└── viewmodel
    └── ItemViewModel.kt

7 directories, 12 files

So Now Lets Start with dependencies of our application.

Project Level Gradle:

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    ext.kotlin_version = '1.8.10'
    ext {
        compose_ui_version = '1.4.4'
        hilt_version = '2.44'
        room_version = "2.4.3"
        accompanist_version = '0.25.0'
        lifecycle_version = "2.6.0-alpha01"
        agp_version = '8.0.2'

    }
//
    dependencies {
        classpath "com.android.tools.build:gradle:$agp_version"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

plugins {
id 'com.android.application' version '8.1.1' apply false
    id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
    id 'com.android.library' version '8.1.1' apply false
    id 'com.google.dagger.hilt.android' version '2.44' apply false

}

App Level Gradle:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
    id 'kotlin-android'
    id 'dagger.hilt.android.plugin'

}

android {
    namespace 'com.simplifymindfulness.composeroommvvmflow'
    compileSdk 34

    defaultConfig {
        applicationId "com.simplifymindfulness.composeroommvvmflow"
        minSdk 24
        targetSdk 34
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary true
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_18
        targetCompatibility JavaVersion.VERSION_18
    }


    kotlinOptions {
        jvmTarget = '18'
        freeCompilerArgs += ["-Xjvm-default=compatibility"]
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.4.3'
    }
    packaging {
        resources {
            excludes += '/META-INF/{AL2.0,LGPL2.1}'
        }
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.9.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
    implementation 'androidx.activity:activity-compose:1.8.1'
    implementation platform('androidx.compose:compose-bom:2023.03.00')
    implementation 'androidx.compose.ui:ui'
    implementation 'androidx.compose.ui:ui-graphics'
    implementation 'androidx.compose.ui:ui-tooling-preview'
    implementation 'androidx.compose.material3:material3'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation platform('androidx.compose:compose-bom:2023.03.00')
    androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
    debugImplementation 'androidx.compose.ui:ui-tooling'
    debugImplementation 'androidx.compose.ui:ui-test-manifest'

    //room
    implementation "androidx.room:room-runtime:2.6.0"
    annotationProcessor "androidx.room:room-compiler:2.6.0"
    kapt "androidx.room:room-compiler:2.6.0"
    implementation "androidx.room:room-ktx:2.6.0"

    //dagger hilt
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-compiler:$hilt_version"

    // Coroutine support for Retrofit
    implementation "io.coil-kt:coil-compose:1.3.2"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1"
    implementation 'androidx.navigation:navigation-compose:2.7.2'

    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    // LiveData
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
}

Application Setup with Hilt:

Here's a glimpse of how we set up a basic Compose application with Hilt:

1. Application Class:

Kotlin

@HiltAndroidApp
class MyApplication : Application()

Annotating the application class with @HiltAndroidApp enables Hilt's dependency injection capabilities throughout the application.

2. MainActivity:

Kotlin

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      // ... Compose UI
    }
  }
}

@AndroidEntryPoint annotation on the activity informs Hilt to manage its dependencies.

Hilt's Role:

Hilt simplifies dependency injection by automatically providing the necessary instances to your classes. It manages the lifecycle of these dependencies and eliminates the need for manual dependency management, making your code cleaner and more maintainable.

Note: We need to write more of Hilt code but first we will setup other files.

Room Structure:

1. Entity (Item):

Kotlin

@Entity
data class Item(
  @PrimaryKey(autoGenerate = true) val id: Int = 0,
  val name: String
)

This class defines the structure of a single data item stored in the Room database. It includes an auto-generated primary key (id) and a String field for the item name.

2. DAO (ItemDao):

Kotlin

@Dao
interface ItemDao {
  @Query("SELECT * FROM item")
  fun getAllItems(): Flow<List<Item>>

  @Insert
  suspend fun insertItem(item: Item)

  @Update
  suspend fun updateItem(item: Item)

  @Delete
  suspend fun deleteItem(item: Item)
}

The DAO interface defines methods for interacting with the database:

  • getAllItems: Returns a Flow of all items stored in the database.

  • insertItem: Inserts a new item into the database.

  • updateItem: Updates an existing item in the database.

  • deleteItem: Deletes an item from the database.

Database Structure:

The Room database is essentially a collection of tables. In this case, we have a single table named "item" that stores item objects.

DAO Functionalities:

The DAO provides functionalities for CRUD (Create, Read, Update, Delete) operations on the item data. It uses annotations like @Query, @Insert, @Update, and @Delete to specify the SQL queries associated with each operation.

Flow Usage:

Flow is used to manage the asynchronous nature of database operations. It allows the UI to react to changes in the data seamlessly.

Now Combination of Hilt + Room :

AppModule:

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

  @Provides
  @Singleton
  fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase =
    Room.databaseBuilder(context, AppDatabase::class.java, "item_database")
      .fallbackToDestructiveMigration()
      .build()

  @Provides
  fun provideItemDao(appDatabase: AppDatabase): ItemDao = appDatabase.itemDao()

  @Provides
  fun provideItemRepository(itemDao: ItemDao): ItemRepository = ItemRepository(itemDao)
}

This module provides instances of the following classes:

  • AppDatabase: The Room database instance.

  • ItemDao: The DAO interface for accessing the database.

  • ItemRepository: The repository class that manages data access logic.

Repository and ViewModel

1. Repository (ItemRepository):

class ItemRepository(private val itemDao: ItemDao) {

  fun getAllItems(): Flow<List<Item>> = itemDao.getAllItems()

  suspend fun insertItem(item: Item) = itemDao.insertItem(item)

  suspend fun updateItem(item: Item) = itemDao.updateItem(item)

  suspend fun deleteItem(item: Item) = itemDao.deleteItem(item)
}

The repository class acts as an abstraction layer between the UI and the DAO. It exposes methods for:

  • Getting all items: Returns a Flow of all items stored in the database.

  • Inserting an item: Inserts a new item into the database using the DAO.

  • Updating an item: Updates an existing item in the database using the DAO.

  • Deleting an item: Deletes an item from the database using the DAO.

2. ViewModel (ItemViewModel):

@HiltViewModel
class ItemViewModel @Inject constructor(private val itemRepository: ItemRepository) : ViewModel() {

  private val allItems = itemRepository.getAllItems()

  val items: LiveData<List<Item>> = allItems.asLiveData()

  fun addItem(item: Item) = viewModelScope.launch { itemRepository.insertItem(item) }

  fun updateItem(item: Item) = viewModelScope.launch { itemRepository.updateItem(item) }

  fun deleteItem(item: Item) = viewModelScope.launch { itemRepository.deleteItem(item) }
}

The ViewModel manages the data flow and exposes observable data to the UI. It utilizes the repository for data access and provides methods for:

  • Accessing all items: Exposes the Flow of all items as a LiveData object, allowing observation by the UI.

  • Adding an item: Uses coroutines to launch the item insertion in the background.

  • Updating an item: Uses coroutines to launch the item update in the background.

  • Deleting an item: Uses coroutines to launch the item deletion in the background.

Using Flow for Asynchronous Data Updates:

Flow provides a reactive way to handle asynchronous data updates. This means the UI is automatically notified when data changes occur in the database, without the need for explicit polling or manual data retrieval. This simplifies the UI development and ensures data consistency.

UI (ItemListScreen):

1. Compose Components:

@Composable
fun ItemListScreen(viewModel: ItemViewModel) {
  val items: List<Item> by viewModel.items.collectAsState(initial = emptyList())

  Column {
    Text(text = "Item List", style = MaterialTheme.typography.h5)
    Spacer(modifier = Modifier.height(16.dp))
    ItemList(items = items)
    Spacer(modifier = Modifier.height(16.dp))
    AddButton(onClick = { /* Open add item dialog */ })
  }
}

@Composable
fun ItemList(items: List<Item>) {
  Column {
    items.forEach { item ->
      ItemCard(item = item)
    }
  }
}

@Composable
fun ItemCard(item: Item) {
  Surface(modifier = Modifier.padding(8.dp), elevation = 4.dp) {
    Row(modifier = Modifier.padding(16.dp)) {
      Text(text = item.name)
      Spacer(modifier = Modifier.weight(1f))
      IconButton(onClick = { /* Open edit item dialog */ }) {
        Icon(imageVector = Icons.Filled.Edit, contentDescription = "Edit")
      }
      IconButton(onClick = { /* Show delete confirmation dialog */ }) {
        Icon(imageVector = Icons.Filled.Delete, contentDescription = "Delete")
      }
    }
  }
}

@Composable
fun AddButton(onClick: () -> Unit) {
  FloatingActionButton(onClick = onClick) {
    Icon(imageVector = Icons.Filled.Add, contentDescription = "Add Item")
  }
}

2. Displaying Items:

  • ItemListScreen collects the items LiveData from the ViewModel using collectAsState and passes it to the ItemList composable.

  • ItemList iterates through each item and calls the ItemCard composable to display individual items.

  • ItemCard renders each item's name and provides icons for editing and deletion functionalities.

3. Functionalities:

  • Clicking the AddButton triggers the action passed through the onClick parameter, typically opening an add item dialog.

  • Clicking the edit icon on an item card should open an edit item dialog pre-filled with the item's data.

  • Clicking the delete icon should trigger a confirmation dialog for deleting the specific item.

Dialogs for Adding and Editing Items:

1. Add Item Dialog:

@Composable
fun AddItemDialog(viewModel: ItemViewModel, onDismiss: () -> Unit) {
  AlertDialog(
    onDismissRequest = { onDismiss() },
    title = Text("Add Item"),
    text = Column {
      TextField(
        value = "",
        onValueChange = {},
        placeholder = "Enter item name",
      )
    },
    confirmButton = {
      TextButton(onClick = { /* Add item */ }) {
        Text("Add")
      }
    },
    dismissButton = {
      TextButton(onClick = { onDismiss() }) {
        Text("Cancel")
      }
    }
  )
}

This dialog allows users to enter a new item name. When the "Add" button is clicked, the ViewModel's addItem function is called with the entered name.

2. Edit Item Dialog:

@Composable
fun EditItemDialog(viewModel: ItemViewModel, item: Item, onDismiss: () -> Unit) {
  AlertDialog(
    onDismissRequest = { onDismiss() },
    title = Text("Edit Item"),
    text = Column {
      TextField(
        value = item.name,
        onValueChange = {},
        placeholder = "Enter item name",
      )
    },
    confirmButton = {
      TextButton(onClick = { /* Update item */ }) {
        Text("Save")
      }
    },
    dismissButton = {
      TextButton(onClick = { onDismiss() }) {
        Text("Cancel")
      }
    }
  )
}

This dialog pre-fills the item name and allows editing. When the "Save" button is clicked, the ViewModel's updateItem function is called with the updated item data.

3. Interacting with ViewModel:

  • Both dialogs receive the ViewModel instance as a parameter.

  • When users enter data or click buttons, the dialogs access appropriate ViewModel methods to perform actions like adding or updating items.

  • This ensures proper interaction between the UI and the data layer through the ViewModel.

Complete Code:

MyApplication.kt

package com.simplifymindfulness.composeroommvvmflow

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

@HiltAndroidApp
class MyApplication : Application()

MainActivity.kt

package com.simplifymindfulness.composeroommvvmflow

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import com.simplifymindfulness.composeroommvvmflow.composables.ItemListScreen
import com.simplifymindfulness.composeroommvvmflow.ui.theme.ComposeRoomMVVMFlowTheme
import com.simplifymindfulness.composeroommvvmflow.viewmodel.ItemViewModel
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeRoomMVVMFlowTheme {
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                    val itemViewModel: ItemViewModel = viewModel()
                    ItemListScreen(viewModel = itemViewModel)

                }
            }
        }
    }
}

AppDatabase.kt

package com.simplifymindfulness.composeroommvvmflow.room

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

@Database(entities = [Item::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun itemDao(): ItemDao
}

Item.kt

package com.simplifymindfulness.composeroommvvmflow.room

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

@Entity
data class Item(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val name: String
)

ItemDao.kt

package com.simplifymindfulness.composeroommvvmflow.room

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow

@Dao
interface ItemDao {
    @Query("SELECT * FROM item")
    fun getAllItems(): Flow<List<Item>>

    @Insert
    suspend fun insertItem(item: Item)

    @Update
    suspend fun updateItem(item: Item)

    @Delete
    suspend fun deleteItem(item: Item)
}

AppModule.kt

package com.simplifymindfulness.composeroommvvmflow.di


import android.content.Context
import androidx.room.Room
import com.simplifymindfulness.composeroommvvmflow.room.AppDatabase
import com.simplifymindfulness.composeroommvvmflow.room.ItemDao
import com.simplifymindfulness.composeroommvvmflow.repository.ItemRepository
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 AppModule {

    @Provides
    @Singleton
    fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase =
        Room.databaseBuilder(context, AppDatabase::class.java, "item_database")
            .fallbackToDestructiveMigration()
            .build()

    @Provides
    fun provideItemDao(appDatabase: AppDatabase): ItemDao = appDatabase.itemDao()

    @Provides
    fun provideItemRepository(itemDao: ItemDao): ItemRepository = ItemRepository(itemDao)
}

ItemRepository.kt

package com.simplifymindfulness.composeroommvvmflow.repository

import com.simplifymindfulness.composeroommvvmflow.room.Item
import com.simplifymindfulness.composeroommvvmflow.room.ItemDao
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class ItemRepository @Inject constructor(private val itemDao: ItemDao) {
    val allItems: Flow<List<Item>> = itemDao.getAllItems()

    suspend fun insert(item: Item) {
        itemDao.insertItem(item)
    }

    suspend fun update(item: Item) {
        itemDao.updateItem(item)
    }

    suspend fun delete(item: Item) {
        itemDao.deleteItem(item)
    }
}

ItemViewModel.kt

package com.simplifymindfulness.composeroommvvmflow.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.simplifymindfulness.composeroommvvmflow.repository.ItemRepository
import com.simplifymindfulness.composeroommvvmflow.room.Item
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class ItemViewModel @Inject constructor(private val repository: ItemRepository) : ViewModel() {
    private val _items = MutableStateFlow<List<Item>>(emptyList())
    val items: StateFlow<List<Item>> = _items.asStateFlow()

    init {
        viewModelScope.launch {
            repository.allItems.collect { listOfItems ->
                _items.value = listOfItems
            }
        }
    }
    fun insert(item: Item) = viewModelScope.launch {
        repository.insert(item)
    }

    fun update(item: Item) = viewModelScope.launch {
        repository.update(item)
    }

    fun delete(item: Item) = viewModelScope.launch {
        repository.delete(item)
    }
}

ItemListScreen.kt

package com.simplifymindfulness.composeroommvvmflow.composables

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.simplifymindfulness.composeroommvvmflow.room.Item
import com.simplifymindfulness.composeroommvvmflow.viewmodel.ItemViewModel

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemListScreen(viewModel: ItemViewModel) {
    val items = viewModel.items.collectAsState().value
    var showDialog by remember { mutableStateOf(false) }
    var currentEditItem by remember { mutableStateOf<Item?>(null) }
    var itemToDelete by remember { mutableStateOf<Item?>(null) }
    var showDeleteConfirmDialog by remember { mutableStateOf(false) }

    Scaffold(
        floatingActionButton = {
            FloatingActionButton(onClick = { showDialog = true }) {
                Icon(Icons.Filled.Add, contentDescription = "Add")
            }
        }
    ) { paddingValues ->
        LazyColumn(contentPadding = paddingValues) {
            items(items) { item ->
                ItemRow(
                    item = item,
                    onItemEdit = { editItem ->
                        currentEditItem = editItem
                        showDialog = true
                    },
                    onItemDelete = { deleteItem ->
                        itemToDelete = deleteItem
                        showDeleteConfirmDialog = true
                    }
                )
                Spacer(modifier = Modifier.height(8.dp)) // Spacer for padding between items
            }
        }
    }

    // Add Dialog and Delete Confirmation Dialog
    if (showDialog) {
        if (currentEditItem == null) {
            ItemAddDialog(onDismiss = { showDialog = false }, onAddItem = { itemName ->
                viewModel.insert(Item(name = itemName))
                showDialog = false
            })
        } else {
            ItemEditDialog(item = currentEditItem!!, onDismiss = {
                showDialog = false
                currentEditItem = null
            }, onEditItem = { editedItem ->
                viewModel.update(editedItem)
                showDialog = false
                currentEditItem = null
            })
        }
    }

    if (showDeleteConfirmDialog) {
        AlertDialog(
            onDismissRequest = { showDeleteConfirmDialog = false },
            title = { Text("Confirm Delete") },
            text = { Text("Are you sure you want to delete this item?") },
            confirmButton = {
                Button(
                    onClick = {
                        itemToDelete?.let { viewModel.delete(it) }
                        showDeleteConfirmDialog = false
                    }
                ) { Text("Yes") }
            },
            dismissButton = {
                Button(onClick = { showDeleteConfirmDialog = false }) { Text("No") }
            }
        )
    }

}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemRow(item: Item, onItemEdit: (Item) -> Unit, onItemDelete: (Item) -> Unit) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp, horizontal = 8.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Text(
                text = item.name,
                style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium)
            )
            Row {
                IconButton(onClick = { onItemEdit(item) }) {
                    Icon(
                        imageVector = Icons.Filled.Edit,
                        contentDescription = "Edit",
                        tint = MaterialTheme.colorScheme.primary
                    )
                }
                IconButton(onClick = { onItemDelete(item) }) {
                    Icon(
                        imageVector = Icons.Filled.Delete,
                        contentDescription = "Delete",
                        tint = MaterialTheme.colorScheme.error
                    )
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemAddDialog(onDismiss: () -> Unit, onAddItem: (String) -> Unit) {
    var text by remember { mutableStateOf("") }

    AlertDialog(
        onDismissRequest = onDismiss,
        title = { Text("Add Item") },
        text = {
            TextField(
                value = text,
                onValueChange = { text = it },
                label = { Text("Item Name") }
            )
        },
        confirmButton = {
            Button(onClick = { onAddItem(text) }) {
                Text("Add")
            }
        },
        dismissButton = {
            Button(onClick = onDismiss) {
                Text("Cancel")
            }
        }
    )
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemEditDialog(item: Item, onDismiss: () -> Unit, onEditItem: (Item) -> Unit) {
    var text by remember { mutableStateOf(item.name) }

    AlertDialog(
        onDismissRequest = onDismiss,
        title = { Text("Edit Item") },
        text = {
            TextField(
                value = text,
                onValueChange = { text = it },
                label = { Text("Item Name") }
            )
        },
        confirmButton = {
            Button(onClick = { onEditItem(item.copy(name = text)) }) {
                Text("Save")
            }
        },
        dismissButton = {
            Button(onClick = onDismiss) {
                Text("Cancel")
            }
        }
    )
}

Output:

Conclusion:

This article provided a comprehensive overview of building modern Android applications utilizing Compose, MVVM architecture, RoomDB for data persistence, Flow for asynchronous data updates, and Dagger Hilt for dependency injection. We covered key aspects like dependencies setup, database and DAO implementation, repository and ViewModel functionalities, UI development using Compose components, dialogs for item addition and editing, and their interaction with the ViewModel.

By combining these powerful technologies, you can build efficient, maintainable, and scalable Android applications with a clean and well-structured codebase. For further exploration and access to the full source code, visit this GitHub repository: https://github.com/saurabhthesuperhero/composeRoomMVVMFlow.

Did you find this article valuable?

Support saurabh jadhav by becoming a sponsor. Any amount is appreciated!