如何使用Flutter封裝即時通信IM框架開發插件

Flutter自去年12月發佈1.0版後就引發了大量開發者的關注,我的以爲它最大特色應該是可以在跨平臺的狀況下保持較好的用戶體驗,相比React和Weex來講它更接近原生的體驗。而且dart代碼要比原生的iOS代碼和Java代碼來講簡單的多,但dart也有不少坑。綜上,我以爲Flutter應該是可預見的移動端將來的一項熱門技術。對於創業公司來講Flutter絕對是一個很誘人的技術,理想狀況下:公司只須要一個會寫Flutter的程序就能寫出跨平臺的App,減小了多端開發成本,但這只是理想狀況:「理想很美好,現實很骨感。」Flutter的生態還不算很好,不少必備的功能都不完善,甚至是沒有。例如沒有一個很好的播放器,原生的播放器功能太少了,連快進都沒有,就連播放器的建立和銷燬都很奇怪。經常使用的即時通信功能一個框架也沒有,各個IM公司也沒給出Flutter版本的框架,而在社交如此流行的現在,不少不少不少...的App中IM功能已經成爲了必備的功能之一。其實我最開始的打算是等那些大公司開發Flutter版本,如騰訊、融雲、環信,覺得他們會很快推出IM框架。而然。。。。。這也是我爲何寫這篇文章的緣由了。android

在開發IM插件以前,要作的第一件事是選擇一款「便宜又好用」的IM框架進行封裝。那麼選哪家的比較合適呢???固然是性價比最高的最合適,在瞭解Mob、Bomb、融雲、環信、阿里、野馬、騰訊、網易等產品後,最終選擇LeanCloud。其實最划算的應該是Mob,畢竟對開發者徹底免費,但今年忽然宣佈下架該功能。其次是Bomb,可是Bomb的客戶端代碼寫的不太友好,但對於碼農來講,這都沒啥。最主要的一個緣由是Bomb IM框架的UI實在是太難看了,基本的語音上傳圖片等功能都沒優化,改UI?不能可能的!最後只能放棄Bomb,選擇LeanCloud。LeanCloud對於我的開發者和初創公司來講仍是挺好的,有必定的免費額度,這裏不作介紹了,都懂得。下面進入文章的主題:如何使用Flutter封裝IM框架開發插件。下面先給我我項目中的IM的界面:git

其中第一個界面是dart寫的,第二個界面是原生的界面

因爲我原本是一個iOSer,所以本文我只對iOS的封裝進行詳細講解,Andorid方面只能是業餘封裝,但Andorid上其實有幾個大坑,最後再說。本文經過FLutter封裝的是IM功能主要有兩個,第一個是獲取聊天列表,第二個是一對一的單聊。這兩個界面基本知足一對一聊天的場景。先給出單聊中使用的dart代碼:github

//第一步註冊
    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是聊天對象的信息,第四步是跳轉到聊天界面,跳轉過去就是圖二了。整體來講,封裝之後使用起來仍是比較簡單的。下面結合流程圖分析,爲何須要以上四步:redux

下面給出LeanCloud的單聊時的流程圖,第一步註冊AppId和AppKey,同時還須要初始化遠程推送UNUserNotificationCenter和聊天時的底部組件如上傳圖片和地理位置等組件;第二步,經過invokeThisMethodAfterLoginSuccessWithClientId方法註冊clientId,clientId爲當前用戶的Id,若是clientId已經註冊過則直接進入登陸狀態,須要注意的是clientId爲NSString類型,若是爲int類型程序則會崩潰,因此須要轉下字符串。第三步,獲取設置用戶體系,由於LeanCloud不保存用戶的頭像和暱稱等信息,只保存一個clientId,所以用戶須要本身設置用戶體系,經過setFetchProfilesBlock對當前聊天用戶體系。這裏有兩種經常使用的方案:第一種是本地靜態設置,第二種是經過Id到服務器上獲取對應用戶的數據後再設置,顯然第二種更符合咱們開發的需求。爲了更加簡單的實現用戶體系的設置,對用戶體系的設置進行了一層封裝,簡化了用戶體系的邏輯,後面會講。第四步,經過push到ConverationViewController進行聊天,須要注意的是,在聊天以前必定要設置好用戶體系,不然不能進行聊天!數組

在瞭解了單聊的邏輯後,要封裝單聊的功能其實就變得很簡單,下面給出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中數據獲取的調用:bash

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);
        ......//此處表示省略
        ......
        }
}
複製代碼

到此就差很少完成了!真的是這樣嗎?咱們來想一下聊天列表須要有那些功能:一、顯示用戶信息,二、顯示最後一個聊天,三、顯示未讀消息有幾個....最重要的是能根據聊天的狀況刷新聊天列表的數據!例如經過聊天列表進入聊天界面聊天后返回聊天列表,此時聊天列表的數據須要根據聊天的狀況進行更新。所以,這裏須要用到iOS中的KVO機制監聽數據的變化,並返回給flutter。這裏是iOS主動返回給flutter,而以前大部分功能都是flutter主動調用iOS。爲了解決這個問題,就須要用到flutter中的EventChannel,在flutter中監聽一個信道,等待iOS返回數據,當信道中監聽到數據時,更新UI。先給出EventChannel的代碼:app

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的實現能夠看源碼實現,下面附上源碼地址:ide

flutter_lc_im github地址

flutter_lc_im flutter.pub地址

IM Android裏面的坑

一、最大的坑不支持androidx,所以項目中全部flutter框架都不能使用androidx,以前天真的的將SKD升級到androidx,結果跑不起來,坑啊,浪費三天時間。

二、沒有單聊時離線推送,最後找到緣由,sendMessage中本身寫。

總結:

整體來講,IM的封裝仍是有必定的難度的,首先須要理解IM框架的原理,而後才能進一步封裝和優化,對一個flutter新手來講具有必定的挑戰。對於Flutter這門技術來講,我的仍是比較看好的,特別是它在UI方面的開發速度,大大的加快了產品的開發。但因爲dart語言機制的問題,使得dart代碼特別長,不便於定位代碼,所以項目中咱們使用了fish-redux進行解耦,使用了fish-redux後代碼變得很清新、畢竟大廠工具。最後給出咱們的一個產品,一個徹底使用flutter開發的App(full-flutter),下載地址:m.wandoujia.com/apps/com.mu… 若是你還在糾結flutter的應用體驗如何,不以下載下來體驗一下!你會有意想不到的收穫!

相關文章
相關標籤/搜索