Build Your Own UI
This guide explains how to use RealtimeKit UI Kit components to build a custom meeting interface instead of the default full-screen meeting view.
This page builds upon the Initialize SDK, Render Default Meeting UI, and UI Kit States guides. First refer to these pages to understand the core concepts.
The code examples on this page assume you have already imported the necessary packages and initialized the SDK.
If the default meeting component does not provide enough control over layout or behavior, use UI Kit components to build a custom interface. The UI Kit provides pre-built components on top of the Core SDK. You can mix and match pieces while saving time compared to building from scratch.
A custom UI requires you to manage participant audio, notifications, dialogs, component layout, and screen transitions.
Similar to rtk-meeting, rtk-ui-provider, another UI Kit component that acts as a provider, also listens to states and syncs them with the UI Kit components.
Unlike rtk-meeting, rtk-ui-provider allows you to pass any child components to it. If any child component is a RealtimeKit component starting with rtk-, rtk-ui-provider will coordinate with it to sync the states.
Similar to RtkMeeting, RtkUiProvider, another UI Kit component that acts as a provider, also listens to states and syncs them with the UI Kit components.
Unlike RtkMeeting, RtkUiProvider allows you to pass any child components to it. If any child component is a RealtimeKit component starting with Rtk, RtkUiProvider will coordinate with it to sync the states.
Similar to rtk-meeting, rtk-ui-provider, another UI Kit component that acts as a provider, also listens to states and syncs them with the UI Kit components.
Unlike rtk-meeting, rtk-ui-provider allows you to pass any child components to it. If any child component is a RealtimeKit component starting with rtk-, rtk-ui-provider will coordinate with it to sync the states.
The iOS SDK uses a delegation pattern for custom UIs. Instead of wrapping child views in a provider, you conform to RtkUIFlowCoordinatorDelegate and return your own view controllers for each screen in the meeting flow.
There are two levels of customization:
- Replace entire screens — Conform to
RtkUIFlowCoordinatorDelegateand return your ownUIViewControllerfor the setup screen, group call screen, or webinar screen. Returnnilto use the default screen. - Replace sub-components — Conform to
MeetingViewControllerDataSourceand swap the topbar, middle view (participant grid), or bottom control bar within the defaultMeetingViewController.
public protocol RtkUIFlowCoordinatorDelegate { func showSetUpScreen(completion: () -> Void) -> SetupViewControllerDataSource? func showGroupCallMeetingScreen(meeting: RealtimeKitClient, completion: @escaping () -> Void) -> UIViewController? func showWebinarMeetingScreen(meeting: RealtimeKitClient, completion: @escaping () -> Void) -> UIViewController?}When the user joins successfully, the SDK calls showGroupCallMeetingScreen(meeting:completion:) or showWebinarMeetingScreen(meeting:completion:). Use the RealtimeKitClient parameter to bind individual UI Kit components (such as RtkMeetingHeaderView, RtkMeetingControlBar, or RtkParticipantTileView) to the meeting.
The Android SDK does not use a provider/wrapper pattern. Instead, each individual View component has an activate(meeting) method that binds it to the meeting.
The pattern is:
- Build a
RealtimeKitUIinstance withRealtimeKitUIBuilder.build(). - Access the
RealtimeKitClientviartkUI.meeting. - Place individual UI Kit views in your own XML layout or create them in code.
- Call
activate(meeting)on each component to bind it to the meeting.
Meeting-level components (grid, header, control bar) take a RealtimeKitClient:
gridView.activate(meeting)headerView.activate(meeting)controlBarView.activate(meeting)Participant-level components (tile, avatar, name tag) take an RtkMeetingParticipant:
tileView.activate(meeting.localUser)tileView.activate(remoteParticipant)The Flutter SDK provides RtkProvider, a wrapper widget that sets up dependency injection and theming for individual UI Kit widgets.
Wrap your custom widget tree in RtkProvider and use individual exported widgets such as RtkSelfAudioToggleButton, RtkSelfVideoToggleButton, RtkParticipantTile, RtkJoinButton, and RtkLeaveButton.
RtkProvider( meeting: meeting, uiKitInfo: RealtimeKitUIInfo(meetingInfo), child: YourCustomMeetingWidget(),)Each widget that needs the meeting client accepts it as a required meeting parameter. Participant-level widgets such as RtkParticipantTile accept an RtkMeetingParticipant directly.
Similar to RtkMeeting, RtkUIProvider is a wrapper component that provides the design system and UI state context to all child components.
Unlike RtkMeeting, RtkUIProvider allows you to pass any child components to it. All RealtimeKit UI Kit components read from the RtkUIContext for design tokens and state.
Use useContext(RtkUIContext) to read storeStates.meeting and determine which screen to render. When building without RtkMeeting, you must listen to SDK events and call setStates to drive state transitions yourself.
<!DOCTYPE html><html> <head> <script type="module"> import { defineCustomElements } from "https://cdn.jsdelivr.net/npm/@cloudflare/realtimekit-ui@latest/loader/index.es2017.js"; defineCustomElements(); </script> <script src="https://cdn.jsdelivr.net/npm/@cloudflare/realtimekit@latest/dist/browser.js"></script> </head>
<body style="margin: 0;"> <rtk-ui-provider id="rtk-ui-provider" style="display: flex; flex-direction: column; height: 100vh; margin: 0;" > <div id="meeting-container" style="display: flex; flex-direction: column; flex: 1; flex-grow: 1; flex-shrink: 1;" > Meeting will render here... </div> <rtk-participants-audio></rtk-participants-audio> <rtk-dialog-manager></rtk-dialog-manager> <rtk-notifications></rtk-notifications> </rtk-ui-provider> <script type="module"> async function initializeMeeting() { let currentState = "idle";
const meeting = await RealtimeKitClient.init({ authToken: "participant_auth_token", });
function renderSetupScreen() { document.querySelector("#meeting-container").innerHTML = ` <rtk-setup-screen></rtk-setup-screen> `; }
function renderWaitingScreen() { document.querySelector("#meeting-container").innerHTML = ` <rtk-waiting-screen></rtk-waiting-screen> `; }
function renderJoinedScreen() { document.querySelector("#meeting-container").innerHTML = ` <rtk-header style="display: flex; justify-content: space-between;"></rtk-header> <rtk-stage style="flex: 1; flex-grow: 1; flex-shrink: 1;"> <rtk-grid></rtk-grid> <rtk-sidebar style="position: fixed; top:0px;"></rtk-sidebar> </rtk-stage> <rtk-controlbar style="display: flex; justify-content: space-between;"></rtk-controlbar> `; }
function renderEndedScreen() { document.querySelector("#meeting-container").innerHTML = ` <rtk-ended-screen></rtk-ended-screen> `; }
// Listen for state updates from rtk-ui-provider document .querySelector("rtk-ui-provider") .addEventListener("rtkStatesUpdate", (event) => { // Store states to update your custom UI const states = event.detail;
if (states.meeting === "idle" && currentState !== "idle") { currentState = "idle"; document .querySelector("rtk-ui-provider") .querySelector("#meeting-container").innerHTML = "Meeting is loading..."; } else if (states.meeting === "setup" && currentState !== "setup") { currentState = "setup"; renderSetupScreen(); } else if ( states.meeting === "waiting" && currentState !== "waiting" ) { currentState = "waiting"; renderWaitingScreen(); } else if ( states.meeting === "joined" && currentState !== "joined" ) { currentState = "joined"; renderJoinedScreen(); } else if (states.meeting === "ended" && currentState !== "ended") { currentState = "ended"; renderEndedScreen(); }
const sidebarComponent = document .querySelector("rtk-ui-provider") .querySelector("#meeting-container") .querySelector("rtk-sidebar"); if (sidebarComponent) { if (states.activeSidebar) { sidebarComponent.style.display = "block"; } else { sidebarComponent.style.display = "none"; } } });
document.querySelector("rtk-ui-provider").showSetupScreen = true; document.querySelector("rtk-ui-provider").meeting = meeting; } initializeMeeting(); </script> </body>
</html>First level split of rtk-meeting using rtk-ui-provider has the following components:
rtk-header is the header component that shows the session name and the session controls.
rtk-stage is the container component that contains the grid and sidebar components.
rtk-grid is the grid component that shows the participants in the session.
rtk-sidebar is the sidebar component that shows the sidebar, in which chat, polls content shows up.
rtk-controlbar is the controlbar component that shows the controls, such as camera, microphone, etc.
rtk-notifications is the notifications component that shows the notifications for the session.
rtk-participants-audio is the audio component that helps you listen other participants in the session.
rtk-dialog-manager is the dialog-manager component that shows the all supported dialogs, such as settings, breakout rooms, etc.
You can split all of these components further. To see more such components, please refer to our components library.
We have our UI Kit open source on GitHub, you can find it here ↗.
import { RealtimeKitProvider, useRealtimeKitClient,} from "@cloudflare/realtimekit-react";import { RtkUiProvider, RtkHeader, RtkStage, RtkGrid, RtkSidebar, RtkControlbar, RtkNotifications, RtkParticipantsAudio, RtkDialogManager, RtkSetupScreen, RtkWaitingScreen, RtkEndedScreen, States,} from "@cloudflare/realtimekit-react-ui";import { useEffect, useState } from "react";
function MeetingContainer() { const [meeting, initMeeting] = useRealtimeKitClient(); const [currentState, setCurrentState] = useState("idle"); const [showSidebar, setShowSidebar] = useState(false);
useEffect(() => { initMeeting({ authToken: "participant_auth_token", }); }, []);
const renderSetupScreen = () => { return <RtkSetupScreen />; };
const renderWaitingScreen = () => { return <RtkWaitingScreen />; };
const renderJoinedScreen = () => { return ( <> <RtkHeader style={{ display: "flex", justifyContent: "space-between" }} /> <RtkStage style={{ flex: 1, flexGrow: 1, flexShrink: 1 }}> <RtkGrid /> <RtkSidebar style={{ position: "fixed", top: "0px", display: showSidebar ? "block" : "none", }} /> </RtkStage> <RtkControlbar style={{ display: "flex", justifyContent: "space-between" }} /> </> ); };
const renderEndedScreen = () => { return <RtkEndedScreen />; };
// Listen for state updates from RtkUiProvider const handleStatesUpdate = (event: { detail: States }) => { const meetingState = event.detail.meeting; const states = event.detail;
// Store states to update your custom UI if (meetingState === "idle" && currentState !== "idle") { setCurrentState("idle"); } else if (meetingState === "setup" && currentState !== "setup") { setCurrentState("setup"); } else if (meetingState === "waiting" && currentState !== "waiting") { setCurrentState("waiting"); } else if (meetingState === "joined" && currentState !== "joined") { setCurrentState("joined"); } else if (meetingState === "ended" && currentState !== "ended") { setCurrentState("ended"); }
// Update sidebar visibility based on state if (states.activeSidebar !== undefined) { setShowSidebar(states.activeSidebar); } };
return ( <RealtimeKitProvider value={meeting}> <RtkUiProvider meeting={meeting} showSetupScreen={true} onRtkStatesUpdate={handleStatesUpdate} style={{ display: "flex", flexDirection: "column", height: "100vh", margin: 0, }} > <div id="meeting-container" style={{ display: "flex", flexDirection: "column", flex: 1, flexGrow: 1, flexShrink: 1, }} > {currentState === "idle" && <div>Meeting is loading...</div>} {currentState === "setup" && renderSetupScreen()} {currentState === "waiting" && renderWaitingScreen()} {currentState === "joined" && renderJoinedScreen()} {currentState === "ended" && renderEndedScreen()} </div> <RtkParticipantsAudio /> <RtkDialogManager /> <RtkNotifications /> </RtkUiProvider> </RealtimeKitProvider> );}
function App() { return <MeetingContainer />;}First level split of RtkMeeting using RtkUiProvider has the following components:
RtkHeader is the header component that shows the session name and the session controls.
RtkStage is the container component that contains the grid and sidebar components.
RtkGrid is the grid component that shows the participants in the session.
RtkSidebar is the sidebar component that shows the sidebar, in which chat, polls content shows up.
RtkControlbar is the controlbar component that shows the controls, such as camera, microphone, etc.
RtkNotifications is the notifications component that shows the notifications for the session.
RtkParticipantsAudio is the audio component that helps you listen other participants in the session.
RtkDialogManager is the dialog-manager component that shows the dialogs for the session.
You can split all of these components further. To see more such components, please refer to our components library.
We have our UI Kit open source on GitHub, you can find it here ↗.
In your app.module.ts, import the RealtimeKitComponentsModule along with all the custom modules you have built using the component library (example shown below).
import { NgModule } from "@angular/core";import { BrowserModule } from "@angular/platform-browser";import { FormsModule } from "@angular/forms";
import { AppComponent } from "./app.component";import { CustomRtkMeetingComponent } from "./components/custom-rtk-meeting.component";import { SetupScreenComponent } from "./components/setup-screen.component";import { InMeetingComponent } from "./components/in-meeting.component";import { MeetingHeaderComponent } from "./components/meeting-header.component";import { MeetingControlBarComponent } from "./components/meeting-control-bar.component";import { MeetingSidebarComponent } from "./components/meeting-sidebar.component";import { MediaPreviewModalComponent } from "./components/media-preview-modal.component";import { AudioPreviewComponent } from "./components/audio-preview.component";import { VideoPreviewComponent } from "./components/video-preview.component";
import { RealtimeKitComponentsModule } from "@cloudflare/realtimekit-angular-ui";
@NgModule({ declarations: [ AppComponent, CustomRtkMeetingComponent, SetupScreenComponent, InMeetingComponent, MeetingHeaderComponent, MeetingControlBarComponent, MeetingSidebarComponent, MediaPreviewModalComponent, AudioPreviewComponent, VideoPreviewComponent, ], imports: [BrowserModule, FormsModule, RealtimeKitComponentsModule], providers: [], bootstrap: [AppComponent],})export class AppModule {}Initialize the meeting in your app.component.ts
import { Component, OnInit, Inject } from "@angular/core";import { DOCUMENT } from "@angular/common";import { StatesService } from "./services/states.service";import RealtimeKitClient from "@cloudflare/realtimekit";
@Component({ selector: "app-root", templateUrl: "./app.component.html", styleUrls: ["./app.component.css"],})export class AppComponent implements OnInit { meeting: any = null;
constructor( private statesService: StatesService, @Inject(DOCUMENT) private document: Document, ) {}
async ngOnInit() { await this.initializeMeeting(); }
private async initializeMeeting() { const searchParams = new URLSearchParams( this.document.defaultView?.location.search, ); const authToken = searchParams.get("authToken");
if (!authToken) { alert( "An authToken wasn't passed, please pass an authToken in the URL query to join a meeting.", ); return; }
if (!this.meeting) { try { // Initialize RealtimeKit client const meeting = await RealtimeKitClient.init({ authToken, });
this.meeting = meeting;
// Expose meeting object to window for debugging Object.assign(this.document.defaultView as any, { meeting: this.meeting, }); } catch (error) { console.error("Failed to initialize meeting:", error); } } }
onRtkStatesUpdate(event: any) { this.statesService.setStates(event.detail); }}<rtk-ui-provider [meeting]="meeting" (rtkStatesUpdate)="onRtkStatesUpdate($event)" [showSetupScreen]="true" style="height: 100%; width: 100%; display: block;"> <app-custom-rtk-meeting></app-custom-rtk-meeting> <rtk-dialog-manager></rtk-dialog-manager></rtk-ui-provider>This is an example of what a custom element built using UI Kit components looks like:
import { Component, OnInit, OnDestroy } from "@angular/core";import { Subject, takeUntil } from "rxjs";import { StatesService, CustomStatesService } from "../services/states.service";import { States } from "@cloudflare/realtimekit-ui";import { CustomStates } from "../types";
@Component({ selector: "app-custom-rtk-meeting", template: ` <rtk-idle-screen *ngIf="states.meeting === 'idle'"></rtk-idle-screen> <app-setup-screen *ngIf="states.meeting === 'setup'"></app-setup-screen> <rtk-waiting-screen *ngIf="states.meeting === 'waiting'" ></rtk-waiting-screen> <rtk-ended-screen *ngIf="states.meeting === 'ended'"></rtk-ended-screen> <app-in-meeting *ngIf="states.meeting === 'joined' || !states.meeting" ></app-in-meeting> `,})export class CustomRtkMeetingComponent implements OnInit, OnDestroy { states: States = { meeting: "idle" } as States; customStates: CustomStates = {}; private destroy$ = new Subject<void>();
constructor( private statesService: StatesService, private customStatesService: CustomStatesService, ) {}
ngOnInit() { this.statesService.states$ .pipe(takeUntil(this.destroy$)) .subscribe((states) => { this.states = states; console.log(states, this.customStates); });
this.customStatesService.customStates$ .pipe(takeUntil(this.destroy$)) .subscribe((customStates) => { this.customStates = customStates; console.log(this.states, customStates); }); }
ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }}First level split of rtk-meeting using rtk-ui-provider has the following components:
rtk-header is the header component that shows the session name and the session controls.
rtk-stage is the container component that contains the grid and sidebar components.
rtk-grid is the grid component that shows the participants in the session.
rtk-sidebar is the sidebar component that shows the sidebar, in which chat, polls content shows up.
rtk-controlbar is the controlbar component that shows the controls, such as camera, microphone, etc.
rtk-notifications is the notifications component that shows the notifications for the session.
rtk-participants-audio is the audio component that helps you listen other participants in the session.
rtk-dialog-manager is the dialog-manager component that shows the all supported dialogs, such as settings, breakout rooms, etc.
You can split all of these components further. To see more such components, please refer to our components library.
We have our UI Kit open source on GitHub, you can find it here ↗.
The following example replaces the group call meeting screen with a custom view controller that uses individual UI Kit components:
import UIKitimport RealtimeKitimport RealtimeKitUI
class CustomFlowDelegate: RtkUIFlowCoordinatorDelegate { // Return nil to use the default setup screen func showSetUpScreen(completion: () -> Void) -> SetupViewControllerDataSource? { return nil }
// Return a custom view controller for group calls func showGroupCallMeetingScreen( meeting: RealtimeKitClient, completion: @escaping () -> Void ) -> UIViewController? { return CustomMeetingViewController(meeting: meeting, completion: completion) }
// Return nil to use the default webinar screen func showWebinarMeetingScreen( meeting: RealtimeKitClient, completion: @escaping () -> Void ) -> UIViewController? { return nil }}
class CustomMeetingViewController: UIViewController { private let meeting: RealtimeKitClient private let completion: () -> Void private var selfListener: RtkEventSelfListener?
init(meeting: RealtimeKitClient, completion: @escaping () -> Void) { self.meeting = meeting self.completion = completion super.init(nibName: nil, bundle: nil) }
required init?(coder: NSCoder) { fatalError("init(coder:) not supported") }
override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black
// Header let header = RtkMeetingHeaderView(meeting: meeting) header.translatesAutoresizingMaskIntoConstraints = false view.addSubview(header)
// Control bar let controlBar = RtkMeetingControlBar( meeting: meeting, delegate: nil, presentingViewController: self ) controlBar.translatesAutoresizingMaskIntoConstraints = false view.addSubview(controlBar)
NSLayoutConstraint.activate([ header.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), header.leadingAnchor.constraint(equalTo: view.leadingAnchor), header.trailingAnchor.constraint(equalTo: view.trailingAnchor), header.heightAnchor.constraint(equalToConstant: 50), controlBar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), controlBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), controlBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), controlBar.heightAnchor.constraint(equalToConstant: 80), ])
// Listen for meeting events selfListener = RtkEventSelfListener(rtkClient: meeting) selfListener?.observeSelfRemoved { [weak self] _ in self?.dismiss(animated: true, completion: self?.completion) } selfListener?.observeSelfMeetingEndForAll { [weak self] _ in self?.dismiss(animated: true, completion: self?.completion) } }}Present the meeting with your custom flow delegate:
let delegate = CustomFlowDelegate()let rtkUI = RealtimeKitUI( meetingInfo: RtkMeetingInfo( authToken: "<auth-token>", enableAudio: true, enableVideo: true ), flowDelegate: delegate)
let controller = rtkUI.startMeeting { self.dismiss(animated: true)}controller.modalPresentationStyle = .fullScreenpresent(controller, animated: true)Individual components available for custom meeting screens:
| Component | Description |
|---|---|
RtkMeetingHeaderView | Header with meeting title, participant count, clock, and recording indicator |
RtkMeetingControlBar | Control bar with microphone, camera, and end meeting buttons |
RtkParticipantTileView | Single participant video tile with avatar fallback and name tag |
RtkVideoView | Raw video rendering view |
RtkAvatarView | Participant avatar with initials |
RtkNameTag | Participant name with microphone status icon |
RtkAudioButtonControlBar | Standalone microphone toggle button |
RtkVideoButtonControlBar | Standalone camera toggle button |
RtkEndMeetingControlBarButton | End/leave meeting button |
RtkEventSelfListener | Observer for audio/video state, removal, reconnection, and permissions |
The following example creates a custom meeting screen using individual UI Kit views in an XML layout with the activate(meeting) pattern:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="#000000">
<com.cloudflare.realtimekit.ui.view.headers.RtkMeetingHeaderView android:id="@+id/headerView" android:layout_width="match_parent" android:layout_height="wrap_content" />
<com.cloudflare.realtimekit.ui.view.grid.RtkGridView android:id="@+id/gridView" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" />
<com.cloudflare.realtimekit.ui.view.controlbars.RtkMeetingControlBarView android:id="@+id/controlBarView" android:layout_width="match_parent" android:layout_height="wrap_content" />
</LinearLayout>import android.os.Bundleimport androidx.appcompat.app.AppCompatActivityimport com.cloudflare.realtimekit.RealtimeKitClientimport com.cloudflare.realtimekit.RtkMeetingRoomEventListenerimport com.cloudflare.realtimekit.models.RtkMeetingInfoimport com.cloudflare.realtimekit.ui.RealtimeKitUIBuilderimport com.cloudflare.realtimekit.ui.RealtimeKitUIInfoimport com.cloudflare.realtimekit.ui.view.controlbars.RtkMeetingControlBarViewimport com.cloudflare.realtimekit.ui.view.grid.RtkGridViewimport com.cloudflare.realtimekit.ui.view.headers.RtkMeetingHeaderView
class CustomMeetingActivity : AppCompatActivity() { private lateinit var headerView: RtkMeetingHeaderView private lateinit var gridView: RtkGridView private lateinit var controlBarView: RtkMeetingControlBarView
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_custom_meeting)
headerView = findViewById(R.id.headerView) gridView = findViewById(R.id.gridView) controlBarView = findViewById(R.id.controlBarView)
val meetingInfo = RtkMeetingInfo(authToken = "<auth-token>") val uiKitInfo = RealtimeKitUIInfo( activity = this, rtkMeetingInfo = meetingInfo, ) val rtkUI = RealtimeKitUIBuilder.build(uiKitInfo) val meeting: RealtimeKitClient = rtkUI.meeting
// Listen for meeting lifecycle events meeting.addMeetingRoomEventListener(object : RtkMeetingRoomEventListener { override fun onMeetingRoomJoinCompleted(meeting: RealtimeKitClient) { runOnUiThread { // Activate all components once the meeting is joined headerView.activate(meeting) gridView.activate(meeting) controlBarView.activate(meeting) } }
override fun onMeetingEnded() { finish() }
override fun onMeetingRoomLeaveCompleted() { finish() } }) }}Individual components available for custom layouts:
| Component | Activate with | Description |
|---|---|---|
RtkGridView | RealtimeKitClient | Paginated participant video grid |
RtkMeetingHeaderView | RealtimeKitClient | Header with title, participant count, clock, and recording indicator |
RtkMeetingControlBarView | RealtimeKitClient | Control bar with mic, camera, more, and leave buttons |
RtkParticipantTileView | RtkMeetingParticipant | Single participant video tile with avatar and name tag |
RtkMicToggleButton | RealtimeKitClient | Standalone microphone toggle |
RtkCameraToggleButton | RealtimeKitClient | Standalone camera toggle |
RtkLeaveButton | RealtimeKitClient | Leave/end meeting button |
RtkAvatarView | RtkMeetingParticipant | Participant avatar |
RtkNameTagView | RtkMeetingParticipant | Name tag with audio indicator |
RtkJoinButton | RealtimeKitClient | Join meeting button for setup screens |
The following example wraps a custom widget tree in RtkProvider and uses individual UI Kit widgets:
import 'package:flutter/material.dart';import 'package:realtimekit_ui/realtimekit_ui.dart';
class CustomMeetingPage extends StatefulWidget { final String authToken; const CustomMeetingPage({required this.authToken, super.key});
@override State<CustomMeetingPage> createState() => _CustomMeetingPageState();}
class _CustomMeetingPageState extends State<CustomMeetingPage> implements RtkMeetingRoomEventListener { late final RealtimekitClient meeting; late final RealtimeKitUIInfo uiKitInfo; String meetingState = 'idle';
@override void initState() { super.initState(); meeting = RealtimekitClient(); uiKitInfo = RealtimeKitUIInfo( RtkMeetingInfo(authToken: widget.authToken), ); meeting.addMeetingRoomEventListener(this); meeting.init( RtkMeetingInfo(authToken: widget.authToken), onSuccess: () => setState(() => meetingState = 'setup'), onError: (error) => setState(() => meetingState = 'error'), ); }
@override void onMeetingRoomJoinCompleted() { setState(() => meetingState = 'joined'); }
@override void onMeetingEnded() { setState(() => meetingState = 'ended'); }
@override void onMeetingRoomLeaveCompleted() { Navigator.of(context).pop(); }
// Required interface methods — no-op for this example @override void onMeetingInitCompleted() {} @override void onMeetingInitFailed(Exception exception) {} @override void onMeetingInitStarted() {} @override void onMeetingRoomJoinStarted() {} @override void onMeetingRoomJoinFailed(Exception exception) {} @override void onMeetingRoomLeaveStarted() {} @override void onActiveTabUpdate(ActiveTab? activeTab) {} @override void onSocketConnectionUpdate(SocketConnectionState state) {}
@override Widget build(BuildContext context) { return RtkProvider( meeting: meeting, uiKitInfo: uiKitInfo, child: Scaffold( backgroundColor: Colors.black, body: _buildForState(), ), ); }
Widget _buildForState() { switch (meetingState) { case 'setup': return Column( children: [ Expanded( child: RtkParticipantTile(meeting.localUser, height: 300, width: 200), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ RtkSelfAudioToggleButton(meeting: meeting), const SizedBox(width: 16), RtkSelfVideoToggleButton(meeting: meeting), const SizedBox(width: 16), RtkJoinButton(meeting: meeting, onMeetingJoined: () {}), ], ), ], ); case 'joined': return Column( children: [ RtkMeetingTitle(meeting: meeting), Expanded( child: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 3 / 4, ), itemCount: meeting.participants.active.length + 1, itemBuilder: (context, index) { final participant = index == 0 ? meeting.localUser : meeting.participants.active[index - 1]; return RtkParticipantTile(participant); }, ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ RtkSelfAudioToggleButton(meeting: meeting), RtkSelfVideoToggleButton(meeting: meeting), RtkLeaveButton(meeting: meeting), ], ), ], ); case 'ended': return const Center(child: Text('Meeting ended', style: TextStyle(color: Colors.white))); default: return const Center(child: CircularProgressIndicator()); } }
@override void dispose() { meeting.removeMeetingRoomEventListener(this); super.dispose(); }}Individual widgets available for custom UIs:
| Widget | Required parameter | Description |
|---|---|---|
RtkSelfAudioToggleButton | RealtimekitClient | Microphone toggle with permission handling |
RtkSelfVideoToggleButton | RealtimekitClient | Camera toggle with permission handling |
RtkJoinButton | RealtimekitClient | Join meeting button with loading state |
RtkLeaveButton | RealtimekitClient | Leave meeting button with confirmation dialog |
RtkParticipantTile | RtkMeetingParticipant | Video tile with avatar fallback, name tag, and audio indicator |
RtkMeetingTitle | RealtimekitClient | Meeting title text |
RtkNameTag | RtkMeetingParticipant | Participant name label |
RtkAudioIndicatorIconWidget | RtkMeetingParticipant | Microphone status icon |
The following example uses RtkUIProvider and individual components with state-based screen rendering, similar to the web React pattern:
import React, { useContext, useEffect } from "react";import { View, Text } from "react-native";import { useRealtimeKitClient, RealtimeKitProvider,} from "@cloudflare/realtimekit-react-native";import { RtkUIProvider, RtkUIContext, RtkHeader, RtkGrid, RtkControlbar, RtkSidebar, RtkDialogManager, RtkSetupScreen, RtkWaitingScreen, RtkEndedScreen,} from "@cloudflare/realtimekit-react-native-ui";
function MeetingScreens({ meeting }) { const { storeStates, setStates } = useContext(RtkUIContext); const currentState = storeStates.meeting;
useEffect(() => { // Listen for meeting events and drive state transitions const onRoomJoined = () => { setStates({ ...storeStates, meeting: "joined" }); }; const onRoomLeft = ({ state }) => { if (state === "ended" || state === "left") { setStates({ ...storeStates, meeting: "ended" }); } }; meeting.self.addListener("roomJoined", onRoomJoined); meeting.self.addListener("roomLeft", onRoomLeft); return () => { meeting.self.removeListener("roomJoined", onRoomJoined); meeting.self.removeListener("roomLeft", onRoomLeft); }; }, [meeting]);
if (currentState === "setup") { return <RtkSetupScreen meeting={meeting} />; }
if (currentState === "waiting") { return <RtkWaitingScreen />; }
if (currentState === "joined") { return ( <View style={{ flex: 1 }}> <RtkHeader meeting={meeting} /> <View style={{ flex: 1 }}> <RtkGrid meeting={meeting} /> {storeStates.activeSidebar && <RtkSidebar meeting={meeting} />} </View> <RtkControlbar meeting={meeting} /> <RtkDialogManager meeting={meeting} /> </View> ); }
if (currentState === "ended") { return <RtkEndedScreen meeting={meeting} />; }
return <Text>Loading...</Text>;}
function CustomMeeting({ authToken }) { const [meet, initMeeting] = useRealtimeKitClient(); const { setStates, storeStates } = useContext(RtkUIContext);
useEffect(() => { initMeeting({ authToken, defaults: { audio: true, video: true }, }); }, [authToken]);
useEffect(() => { if (meet) { setStates({ ...storeStates, meeting: "setup" }); } }, [meet]);
if (!meet) return <Text>Initializing...</Text>;
return ( <RealtimeKitProvider value={meet}> <MeetingScreens meeting={meet} /> </RealtimeKitProvider> );}
export default function App() { return ( <RtkUIProvider> <CustomMeeting authToken="<auth-token>" /> </RtkUIProvider> );}First level split of RtkMeeting has the following components:
| Component | Required prop | Description |
|---|---|---|
RtkHeader | meeting | Header with meeting title, participant count, clock, and recording indicator |
RtkGrid | meeting | Paginated participant video grid |
RtkControlbar | meeting | Control bar with mic, camera, screenshare, and more toggles |
RtkSidebar | meeting | Sidebar with chat, polls, participants, and plugins panels |
RtkDialogManager | meeting | Manages all dialogs (settings, leave confirmation, join stage) |
RtkSetupScreen | meeting | Pre-join screen with video preview and device selection |
RtkWaitingScreen | — | Waiting room screen |
RtkEndedScreen | meeting | Meeting ended screen |
RtkMicToggle | meeting | Standalone microphone toggle button |
RtkCameraToggle | meeting | Standalone camera toggle button |
RtkLeaveButton | — | Leave/end meeting button |
RtkParticipantTile | meeting, participant | Single participant video tile |
RtkNotifications | meeting | Toast notifications for join/leave/chat events |