环信em_chat_uikit(Flutter)适配鸿蒙
环信IM为开发者提供高可靠、低延时的即时通讯服务,支持多平台开发。本文介绍了如何将Flutter项目适配鸿蒙系统,重点包括:1) 调整Flutter版本至支持鸿蒙的3.22.0-ohos;2) 替换第三方库为鸿蒙兼容版本;3) 代码调整方案,包括隐藏子区功能、移除翻译和举报菜单、优化弹出菜单布局等。需要注意的是,官方并不完全支持此操作,开发者需自行处理兼容性问题。通过以上配置和修改,可实现环信Ch
环信即时通讯 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 向鸿蒙端的整体适配,如有其他问题也可以私信博主进行讨论。
更多推荐




所有评论(0)