Android contacts manager for easy move operations.
  • Dart 88.5%
  • C++ 4.5%
  • CMake 3.6%
  • Kotlin 1.9%
  • Makefile 0.5%
  • Other 1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-13 00:17:19 +02:00
.forgejo/workflows chore: scaffold flutter project with lullalink-shaped tooling 2026-10-06 09:42:59 +00:00
.githooks chore: scaffold flutter project with lullalink-shaped tooling 2026-10-06 09:42:59 +00:00
android feat(android): raw-contact channel for account-exact moves 2026-10-09 15:34:59 +00:00
ios chore: scaffold flutter project with lullalink-shaped tooling 2026-10-06 09:42:59 +00:00
lib feat(presentation): ease the staged-move progress bar 2026-10-12 22:12:12 +00:00
linux chore: scaffold flutter project with lullalink-shaped tooling 2026-10-06 09:42:59 +00:00
macos refactor(domain)!: contact projection and real-contacts service seam 2026-10-09 02:03:12 +00:00
test feat(presentation): ease the staged-move progress bar 2026-10-12 22:12:12 +00:00
tool chore: scaffold flutter project with lullalink-shaped tooling 2026-10-06 09:42:59 +00:00
windows chore: scaffold flutter project with lullalink-shaped tooling 2026-10-06 09:42:59 +00:00
.gitignore chore: scaffold flutter project with lullalink-shaped tooling 2026-10-06 09:42:59 +00:00
.metadata chore: scaffold flutter project with lullalink-shaped tooling 2026-10-06 09:42:59 +00:00
analysis_options.yaml chore: scaffold flutter project with lullalink-shaped tooling 2026-10-06 09:42:59 +00:00
CONTRIBUTING.md chore: scaffold flutter project with lullalink-shaped tooling 2026-10-06 09:42:59 +00:00
Makefile chore: scaffold flutter project with lullalink-shaped tooling 2026-10-06 09:42:59 +00:00
pubspec.lock refactor(domain)!: contact projection and real-contacts service seam 2026-10-09 02:03:12 +00:00
pubspec.yaml refactor(domain)!: contact projection and real-contacts service seam 2026-10-09 02:03:12 +00:00
README.md docs: sync the transfer flow docs with the shipped UX 2026-10-11 10:58:19 +00:00

Contacts Manager

Contact account transfer manager for Flutter.

The app moves contacts between the contact accounts configured on the device (Google, Outlook, the local Device bucket, …) from a single screen: one list shows the source account's contacts, a dropdown below the app bar picks the target account, and a per-row swipe stages the contact for the move. Staged moves queue in a move list — opened from the outbox action at any time — where each entry shows its from → to account pair, can be removed individually, and — after a confirmation — executes as one batch with a single Undo. The address book is the device's real one, read through flutter_contacts and written through a first-party raw-contact channel.

Transfer flow

  1. Source view — the list shows one account's contacts, switched through the app-bar account menu. The search field below the target dropdown filters it by name.
  2. Target account — the dropdown offers every account except the source; it is disabled while the device has only one account.
  3. Swipe to stage — dragging a row right stages it for the current target (dragging left unstages); a long-press enters selection mode, where select-all and stage-all operate on the visible rows. Nothing is moved by staging — it only queues the instruction.
  4. Move list — the outbox action (badge counting staged moves) opens the pending list: one row per staged contact, its from → to pair with account pills, a per-row remove, and a "Move N contacts" button that first asks for confirmation ("Move N contacts from A to B?"). Confirming runs the batch: the button is replaced by a determinate progress panel ("Moving k of N") with an Abort action. Aborting stops between contacts — never mid-move — reverts everything already moved, and re-stages the batch. Each move that lands leaves the list; each failure keeps its entry staged and is reported. The all-success outcome returns to the transfer screen with a snackbar whose one Undo reverses the whole batch in reverse completion order and re-queues the reverted contacts as a staged batch — the pending list ends up exactly as it was before the move; the snackbar dismisses itself after ten seconds.

Accounts and colors

Accounts are derived from raw-contact attribution — an account exists iff at least one raw contact belongs to it. Each account gets a deterministic identity accent: FNV-1a 32-bit over the account id's UTF-8 bytes selects one of eight hue slots, shaded by theme brightness. The same id resolves to the same color on every run; there is no registry and no persistence.

Permissions

The app requests READ_CONTACTS and WRITE_CONTACTS together in one system dialog, asked only on the first explicit "Grant access" tap — never automatically on launch. Until access is granted the app body is replaced by a blocked state offering the dialog, or the system settings page when permission is permanently denied. The manifest declares no INTERNET permission: the app never touches the network.

Data safety

  • Create before delete — a transfer first creates the contact in the target account and only then deletes the source's raw-contact rows, so a failed second write never loses the contact.
  • Source-account-only deletion — exactly the raw contacts the move proved as its source are deleted; nothing else on the device is touched.
  • Undo semantics — a move returns a receipt; Undo recreates the contact in its source account from the current device data (edits made since the move are kept — only the account change is reversed) and deletes the created rows. A receipt made stale by outside changes is skipped, not retried; the rest of the batch still unwinds.

Quickstart

flutter pub get
flutter run                # default platform
flutter run -d linux       # Linux desktop
flutter build apk --debug --target-platform android-arm64
make hooks                 # install pre-commit / commit-msg git hooks

Verification

make check                 # format-check + analyze + full headless test suite
make test                  # unit + widget + golden + tool tests (headless)
make coverage              # suite with --coverage + line-coverage gate
make update-goldens        # regenerate golden references after UI changes

make check is the one gate: run it before every commit. make coverage enforces the line-coverage floor (COVERAGE_MIN, default 0.85) through tool/coverage_gate.dart. Golden tests pin the rendered UI to committed reference PNGs (13 images), so a visual regression fails the suite in headless CI; regenerate them with make update-goldens only after an intentional UI change, and review the diff.

Test layout

test/
├── unit/       Business logic & data mapping (no widgets, no platform channels)
├── widget/     Widgets/screens in isolation, headless
├── golden/     Rendered UI pinned to committed reference PNGs
├── fixtures/   Deterministic shared test data
├── fakes/      Fake platform-interface implementations
└── tool/       Tests for the repo's tool/ scripts

Interactive widgets expose stable Keys (from lib/core/constants/app_keys.dart) so tests and automation can drive the app without matching on text.

Architecture

Clean architecture with three layers:

lib/
├── core/           Constants, utilities, extensions
├── data/           Data sources, models, repository implementations
├── domain/         Entities, use cases, repository interfaces
└── presentation/   UI screens, widgets, providers, routing

State management is Riverpod, routing is GoRouter. The code generation toolchain (make gen: build_runner, freezed, json_serializable) is set up in pubspec.yaml for when generated models arrive; none exist yet, so day-to-day edits need no generation step.

Contributing

Development workflow, test pyramid, branch naming, and the review loop are documented in CONTRIBUTING.md. Every feature follows it.