Skip to main content

Command Palette

Search for a command to run...

Simple Notes App with Jetpack Compose, Room & Hilt

Published
5 min read
Simple Notes App with Jetpack Compose, Room & Hilt
G

Software Developer with over 5 years of experience building high-quality web, mobile, and Android applications. Proficient in JavaScript, TypeScript, Flutter, React, Node.js, and Android development using Kotlin, with a strong background in designing scalable backend services using microservices and event-driven architectures. Experienced in building resilient APIs, asynchronous workflows, and distributed systems that power dynamic user interfaces across platforms. Skilled in solving complex problems, optimizing application performance, and writing clean, maintainable code. Hands-on experience with Docker, Kubernetes, and AWS for containerization, orchestration, and cloud-native deployments. Passionate about turning technical challenges into smart, scalable, and production-ready solutions. As an active competitive programmer, I’ve solved 1,000+ LeetCode problems and rank in the Top 2.22% globally (Knight badge), demonstrating strong expertise in algorithms, data structures, and dynamic programming. I thrive on applying algorithmic thinking to build scalable, real-world systems that create lasting impact.

This small Android app demonstrates a clean, modern stack:Jetpack Compose for UI, Room for local persistence, and Hilt for dependency injection.The code is compact and organized into Application, data(Room), repository, ViewModel, and Compose UI layers- perfect as a learning example or starting point for a producition - readt notes app.

GitRepo : https://github.com/oganaa2472/notes_app/

What you’ll learn

  • How the app is structured (files & MVVM architechture)

  • How Room, DAO and database are defined and used

  • How Hilt provides dependencies

  • How the VIewModel and repository connect the UI to the data layer

  • How a simple compose UI collects user input and displays notes

Project Overview

This repository contains a single app module (app) implementing a basic notes application. Major files to inspect:

  • MyApp.kt — Hilt-enabled Application class

  • MainActivity.kt — Compose host and entry point

  • Note.kt — Room entity for a note

  • NoteDao.kt — DAO with database operations

  • NoteDatabase.kt — Room database singleton

  • AppModule.kt — Hilt module to provide Room and DAO

  • NoteRepository.kt — Data abstraction over DAO

  • NoteViewModel.kt — Exposes flows and actions to UI

  • NoteScreen.kt & NoteItem.kt — Compose UI for creating and listing notes

This layering keeps UI, business logic, and persistence concerns separated and testable.

Files & key snippets explained

1) Application entry: MyApp.kt

The application is annotated with Hilt to enable dependency injection across the app.

@HiltAndroidApp
class MyApp: Application()

This wire-up allows Hilt to inject dependencies into Activities, ViewModels, and other components.

2) Main activity: MainActivity.kt

A minimal Compose host that applies an app theme and loads NoteScreen.

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    enableEdgeToEdge()
    setContent { NoteAppTheme { NoteScreen() } }
  }
}

@AndroidEntryPoint allows Hilt to inject dependencies (for example ViewModels using Hilt).

3) Data model: Note.kt

A straightforward Room entity representing a note.

@Entity(tableName = "Notes")
data class Note(
  @PrimaryKey(autoGenerate = true) val id: Int = 0,
  val title: String,
  val content: String,
  val timestamp: Long
)

The timestamp is used for ordering notes by recency.

4) DAO: NoteDao.kt

A DAO using Kotlin Flow to observe note lists:

@Dao
interface NoteDao {
  @Insert suspend fun insert(note: Note)
  @Query("Select * from notes order by timestamp desc")
  fun getAllNotes(): Flow<List<Note>>
}

Using Flow<List<Note>> makes UI reactive: updates are observed automatically.

Note: the SQL uses notes in lowercase while the entity tableName is Notes; Room is usually case-insensitive for table name mapping but it's good practice to keep the names consistent (e.g., both notes).

5) Database singleton: NoteDatabase.kt

A classic double-checked, synchronized singleton to create the Room database:

@Database(entities = [Note::class], version = 1, exportSchema = false)
abstract class NoteDatabase: RoomDatabase() {
  abstract fun noteDao(): NoteDao

  companion object {
    @Volatile private var INSTANCE: NoteDatabase? = null
    fun getDatabase(context: Context): NoteDatabase {
      return INSTANCE ?: synchronized(this) {
        val instance = Room.databaseBuilder(
            context.applicationContext,
            NoteDatabase::class.java,
            "notes_database"
        ).build()
        INSTANCE = instance
        instance
      }
    }
  }
}

This provides a thread-safe single database instance across the app.

6) Hilt module: AppModule.kt

Provides the database and DAO as singletons:

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
  @Provides @Singleton fun provideNotDatabase(@ApplicationContext context: Context): NoteDatabase =
    NoteDatabase.getDatabase(context)

  @Provides @Singleton fun provideNoteDao(noteDatabase: NoteDatabase) = noteDatabase.noteDao()
}

Because these are singletons, they are shared across the app lifetime and can be injected into repositories and ViewModels.

7) Repository: NoteRepository.kt

A thin repository that wraps DAO calls. This isolates the rest of the app from direct DAO usage:

class NoteRepository @Inject constructor(private val noteDoa: NoteDao) {
  suspend fun insertNote(note: Note) { noteDoa.insert(note) }
  val allNotesInDb: Flow<List<Note>> = noteDoa.getAllNotes()
}

Note: a small typo noteDoa — consider renaming to noteDao for clarity.

8) ViewModel: NoteViewModel.kt

Exposes the Flow of notes to the UI and a method to insert notes:

@HiltViewModel
class NoteViewModel @Inject constructor(private val repo: NoteRepository): ViewModel() {
  val allNotes: Flow<List<Note>> = repo.allNotesInDb
  fun insert(note: Note) = viewModelScope.launch { repo.insertNote(note) }
}

viewModelScope ensures DB operations run off the main thread.

9) Compose UI: NoteScreen.kt & NoteItem.kt

The NoteScreen collects title/content via TextFields, inserts a new Note on Button click, and lists notes with LazyColumn:

val notes by viewModel.allNotes.collectAsState(emptyList())

var title by remember { mutableStateOf("") }
var content by remember { mutableStateOf("") }

Button(onClick = {
  viewModel.insert(Note(0, title, content, System.currentTimeMillis()))
  title = ""
  content = ""
}) { Text("Add note") }

LazyColumn { items(notes) { note -> NoteItem(note) } }

And NoteItem renders a card with title and content:

@Composable
fun NoteItem(note: Note) {
  Card(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
    Column(modifier = Modifier.padding(16.dp)) {
      Text(note.title, style = MaterialTheme.typography.titleMedium)
      Text(note.content, style = MaterialTheme.typography.bodyMedium)
    }
  }
}

This Compose UI is compact and idiomatic: state hoisted into local remember variables, collectAsState to subscribe to the Flow, and a LazyColumn for performance.

Design choices & rationale

  • Room + Flow: keeps UI reactive and avoids manual refresh logic.

  • Hilt: simple DI for singletons (DB/DAO) and ViewModel injection.

  • Repository layer: separates data-source details from ViewModel (helps testing and future extension to remote sources).

Compose UI: minimal, clean, and modern Android UI toolkit.

  • Table name consistency: @Entity(tableName = "Notes") vs DAO query "Select * from notes ...". Use consistent casing (e.g., notes) to avoid confusion.

  • DAO query ordering: good use of timestamp desc, but consider storing updatedAt vs createdAt to manage edits.

  • Deletions/updates: currently only insertion and listing are implemented. Add @Update and @Delete methods in NoteDao and corresponding UI actions.

  • Error handling & validation: validate empty title/content, and show user feedback (snackbar/toast) on success/failure.

  • Migration strategy: the database uses version 1 with no migration. For production, either add migrations or use fallbackToDestructiveMigration() carefully in dev only.

  • Tests: add unit tests for repository and ViewModel, and instrumented tests for DAO using an in-memory Room database.

How to run locally

From project root (macOS / zsh):

./gradlew assembleDebug
# install on a connected device/emulator:
./gradlew installDebug

Or open the project in Android Studio and run the app configuration.

Conclusion

This NoteApp is a compact, practical demonstration of modern Android development: Jetpack Compose for a declarative UI, Room for reliable local persistence, and Hilt for simple, testable dependency injection. The project shows how a small, well-layered architecture keeps concerns separated—UI, ViewModel, repository, and data—making the app easy to reason about, extend, and test.