Skip to content

fix(ios): Avoid duplicate app-delegate on background engine#413

Open
cyclone-pk wants to merge 1 commit into
ABausG:mainfrom
cyclone-pk:fix/ios-408-duplicate-app-delegate
Open

fix(ios): Avoid duplicate app-delegate on background engine#413
cyclone-pk wants to merge 1 commit into
ABausG:mainfrom
cyclone-pk:fix/ios-408-duplicate-app-delegate

Conversation

@cyclone-pk

@cyclone-pk cyclone-pk commented Apr 13, 2026

Copy link
Copy Markdown

HomeWidgetBackgroundWorker.setupEngine used HomeWidgetPlugin.register on the secondary FlutterEngine, which also calls addApplicationDelegate: and inserts another HomeWidgetPlugin into the host app's UIApplication delegate chain. That extra delegate disturbs URL/life-cycle propagation for other plugins (e.g. appsflyer_sdk deep-link/attribution callbacks).

Split register into a channel-only registerChannels path and use it for the background engine so the background isolate still has the home_widget channel without polluting the main app's delegate chain. Also guard setupEngine so repeated registerBackgroundCallback calls do not leak engines.

Closes #408

Description

Replace this text.

Checklist

  • I have updated/added tests for ALL new/updated/fixed functionality.
  • I have updated/added relevant documentation and added code (documentation) comments where necessary.
  • I have updated/added relevant examples in example or documentation.

Breaking Change?

  • Yes, this PR is a breaking change.
  • No, this PR is not a breaking change.

Related Issues

@docs-page

docs-page Bot commented Apr 13, 2026

Copy link
Copy Markdown

To view this pull requests documentation preview, visit the following URL:

docs.page/abausg/home_widget~413

Documentation is deployed and generated using docs.page.

@coderabbitai

coderabbitai Bot commented Apr 13, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3acd4166-2662-4cde-bd79-9cfa38588da9

📥 Commits

Reviewing files that changed from the base of the PR and between 8adde37 and ae95805.

📒 Files selected for processing (2)
  • packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetBackgroundWorker.swift
  • packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetPlugin.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetPlugin.swift
  • packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetBackgroundWorker.swift

Walkthrough

Make background engine setup re-entrant and dispatcher-aware: HomeWidgetBackgroundWorker tracks currentDispatcher, avoids re-creating an engine for the same dispatcher, tears down and resets the engine when dispatcher differs or setup incomplete, resumes pending continuations to avoid deadlock, and registers only channels on the background engine via HomeWidgetPlugin.registerChannels(with:).

Changes

Cohort / File(s) Summary
Background Worker
packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetBackgroundWorker.swift
Added static var currentDispatcher: Int64?. Made setupEngine(dispatcher:) re-entrant: returns early when an engine exists for the same dispatcher and isSetupCompleted is true; tears down existing engine if dispatcher differs or setup incomplete (clearing handlers, destroying engine context, resetting engine, channel, isSetupCompleted); resumes any pending run continuations immediately; removed local callback name/library variables and call engine?.run(...) directly; switched to calling HomeWidgetPlugin.registerChannels(with:) when registerPlugins is nil.
Plugin channel extraction
packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetPlugin.swift
Added @discardableResult public static func registerChannels(with registrar: FlutterPluginRegistrar) -> HomeWidgetPlugin which constructs a HomeWidgetPlugin, registers the home_widget method channel and home_widget/updates event channel, and returns the instance. Updated register(with:) to delegate channel registration to registerChannels(with:) while preserving app-extension guard and delegate insertion logic.

Sequence Diagram(s)

sequenceDiagram
  participant Worker as HomeWidgetBackgroundWorker
  participant Engine as FlutterEngine
  participant Registrar as FlutterPluginRegistrar
  participant Plugin as HomeWidgetPlugin
  participant Continuation as PendingRunContinuation

  Worker->>Worker: setupEngine(dispatcher)
  Worker->>Engine: check existing engine & currentDispatcher
  alt same dispatcher and isSetupCompleted == true
    Worker-->>Worker: return early (no-op)
  else dispatcher differs or setup incomplete
    Worker->>Engine: clear method handler
    Worker->>Engine: destroy context / teardown
    Worker->>Engine: engine = nil, channel = nil, isSetupCompleted = false
    Worker->>Continuation: resume any pending run continuations
    Worker->>Engine: create headless FlutterEngine
    Worker->>Registrar: registrar(for: engine)
    Worker->>Plugin: HomeWidgetPlugin.registerChannels(with: Registrar)
    Plugin->>Registrar: register method channel "home_widget"
    Plugin->>Registrar: register event channel "home_widget/updates" (set handler)
    Worker->>Engine: run(withEntrypoint: callbackName?, libraryURI: callbackLibrary?)
    Worker->>Worker: set currentDispatcher = dispatcher, isSetupCompleted = true
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: avoiding duplicate app-delegate registration on the background engine.
Description check ✅ Passed The PR description clearly explains the problem and solution, though the checklist items are marked without substantiation.
Linked Issues check ✅ Passed The code changes directly address issue #408 by preventing duplicate app-delegate insertion into the UIApplication delegate chain and guarding setupEngine against repeated calls.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to the iOS background engine registration issue; no unrelated modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cyclone-pk cyclone-pk force-pushed the fix/ios-408-duplicate-app-delegate branch from c5d65bd to d9843d6 Compare April 13, 2026 01:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetPlugin.swift (1)

41-50: ⚠️ Potential issue | 🟠 Major

register(with:) can re-add the application delegate when called from a custom registerPlugins callback on the background engine.

HomeWidgetBackgroundWorker.swift (lines 66-74) delegates plugin registration to registerPlugins when that callback is set. If that callback registers HomeWidgetPlugin by calling register(with:) instead of registerChannels(with:), the isRunningInAppExtension() check at line 44 will not block the addApplicationDelegate: call (since a background engine is not an app extension), causing the duplicate-delegate bug from issue #408 to resurface. The register(with:) method needs to distinguish between main app and background engine contexts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetPlugin.swift`
around lines 41 - 50, register(with:) can be invoked from a background engine
and should not add the application delegate in that context; modify
register(with:) to always call registerChannels(with: registrar) but only invoke
registrar.perform("addApplicationDelegate:", with: instance) when running in the
main app (not an app extension or a background engine). Implement an extra
runtime check before performing the selector — for example ensure
isRunningInAppExtension() == false AND that UIApplication.shared.delegate != nil
(or otherwise detect main app context) — so that
HomeWidgetPlugin.register(with:) never re-adds the app delegate when
registration is delegated by HomeWidgetBackgroundWorker.registerPlugins.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetBackgroundWorker.swift`:
- Around line 48-52: The early return guards against multiple registrations by
checking engine == nil, which prevents subsequent setupEngine(dispatcher:) calls
from replacing a stale or uninitialized engine; change this to key the fast-path
on both the dispatcher and the engine's startup state: track the last-registered
dispatcher (e.g. registeredDispatcher) and an engine-initialized flag (or use
engine.backgroundInitialized) and only short-circuit when engine != nil AND
registeredDispatcher == dispatcher AND the engine is fully initialized;
otherwise allow setupEngine(dispatcher:) to recreate or reinitialize the engine
(tearing down the old engine if necessary) so new dispatcher values or failed
initializations can recover.

---

Outside diff comments:
In
`@packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetPlugin.swift`:
- Around line 41-50: register(with:) can be invoked from a background engine and
should not add the application delegate in that context; modify register(with:)
to always call registerChannels(with: registrar) but only invoke
registrar.perform("addApplicationDelegate:", with: instance) when running in the
main app (not an app extension or a background engine). Implement an extra
runtime check before performing the selector — for example ensure
isRunningInAppExtension() == false AND that UIApplication.shared.delegate != nil
(or otherwise detect main app context) — so that
HomeWidgetPlugin.register(with:) never re-adds the app delegate when
registration is delegated by HomeWidgetBackgroundWorker.registerPlugins.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 78816f5c-7b46-4773-ba78-cd6969acac90

📥 Commits

Reviewing files that changed from the base of the PR and between ac84f31 and c5d65bd.

📒 Files selected for processing (2)
  • packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetBackgroundWorker.swift
  • packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetPlugin.swift

@cyclone-pk cyclone-pk changed the title fix(ios): avoid duplicate app-delegate on background engine fix(ios): Avoid duplicate app-delegate on background engine Apr 13, 2026
@cyclone-pk cyclone-pk force-pushed the fix/ios-408-duplicate-app-delegate branch from d9843d6 to 8adde37 Compare April 13, 2026 01:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetBackgroundWorker.swift (1)

81-82: Consider guarding the registrar force-unwrap.

engine!.registrar(forPlugin:) returns an optional. While it should always succeed for "home_widget" on a running engine, a defensive guard would prevent a crash if something unexpected occurs.

🛡️ Suggested defensive unwrap
-      HomeWidgetPlugin.registerChannels(
-        with: engine!.registrar(forPlugin: "home_widget")!)
+      if let registrar = engine?.registrar(forPlugin: "home_widget") {
+        HomeWidgetPlugin.registerChannels(with: registrar)
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetBackgroundWorker.swift`
around lines 81 - 82, The call to engine!.registrar(forPlugin: "home_widget") is
force-unwrapped and can crash if the registrar is nil; update the
HomeWidgetPlugin.registerChannels call to guard the registrar using
engine?.registrar(forPlugin: "home_widget") (or guard let registrar =
engine?.registrar(forPlugin: "home_widget") else { ... }) and handle the nil
case (log an error via the existing logger or return early) before passing the
registrar into HomeWidgetPlugin.registerChannels so the app fails gracefully
instead of crashing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetBackgroundWorker.swift`:
- Around line 81-82: The call to engine!.registrar(forPlugin: "home_widget") is
force-unwrapped and can crash if the registrar is nil; update the
HomeWidgetPlugin.registerChannels call to guard the registrar using
engine?.registrar(forPlugin: "home_widget") (or guard let registrar =
engine?.registrar(forPlugin: "home_widget") else { ... }) and handle the nil
case (log an error via the existing logger or return early) before passing the
registrar into HomeWidgetPlugin.registerChannels so the app fails gracefully
instead of crashing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 47cc147a-ba45-4dd9-b10e-69f612f12e07

📥 Commits

Reviewing files that changed from the base of the PR and between d9843d6 and 8adde37.

📒 Files selected for processing (2)
  • packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetBackgroundWorker.swift
  • packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetPlugin.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/home_widget/ios/home_widget/Sources/home_widget/HomeWidgetPlugin.swift

HomeWidgetBackgroundWorker.setupEngine used HomeWidgetPlugin.register on
the secondary FlutterEngine, which also calls addApplicationDelegate:
and inserts another HomeWidgetPlugin into the host app's UIApplication
delegate chain. That extra delegate disturbs URL/life-cycle propagation
for other plugins (e.g. appsflyer_sdk deep-link/attribution callbacks).

Split register into a channel-only registerChannels path and use it for
the background engine so the background isolate still has the
home_widget channel without polluting the main app's delegate chain.
Also guard setupEngine so repeated registerBackgroundCallback calls do
not leak engines.

Closes ABausG#408
@cyclone-pk cyclone-pk force-pushed the fix/ios-408-duplicate-app-delegate branch from 8adde37 to ae95805 Compare April 13, 2026 01:45
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.

registerInteractivityCallback might be affecting callbacks in other plugins.

1 participant