news 2026/10/18 1:33:39

环信em_chat_uikit(Flutter)适配鸿蒙

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
环信em_chat_uikit(Flutter)适配鸿蒙

环信即时通讯 IM 为开发者提供高可靠、低时延、高并发、安全、全球化的通信云服务,帮助开发者快速构建端到端通信的场景。环信提供 SDK 和 RESTful API,支持同时在线人数无上限,聊天室亿级消息并发,全球平均延时小于 200 毫秒,相同区域平均延时小于 100 毫秒。

  • 支持单聊、群聊、聊天室服务;
  • 提供服务端 RESTful API 和 回调服务;
  • 提供多平台 SDK,包括 Android、iOS、Web、HarmonyOS、Windows、Linux、Unity、Flutter、React Native、小程序、uni-app 和 Electron;
  • 提供 Demo 和 UIKit。

如果你的Flutter项目想运行在鸿蒙端而且你还使用了 uikit 层面的依赖库,那么还是需要做一些兼容和割舍的。「博主并不建议你这么操作,毕竟官方并不提供支持」,接下来就让博主带着这些刚需的朋友们来看一下该如何使用环信em_chat_uikit运行到鸿蒙端。

配置鸿蒙环境

参照官方说明配置环境依赖

Flutter版本调整为支持鸿蒙的版本3.22.0-ohos,目前我使用的是3.22.1-ohos-1.0.4也是可以的

environment: sdk: '3.4.0' flutter: "3.22.1-ohos-1.0.4"

依赖调整

由于涉及到ui层面的修改,因此需要本地依赖em_chat_uikit,添加im sdk插件

dependencies: flutter: sdk: flutter ... em_chat_uikit: path: ../em_chat_uikit-2.2.0 im_flutter_sdk_ohos: git: url: "https://github.com/easemob/im_flutter_sdk_oh.git" ref: 1.5.3

替换所有需要兼容鸿蒙的第三方flutter库

dependency_overrides: im_flutter_sdk: 4.13.0 im_flutter_sdk_ios: 4.13.0 im_flutter_sdk_android: 4.13.0 im_flutter_sdk_interface: 4.13.0 chat_uikit_keyboard_panel: path: ../chat_uikit_keyboard_panel record: git: url: "https://gitcode.com/openharmony-sig/fluttertpc_record.git" path: "record" ref: "d40e26bd4052362d505ef8c2c600ac69aa5a967a" record_platform_interface: git: url: "https://gitcode.com/openharmony-sig/fluttertpc_record.git" path: "record_platform_interface" ref: "d40e26bd4052362d505ef8c2c600ac69aa5a967a" shared_preferences: git: url: "https://gitcode.com/openharmony-sig/flutter_packages.git" path: "packages/shared_preferences/shared_preferences" path_provider: git: url: "https://gitcode.com/openharmony-sig/flutter_packages.git" path: "packages/path_provider/path_provider" file_picker: git: url: "https://gitcode.com/openharmony-sig/fluttertpc_file_picker.git" ref: "br_v8.0.7_ohos" image_picker: git: url: "https://gitcode.com/openharmony-sig/flutter_packages.git" path: "packages/image_picker/image_picker" audioplayers: git: url: "https://gitcode.com/openharmony-sig/flutter_audioplayers.git" path: "packages/audioplayers" video_compress: git: url: "https://gitcode.com/openharmony-sig/fluttertpc_video_compress.git" video_player: git: url: "https://gitcode.com/openharmony-sig/flutter_packages.git" path: "packages/video_player/video_player" flutter_localization: git: url: "https://gitcode.com/openharmony-sig/flutter_localization.git" sqflite: git: url: "https://gitcode.com/OpenHarmony-SIG/flutter_sqflite.git" ref: 'github.com/tekartik/sqflite.git/v2.3.3+1' path: 'sqflite'

注意:chat_uikit_keyboard_panel插件需要额外兼容鸿蒙,目前仅在本地做修改,需要插件的可以私信博主,或者参照我之前的文章自行编写。

代码调整

检索im_flutter_sdk_oh插件中所有调用noSupport实现的方法,将em_chat_uikit中所有使用到的地方进行调整,或隐藏、或替换、或修改、或删除,例如:聊天页面获取子区方法

// 修改前 ChatThread? threadOverView = await msg.chatThread(); MessagePinInfo? pinInfo = await msg.pinInfo(); modelLists.add( MessageModel( message: msg, reactions: reactions, thread: threadOverView, pinInfo: pinInfo, ), ); // 修改后 MessagePinInfo? pinInfo = await msg.pinInfo(); modelLists.add( MessageModel( message: msg, reactions: reactions, thread: null, pinInfo: pinInfo, ), );

以下做修改记录(如有缺失请继续补充):

  • 子区功能调整

隐藏子区

ChatUIKitSettings.enableMessageThread = false;
  • 翻译功能调整

translateMessage以及fetchSupportedLanguages均未实现,因此翻译目标语言不支持

// 隐藏消息菜单 ChatUIKitSettings.msgItemLongPressActions .remove(ChatUIKitActionType.translate);
  • 举报功能调整
// 隐藏举报菜单 ChatUIKitSettings.msgItemLongPressActions .remove(ChatUIKitActionType.report);
  • ChatUIKitPopupMenu溢出适配
// 1.移除Container的vertical padding - 将Container改为SizedBox,去除了上下各4像素的padding,这样释放了8像素的可用空间 // 2.减小图标和文本之间的间隔 - 将SizedBox(height: 4)改为SizedBox(height: 2),进一步减少2像素 @override Widget build(BuildContext context) { ... Widget content = Wrap( direction: Axis.horizontal, alignment: WrapAlignment.start, children: widget.actions.map((item) { return InkWell( onTap: () { widget.close?.call(); item.onTap?.call(); }, child: SizedBox( width: itemWidth, height: itemHeight, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ if (item.icon != null) SizedBox( height: 28, width: 28, child: item.icon!, ), const SizedBox(height: 2), Text( item.label, maxLines: 1, overflow: TextOverflow.ellipsis, textScaler: TextScaler.noScaling, style: TextStyle( color: widget.style.foregroundColor, fontSize: 12, fontWeight: FontWeight.w500, ), ), ], ), ), ); }).toList(), ); ... return content; } }

注意:在调用sdk的api实现自己的功能时,需要确认一下sdk是否真正的实现了该api而不是nosupport

权限调整

  • 添加麦克风权限

在项目目录/ohos/entry/src/main/module.json5中新增麦克风权限

{ "module": { ... "requestPermissions": [ { "name": "ohos.permission.MICROPHONE", "reason": "$string:reason", "usedScene": { "when": "always", "abilities": [ "EntryAbility" ] } } ] } }

在项目目录/ohos/AppScope/resources/base/element/string.json中添加reason描述

{ "string": [ ... { "name": "reason", "value": "录制音频,音视频通话等需要麦克风权限" } ] }
  • 处理异常问题

目前record_bar中对于获取用户麦克风权限失败后直接抛出异常,最好是给用户一个简单的提示,因此做以下修改

Future<void> startRecording() async { if (await record.hasPermission()) { if (await record.isRecording()) { return; } try { fileName = "${DateTime.now().millisecondsSinceEpoch.toString()}.$extensionName"; record.start(recordConfig, path: "${_directory!.path}/$fileName"); _state?.switchRecordType(RecordBarRecordType.recording); } catch (e) { throw RecordError(recordFailed, 'Failed to start recording'); } } else { // 发送麦克风权限未授权事件 ChatUIKit.instance .sendChatUIKitEvent(ChatUIKitEvent.noMicrophonePermission); // 显示提示对话框 _showPermissionDeniedDialog(); } } void _showPermissionDeniedDialog() { final context = _state?.context; if (context == null || !context.mounted) return; showChatUIKitDialog( context: context, title: ChatUIKitLocal.microphonePermissionDeniedTitle.localString(context), content: ChatUIKitLocal.microphonePermissionDeniedContent.localString(context), actionItems: [ ChatUIKitDialogAction.confirm( label: ChatUIKitLocal.confirm.localString(context), onTap: () { Navigator.of(context).pop(); }, ), ], ); }

chat_uikit_localizayions.dart中添加国际化字符串

microphonePermissionDeniedTitle: '麦克风权限未授权', microphonePermissionDeniedContent: '需要麦克风权限才能录制语音消息,请前往设备设置中开启当前应用的麦克风权限。', microphonePermissionDeniedTitle: 'Microphone Permission Denied', microphonePermissionDeniedContent: 'Microphone permission is required to record voice messages. Please enable it in Settings.',

其他问题

  • 如果项目中有使用到open_file插件,需要更换成open_filex,因为查看open_file源码发现它并没有对鸿蒙平台做兼容
// 嗯,可能是BUG吧 class OpenFile { static const MethodChannel _channel = const MethodChannel('open_file'); OpenFile._(); ///[filePath] On web you need to pass the file name to determine the file type ///[linuxDesktopName] like 'xdg'/'gnome' static Future<OpenResult> open(String? filePath, {String? type, String? uti, String linuxDesktopName = "xdg", bool linuxUseGio = false, bool linuxByProcess = false, Uint8List? webData}) async { assert(filePath != null); assert(linuxUseGio != false || linuxByProcess != false, "can't have both linuxUseGio and linuxByProcess"); if (!Platform.isMacOS && !Platform.isIOS && !Platform.isAndroid) { int _result; var _windowsResult; if (Platform.isLinux) { var filePathLinux = Uri.file(filePath!); if (linuxByProcess) { _result = Process.runSync('xdg-open', [filePathLinux.toString()]).exitCode; } else if (linuxUseGio) { _result = linux.system(['gio', 'open', filePathLinux.toString()]); } else { _result = linux .system(['$linuxDesktopName-open', filePathLinux.toString()]); } } else if (Platform.isWindows) { _windowsResult = windows.shellExecute('open', filePath!); _result = _windowsResult <= 32 ? 1 : 0; } else { _result = -1; } return OpenResult( type: _result == 0 ? ResultType.done : ResultType.error, message: _result == 0 ? "done" : _result == -1 ? "This operating system is not currently supported" : "there are some errors when open $filePath${Platform.isWindows ? " HINSTANCE=$_windowsResult" : ""}"); } Map<String, String?> map = { "file_path": filePath!, "type": type, "uti": uti, }; final _result = await _channel.invokeMethod('open_file', map); final resultMap = json.decode(_result) as Map<String, dynamic>; return OpenResult.fromJson(resultMap); } }

在使用open_filex打开文件是需要对下载的文件路径做编码处理,因为我们的appkey是包含#字符的,直接访问会查找不到文件,这不禁让我想起了刚开始接触鸿蒙时安装ide的路径不能包含中文「嗯是国产没错了」

String filePath = "com.example.chat_uikit_harmony$path"; Uri fileUri = Uri.file(filePath); final result = await OpenFilex.open(fileUri.toString()); debugPrint('result: ${result.toString()}');
  • Reaction添加或者移除页面未更新

在消息列表页面添加更新操作

/// 消息列表控制器 class MessagesViewController extends ChangeNotifier with SafeAreaDisposed, ChatObserver, MessageObserver, ThreadObserver { ... Future<void> updateReaction( String messageId, String reaction, bool isAdd, ) async { try { ... // 操作成功后立即刷新本地 UI await refreshMessageReaction(messageId); } catch (e) { chatPrint('updateReaction: $e'); } } Future<void> refreshMessageReaction(String messageId) async { final index = msgModelList .indexWhere((element) => element.message.msgId == messageId); if (index != -1) { Message? msg = await ChatUIKit.instance.loadMessage(messageId: messageId); if (msg != null) { List<MessageReaction>? reactions = await msg.reactionList(); msgModelList[index] = msgModelList[index].copyWith( message: msg, reactions: reactions, ); lastActionType = MessageLastActionType.originalPosition; refresh(); } } } ... }

在ReactionInfo添加onReactionChanged回调

/// Reaction页面 /// 添加onReactionChanged回调 class ChatUIKitMessageReactionInfo extends StatefulWidget { const ChatUIKitMessageReactionInfo( this.model, { this.onReactionChanged, super.key, }); final MessageModel model; /// 当 reaction 发生变化时的回调(添加或删除) final VoidCallback? onReactionChanged; @override State<ChatUIKitMessageReactionInfo> createState() => _ChatUIKitMessageReactionInfoState(); } class _ChatUIKitMessageReactionInfoState extends State<ChatUIKitMessageReactionInfo> with SingleTickerProviderStateMixin, ChatUIKitThemeMixin { ... @override Widget themeBuilder(BuildContext context, ChatUIKitTheme theme) { return Column( children: [ Container( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), height: 28, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: reactions.length, itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: InkWell( highlightColor: Colors.transparent, splashColor: Colors.transparent, onTap: () { tabController.animateTo(index); }, child: ChatUIkitReactionWidget( reactions[index], highlightColor: Colors.transparent, highlightTextColor: theme.color.isDark ? theme.color.neutralColor95 : theme.color.neutralColor3, bgColor: selectIndex == index ? (theme.color.isDark ? theme.color.neutralColor3 : theme.color.neutralColor9) : Colors.transparent, ), ), ); }, ), ), Expanded( child: TabBarView( controller: tabController, children: reactions .map( (e) => ChatReactionInfoWidget( msgId: messageID, reaction: e, onReactionDeleteTap: () { onReactionDeleteTap(e); // 通知消息列表页面刷新 widget.onReactionChanged?.call(); }, ), ) .toList(), ), ), ], ); } ... }

在消息页面处理reaction变化逻辑

/// 消息页面 class MessagesView extends StatefulWidget { ... @override State<MessagesView> createState() => _MessagesViewState(); } class _MessagesViewState extends State<MessagesView> with ChatObserver, ChatUIKitThemeMixin { ... void showReactionsInfo(BuildContext context, MessageModel model) { showChatUIKitBottomSheet( context: context, showCancel: false, body: ChatUIKitMessageReactionInfo( model, onReactionChanged: () { // 刷新消息列表中的 reaction 显示 controller.refreshMessageReaction(model.message.msgId); }, ), ); } ... }

以上我们基本上就完成了 Flutter 向鸿蒙端的整体适配,如有其他问题也可以私信博主进行讨论。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 10:23:29

LobeChat能否预订门票?智能服务再升级

LobeChat能否预订门票&#xff1f;智能服务再升级 在AI助手逐渐渗透日常生活的今天&#xff0c;我们已经不再满足于“问一句答一句”的简单交互。当用户对聊天机器人说“帮我订张周杰伦演唱会的票”&#xff0c;期望得到的不再是“我无法执行此操作”的冰冷回复&#xff0c;而是…

作者头像 李华
网站建设 2026/10/17 16:27:02

火山引擎AI大模型图像描述生成后交由Anything-LLM组织报告

火山引擎AI大模型图像描述生成后交由Anything-LLM组织报告 在企业智能化转型的浪潮中&#xff0c;一个日益突出的问题浮出水面&#xff1a;AI看得见世界&#xff0c;却记不住它说过的话。 当视觉模型从一张会议照片中精准识别出“两人正在审阅合同”&#xff0c;几天后用户再问…

作者头像 李华
网站建设 2026/10/18 4:58:13

权限设计陷进频发?,一文读懂 Dify 混合检索场景下的安全边界控制

第一章&#xff1a;权限设计陷进频发&#xff1f;一文读懂 Dify 混合检索场景下的安全边界控制在构建基于 Dify 的混合检索系统时&#xff0c;权限边界模糊常引发数据越权访问问题。尤其是在多租户、多角色协作的场景下&#xff0c;若未对检索请求链路实施细粒度控制&#xff0…

作者头像 李华
网站建设 2026/10/17 16:22:33

Dify工作流并行执行陷阱:90%开发者忽略的3个性能瓶颈

第一章&#xff1a;Dify工作流并行执行的核心机制Dify 工作流引擎通过任务图&#xff08;Task Graph&#xff09;与运行时调度器的协同&#xff0c;实现了高效的并行执行能力。其核心在于将工作流中的各个节点解析为可独立运行的任务单元&#xff0c;并依据依赖关系动态调度执行…

作者头像 李华
网站建设 2026/10/17 17:37:58

LobeChat能否支持虚拟试衣?服装搭配AI推荐引擎

LobeChat能否支持虚拟试衣&#xff1f;服装搭配AI推荐引擎 在电商直播和社交种草盛行的今天&#xff0c;用户已经不再满足于“看看图、点点购”的购物方式。他们更希望获得一种接近线下门店的沉浸式体验——比如上传一张自己的上衣照片&#xff0c;立刻得到&#xff1a;“这件…

作者头像 李华