Zoom Video SDK Flutter: fullScreenUser video view not updating on change (active speaker or selection)

I’m using the official Zoom Video SDK Flutter wrapper from GitHub.
I want to dynamically update the full-screen video view whenever a user starts speaking or is manually selected from the list.

:hammer_and_wrench: My Setup

  • Zoom Video SDK Flutter (latest GitHub version)
  • Custom UI layout with a fullScreenUser state that controls the main video view
  • I’m rendering the selected user’s video like this:
if (isInSession.value && fullScreenUser.value != null && users.value.isNotEmpty) {
  fullScreenView = AnimatedOpacity(
    opacity: opacityLevel,
    duration: const Duration(seconds: 3),
    child: VideoView(
      user: fullScreenUser.value,
      ...
    ),
  );
}

When a user is selected via:

void onSelectedUser(ZoomVideoSdkUser user) {
  setState(() {
    fullScreenUser.value = user;
  });
}
  • :small_orange_diamond: The log() shows the change — but the actual big screen video view does not update.
  • Even when using setState(), the UI does not refresh with the new video stream.

I need, when fullScreenUser changes (manually or via onUserActiveAudioChanged), the big video view should switch to show the new user’s video.

Hi @App.Developer,

This seems to be an integration question, is it possible for you submit a reproduction for the view not updating?

Here is my whole code in a single page provided below.

import 'dart:async';
import 'dart:convert';
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:zoom_flutter_hello_world/config.dart';
import 'package:zoom_flutter_hello_world/utils/jwt.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart';
import 'package:flutter_zoom_videosdk/native/zoom_videosdk_user.dart';
import 'package:flutter_zoom_videosdk/flutter_zoom_view.dart' as zoom_view;
import 'package:flutter_zoom_videosdk/native/zoom_videosdk_event_listener.dart';

class Videochat extends StatefulWidget {
  const Videochat({super.key});
  @override
  State<Videochat> createState() => _VideochatState();
}

class _VideochatState extends State<Videochat> {
  final zoom = ZoomVideoSdk();
  final eventListener = ZoomVideoSdkEventListener();
  bool isInSession = false;
  List<StreamSubscription> subscriptions = [];
  List<ZoomVideoSdkUser> users = [];
  bool isMuted = true;
  bool isVideoOn = false;
  bool isLoading = false;

  @override
  void initState() {
    super.initState();
    if (Platform.isAndroid) {
      _checkPermissions();
    }
  }

  Future<void> _checkPermissions() async {
    await Permission.camera.request();
    await Permission.microphone.request();
    final camera = await Permission.camera.status;
    final mic = await Permission.microphone.status;
    debugPrint('Camera permission: $camera, Microphone permission: $mic');
  }

  _handleSessionJoin(data) async {
    if (!mounted) return;
    final mySelf = ZoomVideoSdkUser.fromJson(jsonDecode(data['sessionUser']));
    final remoteUsers = await zoom.session.getRemoteUsers() ?? [];
    final isMutedState = await mySelf.audioStatus?.isMuted() ?? true;
    final isVideoOnState = await mySelf.videoStatus?.isOn() ?? false;
    setState(() {
      isInSession = true;
      isLoading = false;
      isMuted = isMutedState;
      isVideoOn = isVideoOnState;
      users = [mySelf, ...remoteUsers];
    });
  }

  _updateUserList(data) async {
    final mySelf = await zoom.session.getMySelf();
    if (mySelf == null) return;
    final remoteUserList = await zoom.session.getRemoteUsers() ?? [];
    remoteUserList.insert(0, mySelf);
    setState(() {
      users = remoteUserList;
    });
  }

  _handleVideoChange(data) async {
    if (!mounted) return;
    final mySelf = await zoom.session.getMySelf();
    final videoStatus = await mySelf?.videoStatus?.isOn() ?? false;
    setState(() {
      isVideoOn = videoStatus;
    });
  }

  _handleAudioChange(data) async {
    if (!mounted) return;
    final mySelf = await zoom.session.getMySelf();
    final audioStatus = await mySelf?.audioStatus?.isMuted() ?? true;
    setState(() {
      isMuted = audioStatus;
    });
  }

  _setupEventListeners() {
    subscriptions = [
      eventListener.addListener(EventType.onSessionJoin, _handleSessionJoin),
      eventListener.addListener(EventType.onSessionLeave, handleLeaveSession),
      eventListener.addListener(EventType.onUserJoin, _updateUserList),
      eventListener.addListener(EventType.onUserLeave, _updateUserList),
      eventListener.addListener(EventType.onUserVideoStatusChanged, _handleVideoChange),
      eventListener.addListener(EventType.onUserAudioStatusChanged, _handleAudioChange),
    ];
  }

  Future startSession() async {
    setState(() => isLoading = true);
    try {
      final token = generateJwt(sessionDetails['sessionName'], sessionDetails['roleType']);
      _setupEventListeners();
      await zoom.joinSession(JoinSessionConfig(
        sessionName: sessionDetails['sessionName']!,
        sessionPassword: sessionDetails['sessionPassword']!,
        token: token,
        userName: sessionDetails['displayName']!,
        audioOptions: {"connect": true, "mute": true},
        videoOptions: {"localVideoOn": true},
        sessionIdleTimeoutMins: int.parse(sessionDetails['sessionTimeout']!),
      ));
    } catch (e) {
      debugPrint("Error: $e");
      setState(() => isLoading = false);
    }
  }

  handleLeaveSession([data]) {
    setState(() {
      isInSession = false;
      isLoading = false;
      users = [];
    });
    for (var subscription in subscriptions) {
      subscription.cancel();
    }
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        backgroundColor: Colors.grey[900],
        body: Stack(
          children: [
            if (!isInSession)
              Center(
                child: ElevatedButton(
                  onPressed: isLoading ? null : startSession,
                  child: Text(isLoading ? 'Connecting...' : 'Start Session'),
                ),
              )
            else
              Stack(
                children: [
                  VideoGrid(users: users),
                  ControlBar(
                    isMuted: isMuted,
                    isVideoOn: isVideoOn,
                    onLeaveSession: handleLeaveSession,
                  ),
                ],
              ),
          ],
        ),
      ),
    );
  }
}

class VideoGrid extends StatelessWidget {
  final List<ZoomVideoSdkUser> users;
  const VideoGrid({
    super.key,
    required this.users,
  });

  @override
  Widget build(BuildContext context) {
    if (users.isEmpty) {
      return const Center(child: CircularProgressIndicator());
    }
    return GridView.builder(
      physics: const NeverScrollableScrollPhysics(),
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: users.length <= 2 ? 1 : 2,
        crossAxisSpacing: 2,
        mainAxisSpacing: 2,
      ),
      itemCount: users.length,
      itemBuilder: (context, index) => _VideoTile(user: users[index]),
    );
  }
}

class _VideoTile extends StatelessWidget {
  final ZoomVideoSdkUser user;
  const _VideoTile({required this.user});
  @override
  Widget build(BuildContext context) {
    return Material(
      color: Colors.black,
      child: SizedBox.expand(
        child: zoom_view.View(
          key: Key(user.userId),
          creationParams: {
            "userId": user.userId,
            "videoAspect": VideoAspect.FullFilled,
            "fullScreen": false,
          },
        ),
      ),
    );
  }
}

class ControlBar extends StatelessWidget {
  final bool isMuted;
  final bool isVideoOn;
  final double circleButtonSize = 40.0;
  final zoom = ZoomVideoSdk();
  final VoidCallback onLeaveSession;

  ControlBar({
    super.key,
    required this.isMuted,
    required this.isVideoOn,
    required this.onLeaveSession,
  });

  Future toggleAudio() async {
    final mySelf = await zoom.session.getMySelf();
    if (mySelf?.audioStatus == null) return;
    final isMuted = await mySelf!.audioStatus!.isMuted();
    isMuted ? await zoom.audioHelper.unMuteAudio(mySelf.userId) : await zoom.audioHelper.muteAudio(mySelf.userId);
  }

  Future toggleVideo() async {
    final mySelf = await zoom.session.getMySelf();
    if (mySelf?.videoStatus == null) return;
    final isOn = await mySelf!.videoStatus!.isOn();
    isOn ? await zoom.videoHelper.stopVideo() : await zoom.videoHelper.startVideo();
  }

  Future leaveSession() async {
    await zoom.leaveSession(false);
    onLeaveSession();
  }

  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: Alignment.bottomCenter,
      child: FractionallySizedBox(
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            IconButton(
              onPressed: toggleAudio,
              icon: Icon(
                isMuted ? Icons.mic_off : Icons.mic,
              ),
              iconSize: circleButtonSize,
              tooltip: isMuted ? "Unmute" : "Mute",
              color: Colors.white,
            ),
            IconButton(
              onPressed: toggleVideo,
              iconSize: circleButtonSize,
              icon: Icon(
                isVideoOn ? Icons.videocam : Icons.videocam_off,
                color: Colors.white,
              ),
            ),
            IconButton(
              onPressed: leaveSession,
              iconSize: circleButtonSize,
              icon: const Icon(Icons.call_end, color: Colors.red),
            ),
          ],
        ),
      ),
    );
  }
}

This is the same code provided by Zoom via the Github for VideoSdk (Flutter)

I don’t see a reference to onUserActiveAudioChanged in the code that you have shared.

Sorry for that,

here is the updated code

import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_zoom_videosdk/flutter_zoom_view.dart';
import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart';
import 'package:flutter_zoom_videosdk/native/zoom_videosdk_event_listener.dart';
import 'package:flutter_zoom_videosdk/native/zoom_videosdk_live_transcription_message_info.dart';
import 'package:flutter_zoom_videosdk/native/zoom_videosdk_share_action.dart';
import 'package:flutter_zoom_videosdk/native/zoom_videosdk_user.dart';
import 'package:flutter_zoom_videosdk_example/utils/jwt.dart';
import 'package:google_fonts/google_fonts.dart';
import '../components/video_view.dart';
import 'intro_screen.dart';
import 'join_screen.dart';

class CallScreen extends StatefulHookWidget {
  const CallScreen({Key? key}) : super(key: key);

  @override
  State<CallScreen> createState() => _CallScreenState();
}

class _CallScreenState extends State<CallScreen> {
  double opacityLevel = 1.0;

  void _changeOpacity() {
    setState(() => opacityLevel = opacityLevel == 0 ? 1.0 : 0.0);
  }

  @override
  Widget build(BuildContext context) {
    var zoom = ZoomVideoSdk();
    var eventListener = ZoomVideoSdkEventListener();
    var isInSession = useState(false);
    var sessionName = useState('');
    var sessionPassword = useState('');
    var users = useState(<ZoomVideoSdkUser>[]);
    var fullScreenUser = useState<ZoomVideoSdkUser?>(null);
    var sharingUser = useState<ZoomVideoSdkUser?>(null);
    var isSharing = useState(false);
    var isMuted = useState(true);
    var isVideoOn = useState(false);
    var isSpeakerOn = useState(false);
    var isRecordingStarted = useState(false);
    var isMounted = useIsMounted();
    var audioStatusFlag = useState(false);
    var videoStatusFlag = useState(false);
    var userNameFlag = useState(false);
    var userShareStatusFlag = useState(false);
    var isReceiveSpokenLanguageContentEnabled = useState(false);
    var isPiPView = useState(false);
    var isSharedCamera = useState(false);
    CameraShareView cameraShareView = const CameraShareView(creationParams: {});

    //hide status bar
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.leanBack);
    Color backgroundColor = const Color(0xFF232323);
    final args = ModalRoute.of(context)!.settings.arguments as CallArguments;

    useEffect(() {
      Future<void>.microtask(() async {
        var token = generateJwt(args.sessionName, args.role);
        try {
          Map<String, bool> SDKaudioOptions = {"connect": true, "mute": true, "autoAdjustSpeakerVolume": false};
          Map<String, bool> SDKvideoOptions = {
            "localVideoOn": true,
          };
          JoinSessionConfig joinSession = JoinSessionConfig(
            sessionName: args.sessionName,
            sessionPassword: args.sessionPwd,
            token: token,
            userName: args.displayName,
            audioOptions: SDKaudioOptions,
            videoOptions: SDKvideoOptions,
            sessionIdleTimeoutMins: int.parse(args.sessionIdleTimeoutMins),
          );
          await zoom.joinSession(joinSession);
        } catch (e) {
          const AlertDialog(
            title: Text("Error"),
            content: Text("Failed to join the session"),
          );
          Future.delayed(const Duration(milliseconds: 1000)).asStream().listen((event) {
            Navigator.popAndPushNamed(
              context,
              "Join",
              arguments: JoinArguments(args.isJoin, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role),
            );
          });
        }
      });
      return null;
    }, []);

    useEffect(() {
      final sessionJoinListener = eventListener.addListener(EventType.onSessionJoin, (data) async {
        data = data as Map;
        isInSession.value = true;
        zoom.session.getSessionName().then((value) => sessionName.value = value!);
        sessionPassword.value = await zoom.session.getSessionPassword();
        debugPrint("sessionPhonePasscode: ${await zoom.session.getSessionPhonePasscode()}");
        ZoomVideoSdkUser mySelf = ZoomVideoSdkUser.fromJson(jsonDecode(data['sessionUser']));
        List<ZoomVideoSdkUser>? remoteUsers = await zoom.session.getRemoteUsers();
        var muted = await mySelf.audioStatus?.isMuted();
        var videoOn = await mySelf.videoStatus?.isOn();
        var speakerOn = await zoom.audioHelper.getSpeakerStatus();
        fullScreenUser.value = mySelf;
        remoteUsers?.insert(0, mySelf);
        isMuted.value = muted!;
        isSpeakerOn.value = speakerOn;
        isVideoOn.value = videoOn!;
        users.value = remoteUsers!;
        isReceiveSpokenLanguageContentEnabled.value = await zoom.liveTranscriptionHelper.isReceiveSpokenLanguageContentEnabled();
      });

      final sessionLeaveListener = eventListener.addListener(EventType.onSessionLeave, (data) async {
        data = data as Map;
        debugPrint("onSessionLeave: ${data['reason']}");
        isInSession.value = false;
        users.value = <ZoomVideoSdkUser>[];
        fullScreenUser.value = null;
        Navigator.popAndPushNamed(
          context,
          "Join",
          arguments: JoinArguments(args.isJoin, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role),
        );
      });

      final sessionNeedPasswordListener = eventListener.addListener(EventType.onSessionNeedPassword, (data) async {
        showDialog<String>(
          context: context,
          builder: (BuildContext context) => AlertDialog(
            title: const Text('Session Need Password'),
            content: const Text('Password is required'),
            actions: <Widget>[
              TextButton(
                onPressed: () async => {
                  Navigator.popAndPushNamed(context, 'Join',
                      arguments: JoinArguments(args.isJoin, args.sessionName, "", args.displayName, args.sessionIdleTimeoutMins, args.role)),
                  await zoom.leaveSession(false),
                },
                child: const Text('OK'),
              ),
            ],
          ),
        );
      });

      final sessionPasswordWrongListener = eventListener.addListener(EventType.onSessionPasswordWrong, (data) async {
        showDialog<String>(
          context: context,
          builder: (BuildContext context) => AlertDialog(
            title: const Text('Session Password Incorrect'),
            content: const Text('Password is wrong'),
            actions: <Widget>[
              TextButton(
                onPressed: () async => {
                  Navigator.popAndPushNamed(context, 'Join',
                      arguments: JoinArguments(args.isJoin, args.sessionName, "", args.displayName, args.sessionIdleTimeoutMins, args.role)),
                  await zoom.leaveSession(false),
                },
                child: const Text('OK'),
              ),
            ],
          ),
        );
      });

      final userVideoStatusChangedListener = eventListener.addListener(EventType.onUserVideoStatusChanged, (data) async {
        data = data as Map;
        ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf();
        var userListJson = jsonDecode(data['changedUsers']) as List;
        List<ZoomVideoSdkUser> userList = userListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList();
        for (var user in userList) {
          {
            if (user.userId == mySelf?.userId) {
              mySelf?.videoStatus?.isOn().then((on) => isVideoOn.value = on);
            }
          }
        }
        videoStatusFlag.value = !videoStatusFlag.value;
      });

      final userAudioStatusChangedListener = eventListener.addListener(EventType.onUserAudioStatusChanged, (data) async {
        data = data as Map;
        ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf();
        var userListJson = jsonDecode(data['changedUsers']) as List;
        List<ZoomVideoSdkUser> userList = userListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList();
        for (var user in userList) {
          {
            if (user.userId == mySelf?.userId) {
              mySelf?.audioStatus?.isMuted().then((muted) => isMuted.value = muted);
            }
          }
        }
        audioStatusFlag.value = !audioStatusFlag.value;
      });

      final userShareStatusChangeListener = eventListener.addListener(EventType.onUserShareStatusChanged, (data) async {
        data = data as Map;
        ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf();
        ZoomVideoSdkUser shareUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['user'].toString()));
        ZoomVideoSdkShareAction? shareAction = ZoomVideoSdkShareAction.fromJson(jsonDecode(data['shareAction']));

        if (shareAction.shareStatus == ShareStatus.Start || shareAction.shareStatus == ShareStatus.Resume) {
          sharingUser.value = shareUser;
          fullScreenUser.value = shareUser;
          isSharing.value = (shareUser.userId == mySelf?.userId);
        } else {
          sharingUser.value = null;
          isSharing.value = false;
          isSharedCamera.value = false;
          fullScreenUser.value = mySelf;
        }
        userShareStatusFlag.value = !userShareStatusFlag.value;
      });

      final shareContentChangedListener = eventListener.addListener(EventType.onShareContentChanged, (data) async {
        data = data as Map;
        ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf();
        ZoomVideoSdkUser shareUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['user'].toString()));
        ZoomVideoSdkShareAction? shareAction = ZoomVideoSdkShareAction.fromJson(jsonDecode(data['shareAction']));
        if (shareAction.shareType == ShareType.Camera) {
          debugPrint("Camera share started");
          isSharedCamera.value = (shareUser.userId == mySelf?.userId);
        }
      });

      final userJoinListener = eventListener.addListener(EventType.onUserJoin, (data) async {
        if (!isMounted()) return;
        data = data as Map;
        ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf();
        var userListJson = jsonDecode(data['remoteUsers']) as List;
        List<ZoomVideoSdkUser> remoteUserList = userListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList();
        remoteUserList.insert(0, mySelf!);
        users.value = remoteUserList;
      });

      final userLeaveListener = eventListener.addListener(EventType.onUserLeave, (data) async {
        if (!isMounted()) return;
        ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf();
        data = data as Map;
        List<ZoomVideoSdkUser>? remoteUserList = await zoom.session.getRemoteUsers();
        var leftUserListJson = jsonDecode(data['leftUsers']) as List;
        List<ZoomVideoSdkUser> leftUserLis = leftUserListJson.map((userJson) => ZoomVideoSdkUser.fromJson(userJson)).toList();
        if (fullScreenUser.value != null) {
          for (var user in leftUserLis) {
            {
              if (fullScreenUser.value?.userId == user.userId) {
                fullScreenUser.value = mySelf;
              }
            }
          }
        } else {
          fullScreenUser.value = mySelf;
        }
        remoteUserList?.add(mySelf!);
        users.value = remoteUserList!;
      });

      final userNameChangedListener = eventListener.addListener(EventType.onUserNameChanged, (data) async {
        if (!isMounted()) return;
        data = data as Map;
        ZoomVideoSdkUser? changedUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['changedUser']));
        int index;
        for (var user in users.value) {
          if (user.userId == changedUser.userId) {
            index = users.value.indexOf(user);
            users.value[index] = changedUser;
          }
        }
        userNameFlag.value = !userNameFlag.value;
      });

      final commandReceived = eventListener.addListener(EventType.onCommandReceived, (data) async {
        data = data as Map;
        debugPrint("sender: ${ZoomVideoSdkUser.fromJson(jsonDecode(data['sender']))}, command: ${data['command']}");
      });

      final liveStreamStatusChangeListener = eventListener.addListener(EventType.onLiveStreamStatusChanged, (data) async {
        data = data as Map;
        debugPrint("onLiveStreamStatusChanged: status: ${data['status']}");
      });

      final liveTranscriptionStatusChangeListener = eventListener.addListener(EventType.onLiveTranscriptionStatus, (data) async {
        data = data as Map;
        debugPrint("onLiveTranscriptionStatus: status: ${data['status']}");
      });

      final cloudRecordingStatusListener = eventListener.addListener(EventType.onCloudRecordingStatus, (data) async {
        data = data as Map;
        debugPrint("onCloudRecordingStatus: status: ${data['status']}");
        ZoomVideoSdkUser? mySelf = await zoom.session.getMySelf();
        if (data['status'] == RecordingStatus.Start) {
          if (mySelf != null && !mySelf.isHost!) {
            showDialog<String>(
              context: context,
              builder: (BuildContext context) => AlertDialog(
                content: const Text('The session is being recorded.'),
                actions: <Widget>[
                  TextButton(
                    onPressed: () async {
                      await zoom.acceptRecordingConsent();
                      if (context.mounted) {
                        Navigator.pop(context);
                      }
                      ;
                    },
                    child: const Text('accept'),
                  ),
                  TextButton(
                    onPressed: () async {
                      String currentConsentType = await zoom.getRecordingConsentType();
                      if (currentConsentType == ConsentType.ConsentType_Individual) {
                        await zoom.declineRecordingConsent();
                        Navigator.pop(context);
                      } else {
                        await zoom.declineRecordingConsent();
                        zoom.leaveSession(false);
                        if (!context.mounted) return;
                        Navigator.popAndPushNamed(
                          context,
                          "Join",
                          arguments:
                              JoinArguments(args.isJoin, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role),
                        );
                      }
                    },
                    child: const Text('decline'),
                  ),
                ],
              ),
            );
          }
          isRecordingStarted.value = true;
        } else {
          isRecordingStarted.value = false;
        }
      });

      final liveTranscriptionMsgInfoReceivedListener = eventListener.addListener(EventType.onLiveTranscriptionMsgInfoReceived, (data) async {
        data = data as Map;
        ZoomVideoSdkLiveTranscriptionMessageInfo? messageInfo = ZoomVideoSdkLiveTranscriptionMessageInfo.fromJson(jsonDecode(data['messageInfo']));
        debugPrint("onLiveTranscriptionMsgInfoReceived: content: ${messageInfo.messageContent}");
      });

      final inviteByPhoneStatusListener = eventListener.addListener(EventType.onInviteByPhoneStatus, (data) async {
        data = data as Map;
        debugPrint("onInviteByPhoneStatus: status: ${data['status']}, reason: ${data['reason']}");
      });

      final multiCameraStreamStatusChangedListener = eventListener.addListener(EventType.onMultiCameraStreamStatusChanged, (data) async {
        data = data as Map;
        ZoomVideoSdkUser? changedUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['changedUser']));
        var status = data['status'];
        for (var user in users.value) {
          {
            if (changedUser.userId == user.userId) {
              if (status == MultiCameraStreamStatus.Joined) {
                user.hasMultiCamera = true;
              } else if (status == MultiCameraStreamStatus.Left) {
                user.hasMultiCamera = false;
              }
            }
          }
        }
      });

      final requireSystemPermission = eventListener.addListener(EventType.onRequireSystemPermission, (data) async {
        data = data as Map;
        ZoomVideoSdkUser? changedUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['changedUser']));
        var permissionType = data['permissionType'];
        switch (permissionType) {
          case SystemPermissionType.Camera:
            showDialog<String>(
              context: context,
              builder: (BuildContext context) => AlertDialog(
                title: const Text("Can't Access Camera"),
                content: const Text("please turn on the toggle in system settings to grant permission"),
                actions: <Widget>[
                  TextButton(
                    onPressed: () => Navigator.pop(context, 'OK'),
                    child: const Text('OK'),
                  ),
                ],
              ),
            );
            break;
          case SystemPermissionType.Microphone:
            showDialog<String>(
              context: context,
              builder: (BuildContext context) => AlertDialog(
                title: const Text("Can't Access Microphone"),
                content: const Text("please turn on the toggle in system settings to grant permission"),
                actions: <Widget>[
                  TextButton(
                    onPressed: () => Navigator.pop(context, 'OK'),
                    child: const Text('OK'),
                  ),
                ],
              ),
            );
            break;
        }
      });

      final networkStatusChangeListener = eventListener.addListener(EventType.onUserVideoNetworkStatusChanged, (data) async {
        data = data as Map;
        ZoomVideoSdkUser? networkUser = ZoomVideoSdkUser.fromJson(jsonDecode(data['user']));

        if (data['status'] == NetworkStatus.Bad) {
          debugPrint("onUserVideoNetworkStatusChanged: status: ${data['status']}, user: ${networkUser.userName}");
        }
      });

      final eventErrorListener = eventListener.addListener(EventType.onError, (data) async {
        data = data as Map;
        String errorType = data['errorType'];
        showDialog<String>(
          context: context,
          builder: (BuildContext context) => AlertDialog(
            title: const Text("Error"),
            content: Text(errorType),
            actions: <Widget>[
              TextButton(
                onPressed: () => Navigator.pop(context, 'OK'),
                child: const Text('OK'),
              ),
            ],
          ),
        );
        if (errorType == Errors.SessionJoinFailed || errorType == Errors.SessionDisconnecting) {
          Timer(
              const Duration(milliseconds: 1000),
              () => {
                    Navigator.popAndPushNamed(
                      context,
                      "Join",
                      arguments: JoinArguments(args.isJoin, sessionName.value, sessionPassword.value, args.displayName, args.sessionIdleTimeoutMins, args.role),
                    ),
                  });
        }
      });

      final userRecordingConsentListener = eventListener.addListener(EventType.onUserRecordingConsent, (data) async {
        data = data as Map;
        ZoomVideoSdkUser? user = ZoomVideoSdkUser.fromJson(jsonDecode(data['user']));
        debugPrint('userRecordingConsentListener: user= ${user.userName}');
      });

      final callCRCDeviceStatusListener = eventListener.addListener(EventType.onCallCRCDeviceStatusChanged, (data) async {
        data = data as Map;
        debugPrint('onCallCRCDeviceStatusChanged: status = ${data['status']}');
      });

      final originalLanguageMsgReceivedListener = eventListener.addListener(EventType.onOriginalLanguageMsgReceived, (data) async {
        data = data as Map;
        ZoomVideoSdkLiveTranscriptionMessageInfo? messageInfo = ZoomVideoSdkLiveTranscriptionMessageInfo.fromJson(jsonDecode(data['messageInfo']));
        debugPrint("onOriginalLanguageMsgReceived: content: ${messageInfo.messageContent}");
      });

      final chatPrivilegeChangedListener = eventListener.addListener(EventType.onChatPrivilegeChanged, (data) async {
        data = data as Map;
        String type = data['privilege'];
        debugPrint('chatPrivilegeChangedListener: type= $type');
      });

      final testMicStatusListener = eventListener.addListener(EventType.onTestMicStatusChanged, (data) async {
        data = data as Map;
        String status = data['status'];
        debugPrint('testMicStatusListener: status= $status');
      });

      final micSpeakerVolumeChangedListener = eventListener.addListener(EventType.onMicSpeakerVolumeChanged, (data) async {
        data = data as Map;
        int type = data['micVolume'];
        debugPrint('onMicSpeakerVolumeChanged: micVolume= $type, speakerVolume');
      });

      final cameraControlRequestResultListener = eventListener.addListener(EventType.onCameraControlRequestResult, (data) async {
        data = data as Map;
        bool approved = data['approved'];
        debugPrint('onCameraControlRequestResult: approved= $approved');
      });

      final callOutUserJoinListener = eventListener.addListener(EventType.onCalloutJoinSuccess, (data) async {
        data = data as Map;
        String phoneNumber = data['phoneNumber'];
        ZoomVideoSdkUser? user = ZoomVideoSdkUser.fromJson(jsonDecode(data['user']));
        debugPrint('onCalloutJoinSuccess: phoneNumber= $phoneNumber, user= ${user.userName}');
      });

      return () => {
            sessionJoinListener.cancel(),
            sessionLeaveListener.cancel(),
            sessionPasswordWrongListener.cancel(),
            sessionNeedPasswordListener.cancel(),
            userVideoStatusChangedListener.cancel(),
            userAudioStatusChangedListener.cancel(),
            userJoinListener.cancel(),
            userLeaveListener.cancel(),
            userNameChangedListener.cancel(),
            userShareStatusChangeListener.cancel(),
            liveStreamStatusChangeListener.cancel(),
            cloudRecordingStatusListener.cancel(),
            inviteByPhoneStatusListener.cancel(),
            eventErrorListener.cancel(),
            commandReceived.cancel(),
            liveTranscriptionStatusChangeListener.cancel(),
            liveTranscriptionMsgInfoReceivedListener.cancel(),
            multiCameraStreamStatusChangedListener.cancel(),
            requireSystemPermission.cancel(),
            userRecordingConsentListener.cancel(),
            networkStatusChangeListener.cancel(),
            callCRCDeviceStatusListener.cancel(),
            originalLanguageMsgReceivedListener.cancel(),
            chatPrivilegeChangedListener.cancel(),
            testMicStatusListener.cancel(),
            micSpeakerVolumeChangedListener.cancel(),
            cameraControlRequestResultListener.cancel(),
            callOutUserJoinListener.cancel(),
            shareContentChangedListener.cancel(),
          };
    }, [zoom, users.value, isMounted]);

    void onSelectedUserVolume(ZoomVideoSdkUser user) async {
      var isShareAudio = user.isSharing;
      bool canSetVolume = await user.canSetUserVolume(user.userId, isShareAudio);
      num userVolume;

      List<ListTile> options = [
        ListTile(
          title: Text(
            'Adjust Volume',
            style: GoogleFonts.lato(
              textStyle: const TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.w600,
                color: Colors.black,
              ),
            ),
          ),
        ),
        ListTile(
          title: Text(
            'Current volume',
            style: GoogleFonts.lato(
              textStyle: const TextStyle(
                fontSize: 14,
                fontWeight: FontWeight.normal,
                color: Colors.black,
              ),
            ),
          ),
          onTap: () async => {
            debugPrint('user volume'),
            userVolume = await user.getUserVolume(user.userId, isShareAudio),
            debugPrint('user ${user.userName}\'s volume is ${userVolume!}'),
          },
        ),
      ];
      if (canSetVolume) {
        options.add(
          ListTile(
            title: Text(
              'Volume up',
              style: GoogleFonts.lato(
                textStyle: const TextStyle(
                  fontSize: 14,
                  fontWeight: FontWeight.normal,
                  color: Colors.black,
                ),
              ),
            ),
            onTap: () async => {
              userVolume = await user.getUserVolume(user.userId, isShareAudio),
              if (userVolume < 10)
                {
                  await user.setUserVolume(user.userId, userVolume + 1, isShareAudio),
                }
              else
                {
                  debugPrint("Cannot volume up."),
                }
            },
          ),
        );
        options.add(
          ListTile(
            title: Text(
              'Volume down',
              style: GoogleFonts.lato(
                textStyle: const TextStyle(
                  fontSize: 14,
                  fontWeight: FontWeight.normal,
                  color: Colors.black,
                ),
              ),
            ),
            onTap: () async => {
              userVolume = await user.getUserVolume(user.userId, isShareAudio),
              if (userVolume > 0)
                {
                  await user.setUserVolume(user.userId, userVolume - 1, isShareAudio),
                }
              else
                {
                  debugPrint("Cannot volume down."),
                }
            },
          ),
        );
      }
      showDialog(
          context: context,
          builder: (context) {
            return Dialog(
                elevation: 0.0,
                insetPadding: const EdgeInsets.symmetric(horizontal: 40),
                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
                child: SizedBox(
                  height: options.length * 58,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      ListView(
                        shrinkWrap: true,
                        children: ListTile.divideTiles(
                          context: context,
                          tiles: options,
                        ).toList(),
                      ),
                    ],
                  ),
                ));
          });
    }

    void onSelectedUser(ZoomVideoSdkUser user) async {
      log("user selected for FullScreen : ${user.userName}");
      setState(() {
        fullScreenUser.value = user;
      });
    }

    Widget fullScreenView;
    Widget smallView;
    Widget cameraView;

    if (isInSession.value && fullScreenUser.value != null && users.value.isNotEmpty) {
      fullScreenView = AnimatedOpacity(
        opacity: opacityLevel,
        duration: const Duration(seconds: 3),
        child: VideoView(
          user: fullScreenUser.value,
          hasMultiCamera: false,
          isPiPView: isPiPView.value,
          sharing: sharingUser.value == null ? false : (sharingUser.value?.userId == fullScreenUser.value?.userId),
          preview: false,
          focused: false,
          multiCameraIndex: "0",
          videoAspect: VideoAspect.Original,
          fullScreen: true,
          resolution: VideoResolution.Resolution360,
        ),
      );

      smallView = Container(
        height: 110,
        margin: const EdgeInsets.only(left: 20, right: 20),
        alignment: Alignment.center,
        child: ListView.separated(
          scrollDirection: Axis.horizontal,
          itemCount: users.value.length,
          itemBuilder: (BuildContext context, int index) {
            return InkWell(
              onTap: () async {
                onSelectedUser(users.value[index]);
              },
              onDoubleTap: () async {
                onSelectedUserVolume(users.value[index]);
              },
              child: Center(
                child: VideoView(
                  user: users.value[index],
                  hasMultiCamera: false,
                  isPiPView: false,
                  sharing: false,
                  preview: false,
                  focused: false,
                  multiCameraIndex: "0",
                  videoAspect: VideoAspect.Original,
                  fullScreen: false,
                  resolution: VideoResolution.Resolution180,
                ),
              ),
            );
          },
          separatorBuilder: (BuildContext context, int index) => const Divider(),
        ),
      );
    } else {
      fullScreenView = Container(
          color: Colors.black,
          child: const Center(
            child: Text(
              "Connecting...",
              style: TextStyle(
                fontSize: 20,
                color: Colors.white,
              ),
            ),
          ));
      smallView = Container(
        height: 110,
        color: Colors.transparent,
      );
    }

    cameraView = Offstage(
      offstage: !isSharedCamera.value,
      child: AnimatedOpacity(
        opacity: isSharedCamera.value ? 1.0 : 0.0,
        duration: const Duration(milliseconds: 300),
        child: cameraShareView,
      ),
    );

    _changeOpacity;
    return Scaffold(
        resizeToAvoidBottomInset: false,
        backgroundColor: backgroundColor,
        body: Stack(
          children: [
            fullScreenView,
            cameraView,
            Container(
                padding: const EdgeInsets.only(top: 35),
                child: Stack(
                  children: [
                    Container(
                      alignment: Alignment.bottomLeft,
                      margin: const EdgeInsets.only(bottom: 120),
                      child: smallView,
                    ),
                  ],
                )),
          ],
        ));
  }
}

The issue is whenever a user is manually selected from the list.

    void onSelectedUser(ZoomVideoSdkUser user) async {
      log("user selected for FullScreen : ${user.userName}");
      setState(() {
        fullScreenUser.value = user;
      });
    }

The log() shows the change — but the actual big screen video view does not update.

I’m unable to debug your code, I tried to piece together the feature in the quickstart app:

  _moveMeToBottom() async {
    final mySelf = await zoom.session.getMySelf();
    if (mySelf == null) return;
    final remoteUserList = await zoom.session.getRemoteUsers() ?? [];
    remoteUserList.add(mySelf);
    setState(() {
      users = remoteUserList;
    });
  }
....
Widget build(BuildContext context) {
    return SafeArea(
    ....
                  ElevatedButton(
                    onPressed: _moveMeToBottom,
                    child: Text('Move Me to Bottom'),
                  ),

It works as expected here:


Link to the app I’m using: https://github.com/zoom/videosdk-flutter-quickstart/

the code in the link you provided has a code

void onSelectedUser(ZoomVideoSdkUser user) {
  setState(() {
    fullScreenUser.value = user;
  });
}

which performs action to put the video of the grid view users on the main screen.

if (isInSession.value && fullScreenUser.value != null && users.value.isNotEmpty) {
  fullScreenView = AnimatedOpacity(
    opacity: opacityLevel,
    duration: const Duration(seconds: 3),
    child: VideoView(
      user: fullScreenUser.value,
      ...
    ),
  );
}

.....
   return Scaffold(
        resizeToAvoidBottomInset: false,
        backgroundColor: backgroundColor,
        body: Stack(
          children: [
            fullScreenView,
            cameraView,
            Container(
                padding: const EdgeInsets.only(top: 35),
                child: Stack(
                  children: [
                    Container(
                      alignment: Alignment.bottomLeft,
                      margin: const EdgeInsets.only(bottom: 120),
                      child: smallView,
                    ),
                  ],
                )),
          ],
        ));
  }

but whenever i click on any of the item in the gridview the function
onSelectedUser gets called but the view is not getting refreshed,

i even tried your code and the result is still as not refreshed
even a hot reload not works there.

waiting for your response @ekaansh.zoom

still waiting… @ekaansh.zoom