Skip to content

v1.4.5: fix #57 chain placeholder data flow + kmpworkmanager 3.2.0 (Android FGS permission opt-in) - #58

Merged
vietnguyentuan2019 merged 2 commits into
mainfrom
feat/kmpworkmanager-3.2.0-fgs-permissions
Aug 6, 2026
Merged

v1.4.5: fix #57 chain placeholder data flow + kmpworkmanager 3.2.0 (Android FGS permission opt-in)#58
vietnguyentuan2019 merged 2 commits into
mainfrom
feat/kmpworkmanager-3.2.0-fgs-permissions

Conversation

@vietnguyentuan2019

Copy link
Copy Markdown
Contributor

What's in this PR

Two pieces of work, shipped together as v1.4.5 (per maintainer decision — single release, not split):

1. Fix #57{{taskId.outputKey}} chain data-flow placeholders

The documented syntax for passing one chain step's output into a later step's config never actually worked on either platform.

  • iOS: step results were stored under flat, unprefixed keys, but substitution looked up the whole "taskId.key" string as one key — never matched. Parallel steps also overwrote each other's stored result. Separately, AnyCodable had no Int32/Int64/Float/UInt64 cases, so any worker result containing one of those types silently failed to persist.
  • Android: had no substitution mechanism at all — chains were built by enqueuing every step's WorkRequest upfront via WorkManager's native .then() chaining, before any earlier step had run. Rearchitected to dynamic per-step enqueue (ChainHelper.buildAndEnqueueStep) driven by WorkInfo completion, with output captured via a new ChainResultCapturingWorker decorator and resumed idempotently via enqueueUniqueWork(..., KEEP).
  • Both platforms now namespace results "<taskId>.<key>", merge (not overwrite) across parallel tasks, and resolve whole-match placeholders to typed values instead of always stringifying.

Also fixes a doc bug in task_chain.dart's own example, which demonstrated this exact bug (used the input key savePath instead of the output key filePath).

2. kmpworkmanager 3.1.0 → 3.2.0 + Android FGS permission opt-in

  • Bumped kmpworkmanager to 3.2.0 (upstream fix for Play Store rejections on apps that never use isHeavyTask — the core lib no longer force-merges FOREGROUND_SERVICE/FOREGROUND_SERVICE_DATA_SYNC into every consumer manifest). Rebuilt KMPWorkManager.xcframework from source, re-verified against the full test suite.
  • This plugin's own android/src/main/AndroidManifest.xml had the identical problem independently (not inherited from kmpworkmanager) — fixed the same way, with a new ManifestGuardTest so it can't regress. Documented the opt-in snippet in doc/ANDROID_SETUP.md/doc/PRODUCTION_GUIDE.md.
  • native_workmanager_gen: widened stale analyzer constraint (<14.0.0<15.0.0), verified against 14.1.0.

Testing

  • pana: 160/160 both packages
  • 1316 Dart unit/integration/security/performance tests pass
  • 125 Kotlin unit tests pass (10 new ChainHelperSubstitutionTest, 1 new ManifestGuardTest)
  • Device tests (real Pixel 6 Pro + iPhone 17 simulator): issue_57 chain-placeholder group (3/3), FGS group (2/2), pre-existing Task Chains + Chain data flow groups re-verified
  • Demo app launches clean on both platforms (verified via logcat / simulator console, no crashes, KmpWorkManager initializes correctly)
  • Control experiment (4 independent runs comparing old/new xcframework, erased simulator, pre/post-fix code) confirmed an unrelated flaky device test is a pre-existing environment issue, not a regression from this PR

Not yet done (release-gated, tracked separately)

GitHub release v1.4.5 + xcframework asset upload, full SPM layer 2-4 scratch-app verification (requires the release to exist first — SPM only resolves https binary target URLs), pub publish for both packages.

Both platforms: the documented {{task_id.output_key}} chain placeholder
syntax never actually worked.

iOS root cause:
- Step results were stored under flat, unprefixed keys, but substitution
  looked up the whole "taskId.key" string as one key — the two never
  matched, so placeholders always stayed literal.
- A parallel step's tasks overwrote each other's stored result; only the
  last task to finish survived.
- AnyCodable's Codable conformance had no Int32/Int64/Float/UInt64 cases,
  so any worker whose result data included one of those types (e.g.
  ImageProcessWorker's originalSize/processedSize) silently failed to
  persist via the try?-guarded saveStepResult call — the next step's
  substitution data came back empty with no error surfaced anywhere.

Android had no substitution mechanism at all. Its chains were built by
enqueuing every step's WorkRequest upfront via WorkManager's native
.then() chaining, which freezes each step's config before any earlier
step has even run — there was no point in time a later step could see a
real predecessor output. Fixed by moving to a dynamic per-step enqueue
(ChainHelper.buildAndEnqueueStep) driven by WorkInfo completion, with
output captured via a new ChainResultCapturingWorker decorator (WorkInfo
data is always empty coming out of BaseKmpWorker) and resumed
idempotently via enqueueUniqueWork(..., KEEP).

Both platforms now: namespace each task's result under "<taskId>.<key>",
merge (don't overwrite) across parallel tasks, and resolve a whole-match
placeholder (the entire config value is one {{...}}) to the original
typed value rather than a stringified one, so substitution can target
numeric/bool fields, not just strings.

Also fixes the task_chain.dart doc example, which had been demonstrating
the exact same key-name bug (used the input key savePath instead of the
output key filePath) — and expands doc/use-cases/06-chain-processing.md
with a full data-flow section.

Tests: 10 new Kotlin unit tests (ChainHelperSubstitutionTest), 3 new
device integration tests (issue_57 group, passing on real Pixel 6 Pro +
iPhone 17 sim), full pre-existing Task Chains + Chain data flow groups
re-verified green on both platforms.
…issions opt-in

Bumps kmpworkmanager 3.1.0 -> 3.2.0 (upstream fix for Play Store rejection
on apps that never use isHeavyTask — the core lib no longer force-merges
FOREGROUND_SERVICE / FOREGROUND_SERVICE_DATA_SYNC into every consumer app's
manifest). Rebuilt KMPWorkManager.xcframework from kmpworkmanager v3.2.0
source via scripts/build-xcframework.sh, updated Package.swift/podspec
url+checksum to point at the v1.4.5 release asset (not yet uploaded).

This plugin's OWN android/src/main/AndroidManifest.xml had the identical
problem independently (unconditional FOREGROUND_SERVICE declaration, not
inherited from kmpworkmanager) — fixed the same way: removed the
unconditional permissions/service-type override, documented the opt-in
snippet consumer apps need if they use isHeavyTask in
doc/ANDROID_SETUP.md and doc/PRODUCTION_GUIDE.md. Added ManifestGuardTest
(mirrors kmpworkmanager's own test) so this can't silently regress.

native_workmanager_gen: widened analyzer constraint >=10.0.0 <14.0.0 to
<15.0.0 — the upper bound was stale, verified against analyzer 14.1.0
(15/15 generator tests pass, pana 160/160).

Verification: pana 160/160 both packages; 1316 Dart unit/integration/
security/performance tests pass; issue_57 chain-placeholder device tests
(3/3) and FGS device tests (2/2) pass on real Pixel 6 Pro + iPhone 17 sim
with the new xcframework; demo app launches clean on both platforms with
no crashes (verified via logcat / simulator console — KmpWorkManager
initializes, a real WorkManager task runs successfully on first launch).
Control experiment confirmed an unrelated flaky device test
('missing input key' timeout when run via --plain-name filter in
isolation) is a pre-existing environment issue, reproduced identically on
the pre-3.2.0 code — not a regression from this bump.

Not yet done: GitHub release v1.4.5 + xcframework asset upload (needed
before Package.swift's binaryTarget URL resolves) and the SPM layer-2/3/4
scratch-app verification from CONTRIBUTING.md, which requires that
release to exist. Held pending explicit publish approval.
@vietnguyentuan2019
vietnguyentuan2019 merged commit 9dce57b into main Aug 6, 2026
7 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

android chains don't support {{task_id.output_key}} placeholder substitution (ios only)

1 participant