I maintain roughly two dozen small Android apps and games on my own: a mix of puzzle games and everyday utilities. That only works because almost everything that repeats is either copied from one known-good pattern or moved out of the app entirely. This post describes the patterns that survived, and the habits around them.
The rule behind everything: write it once, copy the shape
With one app you can afford to be clever. With two dozen, every special case you invent is a special case you will forget. So each feature that appears in more than one app has a reference implementation, and new apps copy it instead of reinventing it. The reference lives in one of the newer games, and the next app starts from that file, test included.
Copying is not elegant, and a shared library would be tidier on paper. In practice the apps differ in package names, ad setups and screens, they ship on their own schedules, and a shared module would force a coordinated release of all of them whenever it changed. Copying a small, well-tested file keeps each app independent. When the pattern improves, older apps pick up the change at their next natural release, not in a panic.
Cross-promotion as data, not as code
Each game has an Other Games window that lists the rest of the family. Early on that list was compiled into every app, which meant that launching a new game required releasing an update for all the old ones. That does not scale.
Now the list is a small JSON file served from a static website. A single class in each app fetches it at most once every 48 hours and caches it on the device. If the network is down, or the cached file is missing or corrupted, the app falls back to a copy bundled in its assets, so the window is never empty. A new game is one new row in the JSON file plus an icon, and no old app needs a new version.
Because the file comes from the network, the app treats it as untrusted. It opens only Play Store links, checks the package name and the key against strict patterns, caps the number of entries and the file size, and refuses any host other than the one it expects. The window reads the language of the device and falls back to English. The file is generated by a short script from a list of games, so I never edit the JSON by hand.
Update prompts and cloud saves, copied from one reference
Two features are easy to forget when you are focused on gameplay, and expensive to add later: telling users that a new version exists, and keeping their progress when they change phones. Both are now on my checklist for the very first release of any new app.
For updates I use the Play in-app update API in its flexible mode. A small object checks on resume. It asks only if an update is actually available, only after the new version has been out for at least a day, since Play rolls releases out gradually, and at most once every three days for the same version. When the download finishes it offers a restart. If the app was not installed from Play, or the device is offline, the check silently does nothing. The decision logic is a pure function, so it has unit tests, and the dialog texts come in four languages.
For progress I use Play Games saved games in the games that have a meaningful progress state, so a new phone picks up where the old one stopped. It is a deliberate decision, made once, that every new game gets it from version one. A couple of older games are exceptions on purpose, because their state is trivial.
One upload key, Play App Signing
New apps use Play App Signing. Google holds the real signing key, and I hold only an upload key, which is shared across the newer apps. If an upload key is ever lost or compromised, it can be reset through Play, which is far less scary than losing a real signing key. The older apps from before this setup keep their original signing and are handled separately, and I keep the two groups clearly apart in my notes so I never mix them up.
The release scripts ask for the key password at the moment of signing, and nothing secret is written into a file, a repository or the code.
A website that builds itself
Every app needs a landing page, a privacy page and a terms page, and for the games I also write guides and blog posts. Doing that by hand for two dozen apps in four languages is a way to fall behind quickly, so the website is generated.
One command rebuilds everything. It runs the generators in a fixed order: the home page, the app detail pages, the game guides and the blog, and finally a splitting step that turns one four-language source into separate pages per language. English sits at the root and the others live under language folders. The step also writes hreflang links, canonical addresses, a sitemap with last-modified dates and structured data. The data for the home page lives in a single JSON file, where a new app is one more row and an unreleased app can be marked as coming soon, so the card appears by itself on release day.
A rule I keep: generated files are never edited by hand. If a page is wrong, I fix the generator or the data and rebuild. The language folders are deleted and rewritten on every build, so a manual edit would vanish anyway, and that is the point. It keeps the output consistent, and consistency is the thing a solo developer cannot afford to lose.
Scripts for the boring parts
Anything I do more than twice becomes a script. A sync script pulls every repository, one per app, and then reports which ones have uncommitted or unpushed work, so I can see in one screen whether it is safe to switch machines. Each app has a build script and a release script that produces the signed bundle, and the build output goes to a uniquely named log file, which I search for the words that matter instead of watching it scroll.
I work on two computers in two different places, so the sync script has saved me from forgotten commits more than once. It is a small thing, and it pays for itself every week.
Notes and a task list, kept honest
Memory does not scale to two dozen projects. Each app has a notes file at its root with the current state: what shipped, which version is live, what is pending and why a past decision was made. A single top-level task list holds every open item across all apps, and a short running log records what happened in each working session.
The discipline that matters is keeping them truthful. When I finish a task I update the task list and the app's notes in the same sitting, and before I plan anything I verify the written state against the real one, for example by asking Play which version is live instead of trusting what I wrote last week. A task list that shows finished work as open, or the reverse, is worse than no list, because it makes you distrust all of it.
What this approach costs
It is not free. Copied files drift, so an old app may have a slightly older version of a pattern until its next release. A generated site means the generator itself is a piece of software I have to maintain and test. And a remote list is one more thing that can break, which is why every remote feature has a bundled fallback and strict validation.
What I get in return is predictability. A new app starts with updates, cloud saves, a menu, a privacy page, cross-promotion and a website entry already solved, and I can spend my time on the part that makes the app worth installing. If you maintain more than a few apps, my advice is to pick the two or three things you keep redoing, give each one a reference implementation, and push everything that can be data out of the binary.
FAQ
How many apps does this approach cover?
Roughly two dozen apps and games, mixing puzzle games with small everyday utilities. The patterns matter most for the newer ones.
Why not use a shared library instead of copying files?
Separate apps ship on separate schedules. A shared module would force coordinated releases, while a small copied and tested file keeps each app independent.
How does the remote Other Games list stay safe?
The app fetches it only from one expected host, caches it for 48 hours, validates every field, limits its size and falls back to a bundled copy if anything is wrong.
When should an app ask users to update?
The flexible Play in-app update flow works well when it asks only after the version has been out for a day, no more than once every few days, and at a calm moment such as when the app comes to the foreground.
Do you edit the generated website by hand?
Never. Fixes go into the generator or the data file, and the whole site is rebuilt with one command.


