You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为Flutter下载插件的每个调用创建独立事件通道?

在Flutter插件中为每次下载请求创建独立事件通道

要实现每次调用downloadFile时创建独立的事件通道,核心思路是为每个下载请求生成唯一的通道标识,在Flutter端动态创建EventChannel,同时在原生端维护这些通道的生命周期,确保下载完成或出错后及时清理资源。

Flutter 端插件实现

import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/services.dart';

class FileDownloadPlugin {
  static const MethodChannel _methodChannel = MethodChannel('file_download_plugin');

  Future<Stream<Uint8List>> downloadFile(String url) async {
    // 生成唯一事件通道名称:URL编码+时间戳避免冲突
    final channelId = '${Uri.encodeComponent(url)}-${DateTime.now().microsecondsSinceEpoch}';
    final channelName = 'file_download_plugin/events/$channelId';
    final eventChannel = EventChannel(channelName);

    // 通知原生端启动下载并绑定对应通道
    await _methodChannel.invokeMethod('startDownload', {
      'url': url,
      'channelName': channelName,
    });

    // 转换事件通道流为Uint8List,并添加生命周期处理
    return eventChannel.receiveBroadcastStream()
        .map((data) => data as Uint8List)
        .handleError((error) {
          // 下载出错时清理通道
          _disposeChannel(channelName);
          throw error;
        })
        .whenComplete(() => _disposeChannel(channelName));
  }

  Future<void> _disposeChannel(String channelName) async {
    try {
      await _methodChannel.invokeMethod('disposeChannel', {'channelName': channelName});
    } catch (_) {}
  }
}

Android 端实现(Kotlin)

在原生端维护活跃通道的映射,处理下载请求时创建对应EventChannel,并在任务结束后清理:

import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.EventChannel

class FileDownloadPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
    private lateinit var methodChannel: MethodChannel
    private val activeChannels = mutableMapOf<String, Pair<EventChannel, DownloadTask>>()

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        methodChannel = MethodChannel(binding.binaryMessenger, "file_download_plugin")
        methodChannel.setMethodCallHandler(this)
    }

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "startDownload" -> {
                val url = call.argument<String>("url") ?: run {
                    result.error("INVALID_URL", "URL不能为空", null)
                    return
                }
                val channelName = call.argument<String>("channelName") ?: run {
                    result.error("INVALID_CHANNEL", "通道名称不能为空", null)
                    return
                }

                val eventChannel = EventChannel(binding.binaryMessenger, channelName)
                val downloadTask = DownloadTask(url) { event ->
                    when (event) {
                        is DownloadEvent.Data -> activeChannels[channelName]?.first?.sendEvent(event.data)
                        DownloadEvent.Complete -> {
                            activeChannels.remove(channelName)?.let { (ec, task) ->
                                task.cancel()
                                ec.sendEndOfStream()
                            }
                        }
                        is DownloadEvent.Error -> {
                            activeChannels.remove(channelName)?.let { (ec, task) ->
                                task.cancel()
                                ec.sendError("DOWNLOAD_FAILED", event.message, null)
                            }
                        }
                    }
                }

                eventChannel.setStreamHandler(object : EventChannel.StreamHandler {
                    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
                        downloadTask.setEventSink(events)
                    }

                    override fun onCancel(arguments: Any?) {
                        activeChannels.remove(channelName)?.second?.cancel()
                    }
                })

                activeChannels[channelName] = Pair(eventChannel, downloadTask)
                downloadTask.start()
                result.success(null)
            }
            "disposeChannel" -> {
                val channelName = call.argument<String>("channelName") ?: return
                activeChannels.remove(channelName)?.second?.cancel()
                result.success(null)
            }
            else -> result.notImplemented()
        }
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        methodChannel.setMethodCallHandler(null)
        activeChannels.values.forEach { it.second.cancel() }
        activeChannels.clear()
    }

    // 下载事件封装
    sealed class DownloadEvent {
        data class Data(val data: ByteArray) : DownloadEvent()
        object Complete : DownloadEvent()
        data class Error(val message: String) : DownloadEvent()
    }

    // 模拟下载任务类
    class DownloadTask(private val url: String, private val callback: (DownloadEvent) -> Unit) {
        private var eventSink: EventChannel.EventSink? = null
        private var isCancelled = false

        fun setEventSink(sink: EventChannel.EventSink?) {
            eventSink = sink
        }

        fun start() {
            // 这里实现实际的下载逻辑,比如使用OkHttp分块下载
            // 示例:模拟分块发送数据
            Thread {
                repeat(10) {
                    if (isCancelled) return@Thread
                    val mockData = "Chunk $it".toByteArray()
                    callback(DownloadEvent.Data(mockData))
                    Thread.sleep(500)
                }
                callback(DownloadEvent.Complete)
            }.start()
        }

        fun cancel() {
            isCancelled = true
        }
    }
}

iOS 端实现(Swift)

同样通过字典管理活跃通道,处理动态创建的FlutterEventChannel:

import Flutter
import UIKit

public class FileDownloadPlugin: NSObject, FlutterPlugin {
    private var methodChannel: FlutterMethodChannel!
    private var activeChannels: [String: (channel: FlutterEventChannel, task: DownloadTask)] = [:]

    public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(name: "file_download_plugin", binaryMessenger: registrar.messenger())
        let instance = FileDownloadPlugin()
        instance.methodChannel = channel
        registrar.addMethodCallDelegate(instance, channel: channel)
    }

    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        switch call.method {
        case "startDownload":
            guard let args = call.arguments as? [String: Any],
                  let url = args["url"] as? String,
                  let channelName = args["channelName"] as? String else {
                result(FlutterError(code: "INVALID_ARGS", message: "缺少必要参数", details: nil))
                return
            }

            let eventChannel = FlutterEventChannel(name: channelName, binaryMessenger: methodChannel.binaryMessenger)
            let downloadTask = DownloadTask(url: url) { [weak self] event in
                guard let self = self, let entry = self.activeChannels[channelName] else { return }
                switch event {
                case .data(let data):
                    entry.channel.send(data)
                case .complete:
                    self.activeChannels.removeValue(forKey: channelName)
                    entry.channel.endOfStream()
                case .error(let code, let message):
                    self.activeChannels.removeValue(forKey: channelName)
                    entry.channel.sendError(code, message: message, details: nil)
                }
            }

            eventChannel.setStreamHandler(StreamHandler(task: downloadTask))
            activeChannels[channelName] = (channel: eventChannel, task: downloadTask)
            downloadTask.start()
            result(nil)

        case "disposeChannel":
            guard let channelName = call.arguments as? String else {
                result(nil)
                return
            }
            activeChannels.removeValue(forKey: channelName)?.task.cancel()
            result(nil)

        default:
            result(FlutterMethodNotImplemented)
        }
    }
}

// 下载事件枚举
enum DownloadEvent {
    case data(Data)
    case complete
    case error(String, String?)
}

// 模拟下载任务
class DownloadTask {
    private let url: String
    private let callback: (DownloadEvent) -> Void
    private var isCancelled = false

    init(url: String, callback: @escaping (DownloadEvent) -> Void) {
        self.url = url
        self.callback = callback
    }

    func start() {
        // 实现实际下载逻辑,比如使用URLSession进行分块下载
        // 示例:模拟分块发送数据
        DispatchQueue.global().async { [weak self] in
            guard let self = self, !self.isCancelled else { return }
            for i in 0..<10 {
                if self.isCancelled { break }
                let mockData = "Chunk \(i)".data(using: .utf8)!
                self.callback(.data(mockData))
                Thread.sleep(forTimeInterval: 0.5)
            }
            self.callback(.complete)
        }
    }

    func cancel() {
        isCancelled = true
    }
}

// 事件通道流处理器
class StreamHandler: NSObject, FlutterStreamHandler {
    private let task: DownloadTask

    init(task: DownloadTask) {
        self.task = task
        super.init()
    }

    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        // 可在此处将events关联到DownloadTask,用于直接发送事件
        return nil
    }

    func onCancel(withArguments arguments: Any?) -> FlutterError? {
        task.cancel()
        return nil
    }
}

关键注意事项

  • 唯一通道标识:通过URL编码+时间戳生成唯一通道名称,避免不同下载请求的通道冲突。
  • 资源清理:在下载完成、出错或用户取消监听时,必须从原生端的活跃通道映射中移除对应条目,并终止下载任务,防止内存泄漏。
  • 插件销毁处理:在插件从引擎解绑时,清理所有剩余的下载任务和通道引用。

内容的提问来源于stack exchange,提问作者Hammerhead

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 16:14:52