说起IM在原生平台就头大,更别说在flutter平台了。自己做IM工程量大,不划算。集成三方,又要背书查文档。
很遗憾到目前为止,还没有哪个平台提供一套完善的Flutter平台IMSDK。幸运的是环信有一套demo可供学习,功能也不完善。碰巧我的工作的项目需要使用IM,而我也不会写安卓,于是硬着头皮自己摸索了一下,记一下心得。那些需要写双端,又不擅长另一门语言的可以参考以下思路。
需求:
两端UI一致,能扩展自定义消息
思路:
1、在Flutter端实现UI,保证UI统一,定制基本消息模板,图片,音频,视频,和自定义消息的UI
2、通过与原生的交互通信,做好桥接,调用SDK的Api,并完成回调。聊天通讯、数据存储、都交给SDK自动完成,我们需要做一个中间工具管理,发送和获取到数据。
3、基本聊天实现思路
准备工作:
集成IOS/Android端IM SDK
IOS/ Android: 桥接文件,IM manager单利
实现基本功能:登录/登出、写入会话信息、移除会话信息、开启消息监听、关闭消息监听、发送消息
Flutter:桥接文件,UI实现,部分数据对象模型
会话列表,聊天页面,聊天列表等UI
技能库:
1、Flutter与原生双向交互
2、IOS 和 Android,能基本看懂IM官方文档,能集成SDK到双端,能参考文档写基本逻辑代码
3、Flutter UI 实现能力,和 耐心
4、flutter库:photo_view 大图查看;flutter_plugin_record:声音录制; cached_network_image:网络图片加载; image_picker:图片选择;audioplayers:语音播放;
注意:
我自己项目上使用的是网易云信,而且项目不是一个标准的IM项目。思路简单,唯一需要和原生交互的SDK就是IM自己的SDK,剩下就是需要一条一条去逐步实现各个SDK功能。可以根据项目需要去完成,比如我自己的项目,就不需要视频发送,我就没必要去实现视频发送的功能。
IOS 样例:
@interface IMMessageTools : NSObject
+(instancetype)ShareInstance;
-(BOOL)setSession:(NSString*)session msgType:(NSInteger)type;
-(void)removeSession;
-(nullableNSArray*)getHistoryMessage:(nullableNIMMessage*)message;
-(NSDictionary *)sendMessage:(NSDictionary *)messageDict;
-(void)didRecvMessages:(NSArray*)messages sink:(FlutterEventSink)sink;
@end
@interface IMMessageTools ()
@property (nonatomic) NIMSession *session;
@property (nonatomic, strong) NSString *sessionId;
@end
@implementation IMMessageTools
static IMMessageTools *_instance;
+(instancetype)ShareInstance{
if(!_instance){
_instance= [[IMMessageTools alloc]init];
}
return _instance;
}-(BOOL)setSession:(NSString*)session msgType:(NSInteger)type{
_sessionId= session;
_session= [NIMSessionsession:session type:type];
[NIMSDK.sharedSDK.conversationManager markAllMessagesReadInSession:_session];
return YES;
}-(void)removeSession{
[NIMSDK.sharedSDK.conversationManager markAllMessagesReadInSession:_session];
_sessionId = nil;
_session=nil;
}-(NSDictionary *)sendMessage:(NSDictionary *)messageDict{
NSLog(@"%@",messageDict);
// 构造出具体消息
NIMMessage*message = [[NIMMessage alloc]init];
message.text=@"";
if(messageDict[@"msg"]){
message.text= messageDict[@"msg"];
}
// 图片
if([messageDict[@"type"] isEqual:@"1"]){
NIMImageObject *object = [[NIMImageObject alloc] initWithFilepath:messageDict[@"path"]];
message.messageObject= object;
}
// 语音
if( [messageDict[@"type"]isEqual:@"2"]){
NIMAudioObject*object = [[NIMAudioObject alloc]initWithSourcePath:messageDict[@"path"]];
message.messageObject= object;
}
// 自定义/扩展消息
if( [messageDict[@"type"]isEqual:@"100"]){
if(messageDict[@"ext"]){
AttachmentExt*extMsg = [[AttachmentExt alloc]init];
extMsg.extData= messageDict[@"ext"];
NIMCustomObject*object = [[NIMCustomObject alloc]init];
object.attachment= extMsg;
message.messageObject= object;
}
}
NIMSession*temp =_session;
if(messageDict[@"toSession"]){
NSInteger type = [messageDict[@"roomType"]integerValue];
temp = [NIMSession session:messageDict[@"toSession"]type:type];
}
// 错误反馈对象
NSError*error =nil;
BOOL success =[[NIMSDK sharedSDK].chatManager sendMessage:message toSession:temp error:&error];
if(success){
NSDictionary*dict = [self IMMessageObjToJson:message];
return dict;
}
return@{};
}-(nullableNSArray*)getHistoryMessage:(NIMMessage*)message{
NSArray<NIMMessage *> *messages = [[[NIMSDK sharedSDK] conversationManager] messagesInSession:_session message:message limit:30];
if(messages){
NSMutableArray *array = [NSMutableArray array];
for(NIMMessage*object in messages) {
NSDictionary*dict = [self IMMessageObjToJson:object];
[array addObject:dict];
}
return array;
}
return@[];
}-(NSDictionary*)IMMessageObjToJson:(NIMMessage*)msgObject{
NSMutableDictionary*dict =@{@"messageType":@(msgObject.messageType),
@"from": msgObject.from,
@"messageId": msgObject.messageId,
@"text": [self safeValue:msgObject.text],
@"isOutgoingMsg":@(msgObject.isOutgoingMsg),
@"timestamp":@((long)msgObject.timestamp*1000),
@"deliveryState":@(msgObject.deliveryState),
@"isRemoteRead":@(msgObject.isRemoteRead),
@"nickName": [self getNickName:msgObject.from],
@"avatarUrl": [self getAvatarUrl:msgObject.from],
}.mutableCopy;
///TODO
if(msgObject.messageObject){
if(msgObject.messageObject.type == NIMMessageTypeImage){
NIMImageObject*imageMsg = (NIMImageObject*)msgObject.messageObject;
[dict setValue:imageMsg.url forKey:@"url"];
}
if(msgObject.messageObject.type == NIMMessageTypeAudio){
NIMAudioObject*customMsg = (NIMAudioObject*)msgObject.messageObject;
[dict setValue:customMsg.url forKey:@"url"];
[dict setValue:customMsg.path forKey:@"localPath"];
[dict setValue:@(customMsg.duration) forKey:@"duration"];
}
if(msgObject.messageObject.type == NIMMessageTypeCustom){
NIMCustomObject*customMsg = (NIMCustomObject*)msgObject.messageObject;
AttachmentExt*extMsg = customMsg.attachment;
[dict setValue:extMsg.extData forKey:@"messageExt"];
}
if(msgObject.messageObject.type == NIMMessageTypeNotification){
NIMNotificationObject *notMsg = (NIMNotificationObject *)msgObject.messageObject;
if(notMsg.notificationType == NIMNotificationTypeTeam){
NIMTeamNotificationContent *content = (NIMTeamNotificationContent *)notMsg.content;
NSString*value =@"";
if(content.operationType == NIMTeamOperationTypeInvite){
value =@"邀请成员";
}else if (content.operationType == NIMTeamOperationTypeDismiss){
value =@"解散群聊";
}else if(content.operationType == NIMTeamOperationTypeUpdate){
value =@"群信息更新";
}else if(content.operationType == NIMTeamOperationTypeInvite){
value =@"新成员入群";
}else{
value =@"";
}
[dict setValue:value forKey:@"text"];
}
}
}
if(msgObject.senderName){
[dict setValue:msgObject.senderName forKey:@"senderName"];
}
return dict;
}-(void)didRecvMessages:(NSArray*)messages sink:(FlutterEventSink)sink{
if(_session){
NSMutableArray *array = [NSMutableArray array];
for(NIMMessage*object in messages) {
if([object.session.sessionId isEqualToString:self.sessionId]){
NSDictionary*dict = [self IMMessageObjToJson:object];
[array addObject:dict];
}
}
sink(@{@"type":@2,@"msgDicts":array});
}
}/// 工具方法
-(NSString*)safeValue:(id)value{
if(!value || [value isEqual:[NSNull null]]){
return @"";
}
return value;
}-(NSString*)getNickName:(NSString*)userId{
NIMUser*user = [[NIMSDK sharedSDK].userManager userInfo:userId];
if(user){
returnuser.userInfo.nickName;
}
return@"";
}-(NSString*)getAvatarUrl:(NSString*)userId{
NIMUser*user = [[NIMSDK sharedSDK].userManager userInfo:userId];
if(user){
NSString*avatarUrl = user.userInfo.avatarUrl;
if(avatarUrl){
avatarUrl = [avatarUrl stringByReplacingOccurrencesOfString:@"https" withString:@"http"];
}
return avatarUrl;
}
return @"";
}
聊天内容搜索
Android 样例:
心得:
思路展示,这是一个漫长的过程,需要调完ios又调Android。说说遇到的坑吧.
1、在我的使用的这个SDK里面Android和ios聊天数据不是完全一样的,比如:时间,ios端是doubel 秒,Android是整型毫秒,需要提前转化统一数据格式。
2、文件传输,图片,音频等。。。通过path读取的方式上传,再发送。如果SDK支持Path直接交给SDK,如果SDK不支持,就自己上传再把拿到Url给SDK发送
3、不知道是不是这个SDK特有的坑,修改头像,存入”http://“图片地址 拿到的是 ”https://“,导致图片不能访问。用”http“的小伙伴要小心。
ps:实在不知道怎么调整代码格式,各位将就着看,Android 示例代码使用截图展示。我这个人有点懒,没有写完的内容我之后补上。
网友评论