How to Customize Zoom Meeting SDK UI in Flutter?

,

Meeting SDK Type and Version
SDK Version: v6.5.10.32669

I’m working on a Flutter project where I integrate the Zoom Meeting SDK. The basic join/start meeting functions are working fine, but I’d like to customize the meeting UI (e.g., hiding/showing buttons, modifying the meeting controls layout, embedding inside a Scaffold page instead of the default full Zoom view).

For reference, here’s the snippet of my bridge code connecting Flutter to the Android Zoom SDK:

package com.example.jaring_apps

import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import us.zoom.sdk.JoinMeetingOptions
import us.zoom.sdk.JoinMeetingParams
import us.zoom.sdk.MeetingServiceListener
import us.zoom.sdk.MeetingStatus
import us.zoom.sdk.ZoomError
import us.zoom.sdk.ZoomSDK
import us.zoom.sdk.ZoomSDKInitParams
import us.zoom.sdk.ZoomSDKInitializeListener
import us.zoom.sdk.MeetingParameter

/**
 * FlutterZoomMeetingWrapper
 * - Single source of truth for Zoom initialization (this implements ZoomSDKInitializeListener)
 * - Requires an Activity when joining (never falls back to application Context)
 * - Adds MeetingServiceListener to surface status/error codes
 * - Guards against null/blank parameters (no NULL strings to Parcel)
 * - Ensures join is invoked on the main thread
 */
class FlutterZoomMeetingWrapper :
  FlutterPlugin,
  MethodCallHandler,
  ActivityAware,
  ZoomSDKInitializeListener,
  MeetingServiceListener {

  // Flutter channel
  private lateinit var channel: MethodChannel

  // App context
  private lateinit var appContext: Context

  // Zoom SDK
  private lateinit var zoomSDK: ZoomSDK

  // Bound activity (may be null during configuration changes)
  private var activityBinding: ActivityPluginBinding? = null

  // SDK init state
  @Volatile private var initialized: Boolean = false

  // Pending result for init (so we can respond after async callback)
  private var pendingInitResult: Result? = null

  override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_zoom_meeting_wrapper")
    channel.setMethodCallHandler(this)
    appContext = flutterPluginBinding.applicationContext
    zoomSDK = ZoomSDK.getInstance()
  }

  override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
    channel.setMethodCallHandler(null)
  }

  // ActivityAware
  override fun onAttachedToActivity(binding: ActivityPluginBinding) {
    activityBinding = binding
  }

  override fun onDetachedFromActivityForConfigChanges() {
    activityBinding = null
  }

  override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
    activityBinding = binding
  }

  override fun onDetachedFromActivity() {
    activityBinding = null
  }

  // Method channel entry point
  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
    when (call.method) {
      "getPlatformVersion" -> result.success("Android ${android.os.Build.VERSION.RELEASE}")

      "initZoom" -> {
        val jwt = call.argument<String>("jwt")
        if (jwt.isNullOrBlank()) {
          result.error("INVALID_ARGS", "Missing JWT token", null)
        } else {
          initializeZoomSDK(jwt, result)
        }
      }

      "joinMeeting" -> {
        val meetingId = call.argument<String>("meetingId")
        val password = call.argument<String>("meetingPassword")
        val displayName = call.argument<String>("displayName")
        joinZoomMeeting(meetingId, password, displayName, result)
      }

      else -> result.notImplemented()
    }
  }

  /** Initialize Zoom SDK using a JWT token. Respond to Flutter when the async callback fires. */
  @SuppressLint("LongLogTag")
  private fun initializeZoomSDK(jwt: String, result: Result) {
    // If already initialized, attach listener (idempotent) and return
    if (zoomSDK.isInitialized) {
      if (!initialized) {
        // first time we notice it's already initialized in this process
        zoomSDK.meetingService?.addListener(this)
        initialized = true
      }
      Log.d("ZOOM_SDK_INIT", "Zoom SDK already initialized")
      result.success(true)
      return
    }

    // Avoid multiple in-flight init calls
    if (pendingInitResult != null) {
      Log.d("ZOOM_SDK_INIT", "Initialization already in progress")
      result.error("INIT_IN_PROGRESS", "Zoom SDK initialization already in progress", null)
      return
    }

    val params = ZoomSDKInitParams().apply {
      jwtToken = jwt
      domain = "zoom.us"
      enableLog = true
      enableGenerateDump = true
      logSize = 5
      // If you need raw data later, set memory mode from the app using another method.
    }

    pendingInitResult = result
    Log.d("ZOOM_SDK_INIT", "Calling ZoomSDK.initialize()")
    zoomSDK.initialize(appContext, this, params)
  }

  // ZoomSDKInitializeListener
  override fun onZoomSDKInitializeResult(errorCode: Int, internalErrorCode: Int) {
    val res = pendingInitResult
    pendingInitResult = null

    if (errorCode == ZoomError.ZOOM_ERROR_SUCCESS) {
      initialized = true
      Log.d("ZOOM_SDK_INIT", "Zoom SDK initialized successfully")
      // Start listening to meeting status updates
      zoomSDK.meetingService?.addListener(this)
      res?.success(true)
    } else {
      Log.e(
        "ZOOM_SDK_INIT",
        "Failed to initialize Zoom SDK. error=$errorCode internal=$internalErrorCode"
      )
      res?.error(
        "INIT_ERROR",
        "Failed to initialize Zoom SDK. error=$errorCode internal=$internalErrorCode",
        null
      )
    }
  }

  override fun onZoomAuthIdentityExpired() {
    Log.w("ZOOM_SDK", "Auth identity expired")
    // You may want to re-fetch/refresh JWT on Flutter side and re-init.
  }

  /**
   * Join a Zoom meeting. Requires an Activity; will not fall back to application Context.
   * Avoids passing NULL strings to the SDK by converting nullable params to safe values.
   */
  private fun joinZoomMeeting(
    meetingId: String?,
    password: String?,
    displayName: String?,
    result: Result
  ) {
    // Must have an Activity to host Zoom UI
    val activity = activityBinding?.activity
    if (activity == null) {
      result.error("SDK_ERROR", "No Activity attached. Try again when Activity is available.", null)
      return
    }

    // Must be initialized
    if (!zoomSDK.isInitialized || !initialized) {
      result.error("SDK_ERROR", "Zoom SDK is not initialized", null)
      return
    }

    // Validate params (Zoom does not tolerate NULL strings in Parcel)
    val id = meetingId?.trim()
    val name = displayName?.trim()
    if (id.isNullOrEmpty() || name.isNullOrEmpty()) {
      result.error("INVALID_ARGS", "Meeting ID and display name must not be empty", null)
      return
    }
    val pass = password ?: "" // Empty is OK; NULL is not

    val meetingService = zoomSDK.meetingService
    if (meetingService == null) {
      result.error("SDK_ERROR", "MeetingService unavailable", null)
      return
    }

    val options = JoinMeetingOptions()
    val params = JoinMeetingParams().apply {
      meetingNo = id
      this.password = pass
      this.displayName = name
    }

    // Ensure join on main thread
    activity.runOnUiThread {
      val rc = meetingService.joinMeetingWithParams(activity, params, options)
      Log.d("ZOOM_JOIN", "joinMeetingWithParams returned=$rc for meeting=$id")
      // The result of join is async; success here means the request was accepted.
      result.success(true)
    }
  }

  // MeetingServiceListener — helps diagnose why join/start fails
  override fun onMeetingStatusChanged(status: MeetingStatus, errorCode: Int, internalErrorCode: Int) {
    Log.d("ZOOM_MEETING_STATUS", "status=$status error=$errorCode internal=$internalErrorCode")
    // Optionally forward to Flutter side:
    // channel.invokeMethod("onMeetingStatus", mapOf("status" to status.name, "errorCode" to errorCode, "internalErrorCode" to internalErrorCode))
  }
  override fun onMeetingParameterNotification(param: MeetingParameter) {
    Log.d("ZOOM_MEETING_PARAM", "onMeetingParameterNotification param=$param")
  }
}

1 Like

Hello @Admin3 and welcome. We don’t support Meeting SDK for Flutter, but we do offer Zoom Video SDK for Flutter . I suggest you look into that instead. It allows for a custom integration, albeit using the Video SDK platform.

If you want to continue with the custom UI for Meeting SDK, I suggest you look into the native Zoom Meeting SDK for Android and Zoom Meeting SDK for iOS platforms.

Best regards,

Yves

2 Likes