Optional modules — what to delete, and what breaks when you do
The template ships an app that records things: a Room database, photos, backup export and restore, and reminders. Most of that is machinery with a data-loss story attached, and an app that does not need a piece of it should delete it before the first upload, while deleting is free.
Why before, and not “later, if it turns out unused”. Nothing is shipped yet, so a table deleted
today comes back at version 1 if a feature ever needs it. Ship an unused items table and remove it
later, and that removal is a destructive migration against real installs — the exact failure ADR-0001
exists to prevent. The same asymmetry holds for permissions: one declared today is a Data safety answer
you will have to keep defending.
The first app built from this template stored settings rather than records and deleted everything below except the preferences on day one. It took 69 files, and five things around the code broke that nothing had said depended on the code. This page is that strip written down, so the next one is a checklist rather than a discovery.
How to decide
The test is cardinality, not subject matter. A fixed set of values is settings, however many members it has: four schedule options are settings, and seven per-day windows are fourteen settings. What needs a table is the user creating rows nobody knew about at build time. No such rows, no Room.
The modules
Each row lists what to delete together. Delete a module, then build, then run ./gradlew test and the
scripts in the last column — they are what tells you the strip is finished rather than the compiler.
Room database and the schema gate
data/AppDatabase.kt,ItemDao.kt,ItemEntity.kt,ItemRepository.kt,Converters.kt,Migrations.kt,SchemaGate.kt,DatabasePreserve.ktwork/SchemaGuard.kt,ui/wipe/SchemaMismatchScreen.kt, the wipe guard’s call inMainApplicationapp/schemas/,test/…/data/SchemaGateTest.kt,androidTest/…/data/ItemDaoTest.kt,MigrationTest.kt- Gradle: the
kspplugin andksp {}block, the three Room dependencies,room.testing, and theandroidTestschema-assets line inandroidComponents - What adapts on its own:
scripts/project.pyreadsHAS_DATABASEoffdata/AppDatabase.kt, soschema-gate.pypasses with “no database, nothing to gate”,doze-capture.shstops pulling a file that is not there, and themismatchsuite leavesedge-to-edge.pyandscreenshots.py. - What does not:
docs/DOD.md’s standing schema gate — mark it parked, keep the five points verbatim, and say it comes back the day a feature adds a table. ADR-0001 gets a dated amendment, not a rewrite (docs/adr/0000-template.md).
Photos (the media pipeline)
media/MediaFiles.kt,androidTest/…/media/MediaFilesTest.kt,res/xml/file_paths.xmland theFileProviderin the manifest- Gradle:
coil.compose, bothexifinterfacelines scripts/aab-permissions.py: nothing to remove — the photo path declares no permission — butCAMERAstays in FORBIDDEN, because a dependency that merges it changes the listing.- The privacy policy’s Photos and camera bullets, and
play-app-content.md’s Data safety point 2. - ADR-0002 gets its amendment.
Backup export/restore, and the custom BackupAgent
data/backup/,ui/backup/,test/…/data/backup/,AppBackupAgent.ktand its manifest attributes;scripts/upgrade-diff.py, which reads those backupskotlinx.serializationstays: Navigation 3 serialises everyNavKey.- Platform Auto Backup takes over, with
android:dataExtractionRulesif anything must stay on the phone. ⚠ Every DataStore key is then backed up, whether you meant it to be or not — exclusions are per file, and the preferences store is one file — so every key lands on the user’s next phone with no code of yours involved. Ask what each key means there: a setting is welcome, live state is not (“the service should be running”, “a deadline is armed”). Judge live state at the read, against something the platform never backs up —PackageInfo.firstInstallTimeis one. The first app built from this template met this in its restore test: a restored “running” flag was acted on by a boot receiver on a phone that had never granted the permission it assumed. - ADR-0005 gets its amendment.
Reminders and WorkManager
work/ReminderSweep.kt,BootReceiver.kt,PackageReplacedReceiver.kt,ExactAlarms.kt,ReminderChannels.ktand its test;NotificationPermission.ktif nothing else posts- Manifest:
SCHEDULE_EXACT_ALARM,RECEIVE_BOOT_COMPLETED, the receivers, and the androidx.startuptools:node="remove"for WorkManager’s initializer (the provider itself stays — emoji2 registers there) MainApplication’sConfiguration.Provider; Gradle’swork.runtimeandwork.testing- ⚠ WorkManager merges three permissions your source never declares —
WAKE_LOCK,ACCESS_NETWORK_STATE,FOREGROUND_SERVICE— andRECEIVE_BOOT_COMPLETEDfor good measure. If you keep a boot receiver but drop WorkManager, declareRECEIVE_BOOT_COMPLETEDyourself first, or the receiver stops being called with nothing to say so.ACCESS_NETWORK_STATEon an app with noINTERNETis a line a Play reviewer can ask about; removing WorkManager removes the question. scripts/aab-permissions.py’s EXPECTED rows for all of the above, in the same commit.scripts/device-gate.py’sALARM_TAGand the exact-alarm line, if nothing schedules.- Settings’ Reminders section, and the ask and banner behind it — the
SwitchRow, therememberNotificationPermissionAskcall, the resumed re-read ofnotificationsAllowed, theWarningCardand its threesettings_reminders_blocked_*strings, plusAppPreferences.remindersEnabled. That whole block isPLAN.mdrule 4 worked through for one ask; it is the shape to copy for your first ask, so read it before deleting it. - ADR-0003 gets its amendment.
The placeholder domain
ui/items/, the items routes inNavigation.kt/NavigationKeys.kt, thetab_itemsstringsdebug/…/data/SampleData.kt,debug/…/debug/SeedReceiver.kt, the debug manifest’s receiver, and the seed rows inDebugSettings.kt— or keep the seeding seam and point it at your own datascripts/edge-to-edge.py’s SCENES,TAB_BAR,HOME_TABandSEED_WALK: they walk the template’s screens. Rewrite them against yours the same week — until then the nightly job boots ten emulators to walk screens that do not exist and reports a red everyone learns to ignore.
Around the code
Five places the strip reaches that no compiler will point at:
scripts/aab-permissions.py— EXPECTED must be exactly what the artifact carries. Run it against a localbundleRelease(orbundleDebug); a stale row fails the release upload, after the tag, which is where the first app built from this template found it.- CI’s instrumented job fails on zero tests. If the strip deletes every
androidTest, write one real device test — the thing the app must never get wrong is a good first one — or delete the job. CLAUDE.md— the stack table, the house rules that name deleted code, and the layout block.docs/privacy-policy.mdanddocs/play-app-content.md, together, in the same commit.- The ADRs — each module above names the one that gets a dated amendment. Never delete an ADR: the reasoning is what the next feature that wants the module back will need.