Stitch Counter V2 Icon

Stitch Counter (V2)

One product, two native stacks: Android shipped on Google Play production and an iOS SwiftUI rewrite in progress toward the same local-first experience.

Stitch Counter V2 solves a practical problem for knitters and crocheters: keeping long-running projects safe, readable, and easy to continue across sessions. I rebuilt the original Android app in Kotlin with Jetpack Compose, Room, and DataStore so it can handle real usage patterns and local backup and restore (zip, no cloud). I added CI/CD so regressions and releases stay predictable: CI on every PR, CD to Play internal—same setup I’d use on a team where release discipline matters. The iOS rewrite is in progress toward the same local-first experience.

Play installs

2k+ installs

Stability

0 crashes

Stability figures and install count come from Google Play Console.

Native Android · Shipped

Android CI status badgePlay internal CD status badgeCodecov coverage badge
KotlinJetpack ComposeMaterial3RoomHiltDataStoreCoilRamcosta Compose Destinations

Native iOS · In Progress

CI status badge
SwiftSwiftUISwiftDataCombinePhotosUIZIPFoundation

Case studies (architecture, UI/UX, screenshots) for this app are below on this page.

Engineering snapshot

  • Kotlin app with Jetpack Compose + Material 3 across counter, library, settings, and project detail flows.
  • Room handles structured project and counter data; DataStore stores preferences and active theme state.
  • Six shipped themes with per-theme launcher icon updates and dark mode support.
  • Local backup and restore uses zip metadata + images, with typed failures and safe user messaging—no cloud sync.
  • CI runs on pull requests and pushes to main; CD publishes signed AABs to Play internal after successful CI on main.
  • Real performance details include debounced autosave and controlled list scrolling for large libraries.

Case studies

Feature development: Architecture, data, and reliability

Stitch Counter V2 is a local-first craft app on Google Play. These notes cover how I set up architecture, Room, zip backup, and UI state so new features do not turn into one-off fixes.

Kotlin · Compose · RoomLocal-first, no cloud accountZip backup and restore

How I 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 backup and restore, not just the happy path.
UI that follows real state: Navigation and lists stay tied to the back stack and database, not one-off flags.
Room DAO

Problem

Business rules were ending up in every screen

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

What it does

Compose and ViewModels on top; small use-case classes for validation and orchestration; Room underneath with Flow-backed queries so lists refresh when the database changes. Hilt wires the DB, repository, and use cases once so feature code only pulls what it needs.

Why I built it this way

  • Domain models and mappers sit between Room entities and the UI, so schema tweaks do not ripple through every screen
  • Deletes run through use cases that remove image files and rows together so files and rows stay in sync
  • Compose Destinations plus one root scaffold keeps tabs, rail, and sheets aligned on phones and tablets

Outcome

Most changes stay in one layer. Reviews stay smaller, and new features do not require re-threading the whole codebase.

Takeaway

The win is not “more architecture.” It is knowing where a bug or schema change lives before you open the IDE.

Room → Flow → UI
Root navigation shell

Problem

Project data has to survive restarts and new features

Stitch counts and project notes only matter if they are still there next week. The data model had 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

What it does

Create, edit, and organize projects with persistent local storage through Room. Preferences and active theme state live in DataStore so UI settings stay separate from project rows.

Why I built it this way

  • 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

Outcome

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

Takeaway

For a local-first app, a boring durable schema matters more than fancy sync on day one.

Project library
Project details
Room DAO

Problem

Users need to move their library to a new phone

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

What it does

Export and restore full app data locally: zip metadata with embedded image bytes, typed failures, and clear success or error messaging in Settings. No cloud account required.

Details

  • 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
  • Missing or malformed files handled so behavior stays predictable

Outcome

Users can export a zip and restore on another device. The app stays offline-first, and support has one file to ask about when something goes wrong.

Takeaway

Backup is a product feature for a local-first app. Invalid restores need the same attention as the happy-path export.

Backup & restore UI
BackupManager

Problem

Big libraries and tabs need one source of truth for 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

What it does

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 I built it this way

  • Lazy lists keep large libraries usable as projects accumulate
  • Tab selection follows navigation instead of a separate flag
  • Debounced autosave cuts down writes during fast editing

Outcome

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

Takeaway

Lists observe Flow; chrome observes the back stack. That was less work than keeping screen flags in sync by hand.

Smooth scrolling (LazyColumn)
UI in sync with navigation

Android UI/UX: Built for real devices

Knitters and crocheters tap counters for a long time, rotate the phone, switch themes, and manage projects in bulk. This section covers UI that still works in those conditions.

Adaptive Compose layoutsSix themes · dark modeSwipe · multi-select · sheets

What I build

New features added into an existing Android app
Responsive Compose UI across phones and tablets
Interaction patterns that keep common tasks fast (edit, delete, navigate)
Maintainable feature architecture (MVVM / separation of concerns)
Project library

Problem

The counter has to work in landscape with big numbers

The primary action is rapid, repeated input. Numbers get large. Devices rotate. Layouts that look fine in a portrait mock often overflow or shrink into unreadable type once someone is mid-project.

Dynamic text scaling — portrait
Dynamic text scaling — landscape

Approach · UI / interaction

Smart counter system

What it does

Real-time counter input with dynamic text scaling so large counts stay readable in portrait and landscape on phones and tablets.

Why I built it this way

  • Input stays responsive during long counting sessions
  • Layout adapts as counts grow so text does not overflow or break the screen
  • Portrait and landscape share one adaptive approach instead of two separate layouts

Outcome

The counter stays usable when numbers get large and when the device rotates.

Takeaway

I designed the primary control for large counts and landscape first. Other screens were easier after that.

Problem

Themes only work if dark mode and launcher icons match

Personalization is part of the product for this audience. Multiple color styles only work if every screen, dark mode, and launcher icon stay on the same system.

Approach · UI / UX

Dynamic theme system

What it does

Theming by overriding Material Design 3: six shipped visual styles, dark mode for each, and custom launcher icons that match the active theme. Theme preference is stored in DataStore so it survives restarts.

Details

  • Centralized styling keeps screens consistent as features grow
  • Dark mode is first-class for every theme, not a one-off
  • Per-theme launcher icons so the choice shows on the home screen too

Outcome

Users can switch themes without layout breakage. The app reads as one product instead of unrelated screens with mismatched colors.

Takeaway

Theme work needs tokens, dark variants, and icons together. One-off palette swaps did not survive the next feature.

Theme settings
Counter · dark
Counter · light
Library · Forest
Library · Sea

Problem

Library edits get slow when every flow uses different gestures

Managing a project library means multi-select, swipe, confirmations, quick edits, and dense detail. Without shared patterns, each flow invents its own gestures and users slow down.

Approach · UI / UX

Shared interaction patterns across the app

What it does

Shared patterns used across the app: swipe and multi-select for bulk delete, bottom sheets for quick edits, expandable sections for dense settings, structured detail screens for titles, notes, images, and progress, plus modal confirmations for destructive choices.

Why I built it this way

  • Common tasks stay on or near the current screen instead of deep navigation chains
  • Confirmations protect against accidental deletes without hiding the action
  • Selection and state feedback keeps bulk operations readable

Outcome

Library management stays fast as the catalog grows. Users learn the same gestures once and reuse them across screens.

Takeaway

Swipe, multi-select, sheets, and confirmations were worth doing once. A new pattern per feature would have been slower to build and harder to use.

Multi delete
Swipe to delete
Confirm delete
Project details
Expanding cards