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 shape, not subject matter.
- A fixed set is plain DataStore keys, however many members it has: four schedule options are settings, and seven per-day windows are fourteen settings.
- A small list the user makes is JSON under one DataStore key, read whole and written whole. Its
migration story is the decoder’s tolerance: unknown keys ignored, a default on every field added
later, and one unreadable entry dropped rather than the whole list. The template’s
AppBackupAgentleaves the store out of backups; once Auto Backup takes over (Backup export/restore below) the list reaches the user’s next phone with every other key, which is right for anything that is really a setting. The first app built from this template keeps its saved presets this way rather than bring Room back. - A table is for querying, relations or partial writes — records found by a field, joined to other records, or too many to rewrite whole. Only then Room, and ADR-0001 comes with it.
The modules
Each row lists what to delete together. Delete a module, then build, then run ./gradlew test lint and
the scripts in the last column — they are what tells you the strip is finished rather than the compiler.
Lint fails on every resource the deleted code was the last to use (UnusedResources is an error,
because resource shrinking would strip it from the bundle), so remove those in every language.
Room database and the schema gate
data/AppDatabase.kt,ItemDao.kt,ItemEntity.kt,ItemRepository.kt,Converters.kt,Migrations.kt,SchemaGate.kt,DatabasePreserve.kt,BackgroundSchemaGuard.ktand, if the reminders stay,ReminderSweep’s call to itui/wipe/SchemaMismatchScreen.ktand itsschema_mismatch_*strings, both variants’ui/wipe/SchemaMismatchContinue.ktand its debug string, 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 - The items editor’s picker and its
item_pick_imagestring, if the placeholder domain stays 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 backups- Settings’ Backup row (
onOpenBackup, theBackuproute,settings_backup) and thebackup_*strings - If nothing else shares a file:
ui/common/Share.kt,ShareBook.ktandShareBookTest.kt,androidTest/…/ui/common/ShareChosenCallbackTest.kt,MainActivity.onResume’srevokeUnpickedShare(),ManifestWiringTest’s share case, theShareChosenReceiverentry and the twoSENDentries in the manifest’s<queries>, and the privacy policy’s sentences on sharing kotlinx.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
This one is a pure deletion: outside work/, only MainApplication and Settings’ Reminders
section use it, and both are listed below. startActivitySafely lives in ui/common/Intents.kt and
the background schema check in data/BackgroundSchemaGuard.kt because other code needs them after
the reminders are gone.
work/ReminderSweep.kt,BootReceiver.kt,PackageReplacedReceiver.kt,ReceiverWork.kt,ExactAlarms.kt,ReminderChannels.kt, its test and thechannel_reminders_*strings;NotificationPermission.ktif nothing else posts;Autostart.kt,BatteryExemption.ktand thecom.miui.securitycenter<queries>entry if nothing else runs in the backgroundres/drawable/ic_notification.xmlandres/raw/keep.xml, if nothing else postsandroidTest/…/ManifestWiringTest.kt’s boot, update and boot-permission cases, or the whole file if the share case has gone with the backupapp/proguard-rules.pro’s Workers, across versions note;.gitignore’s/doze-*/andscripts/doze-capture.sh, which captures an overnight sweep- 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.ProviderandensureSweepEnqueuedcall; 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, and the tip merges it too: removing WorkManager removes the question only if the tip goes as well. scripts/aab-permissions.py’s EXPECTED rows for all of the above, in the same commit — exceptACCESS_NETWORK_STATEwhile the tip stays, which then belongs to billing alone: keep the row, and its line inplay-app-content.md, and name billing as the only owner.scripts/device-gate.py’s reminder checks, if nothing schedules:ALARM_TAGand the alarm reads (app_alarms,armed,disarmed), the channel pair (channel_importance,set_channel), the--rebootarm (wait_for_unlock,reboot_and_wait), and the exact-alarm line. Keep the autostart half while anything else runs in the background.- Settings’ Reminders section, and the ask and banner behind it — the
SwitchRow, therememberNotificationPermissionAskcall, the resumed re-read ofnotificationsAllowed, theWarningBanner, and everysettings_reminders*string, plusAppPreferences.remindersEnabled,Keys.REMINDERS_ENABLEDand its place inKeys.SETTINGS, and the reminder lines inAppPreferencesTest. 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. - “reminders” in
settings_restore_hintandsettings_restore_confirm_body, in every language, or Restore defaults promises to reset something that is gone. - ADR-0003 gets its amendment.
The tip (Play Billing)
Every line it adds is deleted outright, along with the imports that leave unused; the two edits are marked.
tip/,test/…/tip/;ui/support/SupportViewModel.ktandTipSection.kt, and thetipargument theSupportentry passes inNavigation.kt- The one-time ask:
ui/home/TipAskCard.kt;HomeScreen’sonOpenSupportparameter and itsshowTipAskitem; theonOpenSupporttheHomeentry passes inNavigation.kt; inHomeViewModel,showTipAsk, thepreferences,tipJarandinstalledAtparameters and their arguments inFactory, thewithTipAskline, theinitblock andanswerTipAsk AppPreferences.tipAsked/markTipAsked,Keys.TIP_ASKEDand its place inKeys.KEPT, and the tip lines inAppPreferencesTestAppContainer.tipJarandinstalledAt(applicationScopekeeps its@Suppress("unused")for this day);settleTipsOnEveryForegroundand its call inMainApplication- The
home_tip_ask_*andsupport_tip*strings, in every language —scripts/sync-support.pyleaves the latter out of the shared block for an app with notip/, so later syncs keep them out - Gradle:
billing.ktx,androidx.lifecycle.processand licensee’sallowUrlfor Google’s terms inapp/build.gradle.kts; thebillingversion and thebilling-ktxandandroidx-lifecycle-processaliases inlibs.versions.toml - Manifest: the
INTERNETtools:node="remove"and its comment — harmless once billing is gone, but it then removes nothing and reads as a reason that no longer exists scripts/aab-permissions.py: theBILLINGrow and its comment, and the datatransport comment aboveACCESS_NETWORK_STATE. Edit that row’s note back to WorkManager alone, or delete the row if WorkManager has already gone.play-app-content.md: theBILLINGrow and thedatatransportparagraph under What the artifact carries; the tip paragraph under Content rating, which leaves the all-No answer; Data safety point 4 and its Would change bullet; and The tip. Edit theACCESS_NETWORK_STATErow back to WorkManager alone. Keep A payment-link tip: an app that drops the billing tip is the one most likely to try a link next.- The privacy policy’s billing-library sentence under The app collects nothing, its billing bullet and its tip paragraph
CLAUDE.md’s Play Billing row in Stack andtip/line in Layout;CONTEXT.md’s Tip rowRELEASING.md’s Selling the tip; the tip section ofDOD.md- ADR-0007 gets its amendment.
The placeholder domain
ui/items/, the items routes inNavigation.kt/NavigationKeys.kt, and thetab_items,item_*,items_*,action_save,action_editandaction_deletestrings- ⚠ The tip’s one ask lives on the items screen and counts items as its measure of use. Move
TipAskCard.ktand itsitems_tip_ask_*strings to your home screen and givetipAskDueyour own measure, or the ask goes with them debug/…/data/SampleData.kt,debug/…/debug/SeedReceiver.kt, the debug manifest’s receiver, and the seed rows inDebugSettings.ktwith theirdebug_seedanddebug_wipestrings — 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.