Flutter has attracted a lot of attention from developers since its 1.0 release in December last year. I think the main feature of Flutter is its ability to maintain a good user experience across platforms, which is closer to the native experience than React and Weex. Dart code is much simpler than native iOS and Java code, but Dart has a lot of bugs. In conclusion, I think Flutter should be a hot technology for the foreseeable mobile future. Flutter is certainly an attractive technology for startups. Ideally, companies could create cross-platform apps with just one application that could write Flutter, reducing multiple development costs. But that’s the ideal: “The ideal is nice, the reality is boring.” The ecology of Flutter is not very good. Many of the necessary functions of Flutter are incomplete or absent. For example, there is no good player, the native player features are too few, not even fast forward, and even the creation and destruction of the player is strange. There is no framework for the commonly used IM functions, and no framework for the version of Flutter is provided by various IM companies. And now that Flutter is so popular, many, many, many… IM function has become one of the necessary functions in App. In fact, I originally planned to wait for big companies like Tencent, Rongyun and Huanxin to develop versions of Flutter, thinking that they would soon launch an IM framework. And yet… That’s why I’m writing this article.

The first thing to do before developing an IM plug-in is to choose a “cheap and easy to use” IM framework to encapsulate. So which one is more suitable?? Of course, it is the most suitable one with the highest cost performance. After learning about Mob, Bomb, Rongyun, Huanxin, Ali, Mustang, Tencent, netease and other products, I finally choose LeanCloud. The best deal was Mob, which was completely free to developers, but this year it was abruptly removed. Second is Bomb, but Bomb’s client code is not very friendly, but for the coder, that’s ok. One of the main reasons is that the UI of Bomb IM framework is really ugly. The basic functions like voice uploading and pictures are not optimized. Impossible! We had to ditch Bomb and go with LeanCloud. LeanCloud is great for individual developers and startups. It has a certain amount of free LeanCloud. Let’s move on to the topic of this article: How to develop plug-ins using Flutter to encapsulate the IM framework. Now give me the IM interface of my project:


Since I’m originally an iOSer, I’m only going to go into the details of iOS encapsulation in this article, and the Android id aspect is just amateur encapsulation, but there are a few holes in The Android ID, I’ll talk about that at the end. There are two IM functions encapsulated by FLutter in this paper. The first one is to get the chat list and the second one to one single chat. These two interfaces are basically suitable for one-to-one chat scenarios. Here is the DART code used in the chat:

// Register flutterlcim.register ("appId"."appKey"); // Step 2 User login flutterlcim.login ("Current user's userId"); // Step 3 Configure user system 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"}; / / jump to step 4 in the chat interface FlutterLcIm pushToConversationView (user, peer);Copy the code

The first and second steps are to initialize LeanCloud’s IM and connect to Lc’s server. The third step is to set the user system, where user is the information of the current object and peer is the information of the chat object. The fourth step is to jump to the chat interface, which is shown in Figure 2. Overall, encapsulation is relatively simple to use. The following is combined with the flow chart to analyze why the above four steps are needed:

The first step is to register the AppId and AppKey. At the same time, you also need to initialize the remote push UNUserNotificationCenter and the bottom components of the chat, such as uploading pictures and location. The second step, through registered clientId invokeThisMethodAfterLoginSuccessWithClientId method, clientId for the current user Id, if the clientId is registered login directly into the state, Note that clientId is an NSString, and if it’s an int it will crash, so you need to go to the string. The third step is to obtain the user system. LeanCloud does not save user profile pictures and nicknames, but only a clientId. Therefore, users need to set the user system by themselves and use setFetchProfilesBlock to set the current chat user system. There are two commonly used schemes: the first is local static setting, the second is through the Id to the server to obtain the corresponding user’s data and then set, obviously the second is more in line with our development needs. In order to more simple implementation of the user system Settings, the user system Settings for a layer of encapsulation, simplify the user system logic, will be discussed later. The fourth step, through the push to ConverationViewController chat, it is important to note that the system must be set before chat users, otherwise unable to chat!

The first step is to register app_id and app_key
if ([@"register"IsEqualToString: call method]) {/ / set a global variable, repeated registration will lead to collapse, only create initialization on the application for the first timeif(! isRegister) { NSString *appId = call.arguments[@"app_id"];
            NSString *appKey   = call.arguments[@"app_key"];
            
            [self registerConversationWithAppId:appId
                                         appKey:appKey];
            isRegister = true; }} / / registered - (void) registerConversationWithAppId: (nsstrings *) appId appKey: (nsstrings *) appKey {NSLog (@"register conversation");
    [self registerForRemoteNotification];
    
    [LCChatKit setAppId:appId appKey:appKey]; // Enable unread messages [AVIMClient]setUnreadNotificationEnabled:true];
    [AVIMClient setTimeoutIntervalInSeconds:20]; / / add input box at the bottom of the plugin. If you want to change the icon title, can subclass, and then call ` + registerSubclass ` [LCCKInputViewPluginTakePhoto registerSubclass]; [LCCKInputViewPluginPickImage registerSubclass]; [LCCKInputViewPluginLocation registerSubclass]; }Copy the code
Step 2 Register and log in
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:@"In connection..."Duration: 10.0 f]; [LCChatKitHelper invokeThisMethodAfterLoginSuccessWithClientId:userId success:^{ NSLog(@"login success@");
        [LCCKUtil hideProgress];
        result(nil);
    } failed:^(NSError *error) {
        [LCCKUtil hideProgress];
        NSLog(@"login error");
        [LCCKUtil hideProgress];
        result(@"login error");
    }];
}
Copy the code
Step 3 set up user system and chat
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]; / / open the chat interface [LCChatKitHelper openConversationViewControllerWithPeerId: peer. The userId]; }Copy the code

So that’s the package of single chat function. The second function is to get the chat list. I did some thinking when I was developing the chat list. The main consideration was whether to encapsulate the UI and logic as a whole like a chat, or separate the UI and logic, encapsulate the logic in native, and draw the UI in Dart. In the end, I chose the second option for two reasons. The first one is that overall encapsulation requires familiarity with the code at both ends, which is much more than single communication, increasing the complexity of encapsulation. On the other hand, using native encapsulation UI will make the UI display at both ends of iOS and Android inconsistent. In particular, the UI on Android is rough, and the UI display cannot be customized. For a variety of reasons, the logic that encapsulates retrieving data in native was used to customize the UI display in DART. The following is a call to data retrieval in DART:

  FlutterLcIm.getRecentConversationUsers().then((res) {
    if(res ! = [] && res ! = null) {//res array}else{}});Copy the code

Because of the amount of code to draw, I don’t show you how to use DART to draw the UI that gives you the source address chat list. In the native access to chat list data is made by findRecentConversationsWithBlock method, therefore need to wrap this method can, as follows:

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); . //...... is omitted }}Copy the code

That’s almost it! Is it really so? Let’s think about what the chat list needs to have: 1, show user information, 2, show the last chat, 3, show how many unread messages…. The most important thing is the ability to refresh the chat list data according to the chat situation! For example, you can enter the chat screen from the chat list and return to the chat list. In this case, the chat list data needs to be updated according to the chat situation. Therefore, the KVO mechanism in iOS is used to listen for changes in data and return them to flutter. Here iOS actively returns to Flutter, whereas previously flutter actively calls iOS. To solve this problem, you need to use EventChannel in flutter, listen for a channel in flutter, wait for iOS to return data, and update the UI when the channel listens for data. Give the code for 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); // update UI}, onError: _onError);Copy the code

Then KVO is used to monitor and push data in the native, and FlutterStreamHandler protocol and FlutterEventChannel are implemented

[[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]; // Set EventChannel [self]setEventToFlutter]; FlutterEventSink eventBlock; - (void)setEventToFlutter {
    NSString *channelName = @"flutter_lc_im_native"; FlutterEventChannel *evenChannal = [FlutterEventChannel eventChannelWithName:channelName binaryMessenger:messager]; // Agent FlutterStreamHandler [evenChannalsetStreamHandler:self];
    
    NSLog(@"print log========================="); } //FlutterStreamHandler must implement two methods - (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(FlutterEventSink)events{if(events) { eventBlock = events; // Assign to global block}return nil;
}
    
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments{
    return nil;
}
Copy the code

The above has basically completed the function of chat list, of course, there are still many deficiencies, if found, please give your suggestions. If you want to further understand the IM implementation can see the source code implementation, the source code address attached below:

Flutter_lc_im making address

Flutter_lc_im flutter. Address of the pub

IM Android inside the pit

1. The biggest bug does not support AndroidX, so all the Flutter frameworks in the project cannot use AndroidX. I innocently upgraded SKD to AndroidX, but it did not work.

2. Offline push when there is no single chat, finally find the reason and write in sendMessage by yourself.

Conclusion:

In general, IM encapsulation is somewhat difficult. First, you need to understand the principles of IM framework before further encapsulation and optimization, which poses certain challenges for a new flutter user. Personally, I am optimistic about Flutter technology, especially its speed of UI development, which has greatly accelerated product development. However, due to the dart language mechanism, the DART code is very long, which makes it difficult to locate the code. Therefore, we used fish-Redux for decoupling in the project. After using fish-Redux, the code became very fresh, after all, big factory tools.