fix(native): bound cross-platform lifecycle ownership

This commit is contained in:
edde746
2026-07-24 03:56:40 +02:00
parent 09656fa4d3
commit 9f2e050797
98 changed files with 12908 additions and 1599 deletions
@@ -5,14 +5,29 @@
import Flutter
import UIKit
private final class ConnectivityListenerEpoch {}
public class ConnectivityPlusPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
private let connectivityProvider: ConnectivityProvider
private let notificationCenter: NotificationCenter
private let applicationState: () -> UIApplication.State
private var eventSink: FlutterEventSink?
private var activationObserver: NSObjectProtocol?
private var pendingConnectivity: [String]?
private var lastDeliveredConnectivity: [String]?
private var activeListenerEpoch: ConnectivityListenerEpoch?
init(connectivityProvider: ConnectivityProvider) {
init(
connectivityProvider: ConnectivityProvider,
notificationCenter: NotificationCenter = .default,
applicationState: @escaping () -> UIApplication.State = {
UIApplication.shared.applicationState
}
) {
self.connectivityProvider = connectivityProvider
self.notificationCenter = notificationCenter
self.applicationState = applicationState
super.init()
self.connectivityProvider.connectivityUpdateHandler = connectivityUpdateHandler
}
public static func register(with registrar: FlutterPluginRegistrar) {
@@ -33,9 +48,43 @@ public class ConnectivityPlusPlugin: NSObject, FlutterPlugin, FlutterStreamHandl
registrar.addMethodCallDelegate(instance, channel: channel)
}
private func onMain<T>(_ block: () -> T) -> T {
if Thread.isMainThread {
return block()
}
return DispatchQueue.main.sync(execute: block)
}
private func installProviderHandler() -> ConnectivityListenerEpoch {
let epoch = ConnectivityListenerEpoch()
activeListenerEpoch = epoch
connectivityProvider.connectivityUpdateHandler = { [weak self] connectivityTypes in
self?.connectivityUpdateHandler(connectivityTypes: connectivityTypes, epoch: epoch)
}
return epoch
}
public func detachFromEngine(for registrar: FlutterPluginRegistrar) {
eventSink = nil
connectivityProvider.stop()
onMain {
stopListening(preservePendingConnectivity: false)
}
}
deinit {
let observer = activationObserver
let provider = connectivityProvider
let center = notificationCenter
let cleanup = {
if let observer {
center.removeObserver(observer)
}
provider.connectivityUpdateHandler = nil
}
if Thread.isMainThread {
cleanup()
} else {
DispatchQueue.main.sync(execute: cleanup)
}
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
@@ -56,12 +105,12 @@ public class ConnectivityPlusPlugin: NSObject, FlutterPlugin, FlutterStreamHandl
case .wiredEthernet:
return "ethernet"
case .other:
return "other"
return "other"
case .none:
return "none"
}
}
private func statusFrom(connectivityTypes: [ConnectivityType]) -> [String] {
return connectivityTypes.map {
self.statusFrom(connectivityType: $0)
@@ -72,26 +121,85 @@ public class ConnectivityPlusPlugin: NSObject, FlutterPlugin, FlutterStreamHandl
withArguments _: Any?,
eventSink events: @escaping FlutterEventSink
) -> FlutterError? {
eventSink = events
connectivityProvider.start()
// Update this to handle a list
connectivityUpdateHandler(connectivityTypes: connectivityProvider.currentConnectivityTypes)
return nil
}
private func connectivityUpdateHandler(connectivityTypes: [ConnectivityType]) {
DispatchQueue.main.async { [weak self] in
guard let self = self, let eventSink = self.eventSink else { return }
// NWPathMonitor can emit after the FlutterEngine shell is torn down.
// Do not call its event sink while tvOS is backgrounded.
guard UIApplication.shared.applicationState != .background else { return }
eventSink(self.statusFrom(connectivityTypes: connectivityTypes))
onMain {
eventSink = events
let listenerEpoch = installProviderHandler()
if activationObserver == nil {
activationObserver = notificationCenter.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.replayPendingConnectivityIfNeeded()
}
}
connectivityProvider.start()
guard applicationState() != .background else { return nil }
let replayedConnectivity = pendingConnectivity
if let replayedConnectivity {
pendingConnectivity = nil
lastDeliveredConnectivity = replayedConnectivity
events(replayedConnectivity)
}
let currentConnectivity = statusFrom(
connectivityTypes: connectivityProvider.currentConnectivityTypes)
if currentConnectivity != replayedConnectivity {
connectivityUpdateHandler(
connectivityTypes: connectivityProvider.currentConnectivityTypes,
epoch: listenerEpoch
)
}
return nil
}
}
public func onCancel(withArguments _: Any?) -> FlutterError? {
connectivityProvider.stop()
private func connectivityUpdateHandler(
connectivityTypes: [ConnectivityType],
epoch: ConnectivityListenerEpoch
) {
let status = statusFrom(connectivityTypes: connectivityTypes)
DispatchQueue.main.async { [weak self] in
guard let self, self.activeListenerEpoch === epoch else { return }
guard self.applicationState() != .background, let eventSink = self.eventSink else {
// Keep only the newest user-visible state. Connectivity changes are
// snapshots, not telemetry; one bounded slot is sufficient.
self.pendingConnectivity = status
return
}
self.pendingConnectivity = nil
guard status != self.lastDeliveredConnectivity else { return }
self.lastDeliveredConnectivity = status
eventSink(status)
}
}
private func replayPendingConnectivityIfNeeded() {
guard applicationState() == .active, let pendingConnectivity, let eventSink else { return }
self.pendingConnectivity = nil
guard pendingConnectivity != lastDeliveredConnectivity else { return }
lastDeliveredConnectivity = pendingConnectivity
eventSink(pendingConnectivity)
}
private func stopListening(preservePendingConnectivity: Bool) {
eventSink = nil
return nil
activeListenerEpoch = nil
connectivityProvider.connectivityUpdateHandler = nil
if !preservePendingConnectivity {
pendingConnectivity = nil
}
lastDeliveredConnectivity = nil
if let activationObserver {
notificationCenter.removeObserver(activationObserver)
self.activationObserver = nil
}
connectivityProvider.stop()
}
public func onCancel(withArguments _: Any?) -> FlutterError? {
onMain {
stopListening(preservePendingConnectivity: true)
return nil
}
}
}
@@ -6,6 +6,8 @@ public class PathMonitorConnectivityProvider: NSObject, ConnectivityProvider {
// Use .utility, as it is intended for tasks that the user does not track actively.
// See: https://developer.apple.com/documentation/dispatch/dispatchqos
private let queue = DispatchQueue.global(qos: .utility)
private let handlerLock = NSLock()
private var updateHandler: ConnectivityUpdateHandler?
private var pathMonitor: NWPathMonitor?
@@ -36,7 +38,18 @@ public class PathMonitorConnectivityProvider: NSObject, ConnectivityProvider {
return connectivityFrom(path: path)
}
public var connectivityUpdateHandler: ConnectivityUpdateHandler?
public var connectivityUpdateHandler: ConnectivityUpdateHandler? {
get {
handlerLock.lock()
defer { handlerLock.unlock() }
return updateHandler
}
set {
handlerLock.lock()
updateHandler = newValue
handlerLock.unlock()
}
}
override init() {
super.init()
@@ -64,6 +77,13 @@ public class PathMonitorConnectivityProvider: NSObject, ConnectivityProvider {
}
private func pathUpdateHandler(path: NWPath) {
connectivityUpdateHandler?(connectivityFrom(path: path))
deliver(connectivityFrom(path: path))
}
func deliver(_ connectivityTypes: [ConnectivityType]) {
handlerLock.lock()
let handler = updateHandler
handlerLock.unlock()
handler?(connectivityTypes)
}
}
File diff suppressed because it is too large Load Diff