说起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 示例代码使用截图展示。我这个人有点懒,没有写完的内容我之后补上。