当前位置: 首页 > news >正文

环信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 向鸿蒙端的整体适配,如有其他问题也可以私信博主进行讨论。

http://www.jsqmd.com/news/100515/

相关文章:

  • 大连艾森诺铸件浇道击断器,铸造降本增效核心装备​
  • 10 个专科生开题报告工具,AI 写作降重软件推荐
  • 【扫盲】什么是API
  • Dify相关性评估技术深度解析(企业级搜索优化必备)
  • 【金融风险分析必备技能】:掌握R语言相关性矩阵的5大核心应用
  • 生物信息分析高手私藏代码(R语言代谢组完整流程大公开)
  • 提升GAN可控性:精确操控合成图像的属性
  • 详细介绍:基于YOLOv5-AUX的棕熊目标检测与识别系统实现
  • 简单大数据分析测试
  • IDEA/pycharm快捷键
  • 状态丢失问题
  • 【权威指南】Dify集成Tesseract 5.3语言包的7个关键步骤
  • 【Agent工具调用Dify参数校验全解析】:掌握高效接口验证的5大核心策略
  • 2025年底,我们用什么框架来开发智能体?
  • R语言处理临床数据缺失值的7种武器(附真实病例数据代码实战)
  • 美国降息,日本加息,为何让币圈交易员紧张不安?
  • 在算家云搭建Linly-Talker数字人语音模型
  • 10 个继续教育课堂汇报工具,AI 工具推荐与对比总结
  • EmotiVoice开源TTS引擎使用教程
  • LobeChat能否支持GraphQL查询?接口灵活性分析
  • python笔记-模块
  • 【R Shiny性能飞跃秘诀】:3步实现多模态内容按需加载,节省70%内存开销
  • 为什么90%的多模态Agent集成失败都源于启动顺序?真相在这里
  • 基于SpringBoot+Vue的电影院管理系统设计与实现开题报告
  • MySQL Shell 使用方法
  • LobeChat能否播报新闻?每日资讯自动推送
  • Dify 1.7.0音频质量检测黑科技(行业首个支持多语种自适应评估)
  • 为什么你的量子模拟无法扩展?R语言多qubit架构陷阱全揭示
  • VSCode远程开发连接云端Anything-LLM进行低延迟交互
  • 【赵渝强老师】Oracle的体系架构