跳转至

环信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 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 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 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 updateReaction(

String messageId,

String reaction,

bool isAdd,

) async {

try {

...

// 操作成功后立即刷新本地 UI

await refreshMessageReaction(messageId);

} catch (e) {

chatPrint(

'updateReaction: $e'

);

}

}

Future 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? 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 createState() =>

_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 createState() => _MessagesViewState();

}

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集成, 跨平台适配