project · android · habit design

Sketch Seed — a 100-day drawing habit that never asks what to draw

One prompt a day, revealed only when you open the app. Day 1 is the day you install, so everyone starts at the beginning — with no server, no accounts and no sync.

Aug 2026 Kotlin · Jetpack Compose Material 3 · DataStore Gemini Nano built with Claude Code source ↗

The app never asks "what do you want to draw today?" — because deciding is the part that kills the habit. It says "here's today's sketch" and gets out of the way.

Everything else in it follows from taking that one sentence seriously.

Three counters that look the same and are not

Most habit apps collapse "what day is it", "how much have you done" and "how consistent are you" into a single number, and then have to decide what a missed day does to it. Sketch Seed keeps them strictly apart:

CounterWhat it meansWhat a missed day does
Day 17 of 100What the calendar says today isNothing — it moves on regardless
CompletedHow many you have actually finishedNothing — missed days stay open
StreakConsecutive calendar days you drewResets to zero, permanently

So the streak is allowed to be brutal, because nothing else depends on it. Skip day 8 and the streak dies — that is what a streak is. But day 8's prompt is still sitting there, and you can draw it whenever. Finishing it does not resurrect the streak, because you genuinely didn't draw that day; the completion is recorded against the day you actually drew it.

One small mercy in the streak maths: having drawn yesterday but not yet today still counts as alive. The streak only dies once a full calendar day passes with nothing in it. An app that shows you a dead streak at 9am for a day you haven't had yet is just lying to make a point.

The payoff: because completion is decoupled from the calendar, missed days simply stay open. Skip a week and those seven prompts are still sitting there to be drawn whenever — they just don't buy back the streak.

The design I got wrong first

Originally day N was the Nth day after a startDate baked into the prompt pack. It was an elegant trick: no server, no accounts, no sync, no clock skew, nothing to run — and hand the APK to a friend and you'd be on the same prompt every day, automatically. The whole sync mechanism was arithmetic on a date.

It was also wrong, for a reason that only shows up in use. Install in month three and the app opens on day 60. The earlier days are technically still there to back-fill, but the app is pushing day-60 difficulty at you every morning, so in practice a late starter never meets the beginner ramp at all — and the ramp is the entire reason the pack is ordered by skill rather than subject. The cleverness was costing exactly the thing the product exists to deliver.

So day 1 is now recorded per install, on first launch. Whoever starts, whenever they start, starts at Apple. The pack's date survives only as a fallback for the moment before a personal one has been written.

The cost is real and worth stating plainly: the prompt order is shared, the schedule is not. Two people who install a month apart are never in step. That is a genuine loss — the "hand your friend the APK" property was lovely — and it is simply worth less than a beginner meeting Apple on their first evening instead of Bicycle Wheel.

Three details make the new anchor safe:

Those last two are the tell that per-install anchoring is the harder design. A date baked into an asset file cannot drift; a date each install owns has to be defended everywhere it could move underneath the data.

One property survives untouched: nothing ahead is readable. Days you haven't reached are locked and don't even carry their prompt text in memory, so the grid shows finished sketches, open days and padlocks — never a spoiler.

The prompts are hand-written, on purpose

100 days ramping from Apple to Self Portrait, ordered by skill rather than subject — the order teaching material tends to converge on:

DaysPractising
1–14Line and shape confidence
15–26Proportion and measuring
27–40Form and ellipses
41–54Perspective
55–66Value and shading
67–76Texture
77–87Organic form and gesture
88–96Faces
97–100Composition

Difficulty climbs continuously rather than in flat blocks, and days are rated on their own merits — Bicycle Wheel is HARD at day 49 while sitting inside the perspective run, because a wheel is an ellipse problem most beginners lose an hour to. Faces get ten days of runway rather than arriving cold.

This is the one thing a generated list would ruin. An LLM will happily produce a hundred drawing subjects; it will not produce a difficulty ramp that survives contact with a beginner at day 60, which is the only part that matters.

Prompt text is also short by design, because the subject doubles as the image-search term. The reference search looks for "<prompt> sketch" rather than the bare noun, which returns line drawings you can actually follow instead of photographs.

Two permissions I did not have to ask for

Capturing a sketch is camera or gallery, chosen per sketch, and neither needs a runtime permission.

The gallery route uses the system photo picker. The camera route delegates to whatever camera app you already have via ACTION_IMAGE_CAPTURE — and it stays permission-free specifically because the app declares no CAMERA permission. The platform only starts demanding the grant for that intent once an app declares it in its manifest. Declaring a permission you don't need is what creates the prompt. The camera writes into a cache directory exposed through a FileProvider, and the temp file is disposable once the image has been downscaled into place.

An earlier version used the ML Kit document scanner for auto-crop and de-skew. It's gone: it framed sketching as document scanning, and it dragged in Play Services for the privilege.

AI as garnish, not dependency

Gemini Nano generates an optional one-line drawing tip through the ML Kit Prompt API. Every design decision around it assumes it will fail:

The base model lives in AICore and is shared system-wide, so the app never pulls down its own copy — the same "one model, many consumers" idea that gi-hub had to build by hand on devices where AICore isn't an option.

Backup, and the 25 MB problem

The first version leaned on Android Auto Backup: no account, no sign-in, no API, the platform just handles it. It was the right instinct and it broke on one number — Auto Backup allows 25 MB per app, and a hundred sketch photos blow straight past that.

Two mitigations kept it alive for a while. Sketches downscale to 1600 px on the long edge and re-encode as JPEG. And because Auto Backup rules are static XML that cannot be flipped at runtime, an Include sketch photos toggle physically moved files between filesDir/sketches (backed up) and noBackupFilesDir/sketches (never backed up) — with Settings showing current usage and warning as the limit approached.

It still wasn't enough, so backup moved to Google Drive's appDataFolder and Auto Backup is now switched off at the manifest. The Drive client is hand-written against the REST endpoints rather than pulling in google-api-services-drive, which would drag a large transitive stack in for the four calls this app actually makes. Everything is confined to the drive.appdata scope, so the app cannot see anything else in your Drive even by accident, and the backups never appear in the Drive UI. There is also plain file export and import for anyone who wants nothing to do with Google.

It runs itself, via a periodic worker — and the worker's contract is the interesting bit: it must never show UI. If consent is missing because you never signed in or revoked access, it gives up quietly and leaves the job to the manual button in Settings. A background task that throws up a sign-in sheet while you are doing something else is worse than no automatic backup at all.

Extras that deliberately do nothing

Some prompts take a minute. Rather than let that spare appetite go nowhere — or worse, tempt someone into burning tomorrow's prompt early — a finished day can hold as many extra sketches as you like.

They are inert by design: extras never count toward the hundred, never move the streak, and never unlock the next day. The journey stays exactly one day wide. This is the same instinct as keeping the three counters separate — give the enthusiasm somewhere to go, but do not let it corrupt the thing that measures consistency.

Tech, and what is worth testing

Single-module Kotlin app: Jetpack Compose and Material 3 with dynamic colour, DataStore plus kotlinx.serialization for persistence, Coil for images. No authentication, no analytics, no backend.

Persistence is one JSON blob rather than a database, because the whole journey is at most a hundred small records. No Room, no schema migrations, no annotation processor — a real saving at this size, and an obviously wrong call at ten times it.

layout
domain/   pure Kotlin — streaks, journey rules, models (unit tested)
data/     DataStore repositories, prompt pack loading, photo storage
backup/   Drive client, archive format, the periodic worker
ai/       Gemini Nano tip generation
ui/       Compose screens, one package per screen

domain/ has no Android dependencies at all, which is precisely why the rules that matter — streak maths, reveal logic, back-filling — are covered by fast JVM tests rather than instrumented ones.

Photo storage is the exception and gets instrumented tests, because the bug they guard against lived in BitmapFactory's bounds-decoding contract, which JVM stubs do not reproduce. That is the honest rule for instrumentation: use it where the platform's real behaviour is the thing under test, and not as a default.

Where it stands

Barely underway — the pack was finished in August 2026 and I am a handful of days into my own hundred. Recent work has been the two things you only discover by using it: backup (off Auto Backup, into appDataFolder, running unattended, and a restore that was deleting sketches instead of restoring them) and the day-1 anchoring above.

Both were cases of a design that read well and behaved badly. That seems to be the tax on building something habit-shaped: the failure modes are all on a timescale of weeks, so no amount of staring at the code surfaces them — you have to be a user who missed a day, or joined late, or got a new phone.

It's open source. github.com/shivam1410/sketchseed — tagged v1.0. Unlike the other three Android projects on this site, this one holds none of my personal data, so there was nothing to keep back. Clone it, change prompts.json, and you have your own hundred days of something else entirely.