Skip to content

[Bug]: iOS connectivity_plus retains callbacks in multi-FlutterEngine apps #3958

Description

@FeliksLv01

Platform

iOS, in applications that register connectivity_plus on more than one FlutterEngine (for example, a long-lived main engine plus short-lived secondary engines).

Plugin

connectivity_plus

Version

7.3.1

Flutter SDK

  • Flutter 3.35.7 / Dart 3.9.2: production crash observed in our multi-engine host.

Steps to reproduce

  1. Create a long-lived FlutterEngine and register connectivity_plus.
  2. Create a second, short-lived FlutterEngine and register the plugin there too.
  3. Subscribe to onConnectivityChanged on the secondary engine.
  4. Destroy or detach the secondary engine while a network callback is pending, then repeat the create/destroy cycle.

Actual behavior

The iOS plugin retains callback chains after an engine is no longer usable:

  • ConnectivityPlusPlugin -> ConnectivityProvider -> connectivityUpdateHandler -> ConnectivityPlusPlugin
  • PathMonitorConnectivityProvider -> NWPathMonitor -> pathUpdateHandler -> PathMonitorConnectivityProvider

The callbacks are assigned as bound method references, which retain their owners. The plugin is also not published through the registrar, so Flutter cannot invoke detachFromEngine(for:) when the engine is deallocated.

In a multi-engine application this leaves stale providers and network callbacks associated with short-lived engines. A later callback can attempt to deliver an EventChannel event after that engine has exited, causing:

Sending a message before the FlutterEngine has been run.

The retained callback graph also prevents timely release of the plugin/provider.

Expected behavior

Each FlutterEngine should own an independent plugin lifecycle. When an engine is detached or deallocated, its connectivity monitor and EventChannel sink should be released without affecting other engines, and no callback should target the detached engine.

Proposed fix

  1. Use weak captures for both callback edges.
  2. Publish the plugin instance with registrar.publish(instance).
  3. Implement detachFromEngine(for:) to clear the event sink and stop the engine-owned monitor.

This keeps normal connectivity monitoring unchanged while making teardown safe for multi-FlutterEngine hosts.

Code sample

final subscription =
    Connectivity().onConnectivityChanged.listen((_) {});

// In a host application, dispose the secondary FlutterEngine while this
// subscription is active, then create another secondary engine.

Logs

Fatal Exception: NSInternalInconsistencyException
Sending a message before the FlutterEngine has been run.

-[FlutterEngine sendOnChannel:message:binaryReply:]
FlutterBinaryMessengerRelay
SetStreamHandlerMessageHandlerOnChannel
SwiftConnectivityPlusPlugin.connectivityUpdateHandler

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions