fix(native): bound cross-platform lifecycle ownership
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+876
-118
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user