环信Flutter UIKit鸿蒙适配实战:从环境配置到UI兼容全解析
原文地址: https://88box.top 生成时间: 2026-05-19 16:32:53
环信Flutter UIKit适配鸿蒙实战指南 - hey99 知识搜索引擎
精选文章
环信Flutter UIKit适配鸿蒙实战指南
随着鸿蒙 NEXT 版本逐步推进,越来越多的 Flutter 开发者希望将现有 IM 应用移植到鸿蒙生态。如果你已经在一个 Flutter 项目中深度使用了 环信UIKit,并且希望快速让它“跑”在鸿
更新于 2026-05-19 08:17
前端
随着鸿蒙 NEXT 版本逐步推进,越来越多的 Flutter 开发者希望将现有 IM 应用移植到鸿蒙生态。如果你已经在一个 Flutter 项目中深度使用了 环信UIKit,并且希望快速让它“跑”在鸿蒙设备上,直接替换 SDK 往往不够——你可能需要处理大量 UI 层面的兼容性问题,包括第三方插件替换、不支持 API 的降级、权限适配等。
本文将通过一份完整的修改记录,带你一步步完成 环信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插件
(flutter uikit 地址:
github.com/easemob/eas…
)
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
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
{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
"file_path"
: filePath!,
"type"
:
type
,
"uti"
: uti,
};
final _result = await _channel.invokeMethod(
'open_file'
, map);
final resultMap = json.decode(_result) as Map
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
String messageId,
String reaction,
bool isAdd,
) async {
try {
...
// 操作成功后立即刷新本地 UI
await refreshMessageReaction(messageId);
} catch (e) {
chatPrint(
'updateReaction: $e'
);
}
}
Future
final index = msgModelList
.indexWhere((element) => element.message.msgId == messageId);
if (index != -
1
) {
Message? msg = await ChatUIKit.instance.loadMessage(messageId: messageId);
if (msg != null) {
List
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
_ChatUIKitMessageReactionInfoState();
}
class _ChatUIKitMessageReactionInfoState
extends State
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
}
class _MessagesViewState extends State
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 向鸿蒙端的整体适配,如有其他问题也可以联系环信技术支持
查看原文
🏷 标签: Flutter, 鸿蒙NEXT, 环信UIKit, IM集成, 跨平台适配