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
| Technology | Purpose | |
|---|---|---|
| Kotlin | Android Programming Language | |
| Jetpack Compose | UI Development | |
| Room Database | Offline Data Storage | |
| ViewModel | State Management | |
| Coroutines | Background Operations | |
| StateFlow | Reactive UI Updates | |
| Material 3 | Modern 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
}
)
}
}
}
}
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

