call_ui_kit 0.4.0
call_ui_kit: ^0.4.0 copied to clipboard
A ready-to-use Flutter call UI kit for personal audio, video, and group calls.
call_ui_kit #
A ready-to-use Flutter call UI kit that handles personal audio calls, personal video calls, and group calls. Zero external dependencies — only Flutter SDK.
Inspired by WhatsApp call design. Fully customizable through themes and localized strings.
Screenshots #
| Screenshot | Description |
|---|---|
| Personal Audio Call | 1:1 audio call with caller avatar, status text, and controls |
| Personal Video Call | 1:1 video call with draggable PiP, flip camera, and more sheet |
| Screen Share | Group call with screen sharing and participant thumbnails |
| More Sheet | Customizable bottom sheet with encryption label |
| Speaker View | 7+ participants — active speaker prominent with thumbnail row |
| Speaking Border | Animated green border on the active speaker's tile |
Features #
- Personal audio call — caller avatar, call status, speaker toggle
- Personal video call — remote/local video, draggable PiP, camera flip, swap on tap
- Group video call — adaptive grid (2x2, 2x3), speaker view (7+ participants), thumbnail row
- Group audio call — participant tiles with avatars, speaking indicators
- Screen sharing — dedicated layout with screen share banner and stop button
- Auto-hide controls — top bar, bottom bar, and side buttons hide after 4 seconds
- Participants panel — draggable bottom sheet with host actions (mute, remove)
- "More" bottom sheet — customizable actions sheet with encryption label
- Full localization — every user-facing string is configurable via
CallStrings - Full theming — every color is configurable via
CallTheme - Speaking indicators — animated sine-wave bars and glowing tile borders
- Signal strength — 4-level indicator per participant
- PiP view — draggable, corner-snapping, responsive to safe areas
Installation #
Add to your pubspec.yaml:
dependencies:
call_ui_kit: 0.2.0
Quick Start #
Personal Audio Call #
CallScreen(
callerName: 'Sarah Johnson',
callerAvatarUrl: 'https://example.com/avatar.jpg',
callType: CallType.audio,
localParticipant: const CallParticipant(
id: 'local',
displayName: 'You',
isLocalUser: true,
),
isMuted: _isMuted,
isSpeakerOn: _isSpeakerOn,
callStatusText: '02:45',
onEndCall: () => Navigator.pop(context),
onToggleMute: () => setState(() => _isMuted = !_isMuted),
onToggleSpeaker: () => setState(() => _isSpeakerOn = !_isSpeakerOn),
)
Personal Video Call #
CallScreen(
callerName: 'Alex Rivera',
callerAvatarUrl: 'https://example.com/avatar.jpg',
callType: CallType.video,
localParticipant: const CallParticipant(
id: 'local',
displayName: 'You',
isLocalUser: true,
),
localVideoWidget: localVideoView, // your camera widget
remoteVideoWidget: remoteVideoView, // remote camera widget
isMuted: _isMuted,
isCameraOff: _isCameraOff,
isSpeakerOn: _isSpeakerOn,
callStatusText: '04:23',
onEndCall: () => Navigator.pop(context),
onToggleMute: () => setState(() => _isMuted = !_isMuted),
onToggleCamera: () => setState(() => _isCameraOff = !_isCameraOff),
onToggleSpeaker: () => setState(() => _isSpeakerOn = !_isSpeakerOn),
onFlipCamera: () => /* flip camera logic */,
)
Group Video Call #
CallScreen(
callerName: 'Team Meeting',
isGroupCall: true,
callType: CallType.video,
localParticipant: const CallParticipant(
id: 'local',
displayName: 'You',
isLocalUser: true,
isHost: true,
),
participants: [
CallParticipant(
id: '2',
displayName: 'Alex Rivera',
avatarUrl: 'https://example.com/alex.jpg',
videoWidget: alexVideoView, // or null if camera off
isSpeaking: true,
),
CallParticipant(
id: '3',
displayName: 'Priya Sharma',
avatarUrl: 'https://example.com/priya.jpg',
isMuted: true,
isCameraOff: true,
),
// ... more participants
],
isMuted: _isMuted,
isCameraOff: _isCameraOff,
isSpeakerOn: _isSpeakerOn,
callStatusText: '12:07',
onEndCall: () => Navigator.pop(context),
onToggleMute: () => setState(() => _isMuted = !_isMuted),
onToggleCamera: () => setState(() => _isCameraOff = !_isCameraOff),
onToggleSpeaker: () => setState(() => _isSpeakerOn = !_isSpeakerOn),
onAddParticipant: () => /* invite logic */,
onMuteParticipant: (p) => /* mute participant */,
onRemoveParticipant: (p) => /* remove participant */,
)
Screen Sharing #
CallScreen(
callerName: 'Design Review',
isGroupCall: true,
callType: CallType.video,
localParticipant: localParticipant,
participants: participants,
screenShareWidget: screenShareView, // the shared screen widget
isScreenSharing: _isLocalSharing,
onStopScreenShare: () => setState(() => _isLocalSharing = false),
// ... other required params
)
API Reference #
CallScreen #
The main widget. All UI is driven by the parameters you pass — the kit does not manage call state.
Required Parameters
| Parameter | Type | Description |
|---|---|---|
callerName |
String |
Caller or group name shown in the top bar |
localParticipant |
CallParticipant |
The local user's participant data |
onEndCall |
VoidCallback |
Called when end-call button is tapped |
onToggleMute |
VoidCallback |
Called when mute button is tapped |
onToggleSpeaker |
VoidCallback |
Called when speaker button is tapped |
Optional Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
callerAvatarUrl |
String? |
null |
Avatar URL for the caller |
isGroupCall |
bool |
false |
Enables group call layout |
callType |
CallType |
CallType.video |
.audio or .video |
participants |
List<CallParticipant> |
[] |
Remote participants (group call) |
localVideoWidget |
Widget? |
null |
Local camera stream widget |
remoteVideoWidget |
Widget? |
null |
Remote camera stream widget (personal call) |
screenShareWidget |
Widget? |
null |
Screen share stream widget |
isMuted |
bool |
false |
Local mic muted state |
isCameraOff |
bool |
false |
Local camera off state |
isSpeakerOn |
bool |
false |
Speaker active state |
isScreenSharing |
bool |
false |
Local screen sharing state |
isHandRaised |
bool |
false |
Local hand raised state |
showEncryptionLabel |
bool |
true |
Show encryption label in "more" sheet |
theme |
CallTheme |
CallTheme.whatsApp() |
Color theme |
strings |
CallStrings? |
null (English defaults) |
Localized strings |
callStatusText |
String? |
null |
Status text override (e.g. "02:45") |
Optional Callbacks
All optional callbacks — when null, the corresponding button is hidden.
| Callback | Type | Description |
|---|---|---|
onToggleCamera |
VoidCallback? |
Camera toggle button |
onFlipCamera |
VoidCallback? |
Flip camera button (top bar) |
onToggleScreenShare |
VoidCallback? |
Screen share button (bottom bar) |
onStopScreenShare |
VoidCallback? |
Stop button on screen share banner |
onAddParticipant |
VoidCallback? |
Add participant button (right side) |
onEffects |
VoidCallback? |
Effects button (right side) |
onMinimize |
VoidCallback? |
Minimize/PiP button (top bar) |
onRaiseHand |
VoidCallback? |
Raise hand button |
onMuteParticipant |
void Function(CallParticipant)? |
Host mutes a participant |
onRemoveParticipant |
void Function(CallParticipant)? |
Host removes a participant |
moreSheetBuilder |
Widget Function(BuildContext, CallTheme)? |
Custom "more" sheet content |
CallParticipant #
Immutable data model for a single participant. Supports copyWith() and proper equality (==/hashCode).
const CallParticipant({
required String id,
required String displayName,
String? avatarUrl,
bool isMuted = false,
bool isCameraOff = false,
bool isSpeaking = false,
bool isScreenSharing = false,
bool isHost = false,
bool isLocalUser = false,
SignalStrength signalStrength = SignalStrength.excellent,
Widget? videoWidget,
Widget? screenShareWidget,
})
| Field | Description |
|---|---|
id |
Unique identifier for the participant |
displayName |
Name shown on tile and in participant list |
avatarUrl |
Network image URL; falls back to colored initials |
isMuted |
Shows mute icon on tile |
isCameraOff |
Shows avatar instead of video |
isSpeaking |
Activates animated speaking border and indicator |
isScreenSharing |
Shows screen share badge |
isHost |
Shows host badge; enables host actions in panel |
isLocalUser |
Marks as local user |
signalStrength |
.excellent, .good, .poor, or .none |
videoWidget |
The live video stream widget |
screenShareWidget |
The screen share stream widget |
CallTheme #
Controls all colors. Use CallTheme.whatsApp() as a starting point and copyWith() to customize.
CallTheme(
background: Color(0xFF000000),
barBackground: Color(0xFF1C1C1E),
buttonBackground: Color(0xFF2C2C2E),
endCallColor: Color(0xFFE53935),
speakingColor: Color(0xFF25D366),
speakerActiveBackground: Colors.white,
speakerActiveIconColor: Colors.black,
textPrimary: Colors.white,
textSecondary: Color(0xFFAAAAAA),
dividerColor: Color(0xFF3A3A3C),
)
Custom theme example:
final myTheme = const CallTheme.whatsApp().copyWith(
endCallColor: Colors.red,
speakingColor: Colors.blue,
);
CallStrings #
Every user-facing string is configurable. Use CallStrings.english() as default, or provide your own for localization.
CallStrings(
calling: 'Calling...',
cameraIsOff: 'Camera is off',
you: 'You',
endToEndEncrypted: 'End-to-end encrypted',
shareScreen: 'Share screen',
sendMessage: 'Send message',
participants: 'Participants',
shareCallLink: 'Share call link',
cancel: 'Cancel',
stop: 'Stop',
youAreSharingYourScreen: 'You are sharing your screen',
speaking: 'Speaking',
muted: 'Muted',
muteAll: 'Mute all',
invite: 'Invite',
mute: 'Mute',
unmute: 'Unmute',
removeFromCall: 'Remove from call',
pictureInPicture: 'Picture in picture',
addParticipant: 'Add participant',
flipCamera: 'Flip camera',
effects: 'Effects',
isSharingScreen: (name) => '$name is sharing their screen',
participantsCount: (count) => '$count participant${count == 1 ? '' : 's'}',
moreParticipants: (count) => '+$count more',
)
CallType #
enum CallType { audio, video }
SignalStrength #
enum SignalStrength { excellent, good, poor, none }
Group Call Layouts #
The layout is automatically resolved based on participant count:
| Participants | Layout |
|---|---|
| 1-2 | Full screen + PiP |
| 3-4 | 2x2 grid |
| 5-6 | 2x3 grid |
| 7+ | Speaker view (active speaker + thumbnail row) |
| Any + screen share | Screen share view + thumbnail row |
"More" Bottom Sheet #
The moreSheetBuilder callback lets you add custom actions to the "more" bottom sheet. The kit wraps your content with a handle bar, encryption label, and cancel button.
CallScreen(
// ...
moreSheetBuilder: (context, theme) => Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: theme.buttonBackground,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
ListTile(
title: Text('Share Screen', style: TextStyle(color: theme.textPrimary)),
trailing: Icon(Icons.screen_share, color: theme.textPrimary),
onTap: () {
Navigator.pop(context);
// your logic
},
),
ListTile(
title: Text('Send Message', style: TextStyle(color: theme.textPrimary)),
trailing: Icon(Icons.message, color: theme.textPrimary),
onTap: () {
Navigator.pop(context);
// your logic
},
),
],
),
),
)
Exported Widgets #
These widgets are exported for standalone use if needed:
| Widget | Description |
|---|---|
CallAvatar |
Circle avatar with network image or colored initials fallback |
ParticipantTile |
Single participant tile with video, overlays, and speaking border |
FloatingPipView |
Draggable picture-in-picture overlay that snaps to corners |
SpeakingIndicator |
Animated 3-bar sine-wave speaking indicator |
ScreenShareBanner |
Slide-in banner for screen sharing status |
MoreBottomSheet |
Bottom sheet with handle bar, encryption label, and cancel button |
ParticipantsPanel |
Draggable scrollable participant list with host actions |
SignalStrengthIcon |
4-bar signal strength indicator |
Exported Utilities #
| Utility | Description |
|---|---|
GroupCallLayoutResolver |
Resolves grid layout mode based on participant count |
PipSnapCalculator |
Calculates nearest corner snap position for PiP view |
Requirements #
- Flutter SDK >= 3.29.0
- Dart SDK >= 3.11.3
License #
See LICENSE file.