In-Depth Guide to work with platform channels by integrating 3rd Party SDK: IOS
In this article we will learn how method channels and event channels work. Also, how we will integrate 3rd party SDK in our Flutter app and use them to invoke native methods using method channels and stream data using event channels. In addition to that, we will structure the code and explore the prerequisites to start writing native code.
Platform Channels
Platform channels are used to communicate with the native side using event streams or method calls. This communication is set up by calling platform-specific APIs from Dart code, and it provides a way to share data among platform and dart sides. These APIs can be called in the language supported by the specific platform, e.g., Java or Kotlin for Android, Swift or Objective-C for IOS.
MethodChannel
They are used to invoke methods with or without arguments at the platform side. And on the platform side, we receive the method calls and send back a result. The method call is asynchronous. On Android, we use MethodChannel
and on IOS we use FlutterMethodChannel
for handling method calls.
EventChannel
They are used to stream data from platform to dart side. These stream requests are encoded into binary at the platform side and then we can receive these stream requests by subscribing to them. The stream data is then decoded into Dart.
The above flow diagram explains the way our Dart code communicates with native code and then we use the Dart SDK as a plugin inside our Flutter app. From the Dart SDK, through method calls, we invoke platform-specific APIs and through the event channel, we get the binary encoded streamed data. Our IOS side uses FlutterMethodChannel
to implement MethodChannel
and FlutterEventChannel
and FlutterStreamHandler
to implement EventChannel
.
Getting Started
Let's create our Flutter App. We will be using CometChatSDK
v3 to demonstrate the working of Platform Channels
.
flutter create --org com.example --template=plugin --platforms=android,ios -a java flutter_comet_chat_sdk
Here, we have created a flutter plugin with Java support for Android and Swift for IOS.
Project Structure
.
└── flutter_comet_chat_sdk/
├── android/
│ ├── gradle
│ ├── src/
│ │ └── main/java/com/example/flutter_comet_chat_sdk/
│ │ ├── FlutterCometChatSdkPlugin.java
│ │ ├── EventChannelHelper.java
│ │ ├── MethodChannelHandler.java
│ │ └── Helper/
│ └── build.gradle
├── example/
│ ├── android
│ ├── ios
│ ├── lib/
│ │ └── main.dart
│ ├── test
│ └── pubspec.yaml
├── ios/
│ ├── Assets
│ ├── Classes/
│ │ ├── SwiftFlutterCometChatSdkPlugin.swift
│ │ ├── FlutterCometChatSdkPlugin.m
│ │ ├── FlutterCometChatSdkPlugin.h
│ │ ├── EventChannelHandler.swift
│ │ ├── Helpers/
│ │ └── Listeners/
│ └── flutter_comet_chat_sdk.podspec
├── lib/
│ ├── src/
│ │ ├── listener/
│ │ ├── model/
│ │ ├── comet_chat_dart.dart
│ │ ├── comet_chat_constants.dart
│ │ ├── comet_constants_keys.dart
│ │ └── error_details.dart
│ └── flutter_comet_chat_sdk.dart
├── test
└── pubspec.yaml
We will use the android and ios folders in the root directory to write our Android and IOS native code respectively. Under the lib folder in the root directory, we will write Dart side implementation of method channels and event channels. In that file, we will subscribe to streams, add listeners, serialize data and add calls to invoke the method on the native side. Then under the example folder, we will test the Dart plugin methods.
Integrating 3rd party SDK with IOS
We are using CometChatSdk
, so we will add that to our IOS file. So as per the docs we will add the SDK as a dependency and set the platform version to 11.0.
Under the file ios/flutter_comet_chat_sdk.podspec
add:
. . .
s.dependency 'CometChatPro', '3.0.900'
s.platform = :ios, '11.0'
. . .
Or you can add your .framework
project file in the ios
folder and under the podspec
file add the path to the framework
file.
. . .
s.platform = :ios, '11.0'
s.preserve_paths = 'ProjectName.framework'
s.xcconfig = { 'OTHER_LDFLAGS' => '-framework ProjectName' }
s.vendored_frameworks = 'ProjectName.framework'
. .
Also, under the example/ios/Podfile
add the platform version as 11.0 as the SDK supports versions equal or above that. To support running simulator with Comet Chat
we need to exclude arm64 i386.
. . .
platform :ios, '11.0'
. . .
. . .
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |build_configuration|
build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386'
end
end
end
Now, go to example/ios
run pod install
and then open the example/ios
folder in Xcode and run the app once. We will use Xcode to run and write swift code. Sometimes the print statements mentioned in the swift code don't show in VS code terminal. So it's better to use Xcode to run and debug while working with IOS native side.
So, we have opened the ios folder under the example folder not the ios in the root directory. But the code for SDK needs to be written in the root ios/Classes folder only. We can access the folder under the Pods folder in Xcode. On the left side, in Xcode, we have Runner and Pods folder. If we go inside
Pods -> Development Pods -> flutter_comet_chat_sdk -> .. -> .. -> example -> iOS -> .symlinks
-> plugins -> flutter_comet_chat_sdk -> iOS -> Classes ->
There we can find the Classes folder with the SwiftFlutterCometChatSdk.swift
file. In this folder, we will implement our platform channel. Or for easy access, you can drag and drop the ios/Classes
folder in the Xcode project and add it as a reference.
Now the setup is ready, we will use some of the Comet Chat
SDK methods to see how we can implement our Platform channel.
Creating MethodChannel
We will define our FlutterMethodChannel
inside SwiftFlutterCometChatSdk.swift
file. To interact with the host platform and set up Platform channels we need to implement the interface FlutterPlugin
. It provides a method to set up and register the plugin. Under the register method, we will set up the FlutterMethodChannel
and FlutterEventChannels
.
The register method provides FlutterPluginRegistrar
which provides a method to add MethodChannel
and set up communication between Dart and IOS. Also, the FlutterPluginRegistrar
method messenger()
, returns a FlutterBinaryMessenger
which helps in asynchronous message passing between Dart and IOS using binary messages. These binary messages are used by the event channels to stream data.
The addMethodCallDelegate
registers the MethodChannel
to receive method calls from the Dart side.
Lastly, we have the handle
method that is invoked when MethodChannel
is defined and the native side is ready to receive method calls.
import Flutter
import UIKit
import CometChatPro
public class SwiftFlutterCometChatSdkPlugin: NSObject, FlutterPlugin {
public override init() {
super.init()
}
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = SwiftFlutterCometChatSdkPlugin()
instance.setupChannel(registrar: registrar)
}
private func setupChannel(registrar: FlutterPluginRegistrar){
let channel = FlutterMethodChannel(name: "plugins.flutter.io/comet_chat_dart", binaryMessenger: registrar.messenger())
registrar.addMethodCallDelegate(self, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let args = call.arguments as? [String: Any]
switch call.method {
case "initialize":
result("success")
default:
result(FlutterMethodNotImplemented)
}
}
}
Here we have created our MethodChannel
with the name plugins.flutter.io/comet_chat_dart
. The same name we will be using on the Dart side.
On the Dart side, we have the src
folder and flutter_comet_chat_sdk.dart
file. Under the flutter_comet_chat_sdk.dart
we will define the exports for the plugin and In the file src/comet_chat_dart.dart
we will set up our MethodChannel
and EventChannel
.
First, we will define the MethodChannel
and then use the invoke method to call the native methods.
import 'dart:async';
import 'dart:convert';
/// Comet Chat utility package.
///
/// This is a preliminary API.
import 'package:flutter/services.dart';
import 'package:flutter_comet_chat_sdk/flutter_comet_chat_sdk.dart';
/// Entry point for the CometChat Dart.
class FlutterCometChatDart {
static const MethodChannel channel =
MethodChannel('plugins.flutter.io/comet_chat_dart');
init() async {
try {
dynamic res = await FlutterCometChatDart.channel.invokeMethod('initialize');
return res;
} on PlatformException catch (e) {
throw FlutterCometChatDart.convertException(e);
}
}
static Exception convertException(PlatformException err) {
return ErrorDetails(
errorCode: err.code, errorDescription: err.message ?? "");
}
}
This is how we are invoking the method init
defined with the key initialize
on the IOS side.
How to pass arguments from MethodChannels?
Let's see how we can pass function arguments from the Dart side to the native side. Also, we will send back the result from the native to the Dart side using MethodChannel
.
So, on the native side, we will define a helper method Helpers/LoginLogoutHelper.swift
to handle the authentication for Comet Chat
. The initialize method has two arguments appId
and appSettings
. The appId
is a String so we can directly share it through MethodChannel
, but appSettings
is an object of class AppSettings
so for that we need to create it in the form of key-value pair and then we will map those key values into AppSettings
class at Dart side.
The initialize
method provides two callbacks based on success or error and we are returning the status based on the callback. Later, we will use this to pass as a FlutterResult
.
import Foundation
import CometChatPro
public class LoginLogoutHelper {
public init() {
}
public func initialize(appId: String?, appSettings: Dictionary<String, Any>?) -> Any {
let mySettings: AppSettings = Helper.getAppSettings(value: appSettings)
var result: Any = "success";
if let tAppId = appId {
CometChat.init(appId: tAppId ,appSettings: mySettings, onSuccess: { (isSuccess) in
if (isSuccess) {
print("CometChat Pro SDK intialise successfully.")
result = true
}
}) { (error) in
print("CometChat Pro SDK failed intialise with error: \(error.errorDescription)")
result = error.errorDescription
}
}
return result
}
}
Helpers/Helper.swift
Adding the method to get key-value data for AppSettings
object.
import CometChatPro
import Foundation
public enum Helper {
static func getAppSettings(value: [String: Any]?) -> AppSettings {
if value != nil {
let region = value?["region"] as? String
let subscriptionType = value?["subscriptionType"] as? String
let roles = value?["roles"] as? [String]
if let tRegion = region,
let tSubscriptionType = subscriptionType
{
var appSettings = AppSettings.AppSettingsBuilder().setRegion(region: tRegion).build()
switch tSubscriptionType {
case "ALL_USERS":
appSettings = AppSettings.AppSettingsBuilder().subscribePresenceForAllUsers().setRegion(region: tRegion).build()
case "FRIENDS":
appSettings = AppSettings.AppSettingsBuilder().subscribePresenceForFriends().setRegion(region: tRegion).build()
case "ROLES":
appSettings = AppSettings.AppSettingsBuilder().subcribePresenceForRoles(roles: roles ?? []).setRegion(region: tRegion).build()
case "NONE":
appSettings = AppSettings.AppSettingsBuilder().setRegion(region: tRegion).build()
default:
return appSettings
}
return appSettings
}
}
return AppSettings.AppSettingsBuilder().subscribePresenceForAllUsers().setRegion(region: "US").build()
}
}
Now, we will call the initialize method in our handle method. Also, we are mapping the arguments coming from the Dart side with their respective key values.
. . .
private var loginLogoutHelper: LoginLogoutHelper?
public override init() {
super.init()
loginLogoutHelper = LoginLogoutHelper()
}
. . .
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let args = call.arguments as? [String: Any]
switch call.method {
case "initialize":
let appId = args?["appId"] as? String
let appSettings = args?["appSettings"] as? Dictionary<String, Any>
let res = loginLogoutHelper?.initialize(appId: appId, appSettings: appSettings)
result(res)
. . .
. . .
On the Dart side, we will define the initialize
method, pass the required arguments, define the model classes and use the result passed by the method channel.
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);
}
}
For AppSettings
class we need to define an AppSettingsBuilder
class that will set the values that are under AppSettings
class. Basically, the builder class will have set methods and a build method that will return the object of AppSettings
class after setting the field values.
Check the AppSettings
code here
.
Then we have a CallbackListener
class that has methods onSuccess
and onError
and based on the response from invokeMethod
we are calling the onSuccess
or onError
method.
class CallbackListener {
Function onSuccess;
Function onError;
CallbackListener({required this.onSuccess, required this.onError});
}
So, now the MethodChannel is ready, we can use this method under our example flutter app.
AppSettings appSettings = (AppSettingsBuilder()
..subscribePresenceForAllUsers()
..setRegion("US"))
.build();
_cometChat.init(
Constant.appID,
appSettings,
CallbackListener(
onSuccess: (dynamic value) {
print("SUCCESSFULLY INITIALIZED $value");
},
onError: (dynamic error) {
print("FAILED TO INITIALIZE $error");
},
),
);
Creating EventChannel
We will create an EventChannelHandler
class that will handle our event channel methods. To implement this we will use FlutterStreamHandler
. This FlutterStreamHandler
exposes the event stream to the Dart side.
Now, we have to override two methods- onListen
and onCancel
. Under onListen method, we will set the FlutterEventSink
with the passed event and in onCancel
method, we will set the event sink to nil.
Also, there is an init
method to initialize the FlutterEventChannel
with the passed name and binary messenger. Then we will register the stream handler with the name on this channel using setStreamHandler
method.
Since we are defining this as a generic class for all the EventChannels
, we will define two more methods - success and error to set the value of the event sink using the object of EventChannelHandler
class.
import Foundation
import Flutter
public class EventChannelHandler: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
return nil
}
public init(id: String, messenger: FlutterBinaryMessenger) {
super.init()
let eventChannel = FlutterEventChannel(name: id, binaryMessenger: messenger)
eventChannel.setStreamHandler(self)
}
public func success(event: Any?) throws {
if eventSink != nil {
eventSink?(event)
}
}
public func error(code: String, message: String?, details: Any? = nil) {
if eventSink != nil {
eventSink?(FlutterError(code: code, message: message, details: details))
}
}
}
How to transfer data through EventChannel?
Now, we will see how we can transfer data through streams. So, first, we will define an EventChannel
and subscribe to the streams on the Dart side.
Create an object of EventChannelHandler
class.
private var loginEventTrigger: EventChannelHandler?
Then under setupChannel method add the declaration for loginEventTrigger with the name as id and pass the binary messenger.
loginEventTrigger = EventChannelHandler(
id: "plugins.flutter.io/login_event_trigger",
messenger: registrar.messenger()
)
To use this trigger we will add one more method from Comet Chat
SDK, the login method. So under the handle method we will add one more case value, calling the selectLoginMethod
defined in our LoginLogutHelper
class.
case "login":
let uid = args?["UID"] as? String
let authKey = args?["authKey"] as? String
let authToken = args?["authToken"] as? String
loginLogoutHelper?.selectLoginMethod(authToken: authToken, authKey: authKey, UID: uid)
result("success")
The selectLoginMethod
will decide whether the user wants to login 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 reference in this project).
Here, I will use the login method with authToken
to demonstrate the flow for transferring data through our event channel. For full code, you can refer to this repo
.
We are calling the Comet Chat
login method and it gives callbacks based on success or error. Under the onSuccess callback
, we are getting the user. Now, we will convert this user in key-value type (Dictionary in case of Swift), then pass the Dictionary in string form. In the stream, we are passing the event with the loginSuccess
or loginFailure
key. On the Dart side we will use this key to get the user or error from the event stream.
private func loginWithAuthToken(tAuthToken: String) {
if CometChat.getLoggedInUser() == nil {
CometChat.login(authToken: tAuthToken , onSuccess: { (user) in
do {
try self.eventChannelHandler?.success(event: ["loginSuccess": Helper.userToJson(user: user).jsonStringRepresentation])
} catch {
self.eventChannelHandler?.error(code: "loginFailure", message: error.localizedDescription)
}
}) { (error) in
print("Login failed with error: " + error.errorDescription);
do {
try self.eventChannelHandler?.success(event: ["loginFailure" : error.errorDescription])
} catch {
self.eventChannelHandler?.error(code: "loginFailure", message: error.localizedDescription)
}
}
}
}
Extension of jsonStringRepresentation
to convert Dictionary to String.
extension Dictionary {
var jsonStringRepresentation: String? {
guard let theJSONData = try? JSONSerialization.data(withJSONObject: self,
options: [.prettyPrinted]) else {
return nil
}
return String(data: theJSONData, encoding: .ascii)
}
}
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();
}
Since this is a trigger stream so we will cancel the stream subscription once the event data is received. Method to cancel the subscription -
cancelSubscription(StreamSubscription streamSubscription) {
return () {
streamSubscription.cancel();
};
}
Now, in the login method, we will first do the method call then listen to the stream, and once we have used the listen()
method to fetch the data we will cancel the subscription.
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);
}
Here, we are using the same keys login_success
and login_failure
to get the data from the event stream. Then we have decoded the JSON String
and serialized it with the User
class. To get the object data on the Dart side we have added a fromJson
method under the User
class that will map all the keys in the JSON to the values of the User
class.
User.fromJson(Map<dynamic, dynamic> json) {
avatar = json['avatar'];
blockedByMe = json['blockedByMe'];
credits = json['credits'];
hasBlockedMe = json['hasBlockedMe'];
lastActiveAt = json['lastActiveAt'];
link = json['link'];
metadata = json['metadata'];
name = json['name'];
role = json['role'];
status = getUserStatus(json['status']);
statusMessage = json['statusMessage'];
uid = json['uid'];
}
Now the method is ready we can use it in our example app.
_cometChat.login(
uid: "superhero2",
authToken: Constant.authToken,
callbackListener: CallbackListener(
onSuccess: (User user) {
print("User $user");
},
onError: (e) {
print("Error $e");
},
),
);
Till now, we have defined and used MethodChannel
, EventChannel
. Let's look at another example for EventChannel
by defining LoginListener
.
According to the Comet Chat
docs, LoginListener
class needs to implement CometChatLoginDelegate
and add CometChat.logindelegate = self
in the init
state of this class.
The LoginListener
will then override 4 methods - onLoginSuccess
, onLoginFailed
, onLogoutSuccess
, and onLogoutFailed
. Similar to the login
method here in the listener we will convert the user object into Dictionary
then into String
and pass it to the event channel success method or in case of error pass it to the error method with the right key.
public class LoginListener: CometChatLoginDelegate {
private var eventChannelHandler: EventChannelHandler?
public init(handler: EventChannelHandler?) {
self.eventChannelHandler = handler
CometChat.logindelegate = self
}
public func onLoginSuccess(user: User) {
do {
try eventChannelHandler?.success(
event: ["loginSuccess": Helper.userToJson(user: user).jsonStringRepresentation]
)
} catch {
eventChannelHandler?.error(
code: "loginFailure",
message: error.localizedDescription
)
}
}
. . .
. . .
Let's use this listener in our method call. First, declare the EventChannelHandler
and LoginListener
object.
private var loginEventListener: EventChannelHandler?
private var loginListener: LoginListener?
Then under setupChannel
method declare the variable with the name used for this EventChannel
and binary messenger.
loginEventListener = EventChannelHandler(
id: "plugins.flutter.io/login_event_listener",
messenger: registrar.messenger()
)
Under the handle
method, we will define the add_login_listener
method call and instantiate the LoginListener
class.
case "add_login_listeners":
loginListener = LoginListener(handler: loginEventListener)
result("success")
Coming on the Dart side, we will declare another event channel and subscribe to the stream of the event.
static const EventChannel loginEventListener =
EventChannel("plugins.flutter.io/login_event_listener");
Subscribing to the event channel stream of login listener.
Stream<dynamic> loginEventListener() {
return FlutterCometChatDart.loginEventListener.receiveBroadcastStream();
}
Now under the addLoginListener
method, we will listen to the event stream and pass the data to the callback
method. Here, we have used LoginListener
as a callback which has 4 methods same as the LoginListener
on the Swift
side.
addLoginListener(String uniqueID, LoginListener loginListener) async {
assert(uniqueID != '');
try {
await FlutterCometChatDart.channel
.invokeMethod('add_login_listeners', {"uniqueId": uniqueID});
_auth.loginEventListener().listen((event) {
if (event.containsKey(CometConstantsKeys.LOGIN_FAILURE)) {
loginListener.loginFailure(event);
}
if (event.containsKey(CometConstantsKeys.LOGIN_SUCCESS)) {
var data = jsonDecode(event[CometConstantsKeys.LOGIN_SUCCESS]);
loginListener.loginSuccess(User.fromJson(data));
}
if (event.containsKey(CometConstantsKeys.LOGOUT_FAILURE)) {
loginListener.logoutFailure(event[CometConstantsKeys.LOGOUT_FAILURE]);
}
if (event.containsKey(CometConstantsKeys.LOGOUT_SUCCESS)) {
loginListener.logoutSuccess(event[CometConstantsKeys.LOGOUT_SUCCESS]);
}
});
} on PlatformException catch (e) {
throw FlutterCometChatDart.convertException(e);
}
}
What's next?
In this tutorial, we learned how to set up Platform
channels for IOS and implemented the 3rd party SDK. Then used the Dart plugin in our example app to access the native methods. In the next tutorial, we will implement the Platform channels and integrate the Comet Chat
SDK for Android.