← BackView on GitHub

Niner: the daily sudoku

A clean, calm sudoku game for Android. One daily puzzle, five modes, zero ads, zero tracking.

Niner app icon

Main menu Game in progress Statistics Achievements


Why Niner exists

Most sudoku apps on the Play Store bury the puzzle under interstitial ads, "remove ads" upsells, and analytics SDKs that track every cell tap. The puzzle itself is fine. Everything around it isn't.

Niner is the opposite. A puzzle, a board, and the time it takes to solve it.

Your puzzles, stats, and achievements live in a single SharedPreferences file on your device. Uninstall the app and they're gone. No server-side data, no shadow copy.


Features

Game

Polish

Accessibility


Tech stack

Layer Technology
Language Kotlin 2.0.21
UI Jetpack Compose, Material 3
Compose BOM 2024.09.03
Architecture Single-activity, AndroidViewModel + StateFlow + mutableStateOf
Persistence SharedPreferences + org.json (no Room, no DataStore)
Concurrency Kotlin Coroutines
Build AGP 8.7.0, Gradle KTS, R8 minification + resource shrinking
Min / Target SDK 26 / 35

Architecture overview

app/src/main/java/com/ninersudoku/
├── MainActivity.kt
├── achievements/      Achievement enum + AchievementManager (StateFlow + persistence)
├── daily/             DailyManager (deterministic per-date seed, streak tracking)
├── game/
│   ├── Board.kt       Immutable 9×9 board with conflict detection
│   ├── Cell.kt        Cell with value, given/hint flags, pencil notes
│   ├── Difficulty.kt  Per-level clue counts + hint caps
│   ├── GameMode.kt    Mode enum with display name + description
│   ├── Generator.kt   Classic puzzle generator with uniqueness check
│   ├── Solver.kt      Bitmask backtracking with MRV
│   ├── HintEngine.kt  Naked + hidden singles + cage-aware hints
│   └── Killer.kt      2- or 3-cell cage generator + Killer-aware solver
├── onboarding/        OnboardingManager (3-page first-run experience)
├── persistence/       SaveManager (in-progress game serialisation)
├── prefs/             DisplayPreferences (a11y + behaviour toggles)
├── sound/             SoundManager (ToneGenerator-based chimes)
├── stats/             StatsManager + per-mode best-time tracking
├── viewmodel/         GameViewModel — single source of state truth
└── ui/
    ├── BoardView.kt        Canvas-rendered grid + transparent semantics overlay for TalkBack
    ├── NumberPad.kt        Digit pad with remaining counts + tap/long-press
    ├── DifficultyScreen.kt Main menu (Daily, Continue, Mode picker, difficulty cards)
    ├── GameScreen.kt       Top bar + board + actions + pause + hint dialog + celebration
    ├── StatsScreen.kt      Hero card + summary + heatmap + per-difficulty cards + achievements
    ├── OnboardingScreen.kt 3-page intro
    ├── AboutScreen.kt
    ├── celebration/        Particle engine + style preview
    ├── components/         SettingsSheet (themes, modes, a11y)
    └── theme/              5 colour palettes × light + dark

Notable design decisions


Build

Prerequisites

Debug build

export JAVA_HOME=$(brew --prefix openjdk@21)/libexec/openjdk.jdk/Contents/Home
export PATH=$JAVA_HOME/bin:$PATH

./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

Release build

You'll need your own keystore. Run once:

keytool -genkeypair -v \
  -keystore app/release.jks \
  -alias niner \
  -keyalg RSA -keysize 2048 \
  -validity 10000

Then add to local.properties (gitignored):

RELEASE_STORE_FILE=release.jks
RELEASE_STORE_PASSWORD=...
RELEASE_KEY_ALIAS=niner
RELEASE_KEY_PASSWORD=...

Build:

./gradlew bundleRelease       # AAB for Play Store
./gradlew assembleRelease     # APK for sideloading

Without a keystore, the release build fails loudly with SigningConfig "release" is missing required property "storeFile". That's intentional. It stops you from accidentally shipping unsigned artefacts.


Privacy

The full privacy policy is hosted at https://niner-privacy.pages.dev/privacy.

The HTML source lives in docs/privacy.html. It's deployed to Cloudflare Pages with:

wrangler pages deploy docs --project-name=niner-privacy --branch=main

Short version: zero data collection, zero permissions, zero network calls. Everything stays on the device. Uninstalling the app removes all of it.


Roadmap

Things deferred for v1.x:


License

MIT. Fork it, learn from it, remix it.


Credits

Built solo by Rayjack CB. Made for people who love sudoku.