如何使用Flutter封裝即時通訊IM框架開發外掛

451518849發表於2019-07-06

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後程式碼變得很清新、畢竟大廠工具。最後給出我們的一個產品,一個完全使用flutter開發的App(full-flutter),下載地址:m.wandoujia.com/apps/com.mu… 如果你還在糾結flutter的應用體驗如何,不如下載下來體驗一下!你會有意想不到的收穫!

相關文章