Repository navigation
Fix iOS callback ownership across Flutter engines - #456
TeddyYeung wants to merge 3 commits into
Conversation
37537c2 to
33c8645
Compare
33c8645 to
df45d8f
Compare
|
When will SPM be supported? |
|
This project is awful; we can't contact them at all, and registering an account requires filling out a ton of mandatory private information. |
|
Plugin AppsflyerSdkPlugin uses deprecated application lifecycle events. Please contact plugin maintainers and request UIScene lifecycle support. This will be required in a future version of Flutter. See https://docs.flutter.dev/release/breaking-changes/uiscenedelegate#migration-guide-for-flutter-plugins |
Upstream 7.x replaced the Objective-C iOS plugin with the Swift RPC implementation, deleting the files the 6.x fix modified. Take master's tree; the ownership fix is re-applied to the Swift sources next.
…strations AFRPCBridge holds a single process-wide event-handler slot, but every plugin registration installed its own handler, so registering appsflyer_sdk on a secondary FlutterEngine (a background engine reusing GeneratedPluginRegistrant, add-to-app) redirected attribution and deep-link events to that engine. Its af-events stream usually has no Dart subscriber, so the events were buffered there and never reached the main engine. Plugin registration now only takes a free slot, and the engine whose Dart side calls init claims it explicitly, matching the owner AppsFlyerAttribution.markBridgeReady(markedBy:) records. Constraint: AppsFlyerRPCBridge exposes one global event handler Rejected: Per-callback-id routing from the 6.x fix | 7.x delivers all events on one af-events stream Confidence: high Scope-risk: narrow Directive: A background engine that calls init still takes the slot by design Tested: RunnerTests on iOS simulator (5 new multi-engine cases; 2 fail against the previous behavior) Not-tested: Physical device, SPM integration build
|
Hi @tlozovyi-af @af-dudka, friendly ping on this PR.
Happy to rebase or adjust anything. Thanks! |
|
I rewrote the description to match the current 7.x diff and added local validation, including a regression check that fails on the pre-PR behavior. The new XCTests exercise |
Summary
On iOS, registering
appsflyer_sdkon a secondFlutterEnginemoves attribution and deep-link events away from the engine that integrates the SDK. This PR changes who holds the bridge's single event-handler slot:initclaims the slot, so the engine that integrates the SDK receives its events.Dart API, channel names, and payloads do not change. Android code does not change.
Problem
AFRPCBridgeinstalls one event handler per process throughAppsFlyerRPCBridge.shared.setEventHandler. Flutter creates oneAppsflyerSdkPlugininstance per engine.On
master(7.0.4):AppsflyerSdkPlugin.init(messenger:)callsregisterEventHandler(), which callsAFRPCBridge.setEventHandler(owner: self).initdoes not take the slot back.registerEventHandler()returns early becauseeventHandlerRegisteredis alreadytruefrom registration.A common trigger is a background engine that reuses
GeneratedPluginRegistrant. For example, the iOS setup offlutter_foreground_taskregisters all plugins on its background engine. That engine registers after the UI engine, takes the slot, and has no Dart listener. Conversion data and UDL deep-link results then go to the background engine and are dropped.We hit this in a production app. On iOS, deep links stopped opening the target screen while a foreground task was running. The 6.x plugin had the same problem through the static
_callbackChannel.Changes
AFRPCBridge.setEventHandlerIfUnowned(owner:_:)(new): installs the handler only when no live instance owns the slot.AppsflyerSdkPlugin.init(messenger:): callssetEventHandlerIfUnowned. A later engine's registration cannot take an owned slot.AppsflyerSdkPlugin.initFromRpc: callssetEventHandler. The engine that calls Dartinitbecomes the owner, even if another engine registered first.eventHandlerRegisteredandregisterEventHandler(). The owner tracking inAFRPCBridgereplaces them.AFRPCBridge.eventHandlerOwneris nowprivate(set)so that tests can read it.master, 7.0.4)initBehavior notes
init, the engine that calledinitlast receives events.initfor the same instance. The same engine receives events.initreceives events only while it owns the slot.Tests
example/ios/RunnerTests/MultiEngineEventHandlerTests.swiftmodels several engines in one process with separateFakeBinaryMessengerinstances.testFirstRegistrationTakesFreeSlottestSecondaryEngineRegistrationDoesNotStealSlottestInitClaimsSlotFromEarlierRegistrationinittakes the slot from an engine that registered earliertestSecondaryEngineDetachDoesNotReleaseOwnersSlottestRegistrationTakesSlotAfterOwnerIsReleasedThe tests check the owner in
AFRPCBridge. They do not deliver a native event end to end.lint-test-build.ymldoes not run XCTest today. Its comment says to add XCTest to CI once tests exerciseAppsflyerSdkPluginorAFRPCBridgedirectly. These tests use@testable import appsflyer_sdkand assert onAFRPCBridgestate. I can add the CI step in this PR or in a follow-up PR.Validation
Branch head
13ebd54(upstreammastermerged at3251787). Since that merge,masterchanged only the version string inAppsflyerSdkPlugin.swiftand did not changeAFRPCBridge.swift.flutter build ios --simulator --debuginexample/: passed. The example app needs anexample/.envfile withDEV_KEYandAPP_ID. I used dummy values. The file is git-ignored.xcodebuild test -workspace example/ios/Runner.xcworkspace -scheme Runner -only-testing:RunnerTests: 9 of 9 passed, including all 5 tests above.init(messenger:)reverted toAFRPCBridge.setEventHandler(the behavior before this PR),testSecondaryEngineRegistrationDoesNotStealSlotandtestSecondaryEngineDetachDoesNotReleaseOwnersSlotfail.Workaround for released versions
Until this change ships, an app can exclude
AppsflyerSdkPluginfrom the background engine's plugin registrant. For example, register a filtered copy ofGeneratedPluginRegistrantin theflutter_foreground_taskregistrant callback.