A small Android app used as a teaching example for the SOLID principles. The app itself is intentionally simple — create a note, save it to either a Room database or a JSON file, mark it as shareable or unshareable, and share shareable notes via the system share sheet. The interesting part is the code structure, which is shaped to make SOLID trade-offs visible and discussable.
- Create text notes with a title and body
- Persist notes to one of two backends:
- Room database (
Note.DatabaseNote) - JSON files in app-private storage (
Note.FileNote)
- Room database (
- Mark notes as
ShareableorUnshareable - Share shareable notes through
Intent.ACTION_SEND - Delete notes
- List all notes (loaded from both backends)
- Language: Kotlin
- UI: Jetpack Compose, Material 3
- Persistence: Room 2.7,
kotlinx.serialization(JSON) for file-backed notes - Architecture:
ViewModel+StateFlow - Build: Android Gradle Plugin 9.0.1, Kotlin 2.3.21
- SDK:
compileSdk36,minSdk29,targetSdk36
app/src/main/java/com/sjaindl/notesdemoapp/
├── MainActivity.kt # Compose entry point
├── NotesScreen.kt # List + FAB to add
├── AddNoteScreen.kt # Form to create a note
├── NotesAppBar.kt
├── SingleNote.kt
├── NotesViewModel.kt # State + dispatch to the right manager
├── NoteAction.kt # share / load / save / delete
├── ShareableNotesManager.kt # NoteAction impl, supports share()
├── UnshareableNotesManager.kt # NoteAction impl, share() is a no-op
├── model/
│ ├── Note.kt # sealed interface: DatabaseNote | FileNote
│ └── ShareType.kt
└── db/
├── AppDatabase.kt # Room DB
├── NoteEntity.kt
└── NotesDao.kt
The code is structured so each principle has something concrete to point at — including a couple of deliberate smells that make good discussion material.
- S — Single Responsibility.
NotesViewModelonly coordinates state; persistence lives in the managers; the Compose screens only render. Each*Managerseparates file I/O from database I/O into private helpers. - O — Open/Closed. Adding a new note kind (e.g. a remote note) means adding a new
Notesubtype and a newNoteActionimplementation, without changing existing managers. - L — Liskov Substitution.
Note.DatabaseNoteandNote.FileNoteare interchangeable wherever aNoteis expected — same contract, same fields. - I — Interface Segregation.
UnshareableNotesManager.share()is an empty no-op, which is a textbook ISP violation: clients are forced to depend on a method they cannot use. - D — Dependency Inversion.
NotesViewModeldepends on theNoteActionabstraction rather than concrete managers, and the managers are injected viaNotesViewModelFactory.
The codebase is small enough to read end-to-end in a sitting, which is the point — students can refactor it and watch the principles tighten or loosen.
Requirements: Android Studio (recent stable), JDK 17+, an emulator or device running Android 10 (API 29) or newer.
./gradlew :app:assembleDebug # build a debug APK
./gradlew :app:installDebug # install on a connected device/emulator
./gradlew test # JVM unit tests
./gradlew connectedAndroidTest # instrumentation tests (requires a device)Or open the project in Android Studio and run the app configuration.
MIT © Stefan Jaindl