← All work

Native Android · Feature development

Ship features that fit your stack
and stay maintainable

Examples from Stitch Counter on Google Play: a local-first craft app, not a sample repo. The sections below show how architecture, Room data, zip backup, and UI state were shaped so features stay shippable. For multiple brands from one Android codebase, see our product flavors guide.

Stitch Counter is a personal project by our lead developer; it is licensed for portfolio use. It was not built for a client.

Kotlin · Compose · RoomLocal-firstZip backupHilt · MVVM

How we structure feature work

Separation of concerns: UI, domain rules, and persistence stay in different layers.
Data as a stable foundation: Room models and Flow queries so lists refresh from real storage.
Reliability by design: Validation and typed failures on paths like backup and restore, not only the happy path.
UI that follows real state: Navigation and lists stay tied to the back stack and database, not one-off flags.

Problem

Feature work gets expensive when every screen owns its own rules

A craft counter looks simple until you add a project library, photos, deletes that must clean up files, and phone plus tablet navigation. Without clear layers, schema tweaks and new screens start rewriting the same logic in three places.

Approach · App architecture

Thin screens over use cases and Room

How it's built

Compose UI and ViewModels sit on top. Small use-case classes own validation and orchestration. Room holds projects; Flow queries feed the list so the UI updates when the DB changes. This is MVVM-style: Compose + ViewModels over use cases and Room. Hilt provides the database, repository, and use cases once; screens inject only what they need.

Why this shape

  • Domain models and mappers sit between entities and UI, so renames or new fields do not spread through every composable
  • Deletes go through use cases that remove image files and rows together, so storage and the DB do not drift apart
  • Compose Destinations plus a single root scaffold keeps tabs, rail, and sheets consistent on phones and tablets

Results

Most feature work touches one layer. That shortens review, cuts surprise bugs, and makes it easier to ship changes without rewiring the whole app.

Lesson

For production Android apps, the payoff is not “more architecture.” It is knowing where a bug or schema change lives before you open the IDE. The same layering is what we bring when we extend an existing codebase.

Room → Flow → UI
Root navigation shell

Problem

User progress only matters if it survives restarts and new features

Stitch counts and project notes have to still be there next week. The data model needed to support a growing library of projects, counters, and related fields without turning every screen into a special case.

Approach · Data architecture

Room-backed project library with clear relationships

How it's built

A structured system for creating, editing, and organizing projects with persistent local storage through Room. Preferences and active theme state live in DataStore so UI settings stay separate from project rows.

Why this shape

  • Data persists across sessions; no cloud account required
  • Relationships between projects, counters, and related fields stay explicit in the schema
  • UI and persistence stay separated so list and detail screens can evolve without rewriting storage

Results

Users keep long-running projects locally. New features can read and write through the same foundation instead of inventing a second store.

Lesson

Local-first products win trust when the schema is boring and durable. Fancy sync can wait; lost user data cannot. When we join your app, we start from how your data is owned today.

Project library
Project details
Room DAO

Problem

Phone upgrades should not strand years of project history

Privacy-first meant no cloud sync. Users still needed a way to move a full library (metadata plus images) to a new device without corruption or a silent half-restore.

Approach · Data safety

Zip backup and restore with validation

How it's built

Local export and restore of full app data: zip metadata paired with embedded image bytes, plus typed failures and clear success or error messaging in Settings. No cloud account in the path.

Why this shape

  • Validation before restore so invalid payloads fail safely instead of crashing mid-import
  • Clear feedback for success and failure so users know whether the library landed
  • Edge cases handled so behavior stays predictable when files are missing or malformed

Results

Users can move projects phone-to-phone while the app stays offline-first. Support paths stay simpler: the backup file is the portable record.

Lesson

For local-first apps, backup is a product feature, not a power-user afterthought. Typed failure modes matter as much as the happy export. We apply the same bias when your users need export, migrate, or device-transfer flows.

Backup & restore UI
BackupManager

Problem

Large libraries and multi-tab navigation fall apart without shared state

As the project list grows, scrolling and tab selection have to stay tied to real data and the navigation back stack. One-off UI flags drift; Flow queries and a root scaffold do not.

Approach · Performance & state

Lazy lists, controlled scrolling, navigation-derived selection

How it's built

LazyColumn-based lists with controlled scrolling for large libraries, debounced autosave where rapid edits would otherwise thrash storage, and a root navigation shell that derives the selected tab from the NavController back stack for the bottom bar and rail.

Why this shape

  • Efficient list rendering so large libraries stay usable as projects accumulate
  • UI selection stays in sync with navigation instead of a separate source of truth
  • Autosave timing reduces unnecessary writes during fast editing sessions

Results

Library browsing and tab switches stay predictable as the catalog grows. State comes from the database and navigation graph, not duplicated screen flags.

Lesson

Smooth UI is easier to keep when lists observe Flow and chrome observes the back stack. Shared sources beat syncing by hand. We look for those shared sources first when we join an existing app.

Smooth scrolling (LazyColumn)
UI in sync with navigation

How we work

Scope tied to real data and behavior, so new work fits your stack and stays reliable after launch.

1

Review

You share the feature goal, affected screens, and constraints around persistence, sync, or existing APIs.

2

Plan

We map flows, state, and data ownership: what persists on device, how it evolves across releases, and how errors and edge cases surface in the UI.

3

Build

Layered Kotlin implementation: clear boundaries, Compose tied to real sources of truth, and performance kept in mind on large lists.

4

Deliver

A production-ready feature with predictable behavior, safer data paths, and code your team can extend without rework.

Need Android feature work like this?

A fit when you need a feature added or improved in an existing Android app, especially with Kotlin, Jetpack Compose, local data, backup or migrate paths, or clearer architecture boundaries.

Feature developmentExisting appsKotlinJetpack Compose

Tell us about the feature you need — we will reply with scope questions and whether the engagement fits before any commitment.