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")
}
}