Example Features
Todos, profile, upload, refer, permissions, and other replaceable product surfaces.
These screens are working product examples, not framework code. Keep the app patterns they demonstrate, then replace the copy, fields, and business logic with your own app's features.
What it provides
| Surface | Status | Implementation |
|---|---|---|
| Home | Example | feature/home/HomeScreen.kt + HomeViewModel |
| Todos | Example | feature/todos/ + TodosRepository |
| Profile | Example | feature/profile/ + ProfileRepository |
| Refer | Example | feature/refer/ReferScreen.kt |
| Permissions | Example | feature/permissions/PermissionsScreen.kt |
| Upload sample | Example | feature/upload/ + UploadsRepository |
Home
The Home tab is the product landing surface inside the app. It demonstrates a stack overview, entitlement-aware license card, capability preview, and shortcuts into live demos.
HomeViewModel reads two sources:
| Source | Purpose |
|---|---|
PurchasesController.isConfigured | Knows whether REVENUECAT_ANDROID_KEY is present |
EntitlementController.state | Knows whether the signed-in account has the Supabase-backed pro entitlement |
That maps into HomeLicenseState.Owned, Available, or Unconfigured. The
screen does not reach into navigation directly; HomeRoute receives callbacks
such as onOpenTodos, onOpenUploads, onGetTemplate, onManageLicense, and
onRedeemCode. Copy that callback-driven pattern when adding your own launch
surface.
The "What's inside" preview links to the removable Showcase leaf. Its seams are
tagged with // Showcase promo (removable leaf), so it can be removed without
touching the core Home demos.
Todos
Todos is the reference CRUD feature. Its canonical entry is the Home stack
route AppRoutes.Todos; it is not one of the bottom tabs.
| Capability | Code path |
|---|---|
| Sign-in gate | TodosScreen shows a sign-in card when SessionController.currentUser is empty |
| Load/create/update/delete | TodosViewModel -> TodosRepository -> SupabaseTodosRemoteDataSource |
| Filters | TodosFilter.All, Active, Completed rendered as FilterChips |
| Inline edit | Row-local edit mode with save/cancel buttons |
| Reordering | Up/down buttons, optimistic UI, then setSortOrders() |
| Error state | errorMessage in TodosUiState |
TodosRepository.clearCompleted() exists, but the current Compose screen does
not expose a clear-completed confirmation dialog. If you want that action, wire
it through TodosViewModel and add a Material AlertDialog before calling the
repository helper.
Profile
The Me tab is the account hub. Signed-out users see a centered sign-in prompt; signed-in users see profile, license, account, notification, permissions, legal, and sign-out rows.
PersonalInfoScreen demonstrates editable profile fields and app preferences:
| Field or setting | Source |
|---|---|
| Full name | profiles.full_name |
| Bio | profiles.bio |
| Phone | profiles.phone |
| Location | profiles.location |
| Avatar display | profiles.avatar_url via AppAsyncImage |
| Theme | ThemeController + DataStore |
| Language | LocaleController + DataStore |
AccountSecurityScreen demonstrates the account actions you are likely to keep:
| Action | Implementation |
|---|---|
| Change email | Sends an email-change OTP, then verifies it |
| Change password | Validates the new password and calls Supabase Auth |
| Biometric app lock | Prompts with BiometricPrompt, then persists the toggle |
| Sign out other sessions | auth.signOut(SignOutScope.OTHERS) |
| Delete account | Confirmation dialog, then AuthRepository.deleteAccount() |
Delete account is intentionally documented as implemented: it deletes the
user's todos, nulls profile fields, and signs out locally. It does not delete
the Supabase auth.users row because no delete-account Edge Function ships
yet. Add a server-side deletion function before relying on this for Google Play
account-deletion policy compliance.
Refer
ReferScreen builds a Play Store URL from the app package name:
https://play.google.com/store/apps/details?id=<package-name>It launches Android's system share sheet with Intent.ACTION_SEND,
Intent.EXTRA_TITLE, and Intent.EXTRA_TEXT. A successful share records a
rating trigger through RatingController.recordTrigger(activity, "referral_shared"),
which feeds the in-app review cooldown logic.
Permissions
The Permissions screen is a priming screen: it explains why the app asks before launching a runtime permission request.
| Row | Android permission | Notes |
|---|---|---|
| Notifications | POST_NOTIFICATIONS on Android 13+; pre-granted below API 33 | Needed before FCM notifications can appear |
| Camera | CAMERA | Demonstrates a normal dangerous permission |
| Tracking | com.google.android.gms.permission.AD_ID | Advertising ID permission used by analytics/attribution SDKs |
The README still mentions Photos, but the current code does not request a photos/media runtime permission. The upload sample uses Android's system photo picker, so it can select media without broad photo-library access.
Upload sample
The upload sample is a practical Supabase Storage example:
PickMultipleVisualMedia -> UploadViewModel -> UploadsRepository -> sample-uploadsUploadViewModel resolves display name, MIME type, and size from the selected
URI, rejects files larger than 25 MB, reads bytes off the main thread, and
collects uploadAsFlow() progress into per-job rows. Successful uploads store
objects at:
<user-id>/<timestamp>-<sanitized-file-name>The bucket id is hardcoded as UploadsRepository.BUCKET_ID = "sample-uploads".
Replace the bucket, policy, and object path when converting this into your own
media feature.
Keep the patterns
- Put the route composable at the feature boundary and pass callbacks inward.
- Keep screen composables stateless where possible.
- Let ViewModels translate repository
AppResultvalues into one UI state. - Route all Supabase table access through a repository or remote data source.
- Use the shared component set before creating one-off controls.
- Label examples honestly in your product so starter/demo code does not ship as accidental product copy.
