Wrap native methods

This section describe how to create a native method handler for the Nabla SDK, so that you can call methods from your Dart code and have them implemented natively.

See the Flutter documentation for more details about how to write platform channels.

Android

Create NablaMethodCallHandler

In android/app/src/main/kotlin create the NablaMethodHandler:

class NablaMethodCallHandler(
    private val activity: ComponentActivity,
    private val methodChannel: MethodChannel,
) : MethodChannel.MethodCallHandler {

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when(call.method) {
            // TODO implement any method you'd like to wire here
            else -> result.notImplemented()
        }
    }
}

Wire NablaMethodCallHandler

In the MainActivity kotlin file (that should be in android/app/src/main/kotlin/your/package/name/MainActivity.kt), register the method handler:

class MainActivity: FlutterFragmentActivity() {
    private val delegate = AppCompatDelegate.create(this, null)

    init {
        addOnContextAvailableListener {
            delegate.installViewFactory()
        }
    }

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        // Your other wiring here...

        val channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "nabla")
        channel.setMethodCallHandler(NablaMethodCallHandler(this, channel))
    }
}

⚠️

If you activity is of type FlutterActivity, change it to FlutterFragmentActivity (io.flutter.embedding.android.FlutterFragmentActivity)

iOS

Create NablaMethodCallHandler

In ios/Runner create a new NablaMethodCallHandler.swift file:

import Foundation
import Flutter

class NablaMethodCallHandler {
    private let nablaChannel: FlutterMethodChannel

    init(nablaChannel: FlutterMethodChannel) {
        self.nablaChannel = nablaChannel
    }

    func handleCall(call: FlutterMethodCall, result: @escaping FlutterResult) {
        switch(call.method) {
        // TODO implement any method you'd like to wire here
        default: result(FlutterMethodNotImplemented)
        }
    }
}

Wire NablaMethodCallHandler

In ios/Runner, open the AppDelegate.swift file and wire the NablaMethodCallHandler:

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  private var nablaMethodCallHandler: NablaMethodCallHandler!

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let nablaChannel = FlutterMethodChannel(name: "nabla", binaryMessenger: controller.binaryMessenger)

    nablaMethodCallHandler = NablaMethodCallHandler(nablaChannel: nablaChannel)

    nablaChannel.setMethodCallHandler(nablaMethodCallHandler.handleCall)

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Flutter

In your Dart code, you should now be able to call the native bridge, and can create a class to embed all of that code:

import 'package:flutter/services.dart';

class NablaNativeBridge {
  static const _nablaChannel = MethodChannel('nabla');

  // TODO implement Nabla methods here
}

The complete "initialize" example

This section provides a full example of how to wire the NablaClient.initialize call via Dart code for Nabla SDK initialization:

Android:

class NablaMethodCallHandler(
    private val activity: ComponentActivity,
    private val methodChannel: MethodChannel,
) : MethodChannel.MethodCallHandler {

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when(call.method) {
            "initialize" -> handleInitialize(call, result)
            else -> result.notImplemented()
        }
    }

    private fun handleInitialize(call: MethodCall, result: MethodChannel.Result) {
        try {
            val apiKey = call.argument<String>("apiKey") ?: throw IllegalStateException("Null apiKey")

            NablaClient.initialize(
                modules = listOf(
                    NablaMessagingModule(), // if you want the messaging feature
                    NablaVideoCallModule(), // if you want to add video call capabilities
                    NablaSchedulingModule(), // if you want the scheduling feature
                ),
                configuration = Configuration(
                    publicApiKey = apiKey,
                ),
                sessionTokenProvider = { userId ->
                    withContext(Dispatchers.Main) {
                        suspendCancellableCoroutine { continuation ->
                            val resultHandler = object : MethodChannel.Result {
                                override fun success(result: Any?) {
                                    val resultMap = result as? Map<Any, Any> ?: kotlin.run {
                                        if (continuation.isActive) {
                                            continuation.resumeWithException(IllegalStateException("Unable to parse user tokens result"))
                                        }
                                        return
                                    }

                                    val accessToken = resultMap["accessToken"] as? String
                                    val refreshToken = resultMap["refreshToken"] as? String

                                    if (accessToken == null || refreshToken == null) {
                                        if (continuation.isActive) {
                                            continuation.resumeWithException(IllegalStateException("Access or refresh token not found"))
                                        }
                                        return
                                    }

                                    if (continuation.isActive) {
                                        continuation.resume(Result.success(AuthTokens(AccessToken(accessToken), RefreshToken(refreshToken))))
                                    }
                                }

                                override fun error(
                                    errorCode: String,
                                    errorMessage: String?,
                                    errorDetails: Any?
                                ) {
                                    if (continuation.isActive) {
                                        continuation.resumeWithException(RuntimeException("$errorCode: $errorMessage ($errorDetails)"))
                                    }
                                }

                                override fun notImplemented() {
                                    if (continuation.isActive) {
                                        continuation.resumeWithException(IllegalStateException("provideUserTokens channel method handler not implemented"))
                                    }
                                }
                            }

                            methodChannel.invokeMethod(
                                "provideUserTokens",
                                mapOf(
                                    "userId" to userId,
                                ),
                                resultHandler,
                            )
                        }
                    }
                }
            )

            result.success(null)
        } catch (e: Exception) {
            result.error("initialize", e.message, null)
        }
    }
}

iOS

import Foundation
import Flutter
import NablaCore
import NablaMessagingCore
import NablaScheduling
import NablaVideoCall

class NablaMethodCallHandler : SessionTokenProvider {
    private let nablaChannel: FlutterMethodChannel

    init(nablaChannel: FlutterMethodChannel) {
        self.nablaChannel = nablaChannel
    }

    func handleCall(call: FlutterMethodCall, result: @escaping FlutterResult) {
        switch(call.method) {
        case "initialize": handleInitialize(call: call, result: result)
        default: result(FlutterMethodNotImplemented)
        }
    }

    private func handleInitialize(call: FlutterMethodCall, result: FlutterResult) {
        guard let arguments = call.arguments as? Dictionary<String, AnyObject> else {
            result(FlutterError(code: "initialize", message: "unable to get arguments", details: nil))
            return
        }

        guard let apiKey = arguments["apiKey"] as? String else {
            result(FlutterError(code: "initialize", message: "unable to get apiKey", details: nil))
            return
        }

        NablaClient.initialize(
            configuration: Configuration(apiKey: apiKey),
            modules: [
                NablaMessagingModule(), // if you want the messaging feature
                NablaVideoCallModule(), // if you want to add video call capabilities
                NablaSchedulingModule() // if you want the scheduling feature
            ],
            sessionTokenProvider: self
        )

        result(nil)
    }

    func provideTokens(forUserId userId: String, completion: @escaping (NablaCore.AuthTokens?) -> Void) {
        nablaChannel.invokeMethod("provideUserTokens", arguments: ["userId": userId]) { result in
            if (result as? NSObject == FlutterMethodNotImplemented) {
                completion(nil)
                return
            }

            guard let dictResult = result as? Dictionary<String, String> else {
                completion(nil)
                return
            }

            guard let accessToken = dictResult["accessToken"] else {
                completion(nil)
                return
            }

            guard let refreshToken = dictResult["refreshToken"] else {
                completion(nil)
                return
            }

            completion(.init(accessToken: accessToken, refreshToken: refreshToken))
        }
    }
}

Flutter

class NablaNativeBridge {
  static const _nablaChannel = MethodChannel('nabla');

  NablaNativeBridge() {
    _nablaChannel.setMethodCallHandler(_nablaNativeCallHandler);
  }

  Future<dynamic> _nablaNativeCallHandler(MethodCall call) async {
    switch(call.method) {
      case "provideUserTokens": return _provideUserTokens(call.arguments["userId"]);
      default: throw MissingPluginException("notImplemented");
    }
  }

  Future<Map<String, String>> _provideUserTokens(String userId) async {
    // TOTO call your backend to fetch Nabla user tokens
    String accessToken = "...";
    String refreshToken = "...";

    return {
      "accessToken": accessToken,
      "refreshToken": refreshToken,
    };
  }

  Future<void> initialize(String apiKey) async {
    try {
      await _nablaChannel.invokeMethod('initialize', {'apiKey' : apiKey});
      debugPrint('NablaNativeBridge.initialize successful');
    } on PlatformException catch (e) {
      debugPrint('Unable to initialize Nabla SDK: ${e.message}');
    }
  }
}