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

Flutter插件多实例仅单个触发回调的问题排查与修复

问题:Flutter插件单Widget多实例仅第二个能收到回调,第一个完全无响应

原本功能正常的Flutter插件,在单个Widget中使用多个实例时,只有第二个实例能收到回调,第一个实例完全无法触发回调。以下是相关代码和日志:

插件代码

import 'dart:async';

import 'package:flutter/services.dart';

class FlutterOnlineRadio {
  static const MethodChannel _channel = MethodChannel('flutter_online_radio');
  static int instance_count = 0;

  static int STATUS_IDLE = 0;
  static int STATUS_PLAYING = 1;
  static int STATUS_BUFFERING = 2;
  static int STATUS_PAUSED = 3;
  static int STATUS_ENDED = 4;

  int _playbackState = 1;
  bool _playWhenReady = true;
  int playerStatus = STATUS_IDLE;
  String playerStatusString = "";
  String instance = "";

  void Function(int, String, dynamic)? onStatusChange;

  FlutterOnlineRadio(String ins) {
    instance_count++;

    print("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-");
    print("Current instance : $instance , new : $ins, totsl instances : $instance_count");
    print("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-");
    instance = ins;
    _playbackState = 1; //STATE_IDLE;
    _playWhenReady = true;

    _channel.setMethodCallHandler((MethodCall call) async {
      print("New Method Call Received : -------------- $instance of $instance_count -------------- : ${call.method}" );

      if (call.method == "onPlayerStateChanged") {
        dynamic args = call.arguments;
        _playbackState = args["playbackState"];
        _playWhenReady = args["playWhenReady"];

        _setPlayingState();

        onStatusChange?.call(playerStatus, playerStatusString, args);
      }
      else if(call.method == "onTrackUpdate")
      {
        onStatusChange?.call(playerStatus, playerStatusString, call.arguments);

      }
    });

    _channel.invokeMethod('initialize');
  }

  void playRadio({String url = '', String title = '', String imagePath =''}) async {
    await _channel.invokeMethod('play',
        {"url": url, "title": title, "imagePath": imagePath});
  }

  void playIfPaused() async {
    await _channel.invokeMethod('playIfPaused');
  }

  void playAlbum(
      {String data ='',
        String imagePath='',
        String baseUrl='',
        String dataDir='',
        int index=0}) async {
    await _channel.invokeMethod('playAlbum', {
      "data": data,
      "imagePath": imagePath,
      "baseUrl": baseUrl,
      "dataDir": dataDir,
      "index": index,
    });
  }

  void stop() async {
    await _channel.invokeMethod('stop');
  }

  void pause() async {
    await _channel.invokeMethod('pause');
  }

  void seekTo(int milisecs) async {
    await _channel.invokeMethod('seekTo', {
      "milisecs" : milisecs
    });
  }

  Future<dynamic> getStatus() async {
    dynamic status = await _channel.invokeMethod('getStatus');

    _playbackState = status["playbackState"];
    _playWhenReady = status["playWhenReady"];

    _setPlayingState();

    dynamic tmp = {
      "playerStatus": playerStatus,
      "playerStatusString": playerStatusString,
    };

    var returnVal = {};

    returnVal.addAll(tmp);
    returnVal.addAll(status);

    return returnVal;
  }

  void _setPlayingState() {
    const int STATE_IDLE = 1;
    const int STATE_BUFFERING = 2;
    const int STATE_READY = 3;
    const int STATE_ENDED = 4;

    switch (_playbackState) {
      case STATE_IDLE:
        playerStatus = STATUS_IDLE;
        playerStatusString = "";
        break;
      case STATE_READY:
        playerStatus = (_playWhenReady) ? STATUS_PLAYING : STATUS_PAUSED;
        playerStatusString = (_playWhenReady) ? "Playing..." : "Paused";
        break;
      case STATE_BUFFERING:
        playerStatus = STATUS_BUFFERING;
        playerStatusString = "Buffering...";
        break;
      case STATE_ENDED:
        playerStatus = STATUS_ENDED;
        playerStatusString = "";
        break;
    }
  }
}

Widget代码

import 'dart:io';
import 'package:flutter/material.dart';
import '../constants.dart';
import 'package:flutter_online_radio/flutter_online_radio.dart';


class MyPersistentAudio extends StatefulWidget {
  MyPersistentAudio();

  @override
  _MyPersistentAudioState createState() => _MyPersistentAudioState();
}

class _MyPersistentAudioState extends State<MyPersistentAudio> {
  FlutterOnlineRadio flutterOnlineRadio =
      new FlutterOnlineRadio("persistent instance 1");
  FlutterOnlineRadio flutterOnlineRadio2 =
  new FlutterOnlineRadio("persistent instance 2");

  int playerStatus = FlutterOnlineRadio.STATUS_IDLE;
  String playerStatusString = "";
  String playingTitle = "";
  String imagePath = "";
  bool widgetActive = false;

  @override
  void initState() {

    flutterOnlineRadio.onStatusChange = (status, statusString, args) {
      kksPrint("my_persistent_audio.dart : onStatusChange");
      kksPrint(status);
      kksPrint(args);

      setState(() {
        //Just Refresh : Important
      });
    };

    flutterOnlineRadio2.onStatusChange = (status, statusString, args) {
      kksPrint("my_persistent_audio.dart : 2 : onStatusChange");
      kksPrint(status);
      kksPrint(args);
    };
    
    super.initState();
  }


  @override
  Widget build(BuildContext context) {
    print(playerStatus);

    return AnimatedContainer(
      duration: Duration(seconds: 1),
      height: 50,
      width: MediaQuery.of(context).size.width,
      color: Colors.white,
      child: Directionality(
        textDirection: TextDirection.ltr,
        child: Text("Radio ..."
        ),
      ),
    );
  }
}

日志信息

I/flutter ( 4848): New Method Call Received : -------------- persistent instance 2 of 2 -------------- : onTrackUpdate
I/flutter ( 4848): my_persistent_audio.dart : 2 : onStatusChange
I/flutter ( 4848): 0
I/flutter ( 4848): {duration: 3476940, audioId: audio_39_1, bufferedPosition: 2022008, albumId: audio_39, position: 2006515}
I/flutter ( 4848): 0
I/flutter ( 4848): New Method Call Received : -------------- persistent instance 2 of 2 -------------- : onTrackUpdate
I/flutter ( 4848): my_persistent_audio.dart : 2 : onStatusChange
I/flutter ( 4848): 0
I/flutter ( 4848): {duration: 3476940, audioId: audio_39_1, bufferedPosition: 2153090, albumId: audio_39, position: 2007498}
I/flutter ( 4848): 0
I/flutter ( 4848): New Method Call Received : -------------- persistent instance 2 of 2 -------------- : onTrackUpdate
I/flutter ( 4848): my_persistent_audio.dart : 2 : onStatusChange
I/flutter ( 4848): 0
I/flutter ( 4848): {duration: 3476940, audioId: audio_39_1, bufferedPosition: 2153090, albumId: audio_39, position: 2008514}
I/flutter ( 4848): 0
I/flutter ( 4848): New Method Call Received : -------------- persistent instance 2 of 2 -------------- : onTrackUpdate
I/flutter ( 4848): my_persistent_audio.dart : 2 : onStatusChange
I/flutter ( 4848): 0
I/flutter ( 4848): {duration: 3476940, audioId: audio_39_1, bufferedPosition: 2153090, albumId: audio_39, position: 2009522}
I/flutter ( 4848): 0
I/flutter ( 4848): New Method Call Received : -------------- persistent instance 2 of 2 -------------- : onTrackUpdate
I/flutter ( 4848): my_persistent_audio.dart : 2 : onStatusChange
I/flutter ( 4848): 0
I/flutter ( 4848): {duration: 3476940, audioId: audio_39_1, bufferedPosition: 2153090, albumId: audio_39, position: 2010503}
I/flutter ( 4848): 0
I/flutter ( 4848): New Method Call Received : -------------- persistent instance 2 of 2 -------------- : onTrackUpdate
I/flutter ( 4848): my_persistent_audio.dart : 2 : onStatusChange

问题原因

核心问题是静态MethodChannel的Handler被覆盖:

  • _channel是类的静态常量,所有实例共享同一个MethodChannel对象
  • 第一个实例初始化时设置了setMethodCallHandler,但第二个实例初始化时会直接覆盖这个Handler
  • 原生端发送的所有回调都会被最后设置的Handler(第二个实例的)处理,第一个实例的Handler完全不会被触发
  • 日志中所有回调都显示来自"persistent instance 2",也验证了这一点

解决方法

推荐使用为每个实例创建独立MethodChannel的方案,让每个实例拥有专属的通信通道,避免Handler覆盖:

修改后的插件代码

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

class FlutterOnlineRadio {
  static int instance_count = 0;

  static int STATUS_IDLE = 0;
  static int STATUS_PLAYING = 1;
  static int STATUS_BUFFERING = 2;
  static int STATUS_PAUSED = 3;
  static int STATUS_ENDED = 4;

  final MethodChannel _channel; // 改为实例成员,不再共享
  int _playbackState = 1;
  bool _playWhenReady = true;
  int playerStatus = STATUS_IDLE;
  String playerStatusString = "";
  String instance = "";
  final String _channelName;

  void Function(int, String, dynamic)? onStatusChange;

  // 初始化时生成专属Channel名称
  FlutterOnlineRadio(String ins) : _channelName = 'flutter_online_radio_${instance_count + 1}' {
    instance_count++;
    instance = ins;
    _playbackState = 1;
    _playWhenReady = true;
    // 为当前实例创建专属MethodChannel
    _channel = MethodChannel(_channelName);

    print("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-");
    print("Current instance : $instance , new : $ins, total instances : $instance_count, channel: $_channelName");
    print("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-");

    _channel.setMethodCallHandler((MethodCall call) async {
      print("New Method Call Received : -------------- $instance of $instance_count -------------- : ${call.method}" );

      if (call.method == "onPlayerStateChanged") {
        dynamic args = call.arguments;
        _playbackState = args["playbackState"];
        _playWhenReady = args["playWhenReady"];

        _setPlayingState();
        onStatusChange?.call(playerStatus, playerStatusString, args);
      }
      else if(call.method == "onTrackUpdate")
      {
        onStatusChange?.call(playerStatus, playerStatusString, call.arguments);
      }
    });

    // 初始化时传递Channel名称给原生,让原生针对该Channel发送回调
    _channel.invokeMethod('initialize', {"channelName": _channelName});
  }

  // 其余方法保持不变,使用当前实例的_channel
  void playRadio({String url = '', String title = '', String imagePath =''}) async {
    await _channel.invokeMethod('play',
        {"url": url, "title": title, "imagePath": imagePath});
  }

  void playIfPaused() async {
    await _channel.invokeMethod('playIfPaused');
  }

  void playAlbum(
      {String data ='',
        String imagePath='',
        String baseUrl='',
        String dataDir='',
        int index=0}) async {
    await _channel.invokeMethod('playAlbum', {
      "data": data,
      "imagePath": imagePath,
      "baseUrl": baseUrl,
      "dataDir": dataDir,
      "index": index,
    });
  }

  void stop() async {
    await _channel.invokeMethod('stop');
  }

  void pause() async {
    await _channel.invokeMethod('pause');
  }

  void seekTo(int milisecs) async {
    await _channel.invokeMethod('seekTo', {
      "milisecs" : milisecs
    });
  }

  Future<dynamic> getStatus() async {
    dynamic status = await _channel.invokeMethod('getStatus');

    _playbackState = status["playbackState"];
    _playWhenReady = status["playWhenReady"];

    _setPlayingState();

    dynamic tmp = {
      "playerStatus": playerStatus,
      "playerStatusString": playerStatusString,
    };

    var returnVal = {};
    returnVal.addAll(tmp);
    returnVal.addAll(status);

    return returnVal;
  }

  void _setPlayingState() {
    const int STATE_IDLE = 1;
    const int STATE_BUFFERING = 2;
    const int STATE_READY = 3;
    const int STATE_ENDED = 4;

    switch (_playbackState) {
      case STATE_IDLE:
        playerStatus = STATUS_IDLE;
        playerStatusString = "";
        break;
      case STATE_READY:
        playerStatus = (_playWhenReady) ? STATUS_PLAYING : STATUS_PAUSED;
        playerStatusString = (_playWhenReady) ? "Playing..." : "Paused";
        break;
      case STATE_BUFFERING:
        playerStatus = STATUS_BUFFERING;
        playerStatusString = "Buffering...";
        break;
      case STATE_ENDED:
        playerStatus = STATUS_ENDED;
        playerStatusString = "";
        break;
    }
  }
}

原生端对应修改(以Android为例)

在原生代码中,需要根据Flutter传递的channelName创建对应的MethodChannel,发送回调时使用该Channel:

// 初始化时获取并保存Channel名称
String channelName = call.argument("channelName");
MethodChannel channel = new MethodChannel(getFlutterView(), channelName);

// 发送回调时使用该channel
channel.invokeMethod("onTrackUpdate", params);
channel.invokeMethod("onPlayerStateChanged", params);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 10:14:52