likes
comments
collection
share

如何使用Flutter封装即时通讯IM框架开发插件

作者站长头像
站长
· 阅读数 4

Flutter自去年12月发布1.0版后就引起了大量开发者的关注,个人觉得它最大特点应该是能够在跨平台的情况下保持较好的用户体验,相比React和Weex来说它更接近原生的体验。并且dart代码要比原生的iOS代码和Java代码来说简单的多,但dart也有很多坑。综上,我觉得Flutter应该是可预见的移动端未来的一项热门技术。对于创业公司来说Flutter绝对是一个很诱人的技术,理想情况下:公司只需要一个会写Flutter的程序就能写出跨平台的App,减少了多端开发成本,但这只是理想情况:“理想很美好,现实很骨感。”Flutter的生态还不算很好,很多必备的功能都不完善,甚至是没有。例如没有一个很好的播放器,原生的播放器功能太少了,连快进都没有,就连播放器的创建和销毁都很奇怪。常用的即时通讯功能一个框架也没有,各个IM公司也没给出Flutter版本的框架,而在社交如此流行的如今,很多很多很多...的App中IM功能已经成为了必备的功能之一。其实我最开始的打算是等那些大公司开发Flutter版本,如腾讯、融云、环信,以为他们会很快推出IM框架。而然。。。。。这也是我为什么写这篇文章的原因了。

在开发IM插件之前,要做的第一件事是选择一款“便宜又好用”的IM框架进行封装。那么选哪家的比较合适呢???当然是性价比最高的最合适,在了解Mob、Bomb、融云、环信、阿里、野马、腾讯、网易等产品后,最终选择LeanCloud。其实最划算的应该是Mob,毕竟对开发者完全免费,但今年突然宣布下架该功能。其次是Bomb,但是Bomb的客户端代码写的不太友好,但对于码农来说,这都没啥。最主要的一个原因是Bomb IM框架的UI实在是太难看了,基本的语音上传图片等功能都没优化,改UI?不能可能的!最后只能放弃Bomb,选择LeanCloud。LeanCloud对于个人开发者和初创公司来说还是挺好的,有一定的免费额度,这里不做介绍了,都懂得。下面进入文章的主题:如何使用Flutter封装IM框架开发插件。下面先给我我项目中的IM的界面:

如何使用Flutter封装即时通讯IM框架开发插件 如何使用Flutter封装即时通讯IM框架开发插件 其中第一个界面是dart写的,第二个界面是原生的界面

由于我本来是一个iOSer,因此本文我只对iOS的封装进行详细讲解,Andorid方面只能是业余封装,但Andorid上其实有几个大坑,最后再说。本文通过FLutter封装的是IM功能主要有两个,第一个是获取聊天列表,第二个是一对一的单聊。这两个界面基本满足一对一聊天的场景。先给出单聊中使用的dart代码:

    //第一步注册
    FlutterLcIm.register("appId", "appKey");
    //第二步用户登录
    FlutterLcIm.login("当前用户的userId");
    //第三步配置用户体系
    Map user = {'name':'jason1','user_id':"1",'avatar_url':"http://thirdqq.qlogo.cn/g?b=oidb&k=h22EA0NsicnjEqG4OEcqKyg&s=100"};
    Map peer = {'name':'jason2','user_id':"3",'avatar_url':"http://thirdqq.qlogo.cn/g?b=oidb&k=h22EA0NsicnjEqG4OEcqKyg&s=100"};
    //第四步跳转到聊天界面
    FlutterLcIm.pushToConversationView(user,peer);

其中第一步和第二步是初始化LeanCloud的IM,连接Lc的服务器。第三步是设置用户体系,user为当前对象的信息,peer是聊天对象的信息,第四步是跳转到聊天界面,跳转过去就是图二了。总体来说,封装以后使用起来还是比较简单的。下面结合流程图分析,为什么需要以上四步:

下面给出LeanCloud的单聊时的流程图,第一步注册AppId和AppKey,同时还需要初始化远程推送UNUserNotificationCenter和聊天时的底部组件如上传图片和地理位置等组件;第二步,通过invokeThisMethodAfterLoginSuccessWithClientId方法注册clientId,clientId为当前用户的Id,如果clientId已经注册过则直接进入登录状态,需要注意的是clientId为NSString类型,如果为int类型程序则会崩溃,所以需要转下字符串。第三步,获取设置用户体系,因为LeanCloud不保存用户的头像和昵称等信息,只保存一个clientId,因此用户需要自己设置用户体系,通过setFetchProfilesBlock对当前聊天用户体系。这里有两种常用的方案:第一种是本地静态设置,第二种是通过Id到服务器上获取对应用户的数据后再设置,显然第二种更符合我们开发的需求。为了更加简单的实现用户体系的设置,对用户体系的设置进行了一层封装,简化了用户体系的逻辑,后面会讲。第四步,通过push到ConverationViewController进行聊天,需要注意的是,在聊天之前一定要设置好用户体系,否则不能进行聊天!

如何使用Flutter封装即时通讯IM框架开发插件 在了解了单聊的逻辑后,要封装单聊的功能其实就变得很简单,下面给出iOS实现的主要代码:
第一步注册app_id和app_key
if ([@"register" isEqualToString:call.method]){
        //设置一个全局变量,重复注册会导致崩溃,只在应用第一次创建初始化
        if (!isRegister) {
            NSString *appId    = call.arguments[@"app_id"];
            NSString *appKey   = call.arguments[@"app_key"];
            
            [self registerConversationWithAppId:appId
                                         appKey:appKey];
            isRegister = true;
        }
    }

//注册
- (void)registerConversationWithAppId:(NSString *)appId
                               appKey:(NSString *)appKey{
    
    NSLog(@"register conversation");
    [self registerForRemoteNotification];
    
    [LCChatKit setAppId:appId appKey:appKey];
    // 启用未读消息
    [AVIMClient setUnreadNotificationEnabled:true];
    [AVIMClient setTimeoutIntervalInSeconds:20];
    // 添加输入框底部插件,如需更换图标标题,可子类化,然后调用 `+registerSubclass`
    [LCCKInputViewPluginTakePhoto registerSubclass];
    [LCCKInputViewPluginPickImage registerSubclass];
    [LCCKInputViewPluginLocation registerSubclass];
   
}
第二步注册登录
if([@"login" isEqualToString:call.method]){
        NSString *userId = call.arguments[@"user_id"];
        [self loginImWithUserId:userId result:result];
    }

- (void)loginImWithUserId:(NSString *)userId result:(FlutterResult)result{
    
    [LCCKUtil showProgressText:@"连接中..." duration:10.0f];
    [LCChatKitHelper invokeThisMethodAfterLoginSuccessWithClientId:userId success:^{
        NSLog(@"login success@");
        [LCCKUtil hideProgress];
        result(nil);
    } failed:^(NSError *error) {
        [LCCKUtil hideProgress];
        NSLog(@"login error");
        [LCCKUtil hideProgress];
        result(@"login error");
    }];
}
第三步设置用户体系并聊天
if ([@"pushToConversationView" isEqualToString:call.method]) {
        [self chatWithUser:call.arguments[@"user"]
                      peer:call.arguments[@"peer"]];
        result(nil);
    }

- (void)chatWithUser:(NSDictionary *)userDic peer:(NSDictionary *)peerDic{
    
    LCCKUser *user = [[LCCKUser alloc] initWithUserId:userDic[@"user_id"] name:userDic[@"name"] avatarURL:userDic[@"avatar_url"]];
    LCCKUser *peer = [[LCCKUser alloc] initWithUserId:peerDic[@"user_id"] name:peerDic[@"name"] avatarURL:peerDic[@"avatar_url"]];
    
    NSMutableArray *users = [NSMutableArray arrayWithCapacity:2];
    [users addObject:user];
    [users addObject:peer];
    
    //通过数据设置用户体系
    [[LCChatKitHelper sharedInstance] lcck_settingWithUsers:users];
    
    //打开聊天界面
    [LCChatKitHelper openConversationViewControllerWithPeerId:peer.userId];
    
}

以上就是单聊功能的封装了。 第二个功能是获取聊天列表,在开发聊天列表时我进行了一些思考,主要考虑是和单聊一样将整体封装UI和逻辑还是将UI和逻辑拆分开,在原生中封装逻辑,在dart中绘制UI,这样的好处是可以定制化UI。最后我选择了第二种,处于两个方面的考虑,第一个方面是如果要整体封装,那需要熟悉两端的代码而这个代码量要比单聊多的多,增加封装的复杂,另一方面是如果使用原生封装UI会使得iOS和Android两端的UI界面显示不一致,特别是Android端的UI做的比较粗糙,同时还不能定制化UI的显示。处于多种考虑,最后采用原生中封装获取数据的逻辑,在dart中定制UI的显示。下面给出dart中数据获取的调用:

  FlutterLcIm.getRecentConversationUsers().then((res) {
    if (res != [] && res != null) {
      //res数组
    }else {
    }
  });

由于绘制的代码量较大,这里不给出如何使用dart绘制出,给出源码地址聊天列表的UI。而在原生中获取聊天列表的数据是由findRecentConversationsWithBlock方法得到的,因此只需封装这个方法即可,如下:

if ([@"getRecentConversationUsers" isEqualToString:call.method]) {
        [self getRecentConversationUsers:result];
    }

- (void)getRecentConversationUsers:(FlutterResult)result {
    [[LCChatKitHelper sharedInstance] lcck_settingWithUsers:@[]];
    NSMutableArray *messages = [NSMutableArray array];
    __block NSUInteger badgeCount = 0;
    
    [[LCCKConversationListService sharedInstance] findRecentConversationsWithBlock:^(NSArray *conversations, NSInteger totalUnreadCount, NSError *error) {
        NSLog(@"totalUnreadCount :%ld",totalUnreadCount);
        ......//此处表示省略
        ......
        }
}

到此就差不多完成了!真的是这样吗?我们来想一下聊天列表需要有那些功能:1、显示用户信息,2、显示最后一个聊天,3、显示未读消息有几个....最重要的是能根据聊天的情况刷新聊天列表的数据!例如通过聊天列表进入聊天界面聊天后返回聊天列表,此时聊天列表的数据需要根据聊天的情况进行更新。因此,这里需要用到iOS中的KVO机制监听数据的变化,并返回给flutter。这里是iOS主动返回给flutter,而之前大部分功能都是flutter主动调用iOS。为了解决这个问题,就需要用到flutter中的EventChannel,在flutter中监听一个信道,等待iOS返回数据,当信道中监听到数据时,更新UI。先给出EventChannel的代码:

  EventChannel eventChannel = const EventChannel('flutter_lc_im_native');
  eventChannel.receiveBroadcastStream('flutter_lc_im_native').listen(
      (Object event) {
    ctx.state.conversations = Conversations.fromJson(event).conversations;
    _fetchImUsers(action, ctx);
    //更新UI
  }, onError: _onError);

然后再原生中使用KVO监听并推送数据,实现FlutterStreamHandler协议和FlutterEventChannel

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pushMessageToFlutter) name:LCCKNotificationMessageReceived object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pushMessageToFlutter) name:LCCKNotificationMessageUpdated object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pushMessageToFlutter) name:LCCKNotificationUnreadsUpdated object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pushMessageToFlutter) name:LCCKNotificationConversationListDataSourceUpdated object:nil];
   
    //设置EventChannel
    [self setEventToFlutter];

//全局block回掉数据
FlutterEventSink eventBlock;
    
- (void)setEventToFlutter {
    NSString *channelName = @"flutter_lc_im_native";
    FlutterEventChannel *evenChannal = [FlutterEventChannel eventChannelWithName:channelName binaryMessenger:messager];
    // 代理FlutterStreamHandler
    [evenChannal setStreamHandler:self];
    
    NSLog(@"print log=========================");
}

//FlutterStreamHandler必须要实现的两个方法
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
                                       eventSink:(FlutterEventSink)events{
    if (events) {
        eventBlock = events; //赋值给全局block
    }
    return nil;
}
    
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments{
    return nil;
}

以上基本完成了聊天列表的功能,当然还存在很多不足,如果发现了请给出你的建议。 如果你想进一步了解IM的实现可以看源码实现,下面附上源码地址:

flutter_lc_im github地址

flutter_lc_im flutter.pub地址

IM Android里面的坑

1、最大的坑不支持androidx,因此项目中所有flutter框架都不能使用androidx,之前天真的的将SKD升级到androidx,结果跑不起来,坑啊,浪费三天时间。

2、没有单聊时离线推送,最后找到原因,sendMessage中自己写。

总结:

总体来说,IM的封装还是有一定的难度的,首先需要理解IM框架的原理,然后才能进一步封装和优化,对一个flutter新手来说具备一定的挑战。对于Flutter这门技术来说,个人还是比较看好的,特别是它在UI方面的开发速度,大大的加快了产品的开发。但由于dart语言机制的问题,使得dart代码特别长,不便于定位代码,因此项目中我们使用了fish-redux进行解耦,使用了fish-redux后代码变得很清新、毕竟大厂工具。