Flutter三方库适配OpenHarmony【flutter_speech】— Android 端实现分析

前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

在做OpenHarmony适配之前,我花了不少时间研究Android端的实现。为什么?因为Android端是flutter_speech最成熟的平台实现,代码经过了大量用户的验证,很多设计决策都值得借鉴。

说实话,Android的SpeechRecognizer API我之前只是用过,从来没有深入研究过。这次为了做适配,把Android端的源码翻了个底朝天,收获还挺大的。特别是RecognitionListener的回调设计,直接影响了我后来在OpenHarmony端的实现思路。

今天这篇文章,我会从插件类结构、SpeechRecognizer API、回调处理、权限管理、生命周期五个方面来分析Android端的实现。每个部分都会和OpenHarmony做对比,方便大家建立映射关系。

💡 说明:本文分析的是flutter_speech插件的Android端Java实现。如果你对Android原生开发不太熟悉,可以先跳过本篇,直接看第7篇的OpenHarmony实现。

一、FlutterSpeechRecognitionPlugin 类结构解析

1.1 类声明与接口实现

Android端的插件类实现了多个接口:

java 复制代码
public class FlutterSpeechRecognitionPlugin implements
    FlutterPlugin,
    MethodCallHandler,
    ActivityAware {

  private MethodChannel channel;
  private SpeechRecognizer speechRecognizer;
  private Activity activity;
  private boolean isListening = false;
  private String lastTranscription = "";
}
接口 职责 OpenHarmony对应
FlutterPlugin 插件生命周期管理 FlutterPlugin
MethodCallHandler 处理Dart方法调用 MethodCallHandler
ActivityAware 获取Activity上下文 AbilityAware

这三个接口的组合是Flutter插件的标准模式。OpenHarmony端的实现也是这三个接口,只是名称略有不同(ActivityAware → AbilityAware)。

1.2 成员变量分析

java 复制代码
private MethodChannel channel;           // 与Dart通信的通道
private SpeechRecognizer speechRecognizer; // Android语音识别引擎
private Activity activity;                // Activity上下文(权限申请需要)
private boolean isListening = false;      // 是否正在监听
private String lastTranscription = "";    // 最后一次识别结果

对比OpenHarmony端的成员变量:

Android OpenHarmony 类型差异
MethodChannel channel MethodChannel channel 相同
SpeechRecognizer speechRecognizer.SpeechRecognitionEngine 不同的引擎类型
Activity activity common.UIAbilityContext abilityContext 不同的上下文类型
boolean isListening boolean isListening 相同
String lastTranscription string lastTranscription 相同

🎯 关键发现 :两个平台的成员变量几乎一一对应,只是类型不同。这说明适配的核心工作就是类型映射------把Android的类型替换成OpenHarmony的对应类型。

1.3 与OpenHarmony实现的结构对比

把两个平台的类声明放在一起看:

java 复制代码
// Android
public class FlutterSpeechRecognitionPlugin implements
    FlutterPlugin, MethodCallHandler, ActivityAware {
  // ...
}
typescript 复制代码
// OpenHarmony
export default class FlutterSpeechPlugin implements
    FlutterPlugin, MethodCallHandler, AbilityAware {
  // ...
}

结构几乎一模一样。Flutter-OHOS团队在设计接口时显然参考了Android端的设计,这大大降低了适配的学习成本。

二、Android SpeechRecognizer API 使用详解

2.1 SpeechRecognizer 概述

Android的SpeechRecognizer是系统级的语音识别API,位于android.speech包中:

java 复制代码
import android.speech.RecognitionListener;
import android.speech.RecognizerIntent;
import android.speech.SpeechRecognizer;
API 功能 说明
SpeechRecognizer.createSpeechRecognizer() 创建引擎 需要Context参数
setRecognitionListener() 设置回调 RecognitionListener接口
startListening() 开始识别 传入Intent参数
stopListening() 停止识别 获取最终结果
cancel() 取消识别 不返回结果
destroy() 销毁引擎 释放资源

2.2 引擎创建流程

java 复制代码
private void createSpeechRecognizer(String locale) {
    if (speechRecognizer != null) {
        speechRecognizer.destroy();
    }

    speechRecognizer = SpeechRecognizer.createSpeechRecognizer(activity);
    speechRecognizer.setRecognitionListener(new SpeechRecognitionListener());

    // 检查语音识别是否可用
    boolean available = SpeechRecognizer.isRecognitionAvailable(activity);
    channel.invokeMethod("speech.onSpeechAvailability", available);
}

对比OpenHarmony端:

typescript 复制代码
// OpenHarmony的引擎创建
this.asrEngine = await speechRecognizer.createEngine({
  language: language,
  online: 1
});
this.setupListener();
this.channel?.invokeMethod('speech.onSpeechAvailability', true);
对比维度 Android OpenHarmony
创建方式 同步创建 异步创建(await)
参数 Context language + online
语言设置 在startListening的Intent中 在createEngine时指定
可用性检查 isRecognitionAvailable() canIUse()

🤔 一个重要差异 :Android的语言设置是在startListening时通过Intent传入的,而OpenHarmony是在createEngine时就要指定。这意味着在OpenHarmony上,切换语言需要重新创建引擎

2.3 启动识别

java 复制代码
private void startListening(String locale) {
    Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
    intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, locale);
    intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
        RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
    intent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
    intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 3);

    speechRecognizer.startListening(intent);
    isListening = true;
}

Android通过Intent传递识别参数,这是Android特有的设计模式。OpenHarmony用的是StartParams结构体:

typescript 复制代码
// OpenHarmony的启动参数
const recognizerParams: speechRecognizer.StartParams = {
  sessionId: this.sessionId,
  audioInfo: {
    audioType: 'pcm',
    sampleRate: 16000,
    soundChannel: 1,
    sampleBit: 16
  },
  extraParams: {
    "recognitionMode": 0,
    "vadBegin": 2000,
    "vadEnd": 3000,
    "maxAudioDuration": 60000
  }
};
this.asrEngine.startListening(recognizerParams);

参数对比:

参数 Android OpenHarmony 说明
语言 EXTRA_LANGUAGE createEngine时指定 设置时机不同
识别模式 LANGUAGE_MODEL_FREE_FORM recognitionMode: 0 自由文本模式
部分结果 EXTRA_PARTIAL_RESULTS: true 默认支持 OHOS默认开启
音频格式 系统自动处理 audioInfo手动指定 OHOS需要显式配置
VAD 系统默认 vadBegin/vadEnd OHOS可精细控制

三、RecognitionListener 回调处理流程

3.1 RecognitionListener 接口

Android的RecognitionListener接口定义了十个回调方法

java 复制代码
class SpeechRecognitionListener implements RecognitionListener {
    @Override
    public void onReadyForSpeech(Bundle params) {
        // 引擎准备就绪,可以开始说话
        channel.invokeMethod("speech.onRecognitionStarted", null);
    }

    @Override
    public void onBeginningOfSpeech() {
        // 检测到用户开始说话
    }

    @Override
    public void onRmsChanged(float rmsdB) {
        // 音量变化(可用于显示音量动画)
    }

    @Override
    public void onBufferReceived(byte[] buffer) {
        // 接收到音频数据
    }

    @Override
    public void onEndOfSpeech() {
        // 用户停止说话
    }

    @Override
    public void onError(int error) {
        // 发生错误
        isListening = false;
        channel.invokeMethod("speech.onError", error);
    }

    @Override
    public void onResults(Bundle results) {
        // 最终识别结果
        ArrayList<String> matches = results.getStringArrayList(
            SpeechRecognizer.RESULTS_RECOGNITION);
        if (matches != null && !matches.isEmpty()) {
            lastTranscription = matches.get(0);
            channel.invokeMethod("speech.onRecognitionComplete", lastTranscription);
        }
        isListening = false;
    }

    @Override
    public void onPartialResults(Bundle partialResults) {
        // 部分识别结果(实时)
        ArrayList<String> matches = partialResults.getStringArrayList(
            SpeechRecognizer.RESULTS_RECOGNITION);
        if (matches != null && !matches.isEmpty()) {
            lastTranscription = matches.get(0);
            channel.invokeMethod("speech.onSpeech", lastTranscription);
        }
    }

    @Override
    public void onEvent(int eventType, Bundle params) {
        // 其他事件
    }
}

3.2 回调映射到Dart层

Android的十个回调最终映射到Dart层的五个事件:

Android回调 Dart事件 映射关系
onReadyForSpeech speech.onRecognitionStarted 直接映射
onPartialResults speech.onSpeech 取第一个结果
onResults speech.onRecognitionComplete 取第一个结果
onError speech.onError 传递错误码
onBeginningOfSpeech (未映射) 被忽略
onRmsChanged (未映射) 被忽略
onBufferReceived (未映射) 被忽略
onEndOfSpeech (未映射) 被忽略
onEvent (未映射) 被忽略

📌 设计取舍:Android有十个回调,但flutter_speech只用了四个。被忽略的回调(如onRmsChanged音量变化)在某些场景下其实很有用,但为了保持API简洁,原作者选择了不暴露。

3.3 对比OpenHarmony的回调

OpenHarmony的setListener只有五个回调,和Dart层的五个事件刚好对应:

typescript 复制代码
this.asrEngine.setListener({
  onStart(sessionId, eventMessage) {
    // → speech.onRecognitionStarted
    channel?.invokeMethod('speech.onRecognitionStarted', null);
  },
  onEvent(sessionId, eventCode, eventMessage) {
    // 事件通知(类似Android的onEvent)
  },
  onResult(sessionId, result) {
    // → speech.onSpeech + speech.onRecognitionComplete
    channel?.invokeMethod('speech.onSpeech', result.result);
    if (result.isLast) {
      channel?.invokeMethod('speech.onRecognitionComplete', result.result);
    }
  },
  onComplete(sessionId, eventMessage) {
    // 识别完成
  },
  onError(sessionId, errorCode, errorMessage) {
    // → speech.onError
    channel?.invokeMethod('speech.onError', errorCode);
  }
});
对比维度 Android OpenHarmony
回调数量 10个 5个
部分结果 onPartialResults onResult (isLast=false)
最终结果 onResults onResult (isLast=true)
结果格式 Bundle (ArrayList) SpeechRecognitionResult
错误信息 int errorCode int errorCode + String message

💡 OpenHarmony的设计更简洁 :用一个onResult回调通过isLast字段区分部分结果和最终结果,比Android的两个独立回调(onPartialResults + onResults)更优雅。

四、权限申请机制:RECORD_AUDIO 动态权限

4.1 Android权限模型

Android从6.0开始引入了动态权限 机制,语音识别需要RECORD_AUDIO权限:

xml 复制代码
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
java 复制代码
// 动态权限申请
private void requestPermission() {
    if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO)
            != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(activity,
            new String[]{Manifest.permission.RECORD_AUDIO},
            REQUEST_CODE_SPEECH);
    }
}

4.2 OpenHarmony权限模型对比

OpenHarmony的权限机制和Android类似但有差异:

json5 复制代码
// module.json5
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:microphone_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}
typescript 复制代码
// 动态权限申请
const atManager = abilityAccessCtrl.createAtManager();
const grantResult = await atManager.requestPermissionsFromUser(
  this.abilityContext,
  ['ohos.permission.MICROPHONE']
);
const allGranted = grantResult.authResults.every(
  (status: number) => status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
);
对比维度 Android OpenHarmony
权限名称 RECORD_AUDIO ohos.permission.MICROPHONE
声明位置 AndroidManifest.xml module.json5
申请API ActivityCompat.requestPermissions atManager.requestPermissionsFromUser
上下文 Activity UIAbilityContext
回调方式 onRequestPermissionsResult Promise (async/await)

🤔 个人感受 :OpenHarmony的权限申请用async/await比Android的回调方式写起来舒服多了。Android那个onRequestPermissionsResult回调嵌套深了很头疼。

4.3 权限拒绝处理

java 复制代码
// Android - 权限被拒绝
if (!hasPermission) {
    result.error("SPEECH_PERMISSION_DENIED",
        "Microphone permission denied", null);
    return;
}
typescript 复制代码
// OpenHarmony - 权限被拒绝
if (!allGranted) {
  result.error('SPEECH_PERMISSION_DENIED',
    'Microphone permission denied', null);
  return;
}

两个平台的错误处理逻辑完全一致,都是返回SPEECH_PERMISSION_DENIED错误码。这种一致性对Dart层的错误处理很友好。

五、Activity 生命周期管理与资源释放

5.1 ActivityAware 生命周期

java 复制代码
// Android的Activity生命周期回调
@Override
public void onAttachedToActivity(ActivityPluginBinding binding) {
    this.activity = binding.getActivity();
}

@Override
public void onDetachedFromActivity() {
    this.activity = null;
}

@Override
public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {
    this.activity = binding.getActivity();
}

@Override
public void onDetachedFromActivityForConfigChanges() {
    this.activity = null;
}

Android的ActivityAware有四个 生命周期方法,而OpenHarmony的AbilityAware只有两个

typescript 复制代码
// OpenHarmony的Ability生命周期
onAttachedToAbility(binding: AbilityPluginBinding): void {
  this.abilityContext = binding.getAbility().context;
}

onDetachedFromAbility(): void {
  this.abilityContext = null;
}
Android OpenHarmony 说明
onAttachedToActivity onAttachedToAbility 获取上下文
onDetachedFromActivity onDetachedFromAbility 释放上下文
onReattachedToActivityForConfigChanges (无) 配置变更重连
onDetachedFromActivityForConfigChanges (无) 配置变更断开

📌 为什么OpenHarmony少了两个方法:Android的"ConfigChanges"回调是为了处理屏幕旋转等配置变更场景。OpenHarmony的Ability模型处理配置变更的方式不同,不需要这两个额外的回调。

5.2 资源释放策略

java 复制代码
// Android的资源释放
@Override
public void onDetachedFromEngine(FlutterPluginBinding binding) {
    channel.setMethodCallHandler(null);
    if (speechRecognizer != null) {
        speechRecognizer.destroy();
        speechRecognizer = null;
    }
    isListening = false;
}
typescript 复制代码
// OpenHarmony的资源释放
onDetachedFromEngine(binding: FlutterPluginBinding): void {
  if (this.channel != null) {
    this.channel.setMethodCallHandler(null);
  }
  this.destroyEngine();
}

private destroyEngine(): void {
  if (this.asrEngine) {
    if (this.isListening) {
      this.asrEngine.cancel(this.sessionId);
    }
    this.asrEngine.shutdown();
    this.asrEngine = null;
    this.isListening = false;
  }
}

两个平台的释放逻辑基本一致:

  1. 取消MethodCallHandler注册
  2. 如果正在监听,先取消
  3. 销毁引擎
  4. 置空引用
  5. 重置状态

⚠️ 切记:资源释放的顺序很重要。必须先取消监听,再销毁引擎。如果顺序反了,可能会触发引擎内部的异常。

六、Android端的错误处理

6.1 SpeechRecognizer 错误码

Android定义了一系列标准错误码:

错误码 常量名 含义 常见原因
1 ERROR_NETWORK_TIMEOUT 网络超时 网络不稳定
2 ERROR_NETWORK 网络错误 无网络连接
3 ERROR_AUDIO 音频错误 麦克风被占用
4 ERROR_SERVER 服务器错误 识别服务异常
5 ERROR_CLIENT 客户端错误 参数错误
6 ERROR_SPEECH_TIMEOUT 语音超时 用户没说话
7 ERROR_NO_MATCH 无匹配结果 无法识别
8 ERROR_RECOGNIZER_BUSY 识别器忙 重复调用
9 ERROR_INSUFFICIENT_PERMISSIONS 权限不足 未授权

6.2 错误处理流程

java 复制代码
@Override
public void onError(int error) {
    isListening = false;
    channel.invokeMethod("speech.onSpeechAvailability", false);
    channel.invokeMethod("speech.onError", error);
}

错误发生时的处理:

  1. isListening设为false
  2. 通知Dart层引擎不可用
  3. 传递错误码给Dart层

6.3 OpenHarmony的错误处理对比

typescript 复制代码
onError(sessionId: string, errorCode: number, errorMessage: string): void {
  console.error(TAG, `onError: code=${errorCode}, message=${errorMessage}`);
  plugin.isListening = false;
  channel?.invokeMethod('speech.onSpeechAvailability', false);
  channel?.invokeMethod('speech.onError', errorCode);
}

处理逻辑完全一致。OpenHarmony额外提供了errorMessage字符串,但当前Dart层的errorHandlerVoidCallback类型,无法接收这个信息。

七、从Android实现中学到的经验

7.1 设计模式总结

通过分析Android端的实现,我总结了几个值得借鉴的设计模式

  1. 状态标志位 :用isListening布尔值管理监听状态,简单有效
  2. 结果缓存 :用lastTranscription缓存最后一次识别结果,避免丢失
  3. 防重入:在开始新的监听前,先取消之前的监听
  4. 优雅降级:权限拒绝时返回明确的错误码,而不是崩溃

7.2 适配OpenHarmony时的注意事项

注意事项 Android做法 OpenHarmony需要调整
引擎创建 同步创建 改为异步(await)
语言设置 startListening时传入 createEngine时指定
音频参数 系统自动处理 需要手动配置AudioInfo
VAD控制 系统默认 可通过extraParams配置
会话管理 无需sessionId 需要传入sessionId

7.3 代码复用策略

虽然不能直接复用Android的Java代码,但逻辑结构可以完全复用

复制代码
Android逻辑                    OpenHarmony逻辑
──────────                    ────────────────
创建SpeechRecognizer    →     创建SpeechRecognitionEngine
设置RecognitionListener →     调用setListener
构建Intent参数          →     构建StartParams
startListening          →     startListening
stopListening           →     finish
cancel                  →     cancel
destroy                 →     shutdown + 置null

八、完整的方法调用时序图

8.1 正常流程时序

复制代码
Dart层          Android原生层              系统服务
  │                │                        │
  │ activate       │                        │
  │───────────────>│ createSpeechRecognizer  │
  │                │───────────────────────>│
  │                │<───────────────────────│
  │                │ onSpeechAvailability    │
  │<───────────────│ (true)                 │
  │                │                        │
  │ listen         │                        │
  │───────────────>│ startListening(Intent)  │
  │                │───────────────────────>│
  │                │ onReadyForSpeech       │
  │                │<───────────────────────│
  │ onRecognition  │                        │
  │ Started        │                        │
  │<───────────────│                        │
  │                │ onPartialResults       │
  │                │<───────────────────────│
  │ onSpeech       │                        │
  │<───────────────│                        │
  │                │ onResults              │
  │                │<───────────────────────│
  │ onRecognition  │                        │
  │ Complete       │                        │
  │<───────────────│                        │

8.2 错误流程时序

复制代码
Dart层          Android原生层              系统服务
  │                │                        │
  │ listen         │                        │
  │───────────────>│ startListening          │
  │                │───────────────────────>│
  │                │ onError(ERROR_NETWORK)  │
  │                │<───────────────────────│
  │ onSpeech       │                        │
  │ Availability   │                        │
  │ (false)        │                        │
  │<───────────────│                        │
  │ onError        │                        │
  │<───────────────│                        │
  │                │                        │
  │ [errorHandler重新activate]              │

总结

通过深入分析Android端的实现,我们获得了以下关键认知:

  1. 类结构映射:Android的FlutterPlugin/MethodCallHandler/ActivityAware三件套,在OpenHarmony上有完全对应的接口
  2. API差异:SpeechRecognizer和Core Speech Kit的API设计不同,但核心流程一致
  3. 回调简化:Android有10个回调,OpenHarmony只有5个,映射关系清晰
  4. 权限模型:两个平台的权限申请逻辑类似,OpenHarmony的async/await写法更简洁
  5. 资源管理:释放策略完全一致,都是"取消监听→销毁引擎→置空引用"

下一篇我们来看iOS/macOS端的实现,对比三个平台的差异,为OpenHarmony适配建立更全面的参考。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

相关推荐
无巧不成书02182 小时前
【RN鸿蒙教学|第3课时】集成网络请求能力(原生API+Axios)+ 数据解析与列表渲染
react native·华为·开源·harmonyos
键盘鼓手苏苏2 小时前
Flutter for OpenHarmony 实战:HTTP Parser — 协议解析的精密仪器
网络协议·flutter·http
哈__2 小时前
基础入门 Flutter for OpenHarmony:geolocator GPS定位详解
flutter
无巧不成书02182 小时前
【RN鸿蒙开发|Day1】0基础搭建React Native兼容开源鸿蒙环境(多终端验证+全踩坑指南)
react native·华为·开源·harmonyos
哈__2 小时前
基础入门 Flutter for OpenHarmony:package_info_plus 应用信息获取详解
flutter
恋猫de小郭11 小时前
Flutter 正在计划提供 Packaged AI Assets 的支持,让你的包/插件可以更好被 AI 理解和选择
android·前端·flutter
无巧不成书021811 小时前
【Harmonyos】Flutter开源鸿蒙跨平台训练营 Day19 分布式架构开发指南
flutter·开源·harmonyos
钛态13 小时前
Flutter for OpenHarmony 实战:flex_color_scheme 打造极致鸿蒙美学 UI
flutter·ui·harmonyos
AD钙奶-lalala14 小时前
Android编译C++代码步骤详解
android·开发语言·c++