A robust, test-driven Flutter MVVM application for managing IPSC match stages, shooters, and scoring with scale factors. Modern UI, persistent storage, Unicode PDF export, and advanced test coverage.
- What: A cross-platform (mobile & desktop) Flutter app to run IPSC-style matches — configure stages, manage shooters, record stage input, and produce ranked results.
- Who: Range officers, match directors, and competitors who need a compact, mobile-first scoring tool that preserves data locally and exports official-looking PDFs.
- Why: Provides a fast, validated, and auditable workflow for score capture and result calculation with backward-compatible persistence and reproducible tests.
- Core guarantees: mobile-optimized input, versioned persistence with migrations, Unicode PDF export using a bundled font, and test-driven development supported by CI.
- Match Setup: Configure stages (1-30) and scoring shoots (1-32)
- Shooter Management: Add shooters with unique names and scale factors (0.100–20.000)
- Stage Input: Record scores with mobile-friendly numeric input, validation, and error feedback
- New: results can be marked with a Status ("Completed", "DNF", "DQ"). When a result is not "Completed" numeric inputs are disabled and submitted values are zeroed by the ViewModel. An RO remark field is available for match officials to record notes.
- Results: Calculate and display hit factors, adjusted hit factors, and rank shooters
- Stage Result Table:
- Rotated (vertical) header labels for all columns to maximize mobile readability
- Fixed column widths (in characters): Name: 10, Raw HF: 5, Scaled HF: 5, Time: 5, A: 2, C: 2, D: 2, Misses: 2, No Shoots: 2, Procedure Errors: 2
- Vertical rules (dividers) between columns for improved alignment and readability on mobile
- Export: Export all stage results to PDF (Unicode support, including Traditional Chinese; uses bundled font for cross-platform reliability)
- Persistence: All data is auto-saved and restored using SharedPreferences
- Note: The persisted schema was recently extended (schema v2) to include
statusandroRemarkonStageResult.PersistenceServiceimplements a migration path that upgrades older data to the new schema on app startup.
- Note: The persisted schema was recently extended (schema v2) to include
- Clear All Data: One-tap clear with confirmation
- Modern UI: Card-based, mobile-optimized, visually appealing
- MVVM Pattern:
- Views: UI only (
lib/views/) - ViewModels: Business logic (
lib/viewmodel/) - Models: Data structures (
lib/models/) - Services: Persistence (
lib/services/)
- Views: UI only (
- State Management: Provider
- Persistence:
- Uses
shared_preferencesfor local storage - Data schema is versioned and backward compatible
- Schema version stored as
dataSchemaVersionin SharedPreferences - Any schema change requires version bump, migration logic, and integration test
- See
data_schema_history.mdanddocs/data_schema_versioning.md
- Schema version stored as
- Uses
- Run app:
flutter run - Add dependency:
flutter pub add <package> - Test:
flutter test- New/Updated tests: migration tests (schema v2), widget tests for DNF/DQ + RO remark behavior, and additional stability improvements to StageInput widget tests.
- Note: Shooter
scaleFactorvalidation now accepts values in the range 0.100–20.000. Validation is enforced inlib/viewmodel/shooter_setup_viewmodel.dartand reflected inlib/views/shooter_setup_view.dart.
- Hot Reload: Supported
- Schema changes:
- Increment schema version in
PersistenceServicefor breaking changes - Add migration logic and integration tests
- Update
data_schema_history.mdanddocs/data_schema_versioning.md
- Increment schema version in
- All features are covered by widget and logic tests (test-driven development)
- Migration logic is covered by integration tests simulating older schema data being loaded and verified.
- Stage Result table tests verify:
- All columns and headers are present and rotated
- All columns are visible and correct on mobile-sized screens
- Vertical rules are present between columns in both header and data rows
- Migration logic is covered by integration tests in
test/persistence_test.dart
- All core features are covered by unit, widget, and integration tests in
test/ - ViewModel logic is tested (e.g.,
test/viewmodel_main_menu_test.dart,test/viewmodel_match_setup_test.dart) - Persistence logic is tested (e.g.,
test/services_test.dart) - Widget navigation and UI are tested (e.g.,
test/widget_test.dart) - PDF export is tested for Unicode (Traditional Chinese) using
pdftotextfor robust extraction - All TODOs for tests have been implemented and committed
flutter test --coverage-
The repository uses a parallel test controller to run long-running test workflows in parallel. The controller dispatches and polls these reusable workflows:
flutter-tests.yml— unit tests (Flutter)integration-tests.yml— integration testscoverage.ymlandcoverage-web.yml— coverage collection (VM + Chrome)check-settings-view-coverage.yml— focused coverage check forsettings_view.dart
-
The controller is implemented at
.github/scripts/dispatch_and_poll.shand is invoked by the top-levelmerge-gate.ymlworkflow. It usesGITHUB_TOKEN(same-repo) to dispatch workflows and poll runs. For cross-repo dispatch or broader permissions use a PAT withreposcope stored in a secret and referenced instead ofGITHUB_TOKEN. -
To run the controller locally (requires a token):
export GITHUB_TOKEN=<token>
./.github/scripts/dispatch_and_poll.sh <owner> <repo> <ref> flutter-tests.yml integration-tests.yml coverage.yml coverage-web.yml check-settings-view-coverage.ymlTo collect merged VM+Chrome coverage and generate an HTML report locally run:
chmod +x ./scripts/collect_coverage.sh
./scripts/collect_coverage.shNotes:
- The script tries
flutter test -d chrome --coveragefirst (some Flutter versions write lcov that way). - If that fails it falls back to
--platform chromeand finally attempts a VM-service based collection (requiresdartanddart pub global activate coverage). - On CI, ensure Chrome is installed and
CHROME_EXECUTABLEis set or Chrome is on PATH.
Recent CI notes
- The GitHub workflows were hardened after CI troubleshooting: the Flutter installer action is invoked with
channel: 'stable', the job now printsflutter --versionto logs for easier debugging, andactions/cacheis used to cache~/.pub-cache. An earliernpm cistep that caused failures was removed.
- The PDF export test requires
pdftotext(from poppler-utils) to be installed for Unicode extraction verification. - On macOS:
brew install poppler
- Follow MVVM and Provider patterns
- Write or update tests for all new features and bug fixes
- Keep the codebase warning- and lint-free (
flutter analyze) - Document all schema changes and migrations
- Data schema history:
data_schema_history.md - Schema versioning and migration:
docs/data_schema_versioning.md - Developer/contributor instructions:
.github/copilot-instructions.md - Unicode PDF export and font bundling: see
.github/copilot-instructions.md
- The persisted data schema was bumped to v4 (2026-02-19): per-record audit timestamps
createdAtandupdatedAt(ISO8601 UTC) were added toMatchStage,Shooter,StageResult, andTeamGame. - Migration/backfill is performed on app startup by
PersistenceService; missing timestamps are backfilled using the system UTC now. Seedata_schema_history.mdfor the changelog anddocs/data_schema_versioning.mdfor migration guidance.
- A helper script to recreate the GitHub release/tag
v2026-02-19is available at.github/scripts/recreate_release.py. It deletes any existing release with that tag and recreates it to point atmain(requiresGITHUB_TOKENwith repo access). Use with caution — this modifies releases and git refs.
- The repository uses a merge-gate controller
merge-gate.ymlthat runs on pushes tomain. It validates reusable workflows and dispatches parallel test workflows using.github/scripts/dispatch_and_poll.sh. - Reusable workflows must declare
workflow_calland (when dispatched by the merge-gate) accept an optionalworkflow_dispatchinput namedmerge_runso the controller can dispatch them via the API. See.github/workflows/*.ymlfor examples.
- To prevent analyzer regressions from reaching CI we added
test/analyzer_regression_test.dart. This test runsflutter analyzeand fails if the analyzer reports issues — runflutter testlocally to run it. - Before pushing, run:
flutter analyze
flutter testThe CI runs flutter analyze as part of the flutter-tests.yml workflow; the merge-gate will fail dispatch if the workflows are not callable or mismatch expected inputs.