Best Tutorial for AndroidDevelopers

Android App Development

Stay ahead with the latest tools, trends, and best practices in Android development

Build a Notes App with Room Database and Jetpack Compose

Build a Notes App with Room Database and Jetpack Compose - Responsive Blogger Template
Build a Notes App with Room Database and Jetpack Compose thumbnail

Build a Notes App with Room Database and Jetpack Compose

Mobile apps that work without an internet connection provide a better user experience and faster performance. In this tutorial, you will learn how to build a modern Notes App using Jetpack Compose, Room Database, and Clean Architecture in Android Studio.

This project is beginner-friendly and helps you understand real-world Android app development concepts such as offline storage, MVVM architecture, repository pattern, and reactive UI updates.

What You Will Build

By the end of this tutorial, your Notes App will support:
  • Creating notes
  • Viewing saved notes
  • Deleting notes
  • Offline data storage
  • Modern Material 3 UI
  • Reactive updates using StateFlow

Technologies Used

TechnologyPurpose
Kotlin Android Programming Language
Jetpack ComposeUI Development
Room DatabaseOffline Data Storage
ViewModelState Management
CoroutinesBackground Operations
StateFlowReactive UI Updates
Material 3Modern Android Design

Why Use Room Database?

Room Database is part of Android Jetpack and provides a clean abstraction layer over SQLite. It helps developers store local data efficiently while reducing boilerplate code.

Benefits of Room Database:

  • Easy database management
  • Compile-time query checking
  • Coroutine support
  • Offline-first applications
  • Better app performance

Project Architecture

This project follows Clean Architecture with MVVM.

com.example.enote
├── data
│ ├── Note.kt
│ ├── NoteDao.kt
│ └── NoteDatabase.kt
├── repository
│ └── NoteRepository.kt
├── ui
│ ├── AddNoteScreen.kt
│ ├── EditNoteScreen.kt
│ ├── HomeScreen.kt
│ ├── NoteItem.kt
│ └── NoteViewModel.kt
├── navigation
│ └── NavGraph.kt
└── MainActivity.kt
This structure keeps the code organized, scalable, and easier to maintain.

Modern Notes App with Room Database, KSP, MVVM & Clean Architecture

This project uses:
  • Jetpack Compose
  • Room Database
  • KSP (Kotlin Symbol Processing)
  • MVVM Architecture
  • StateFlow
  • Material 3
  • Navigation Compose
  • Repository Pattern
  • Clean Architecture
Steps

Step 1: Add Dependencies

Open your build.gradle.kts file and add the required dependencies.
plugins {
    id("com.android.application")
    kotlin("android")
    id("com.google.devtools.ksp")
}

android {

    namespace = "com.example.notesapp"
    compileSdk = 35

    defaultConfig {

        applicationId = "com.example.notesapp"
        minSdk = 24
        targetSdk = 35
        versionCode = 1
        versionName = "1.0"
    }

    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.14"
    }
}

dependencies {

    implementation("androidx.core:core-ktx:1.13.1")

    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4")

    implementation("androidx.activity:activity-compose:1.9.1")

    implementation(platform("androidx.compose:compose-bom:2024.06.00"))

    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.material3:material3")
    implementation("androidx.compose.ui:ui-tooling-preview")

    debugImplementation("androidx.compose.ui:ui-tooling")

    // Navigation
    implementation("androidx.navigation:navigation-compose:2.8.0")

    // Room
    implementation("androidx.room:room-runtime:2.6.1")
    implementation("androidx.room:room-ktx:2.6.1")

    // KSP
    ksp("androidx.room:room-compiler:2.6.1")

    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
}

Step 2: Entity Domain Model

@Entity(tableName = "notes")
data class Note(

    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,

    val title: String,

    val description: String,

    val category: String,

    val isPinned: Boolean = false
)

Step 3: Note DAO


@Dao
interface NoteDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertNote(note: Note)

    @Update
    suspend fun updateNote(note: Note)

    @Delete
    suspend fun deleteNote(note: Note)

    @Query("SELECT * FROM notes ORDER BY isPinned DESC, id DESC")
    fun getNotes(): Flow>

    @Query(
        """
        SELECT * FROM notes
        WHERE title LIKE '%' || :query || '%'
        OR description LIKE '%' || :query || '%'
        ORDER BY isPinned DESC, id DESC
        """
    )
    fun searchNotes(query: String): Flow>

    @Query("SELECT * FROM notes WHERE id = :id")
    suspend fun getNoteById(id: Int): Note
}

Step 4: Database

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

    abstract fun noteDao(): NoteDao
}

Step 5: Note Repository

class NoteRepository(
    private val dao: NoteDao
) {

    suspend fun insert(note: Note) {
        dao.insertNote(note)
    }

    suspend fun update(note: Note) {
        dao.updateNote(note)
    }

    suspend fun delete(note: Note) {
        dao.deleteNote(note)
    }

    fun getAllNotes() = dao.getAllNotes()

    fun searchNotes(query: String) = dao.searchNotes(query)
}

Step 6: ViewModel

class NoteViewModel(
    private val repository: NoteRepository
) : ViewModel() {

    private val _searchText = MutableStateFlow("")

    val searchText = _searchText

    fun updateSearch(text: String) {
        _searchText.value = text
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    val notes = _searchText
        .flatMapLatest {
            repository.searchNotes(it)
        }
        .stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(),
            emptyList()
        )

    fun addNote(
        title: String,
        desc: String,
        category: String
    ) {

        viewModelScope.launch {

            repository.insert(
                Note(
                    title = title,
                    description = desc,
                    category = category
                )
            )
        }
    }

    fun deleteNote(note: Note) {

        viewModelScope.launch {
            repository.delete(note)
        }
    }

    fun updateNote(note: Note) {

        viewModelScope.launch {
            repository.update(note)
        }
    }
}

Step 7: Note Item Component

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
    viewModel: NoteViewModel,
    darkMode: Boolean,
    onDarkModeChange: (Boolean) -> Unit
) {

    val notes by viewModel.notes.collectAsState()

    var title by remember {
        mutableStateOf("")
    }

    var desc by remember {
        mutableStateOf("")
    }

    val categories = listOf(
        "Study",
        "Work",
        "Ideas",
        "Personal"
    )

    var expanded by remember {
        mutableStateOf(false)
    }

    var selectedCategory by remember {
        mutableStateOf("Study")
    }

    Scaffold(

        floatingActionButton = {

            FloatingActionButton(
                onClick = {

                    if (title.isNotEmpty() && desc.isNotEmpty()) {

                        viewModel.addNote(
                            title,
                            desc,
                            selectedCategory
                        )

                        title = ""
                        desc = ""
                    }
                },
                shape = RoundedCornerShape(20.dp)
            ) {

                Icon(painter = painterResource(R.drawable.add),
                    contentDescription = null)
            }
        }

    ) { padding ->

        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(padding)
                .padding(16.dp)
        ) {

            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {

                Column {

                    Text(
                        text = "E Notes",
                        style = MaterialTheme.typography.headlineLarge,
                        fontWeight = FontWeight.Bold
                    )

                    Text(
                        text = "Manage your daily notes",
                        style = MaterialTheme.typography.bodyMedium
                    )
                }

                IconButton(
                    onClick = {
                        onDarkModeChange(!darkMode)
                    }
                ) {

                    Icon(painter = painterResource(R.drawable.mode),
                        contentDescription = null)
                }
            }

            Spacer(modifier = Modifier.height(20.dp))

            OutlinedTextField(
                value = viewModel.searchText.collectAsState().value,
                onValueChange = {
                    viewModel.updateSearch(it)
                },
                modifier = Modifier.fillMaxWidth(),
                shape = RoundedCornerShape(18.dp),
                leadingIcon = {
                    Icon(painter = painterResource(R.drawable.search),
                        contentDescription = null)
                },
                placeholder = {
                    Text("Search notes...")
                }
            )

            Spacer(modifier = Modifier.height(20.dp))

            ElevatedCard(
                modifier = Modifier.fillMaxWidth(),
                shape = RoundedCornerShape(24.dp)
            ) {

                Column(
                    modifier = Modifier.padding(16.dp)
                ) {

                    Text(
                        text = "Create New Note",
                        style = MaterialTheme.typography.titleLarge,
                        fontWeight = FontWeight.Bold
                    )

                    Spacer(modifier = Modifier.height(16.dp))

                    OutlinedTextField(
                        value = title,
                        onValueChange = {
                            title = it
                        },
                        modifier = Modifier.fillMaxWidth(),
                        label = {
                            Text("Note Title")
                        },
                        shape = RoundedCornerShape(16.dp)
                    )

                    Spacer(modifier = Modifier.height(12.dp))

                    OutlinedTextField(
                        value = desc,
                        onValueChange = {
                            desc = it
                        },
                        modifier = Modifier.fillMaxWidth(),
                        label = {
                            Text("Description")
                        },
                        shape = RoundedCornerShape(16.dp)
                    )

                    Spacer(modifier = Modifier.height(12.dp))

                    ExposedDropdownMenuBox(
                        expanded = expanded,
                        onExpandedChange = {
                            expanded = !expanded
                        }
                    ) {

                        OutlinedTextField(
                            value = selectedCategory,
                            onValueChange = {},
                            readOnly = true,
                            modifier = Modifier
                                .menuAnchor(
                                    type = ExposedDropdownMenuAnchorType.PrimaryNotEditable,
                                    enabled = true
                                )
                                .fillMaxWidth(),
                            label = {
                                Text("Category")
                            },
                            shape = RoundedCornerShape(16.dp)
                        )

                        ExposedDropdownMenu(
                            expanded = expanded,
                            onDismissRequest = {
                                expanded = false
                            }
                        ) {

                            categories.forEach {

                                DropdownMenuItem(
                                    text = {
                                        Text(it)
                                    },
                                    onClick = {

                                        selectedCategory = it
                                        expanded = false
                                    }
                                )
                            }
                        }
                    }
                }
            }

            Spacer(modifier = Modifier.height(20.dp))

            Text(
                text = "Your Notes",
                style = MaterialTheme.typography.titleLarge,
                fontWeight = FontWeight.Bold
            )

            Spacer(modifier = Modifier.height(12.dp))

            LazyColumn {

                items(notes) { note ->

                    NoteItem(
                        note = note,
                        onDelete = {
                            viewModel.deleteNote(note)
                        },
                        onPin = {

                            viewModel.updateNote(
                                note.copy(
                                    isPinned = !note.isPinned
                                )
                            )
                        }
                    )
                }
            }
        }
    }
}

Step 8: Home Screen


@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
    navController: NavController,
    viewModel: NoteViewModel,
    darkMode: Boolean,
    onDarkModeChange: (Boolean) -> Unit

) {


    val notes by viewModel.notes.collectAsState()

    val searchText by viewModel.searchText.collectAsState()

    Scaffold(

        topBar = {

            TopAppBar(
                title = {
                    Text("E Notes")
                },
                actions = {

                    Row(
                        verticalAlignment = Alignment.CenterVertically
                    ) {

                        Text(
                            text = if (darkMode)
                                "Dark"
                            else
                                "Light"
                        )

                        Switch(
                            checked = darkMode,
                            onCheckedChange = onDarkModeChange
                        )
                    }


                }
            )
        },

        floatingActionButton = {

            FloatingActionButton(
                onClick = {
                    navController.navigate("add_note")
                }
            ) {

                Icon(
                    painter = painterResource(R.drawable.add),
                    contentDescription = null
                )
            }
        }

    ) { padding ->

        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(padding)
        ) {

            OutlinedTextField(

                value = searchText,

                onValueChange = {

                    viewModel.searchNotes(it)
                },

                modifier = Modifier
                    .fillMaxWidth()
                    .padding(8.dp),

                label = {
                    Text("Search Notes")
                }
            )

            LazyVerticalGrid(

                columns = GridCells.Fixed(2),

                modifier = Modifier
                    .weight(1f)
                    .padding(8.dp)
            ) {

                items(notes) { note ->

                    NoteItem(

                        note = note,
                        darkMode = darkMode,

                        onDelete = {
                            viewModel.deleteNote(note)
                        },

                        onPin = {

                            viewModel.updateNote(

                                note.copy(
                                    isPinned = !note.isPinned
                                )
                            )
                        },

                        onEdit = {

                            navController.navigate(
                                "edit_note/${note.id}"
                            )
                        }
                    )
                }
            }
        }
    }
}

Step 9: NavGraph


@Composable
fun NavGraph(
    viewModel: NoteViewModel,
    darkMode: Boolean,
    onDarkModeChange: (Boolean) -> Unit
) {

    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = "home"
    ) {

        composable("home") {

            HomeScreen(
                navController = navController,
                viewModel = viewModel,
                darkMode = darkMode,
                onDarkModeChange


            )
        }

        composable("add_note") {

            AddNoteScreen(
                navController = navController,
                viewModel = viewModel
            )
        }

        composable(

            route = "edit_note/{id}",

            arguments = listOf(

                navArgument("id") {
                    type = NavType.IntType
                }
            )
        ) { backStackEntry ->

            val id = backStackEntry.arguments
                ?.getInt("id") ?: 0

            EditNoteScreen(
                navController = navController,
                viewModel = viewModel,
                noteId = id
            )
        }
    }
}

Step 10: MainActivity

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val db = Room.databaseBuilder(
            applicationContext,
            NoteDatabase::class.java,
            "note_db"
        ).build()

        val repository = NoteRepository(db.noteDao())

        val viewModel = NoteViewModel(repository)


        enableEdgeToEdge()
        setContent {

            var darkMode by remember {
                mutableStateOf(false)
            }

            ENoteTheme(
                darkTheme = darkMode
            ) {

                NavGraph(
                    viewModel = viewModel,
                    darkMode = darkMode,
                    onDarkModeChange = {
                        darkMode = it
                    }
                )


            }
        }
    }
}
screen shot 3 e note app

e note app screenshot 1
e note app screenshot 2

Frequently Asked Questions (FAQ)

1. What is Room Database in Android?

Room Database is a part of Android Jetpack that provides an abstraction layer over SQLite. It helps developers store offline data easily using Kotlin classes and SQL queries.

2. Why should I use Jetpack Compose for Android development?

Jetpack Compose simplifies Android UI development by using Kotlin code instead of XML layouts. It offers:
  • Faster UI development
  • Less boilerplate code
  • Better state management
  • Modern Material 3 support

3. Is this Notes App fully offline?

Yes. The app uses Room Database for local storage, so users can create and manage notes without an internet connection.

4. What architecture is used in this project?

This project follows MVVM (Model-View-ViewModel) with Clean Architecture principles.

Layers include:
  • UI Layer
  • ViewModel Layer
  • Repository Layer
  • Database Layer

5. Why use MVVM architecture in Android apps?

MVVM helps separate UI and business logic, making apps:
  • Easier to maintain
  • More scalable
  • Cleaner to test
  • Better organized

6. What is the role of Repository in Android architecture?

The Repository layer acts as a single source of truth between the ViewModel and Room Database. It manages data operations cleanly.

7. Does Room Database support coroutines?

Yes. Room Database works perfectly with Kotlin Coroutines for running database operations in the background thread.

8. What is StateFlow in Jetpack Compose?

StateFlow is a state management API from Kotlin Coroutines that helps automatically update the UI whenever data changes.

9. Can I add cloud synchronization later?

Yes. You can integrate:
  • Firebase Firestore
  • Supabase
  • REST APIs
  • Cloud backup services
while keeping Room Database for offline caching.

10. Is this project beginner-friendly?

Yes. This tutorial is designed for beginners learning:
  • Jetpack Compose
  • Room Database
  • Android Architecture
  • Offline storage apps

11. What are the advantages of offline-first apps?

Offline-first apps provide:
  • Faster performance
  • Better reliability
  • Reduced internet dependency
  • Improved user experience

12. Can I use Hilt Dependency Injection in this project?

Yes. Hilt can be added later to simplify dependency management and improve scalability.

13. Is Room Database better than SQLite?

Room Database is built on top of SQLite but provides:
  • Easier APIs
  • Compile-time query checking
  • Better Kotlin support
  • Coroutine integration
making development much simpler.

14. What features can I add next?

You can improve the Notes App by adding:
  • Search functionality
  • Edit notes
  • Dark mode
  • Swipe to delete
  • Categories and tags
  • Backup and restore
  • PIN lock security

15. Which Android Studio version should I use?

Use the latest stable version of Android Studio for the best compatibility with Jetpack Compose and Room Database.

16. Does this project support Material 3?

Yes. The app uses Material 3 components for a modern Android UI design.

17. Can this project be published on Google Play Store?

Yes. After proper testing, optimization, and adding privacy policies, the app can be published on the Google Play Store.

18. Is Jetpack Compose the future of Android UI development?

Yes. Google officially recommends Jetpack Compose for modern Android app development because it improves productivity and reduces boilerplate code.

19. Can I use this project for learning CRUD operations?

Absolutely. This Notes App is a practical CRUD project where you learn:
  • Create notes
  • Read notes
  • Update notes
  • Delete notes
using Room Database.

20. What skills will I gain after completing this project?

You will learn:
  • Modern Android development
  • Offline storage implementation
  • MVVM architecture
  • Compose UI development
  • State management
  • Real-world app structure

Sandeep Kumar - Android Developer

About the Author

Sandeep Kumar is an Android developer and educator who writes beginner-friendly Jetpack Compose tutorials on CodingBihar.com. His focus is on clean UI, Material Design 3, and real-world Android apps.

SkillDedication

— Python High Level Programming Language- Expert-Written Tutorials, Projects, and Tools—

Coding Bihar

Welcome To Coding Bihar👨‍🏫