diff --git a/README.md b/README.md index 6655d28..19e7bfe 100644 --- a/README.md +++ b/README.md @@ -4,194 +4,150 @@ [![Kotlin](https://img.shields.io/badge/Kotlin-2.0.21-7F52FF?logo=kotlin&logoColor=white)](https://kotlinlang.org/) [![Min SDK](https://img.shields.io/badge/minSdk-21-3DDC84?logo=android&logoColor=white)](https://developer.android.com) [![Target SDK](https://img.shields.io/badge/targetSdk-34-3DDC84?logo=android&logoColor=white)](https://developer.android.com) -[![Material](https://img.shields.io/badge/Material-3-757575?logo=materialdesign&logoColor=white)](https://m3.material.io/) [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -[![APK](https://img.shields.io/badge/Download-APK-success?logo=android&logoColor=white)](#download) -> A modern, offline-first Android app for tracking your personal book collection β€” built with Kotlin, MVVM, Room, Coroutines, and Material 3. +An offline-first Android app for keeping track of the books I read. Sign in, add books with covers and ratings, mark them as want-to-read / reading / finished, and export the whole library to PDF or JSON when you need to. -Mini Book Library is a portfolio-grade Android application that lets you sign in, build a personal library, track reading progress, and export your collection. It's designed to demonstrate clean architecture, reactive data flow, and a polished Material 3 user interface. +I started this as a small Java + raw SQLite project for class. After it worked, I rewrote it from scratch in Kotlin with MVVM, Room, Coroutines, and Material 3 so I had something I could actually point to in interviews. ---- +## Features -## ✨ Features +Library: -### Library management -- πŸ“– **Add, edit, and delete** books with cover images, author, year, category, and personal notes -- πŸ” **Real-time search** across title, author, and category -- 🎯 **Sort and filter** by title, date added, rating, or reading status β€” preferences persist across launches -- ⬅️ **Swipe-to-delete** with one-tap **Undo** via Snackbar +- Add, edit, and delete books with title, author, year, category, cover image, and personal notes. +- Live search across title, author, and category as you type. +- Sort by title, date added, or rating. Filter by reading status. Both choices stick across launches. +- Swipe a row to delete with an Undo snackbar so accidental swipes don't lose data. -### Reading tracker -- πŸ“š Three-state reading status: **Want to Read**, **Currently Reading**, **Finished** -- πŸ“Š **Page progress tracking** with a progress bar for "Currently Reading" books -- ⭐ **5-star rating** per book β€” sortable, surfaced on the dashboard average +Reading tracker: -### Dashboard -- πŸ‘‹ Personalized greeting based on time of day -- πŸ“ˆ Live stats: total books, average rating, status breakdown (with colored progress bars) -- πŸ” Recently added carousel with horizontal scrolling +- Three reading states: Want to Read, Currently Reading, Finished. +- Page progress (current / total) for books you're actively reading, with a progress bar. +- 1 to 5 star rating per book, sortable, also rolled up into a dashboard average. -### Cover images -- πŸ–Ό Pick a cover from gallery via the modern **Photo Picker** (no permissions needed on Android 13+) -- πŸ’Ύ Cover bytes copied to internal storage so they survive app restarts and backups -- ⚑ Image loading via [Coil](https://coil-kt.github.io/coil/) +Dashboard: -### ISBN auto-fill -- πŸ”Ž Type an ISBN-10 or ISBN-13 on the Add/Edit screen and tap **Auto-fill** β€” the app calls the public **Google Books API** and pre-populates title, author, year, category, and page count. -- πŸ›‘ Doesn't overwrite anything you've already typed β€” your local input always wins. -- πŸ“¦ Zero new dependencies: uses platform `HttpURLConnection` + `org.json`. No OkHttp, no Retrofit. -- ⏱ 8s timeout so a flaky connection never hangs the UI. +- Greeting that changes with time of day. +- Total count, average rating, and a status breakdown with colored progress bars. +- Recently added books in a horizontal rail at the bottom. -### Data export & backup -- πŸ“„ **Export to PDF** using the platform `PdfDocument` API β€” no third-party PDF dependency -- πŸ’Ύ **JSON backup** of your full library, savable to Downloads -- ♻️ **Restore from JSON** with conflict-free re-import (dataset is appended, not overwritten) +Cover images: -### Account & UX -- πŸ” Login & registration with **salted SHA-256 hashing** (passwords never stored in plaintext) -- πŸŒ— **Theme toggle**: System / Light / Dark, with full Material 3 color tokens -- πŸš€ **3-slide onboarding** (ViewPager2 with dot indicators) shown only on first launch -- πŸ›‘ Session persistence with `SharedPreferences` β€” open the app and pick up where you left off +- Pick from gallery using the platform Photo Picker, which doesn't need any runtime permission on Android 13+. +- The picked bytes are copied into internal storage so the cover survives reinstalls and Auto Backup. +- Image loading via Coil. ---- +ISBN auto-fill: -## πŸ— Architecture +- Type a 10 or 13 digit ISBN on the Add/Edit screen and tap Auto-fill. The app calls the public Google Books API and fills in title, author, year, category, and page count. +- It only fills fields you haven't typed in yet. Anything you've already entered is left alone. +- No new dependencies for this. It uses `HttpURLConnection` and `org.json` which are both on the platform. +- 8 second timeout so a flaky connection doesn't sit and spin. -The app follows clean **MVVM + Repository** architecture with unidirectional data flow: +Export and backup: -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ UI β”‚ -β”‚ Onboarding Β· Auth Β· Dashboard Β· BookList Β· Detail Β· Edit Β· Settingsβ”‚ -β”‚ (Activities + Fragments) β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ collects StateFlow - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ ViewModels β”‚ -β”‚ AuthVM Β· DashboardVM Β· BookListVM Β· BookDetailVM Β· AddEditVM Β· β”‚ -β”‚ SettingsVM β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ suspend / Flow - β–Ό -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Repositories β”‚ -β”‚ UserRepository Β· BookRepository β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β–Ό β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Room Database β”‚ β”‚ PreferencesManager β”‚ - β”‚ (UserDao, β”‚ β”‚ (theme, session, β”‚ - β”‚ BookDao) β”‚ β”‚ sort/filter) β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -**Why this shape:** -- **Single source of truth** β€” Room emits `Flow>`. The list, dashboard, and counters subscribe independently. -- **Repository hides storage details** β€” switching from Room to a remote API later wouldn't change a single ViewModel. -- **State is derived, not pushed** β€” every UI screen consumes a `StateFlow`. Combined with `repeatOnLifecycle`, the UI auto-pauses and resumes with the lifecycle. -- **Hand-rolled DI via `ServiceLocator`** β€” Hilt would be overkill for one module; a tiny container is more interview-friendly to read. +- Export the whole library to PDF using the platform `PdfDocument` API. Saves to Downloads. +- JSON backup with the same approach. Pick the file, open it, mail it to yourself. +- Restore appends imported books rather than replacing the existing ones, so you can merge backups without losing anything. ---- +Account and UX: -## 🧰 Tech stack +- Login and registration with salted SHA-256 password hashing. Plaintext is never persisted. +- Theme picker: Follow system, Light, or Dark. Uses Material 3 color tokens so both modes look right. +- Three-slide onboarding on first launch, ViewPager2 with dot indicators. Skippable. +- Session is held in `SharedPreferences` so the app opens straight to the dashboard once you've signed in. -| Layer | Technology | -| ---------------- | ---------- | -| Language | Kotlin 2.0 | -| Architecture | MVVM + Repository, single-Activity + Navigation Component | -| Async | Kotlin Coroutines + Flow / StateFlow | -| Database | Room 2.6 (KSP) | -| UI | Material 3, ViewBinding, ConstraintLayout, ViewPager2 | -| Image loading | Coil 2.7 | -| Navigation | Jetpack Navigation Component (Safe Args) | -| Networking | Platform `HttpURLConnection` + `org.json` (Google Books API) | -| PDF export | Android `PdfDocument` (platform) | -| Backup format | Hand-rolled JSON via `org.json` | -| Testing | JUnit 4, MockK, Turbine, kotlinx-coroutines-test, Room testing | -| CI | GitHub Actions (build + unit tests + APK artifact) | +## Architecture ---- - -## πŸ“ Module structure +Single-activity for the post-login app, plus separate activities for onboarding and auth so each lives in its own navigation graph. State flows in one direction: Room emits data, the ViewModel shapes it into a `StateFlow`, the Fragment collects it. ``` -app/src/main/java/com/example/minibooklibrary/ -β”œβ”€β”€ MiniBookLibraryApp.kt # Application entry β€” applies persisted theme -β”œβ”€β”€ di/ -β”‚ └── ServiceLocator.kt # Hand-rolled DI container -β”œβ”€β”€ domain/ -β”‚ β”œβ”€β”€ ReadingStatus.kt # Want / Reading / Finished enum -β”‚ β”œβ”€β”€ SortOrder.kt # Title/date/rating sorts -β”‚ └── StatusFilter.kt # Library list filter -β”œβ”€β”€ data/ -β”‚ β”œβ”€β”€ local/ -β”‚ β”‚ β”œβ”€β”€ BookDatabase.kt # Room database singleton -β”‚ β”‚ β”œβ”€β”€ dao/{UserDao, BookDao} # Data access objects (suspend + Flow) -β”‚ β”‚ └── entity/{User, Book} # @Entity-annotated tables -β”‚ β”œβ”€β”€ preferences/ -β”‚ β”‚ └── PreferencesManager.kt # Typed wrapper around SharedPreferences -β”‚ β”œβ”€β”€ remote/ -β”‚ β”‚ └── GoogleBooksService.kt # ISBN lookup via Google Books, parser separated for tests -β”‚ └── repository/ -β”‚ β”œβ”€β”€ UserRepository.kt # Auth + session -β”‚ β”œβ”€β”€ BookRepository.kt # CRUD + reactive reads -β”‚ └── AuthResult.kt # Sealed result type -β”œβ”€β”€ ui/ -β”‚ β”œβ”€β”€ RouterActivity.kt # First-screen routing decision -β”‚ β”œβ”€β”€ onboarding/ # 3-slide ViewPager2 onboarding -β”‚ β”œβ”€β”€ auth/ # AuthActivity + Login/Register fragments -β”‚ β”œβ”€β”€ main/MainActivity.kt # Single-Activity host with bottom nav -β”‚ β”œβ”€β”€ dashboard/ # Home screen with stats + recent rail -β”‚ β”œβ”€β”€ books/ -β”‚ β”‚ β”œβ”€β”€ list/ # Library tab + RecyclerView adapter -β”‚ β”‚ β”œβ”€β”€ detail/ # Book detail screen -β”‚ β”‚ └── edit/ # Add/Edit form -β”‚ β”œβ”€β”€ settings/ # Theme, backup/restore, PDF export -β”‚ └── common/ViewModelFactory.kt # Builds ViewModels from ServiceLocator -└── util/ - β”œβ”€β”€ PasswordHasher.kt # Salted SHA-256 - β”œβ”€β”€ ImageStorage.kt # Copies cover URIs into internal storage - β”œβ”€β”€ PdfExporter.kt # Renders the library to a PDF - β”œβ”€β”€ BackupManager.kt # JSON export / import - └── Extensions.kt # Toast, view-visibility, date formatting +UI (Activities + Fragments) + collects StateFlow + | +ViewModels + AuthViewModel, DashboardViewModel, BookListViewModel, + BookDetailViewModel, AddEditBookViewModel, SettingsViewModel + | + suspend / Flow + | +Repositories + UserRepository, BookRepository + | + +-- Room (UserDao, BookDao) + +-- PreferencesManager (theme, session, last sort/filter) + +-- GoogleBooksService (ISBN lookup) ``` -Tests live under `app/src/test/java/com/example/minibooklibrary/` mirroring the same package structure. +A few things worth calling out: ---- +- Room is the source of truth. The dashboard, library list, and counters each subscribe to their own `Flow` from the same DAOs and re-render independently. Nothing is pushed; everything is derived. +- The Repository layer is the only thing that knows about Room. If I ever swap to a remote API, no ViewModel needs to change. +- DI is a hand-rolled `ServiceLocator`. Hilt would have been fine but felt heavy for one module, and a small container is easier for someone to read in 30 seconds. +- The `GoogleBooksParser` is split off from the service so I can hit it with JSON fixtures in a JVM unit test without touching the network. -## πŸ§ͺ Testing +## Tech stack -Run the unit test suite: +| Layer | What | +| --- | --- | +| Language | Kotlin 2.0 | +| Architecture | MVVM with Repository, single-Activity + Navigation Component | +| Async | Kotlin Coroutines + Flow / StateFlow | +| Database | Room 2.6 (KSP) | +| UI | Material 3, ViewBinding, ConstraintLayout, ViewPager2 | +| Image loading | Coil 2.7 | +| Networking | `HttpURLConnection` + `org.json` (no extra deps) | +| PDF export | Android `PdfDocument` | +| Tests | JUnit 4, MockK, Turbine, kotlinx-coroutines-test, Room testing | +| CI | GitHub Actions (build, unit tests, debug APK artifact) | -```bash -./gradlew test -``` +## Project layout -What's covered: -- `PasswordHasherTest` β€” determinism, salt-isolation, verify success/failure -- `EnumStorageTest` β€” guards `ReadingStatus`, `SortOrder`, `StatusFilter` storage values from accidental rename/reorder -- `UserRepositoryTest` β€” registration validation, duplicate-username, hash-not-plaintext, login success/failure paths, session persistence -- `BookRepositoryTest` β€” timestamp stamping on insert/update, dao forwarding -- `GoogleBooksParserTest` β€” ISBN-lookup JSON parsing (happy path, missing fields, multiple authors, taxonomy splitting, year extraction) plus service-level network and validation paths -- `AuthViewModelTest` β€” login state transitions, register validation, error consumption, logout event -- `BookListViewModelTest` β€” search filters, status filters, A-Z sort, sort/filter persistence, swipe-delete + restore -- `AddEditBookViewModelTest` β€” form validation rules (title, year range, page progress), persistence of new books, edit hydration, ISBN auto-fill (preserves user input, surfaces failures, rejects too-short input) +``` +app/src/main/java/com/example/minibooklibrary/ + MiniBookLibraryApp.kt Application class, applies persisted theme on startup + di/ServiceLocator.kt Hand-rolled DI container + domain/ + ReadingStatus.kt Want / Reading / Finished + SortOrder.kt Sort options + StatusFilter.kt Library filter + BookLookup.kt Result type for ISBN auto-fill + data/ + local/ + BookDatabase.kt Room database singleton + dao/{UserDao, BookDao} Suspend functions + Flow reads + entity/{User, Book} @Entity tables + preferences/PreferencesManager.kt Typed SharedPreferences wrapper + remote/GoogleBooksService.kt Google Books client + parser + repository/{User, Book}Repository Suspend writes, reactive reads + ui/ + RouterActivity.kt Decides where the launcher should go + onboarding/ ViewPager2 onboarding + auth/ Login + Register fragments + main/MainActivity.kt Bottom-nav host + dashboard/ Home screen + books/{list, detail, edit} Library, detail, add/edit + settings/ Theme, backup/restore, PDF export + common/ViewModelFactory.kt Builds ViewModels from the ServiceLocator + util/ + PasswordHasher.kt Salted SHA-256 + ImageStorage.kt Copies cover URIs into internal storage + PdfExporter.kt PdfDocument-based exporter + BackupManager.kt JSON export / import + Extensions.kt Small helpers + +app/src/test/... Mirrors the same package structure +``` ---- +## Running it -## πŸš€ Getting started +You'll need: -### Prerequisites - Android Studio Hedgehog (2023.1.1) or newer -- JDK 17 -- Android SDK Platform 34 +- JDK 17 (or the JBR that ships with Android Studio) +- An emulator or device on API 21 or higher -### Clone and run +Then: ```bash git clone https://github.com/SultanZhalifa/MiniBookLibrary.git @@ -199,21 +155,28 @@ cd MiniBookLibrary ./gradlew assembleDebug ``` -Then open the project in Android Studio and run on any device/emulator with API 21+. +Open the project in Android Studio and hit Run. On first launch you'll go through onboarding, register an account, and land on an empty dashboard. Tap Add a book to start filling it in. The Settings tab has the theme toggle, PDF export, and backup buttons. -### First launch flow +## Tests -1. **Onboarding** β€” three slides introducing core features (skippable) -2. **Register** an account, then **sign in** -3. Land on the **Dashboard** with empty stats -4. Tap **Add a book** to populate your library -5. From **Settings**, try Light/Dark theme, **Export to PDF**, or **Backup to JSON** +```bash +./gradlew testDebugUnitTest +``` + +The suite has 51 tests across these classes: ---- +- `PasswordHasherTest` covers determinism, salt isolation, and verify success/failure. +- `EnumStorageTest` pins the persisted string values for `ReadingStatus`, `SortOrder`, and `StatusFilter` so a future rename can't silently break existing user data. +- `UserRepositoryTest` covers register validation, duplicate username handling, password hashing (never plaintext), login success and failure, and session persistence. +- `BookRepositoryTest` checks timestamp stamping on insert/update and dao forwarding. +- `GoogleBooksParserTest` walks the JSON parser through happy path, missing fields, multiple authors, taxonomy splitting (Google returns nested categories like `Fiction / Romance`), and year extraction. Plus service-level paths for short ISBNs and null HTTP responses. +- `AuthViewModelTest` covers login state transitions, register validation, error consumption, and the logout event. +- `BookListViewModelTest` covers search, status filter, A-Z sort, sort/filter persistence, swipe-delete, and Undo restore. +- `AddEditBookViewModelTest` covers form validation (title, year range, page progress), saving new books, edit hydration, and ISBN auto-fill including the rule that user-typed input is never overwritten. -## πŸ“Έ Screenshots +## Screenshots -> Add real device screenshots to `screenshots/` and they'll show up here. The filenames below are the expected slots. +Screenshots live in `screenshots/`. The README references the filenames below; replace the placeholders by running the app, capturing each screen, and saving the PNGs at the same paths. | Onboarding | Login | Dashboard | | :---: | :---: | :---: | @@ -227,47 +190,40 @@ Then open the project in Android Studio and run on any device/emulator with API | :---: | :---: | :---: | | ![Settings](screenshots/07_settings.png) | ![Dark mode](screenshots/08_dark_mode.png) | ![PDF](screenshots/09_pdf.png) | ---- +See `screenshots/README.md` for the recommended capture process. -## πŸ—Ί Roadmap +## Roadmap -- [x] ISBN auto-fill via Google Books API -- [ ] **Barcode scanner** for ISBN (ML Kit) β€” feeds the existing auto-fill flow -- [ ] Cloud sync via Firebase or a self-hosted backend -- [ ] Reading streak / analytics charts (MPAndroidChart) -- [ ] Book recommendations based on rating history -- [ ] Multi-user library sharing -- [ ] Widget for "Currently Reading" +Things I'd like to add next, roughly in priority order: ---- +- Barcode scanner for ISBN using ML Kit, feeding the existing auto-fill flow. +- A "Currently Reading" home-screen widget. +- Reading streaks and a small analytics chart. +- Book recommendations based on rating history. +- Cloud sync (probably Firebase, since it's the lowest-friction option). -## πŸ“₯ Download +## Download -Pre-built APKs will be attached to GitHub Releases. To produce your own: +CI uploads a debug APK as a workflow artifact on every push to a tracked branch, so the easiest way to grab a build is from the latest run on the [Actions tab](https://github.com/SultanZhalifa/MiniBookLibrary/actions). For a release build: ```bash ./gradlew assembleRelease -# APK at: app/build/outputs/apk/release/app-release.apk -``` - -For an unsigned debug build: - -```bash -./gradlew assembleDebug +# Output: app/build/outputs/apk/release/app-release.apk ``` ---- - -## πŸ™‹ About +This is unsigned by default. Set up a signing config in `app/build.gradle.kts` if you want to install on a real device long-term. -Built by **Sultan Zhalifunnas Musyaffa** as a portfolio piece showcasing modern Android development practices. The original Java prototype has been fully rewritten in Kotlin with a clean MVVM architecture, Room migration, and a Material 3 redesign. +## Notes on a few decisions -If this project helped you, ⭐ the repo or reach out β€” feedback is always welcome. +These are the calls I had to think about more than once while building this: ---- +- **Hand-rolled JSON instead of kotlinx-serialization or Moshi.** The schemas are tiny (book entity, backup file, Google Books volumeInfo) and the dependency cost felt larger than the benefit. The parser tests use the same `org.json` impl that runs on the device, added explicitly as a `testImplementation` so unit tests don't get the stub jar's no-op version. +- **Plain `HttpURLConnection` instead of OkHttp.** One unauthenticated GET endpoint doesn't justify another transitive dependency. The `HttpClient` interface in `GoogleBooksService` makes the network layer trivial to fake in tests. +- **`ServiceLocator` instead of Hilt.** I weighed both. For a single-module app where I wanted readers to follow the dependency graph by clicking through the source rather than reading codegen, a 60-line `object` won. +- **Reactive read, suspend write.** Repositories return cold `Flow` for reads (Room handles invalidation) and `suspend` for writes. ViewModels collect with `repeatOnLifecycle` so the UI doesn't waste work in the background. +- **Salted SHA-256 instead of bcrypt or Argon2.** The realistic threat is somebody pulling the SQLite file off a non-rooted phone. Salted SHA-256 defeats rainbow tables, which covers that. A real production app should use a memory-hard KDF. +- **Schema export off for now.** Room's schema export emits CI-time JSON snapshots that are useful when you ship migrations. I've left it off until v2 of the schema, with a comment in `BookDatabase.kt` explaining when to flip it back on. -## πŸ“„ License +## License -``` -MIT License β€” see LICENSE for details. -``` +MIT. See [LICENSE](LICENSE).