In-Depth Guide to Work with Platform Channels by Integrating 3rd Party SDK: Android

In-Depth Guide to Work with Platform Channels by Integrating 3rd Party SDK: Android

This article is a continuation of In-Depth Guide to work with platform channels by integrating 3rd Party SDK: IOS, where we learned how MethodChannel and EventChannel works. Also, we integrated Comet Chat SDK with our IOS app and created streams and method calls to invoke the methods defined on the native side.

Here, we will integrate the same SDK on the Android side and see how we can transfer data between Dart and Android using our Platform channels.

Let's get started

In Android, the implementation would be similar to that in IOS. Here we will be using MethodChannel and EventChannel Class to implement our Platform channels. So first let's do the setup for Comet Chat and then with some examples we will see the implementation of the channels.

Open the example/android folder with Android Studio. For writing native android code, I would suggest using Android Studio IDE as that will make it easier to debug code, also we can access the definitions of classes and interfaces without using any external plugin or package. Inside the IDE on the left side, you can see one app folder that contains the example/android app code and another flutter_comet_chat_sdk that has the android plugin code. So if we go to -

flutter_comet_chat_sdk -> java -> com.example.flutter_comet_chat_sdk

There we can find the class FlutterCometChatSdkPlugin.java file. In this file, we will implement our platform channel. To set up the Comet Chat SDK, in the root directory android/build.gradle add the Gradle dependencies.

. . .
rootProject.allprojects {
    repositories {
        google()
        mavenCentral()

        maven {
        url "https://dl.cloudsmith.io/public/cometchat/cometchat-pro-android/maven/"
        }
    }
}

. . .

android {
    compileSdkVersion 30

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    defaultConfig {
        minSdkVersion 21
    }
}
. . .
. . .


dependencies {
    implementation 'com.cometchat:pro-android-chat-sdk:3.0.4'
}

Since the minSdkVersion required for Comet Chat is 21 we need to add that in our example/android/app/build.gradle also.

  defaultConfig {
        applicationId "com.example.flutter_comet_chat_sdk_example"
        minSdkVersion 21
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

Everything is set, now sync the project with Gradle files. File -> Sync Project with Gradle Files.

Our setup is ready, you can run the app on an android device or emulator. Now, we will use some methods of Comet Chat SDK to see the implementation of how the Platform channel works.

Creating MethodChannel

To define our MethodChannel we will create a class MethodChannelHandler and for EventChannel, EventChannelHelper. And in FlutterCometChatSdkPlugin class we will register our PlatformChannels. As we did in IOS, here also FlutterCometChatSdkPlugin class would be implementing FlutterPlugin interface. It provides a method to set up and register the plugin.

In android we have to override two methods - onAttachedToEngine and onDetachedFromEngine. Then, onAttachedToEngine is invoked when FlutterPlugin is added to an instance of FlutterEngine. And if the FlutterPlugin is removed or FlutterEngine is destroyed then onDetachedFromEngine is invoked.

For registering the MethodChannel we need the BinaryMessenger that is provided by onAttachedToEngine, and FlutterPlugin.FlutterPluginBinding. The BinaryMessenger is used to communicate with Dart code through the specific EventChannel or MethodChannel. So, in the onAttachedToEngine method, we will define our PlatformChannel and in onDetachedFromEngine, we will set the method channel to nil.

package com.example.flutter_comet_chat_sdk;

import android.content.Context;

import androidx.annotation.NonNull;

import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodChannel;

/** FlutterCometChatSdkPlugin */
public class FlutterCometChatSdkPlugin implements FlutterPlugin {
  /// The MethodChannel that will the communication between Flutter and native Android
  ///
  /// This local reference serves to register the plugin with the Flutter Engine and unregister it
  /// when the Flutter Engine is detached from the Activity
  private MethodChannel channel;
  private EventChannelHelper loginEventTrigger;
  private EventChannelHelper loginEventListener;

  @Override
  public void onAttachedToEngine(@NonNull FlutterPlugin.FlutterPluginBinding flutterPluginBinding) {
    setupChannels(flutterPluginBinding.getBinaryMessenger(), flutterPluginBinding.getApplicationContext());
  }


  private void setupChannels(BinaryMessenger messenger, Context context) {
    channel = new MethodChannel(messenger, "plugins.flutter.io/comet_chat_dart");
    loginEventTrigger = new EventChannelHelper(messenger, "plugins.flutter.io/login_event_trigger");
    loginEventListener = new EventChannelHelper(messenger, "plugins.flutter.io/login_event_listener");

    MethodChannelHandler methodChannelHandler = new MethodChannelHandler(context, loginEventTrigger,loginEventListener);
    channel.setMethodCallHandler(methodChannelHandler);
  }


  @Override
  public void onDetachedFromEngine(@NonNull FlutterPlugin.FlutterPluginBinding binding) {
    channel.setMethodCallHandler(null);
  }
}

Here, we have registered our event trigger and event listeners along with the method channel. Let's create a handler for the MethodChannel.

Our MethodChannelHandler class will implement interface MethodChannel.MethodCallHandler. This interface provider method onMethodCall that will be used to receive method call from Flutter. In the onMethodCall function, we will add the method call for our initialize Comet Chat SDK method. Also, in the same class, we have defined our initialize method and we are sending results back to Flutter using the MethodChannel.Result.

public class MethodChannelHandler implements MethodChannel.MethodCallHandler {
    private Context applicationContext;
    private EventChannelHelper loginEventTrigger;
    private EventChannelHelper loginEventListener;

    private LoginLogoutHelper loginHandler;

    private GetValue getValue;

    public MethodChannelHandler(Context context, EventChannelHelper loginEventTrigger, EventChannelHelper loginEventListener) {
        this.getValue = new GetValue();

        this.applicationContext = context;
        this.loginEventTrigger = loginEventTrigger;
        this.loginEventListener = loginEventListener;
        this.loginHandler = new LoginLogoutHelper(loginEventTrigger, loginEventListener);
    }

    @Override
    public void onMethodCall(MethodCall call, MethodChannel.Result result) {
        switch (call.method) {
            case "initialize":
                initializeApp(result, getValue.getValue(call, "appId"), getValue.getAppSettings(call, "appSettings"));
                break;
            default:
                result.notImplemented();
        }
    }

    private void initializeApp(MethodChannel.Result result, String appID, AppSettings appSettings) {
        CometChat.init(applicationContext, appID, appSettings, new CometChat.CallbackListener<String>() {
            @Override
            public void onSuccess(String successMessage) {
                Log.d("Initilisation", "Initialization completed successfully");
                result.success(true);
            }

            @Override
            public void onError(CometChatException e) {
                Log.d("Initilisation", "Initialization failed with exception: " + e.getMessage());
                result.success(e.toString());
            }
        });
    }
}

The class GetValue has methods that check if the data coming from the Dart side is null or not for common data types like String, int, Boolean, etc. And for JSON it converts the data into the Comet Chat class objects by mapping the keys and values.

In the above code, we have used the getValue method that checks if the call argument is null or not, and if not null it gives us the String value.

public String getValue(@NonNull MethodCall call, String argument) {
        return call.argument(argument) != null ? call.argument(argument).toString() : null;
    }

The other method which is getAppSettings maps out the values for AppSettings and using AppSettingsBuilder creates an object of the AppSettings class which is then passed to the initialize method. Check this GitHub code for the complete code of GetValue class.

On the Dart side, we can use this method with the initialize key and pass arguments with their respective keys. Also as we have seen in the IOS part, we have to create AppSettings class on the Dart side too. For doing that we will create an AppSettingsBuilder class that will set the values of AppSettings class attributes and then the build method will return the object of AppSettings class which we are passing to the native side.

init(String appId, AppSettings appSettings,
      CallbackListener callbackListener) async {
    assert(appId != '');
    assert(appSettings.region != '');

    try {
      dynamic res = await FlutterCometChatDart.channel.invokeMethod(
          'initialize', {'appId': appId, 'appSettings': appSettings.toJson()});

      if (res == true) {
        callbackListener.onSuccess(true);
      } else {
        callbackListener.onError(res);
      }
    } on PlatformException catch (e) {
      throw FlutterCometChatDart.convertException(e);
    }
  }

Creating EventChannel

We will create an EventChannelHelper class that will handle all our EventChannels. In this class first, we will create an object of EventChannel.EventSink. Using that object we can access error and success methods. The error method consumes an error event with params error code, error message, and an object of error details. On the other hand, the success method takes a success event object.

After that, we will define a Handler that will be attached to the main thread of the application using Looper.getMainLooper(). Using the post method we will add the Runnable (Event sink error or success in our case) to the message queue, which will allow us to send and process Message and Runnable.

Then in the EventChannelHelper constructor, we will create an object of EventChannel class (used to communicate with Flutter using asynchronous event streams). That EventChannel class will take the binary messenger for event encoding and the name of the specific EventChannel we want to set up. Once the object is created we will use setStreamHandler to register a stream handler on our EventChannel.

The setStreamHandler will provide EventChannel.StreamHandler which will give two methods onListen(Object, EventChannel.EventSink) and onCancel(Object). Under onListenwe will set the event in the event sink and in onCancel we will deregister or set the event sink to nil. The synchronized keyword is used to make sure that only one thread at a time uses the synchronized methods. The synchronized blocks will be synchronized on the same object i.e., object of EventSink interface.

public class EventChannelHelper {
    public Handler handler;
    private EventChannel.EventSink eventSink;

    public EventChannelHelper(BinaryMessenger messenger, String id) {
        handler = new Handler(Looper.getMainLooper());
        EventChannel eventChannel = new EventChannel(messenger, id);

        eventChannel.setStreamHandler(new EventChannel.StreamHandler() {
            @Override
            public void onListen(final Object arguments, final EventChannel.EventSink eventSink) {
                synchronized (EventChannelHelper.this) {
                    EventChannelHelper.this.eventSink = eventSink;
                }
            }

            @Override
            public void onCancel(final Object arguments) {
                synchronized (EventChannelHelper.this) {
                    eventSink = null;
                }
            }
        });
    }

    public synchronized void error(String errorCode, String errorMessage, Object errorDetails) {
        if (eventSink == null)
            return;
        handler.post(() -> eventSink.error(errorCode, errorMessage, errorDetails));
    }

    public synchronized void success(Object event) {
        if (eventSink == null)
            return;
        handler.post(() -> eventSink.success(event));
    }
}

Now our Event Channel is set up, let's implement the Comet Chat login method to see how it works.

Transfering data through EventChannel

First, in the MethodChannelHandler class, we will define our method call for the login method.

. . .
 @Override
    public void onMethodCall(MethodCall call, MethodChannel.Result result) {
        switch (call.method) {
            case "initialize":
                initializeApp(result, getValue.getValue(call, "appId"), getValue.getAppSettings(call, "appSettings"));
                break;
            case "login":
                loginHandler.selectLoginMethod(getValue.getValue(call, "authToken"), getValue.getValue(call, "authKey"),
                        getValue.getValue(call, "UID"));
                result.success("Success");
                break;
. . .

Here the selectLoginMethod will decide whether the user wants to log in using authKey or authToken. (The authKey and authToken can be generated by creating an app on Comet Chat Pro website, use v3 version for creating the app as we are using that as a reference in this project).

In this demonstration, I will use the login method with authToken to show the flow for transferring data through our event channel. For full code, you can refer to this repo.

We will create a class LoginLogoutHelper where we will define the authentication methods for Comet Chat SDK. The LoginLogoutHelper class will take EventChannelHelper objects that will be used to pass data to the streams.

public class LoginLogoutHelper {
    private String UID;
    private String authKey;
    private String authToken;
    private String uniqueLoginListenerID;
    private static final String TAG = "CometChat";
    private EventChannelHelper loginEventTrigger;
    private EventChannelHelper loginEventListener;

    public LoginLogoutHelper(EventChannelHelper loginEventTrigger, EventChannelHelper loginEventListener) {
        this.loginEventTrigger = loginEventTrigger;
        this.loginEventListener = loginEventListener;
    }
. . .

Now we will define our login method. In that, we will create a HashMap variable that will store the key and value based on success or error. Under the onSuccess, we are getting the User object that we will convert to JSON string and pass to the success method of our EventChannelHelper class. In the onError, we will pass the error object to the event stream.

 public void loginWithAuthToken() {
        Map<String, Object> map = new HashMap<>();

        CometChat.login(authToken, new CometChat.CallbackListener<User>() {
            @Override
            public void onSuccess(User user) {
                Log.d(TAG, "Login Successful : " + user);
                map.put("loginSuccess", user.toJson().toString());
                loginEventTrigger.success(map);
            }

            @Override
            public void onError(CometChatException e) {
                Log.d(TAG, "Login failed with exception: " + e.getMessage());
                map.put("loginFailure", e.toString());
                loginEventTrigger.error(e.getCode(), e.getMessage(), map);
            }
        });
    }

On the Dart side, we will use these keys to fetch the User data and convert it using the fromJson method defined on the User class.

Defining EventChannel and StreamSubscription on the Dart side.

static const EventChannel loginEventTrigger =
      EventChannel('plugins.flutter.io/login_event_trigger');

late StreamSubscription _loginEventsubscription;

Subscribing to the event stream to receive data.

Stream<dynamic> loginEventTrigger() {
    return FlutterCometChatDart.loginEventTrigger.receiveBroadcastStream();
  }

Defining the login method with CallbackListener where we are decoding the JSON string coming in the event stream and serializing it to the object on the Dart side.

login(
      {String? authKey,
      String? uid,
      String? authToken,
      required CallbackListener callbackListener}) {
    assert((uid != '' && authKey != '') || (authToken != ''));

    _auth.login(authKey: authKey, uid: uid, authToken: authToken);

    _loginEventsubscription = _auth.loginEventTrigger().listen((event) {
      if (event.containsKey(CometConstantsKeys.LOGIN_SUCCESS)) {
        var data = jsonDecode(event[CometConstantsKeys.LOGIN_SUCCESS]);
        callbackListener.onSuccess(User.fromJson(data));
      }
      if (event.containsKey(CometConstantsKeys.LOGIN_FAILURE)) {
        callbackListener.onError(event);
      }
    });

    cancelSubscription(_loginEventsubscription);
  }

Finally, we have completed the setup for Platform channels in Android and IOS both. In summary, we have learned the implementation of Platform channels by integrating Comet Chat SDK. Also, we explored how we can pass params in our method calls and share stream data between Dart and native sides. Then we structured our app in such a way that we can use the implemented SDK as a plugin in our example app or any other Flutter app.

What to do next?

For practicing the concept of Platform channels you can try implementing other methods of Comet Chat SDK. Also, you can explore other concepts such as Platform Views to implement native UI views in Flutter.

References