BaxCloud Calls UI Kit

Flutter UI kit for 1:1 and group audio/video calls with native ringing UI, server-mediated invites, and bring-your-own FCM / APNs VoIP push.

Package: baxcloud_calls_uikit_sdk
Requires: baxcloud_core · Flutter 3.10+

Features

  • Outgoing & incoming calls (audio / video)
  • Group invites via toUserIds
  • Native ringing UI (iOS system call UI, full-screen incoming on Android)
  • FaceTime-style in-call UI with configurable layouts (faceTime / grid / carousel / audio)
  • Accept / decline / cancel / end over the BaxCloud Calls API
  • Push registration helpers (android / ios / ios_voip)
  • In-call UI via BaxCallView (or bring your own screen)
  • BaxCallController for mute / camera / flip / speaker / hang-up
  • Call lifecycle events stream
  • Automatic analytics device metadata (via baxcloud_core)
  • Native CallKit / notification strings overridable via BaxCallStrings (i18n)

Documentation

Resource Link
Calls UI Kit guide (install, native ringing, push, events) baxcloud.tech/docs/uikits/calls
Push setup (FCM + APNs VoIP) Push setup section
UI Kits overview baxcloud.tech/docs/uikits
API authentication baxcloud.tech/docs/api/authentication
Dashboard baxcloud.tech/dashboard

Installation

dependencies:
  baxcloud_core: ^0.1.5
  baxcloud_calls_uikit_sdk: ^0.1.1
flutter pub get

Use a client key (bax_pk_…) with the appropriate media / calls scopes from the dashboard.

Quick start

import 'package:baxcloud_core/baxcloud_core.dart';
import 'package:baxcloud_calls_uikit_sdk/baxcloud_calls_uikit_sdk.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await BaxCalls.initialize(
    config: BaxConfig(
      projectId: 'your-project-id',
      apiKey: 'bax_pk_your_client_key',
      // Optional analytics overrides:
      // appVersion: '1.2.3',
      // buildNumber: '45',
      // bundleId: 'com.example.app',
    ),
    localUser: const BaxcloudUser(
      userId: 'u1',
      name: 'Ada',
      avatarUrl: 'https://example.com/ada.png',
      metadata: {'role': 'caller', 'locale': 'en'},
    ),
    theme: const BaxCallTheme(appName: 'MyApp'),
  );

  runApp(const MyApp());
}

Override native CallKit / notification copy for translations via BaxCallTheme.strings:

theme: BaxCallTheme(
  appName: 'MyApp',
  strings: BaxCallStrings(
    textAccept: 'Aceitar',
    textDecline: 'Recusar',
    missedCallSubtitle: 'Chamada perdida',
    missedCallCallback: 'Retornar',
    callingSubtitle: 'Chamando…',
    incomingCallChannelName: 'Chamadas recebidas',
    missedCallChannelName: 'Chamadas perdidas',
  ),
),

Or patch defaults: const BaxCallStrings().copyWith(textAccept: 'Aceitar'). You can also assign BaxCalls.instance.theme = … when the locale changes (applies to the next ring / call).

Update local user

await BaxCalls.updateLocalUser(
  BaxCalls.instance.localUser!.update(
    name: 'Ada Lovelace',
    metadata: {'locale': 'pt-BR'},
  ),
);

// Or patch fields directly
await BaxCalls.updateLocalUserFields(
  name: 'Ada Lovelace',
  metadata: {'locale': 'pt-BR'}, // merged into existing map
);

Start a 1:1 call

final invitation = await BaxCalls.startOutgoing(
  toUserId: 'u2',
  callType: BaxCallType.video,
);

// Navigate to in-call UI when accepted / connected (see Events).

Start a group call

await BaxCalls.startOutgoing(
  toUserIds: ['u2', 'u3', 'u4'],
  callType: BaxCallType.audio,
);

Show an incoming call from push

// FCM data or PushKit payload map
await BaxCalls.showIncoming(
  BaxIncomingCall.fromPush(messageData),
);

In-call UI

Defaults are FaceTime-style for 1:1 video: front camera, full-screen remote, local PiP, floating circular controls.

BaxCallScope(
  config: config,
  child: BaxCallView(
    roomName: invitation.roomName,
    user: localUser,
    callType: invitation.callType,
    isGroup: invitation.isGroup,
    isHost: true, // caller; callees typically omit / use false
    canJoinWithNoHost: true,
    uiConfig: const BaxCallUiConfig(
      layout: BaxCallLayout.faceTime, // or grid / carousel / audio
      cameraFacing: BaxCallCameraFacing.front,
      fullScreen: true,
      showLocalPip: true,
      controlsStyle: BaxCallControlsStyle.floating, // or .bar
      // hangUpColor: Color(0xFFFF3B30),
    ),
  ),
);
Option Values Default
layout faceTime, grid, carousel, audio Auto (faceTime / grid / audio)
cameraFacing front, back front
controlsStyle floating, bar floating
fullScreen bool true
showLocalPip bool true
showDuration bool true (live m:ss timer)
showConnectionStatus bool true (Video/Audio · Secure chips)

Presets: BaxCallUiConfig.faceTime(), .groupGrid(), .audioOnly() — pass named overrides on the preset.

Call controller (mute / camera / speaker)

While a call is connected, drive media from your own buttons, app logic, or overlays:

final c = BaxCalls.instance.activeController;
await c?.setMuted(true);
await c?.toggleCamera();
await c?.flipCamera();
await c?.setSpeakerOn(true);
await c?.hangUp();

Inside BaxCallView / inCallBuilder, prefer:

  • session.controller — same API
  • BaxCallController.of(context) / maybeOf(context) under the call tree

BaxCallController is a ChangeNotifier — listen with AnimatedBuilder / ListenableBuilder for reactive UI.

Bring your own design

Four levels of customization:

  1. Theme / layout — uiConfig (colors, FaceTime vs grid, camera facing)
  2. Partial — controlsBuilder and/or participantTileBuilder
  3. Full chrome — inCallBuilder with kit media + your controls (recommended)
  4. Own navigation only — skip BaxCallView after accept / connected (you own token + media)

The kit always joins the room when you use BaxCallView. Embed session.media and use session.controller — do not open a second media connection with session.credentials.

BaxCallView(
  roomName: roomName,
  user: user, // BaxcloudUser
  callType: BaxCallType.video,
  inCallBuilder: (context, session) {
    return Stack(
      fit: StackFit.expand,
      children: [
        session.media, // full-bleed video + name/duration chrome
        Align(
          alignment: Alignment.bottomCenter,
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              IconButton(
                icon: Icon(session.controller.isMuted ? Icons.mic_off : Icons.mic),
                onPressed: () => session.controller.toggleMute(),
              ),
              IconButton(
                icon: const Icon(Icons.call_end, color: Colors.red),
                onPressed: () => session.hangUp(),
              ),
            ],
          ),
        ),
      ],
    );
  },
);

See In-call UI design and UI design.

Listen to events

BaxCalls.instance.events.listen((event) {
  switch (event.type) {
    case BaxCallEventType.incoming:
    case BaxCallEventType.outgoing:
    case BaxCallEventType.ringing:
    case BaxCallEventType.accepted:
    case BaxCallEventType.connected:
    case BaxCallEventType.declined:
    case BaxCallEventType.cancelled:
    case BaxCallEventType.ended:
    case BaxCallEventType.missed:
    case BaxCallEventType.timeout:
    case BaxCallEventType.muteChanged:
    case BaxCallEventType.cameraChanged:
    case BaxCallEventType.holdChanged:
    case BaxCallEventType.error:
      // Update your UI / navigation
      break;
  }
});

Push notifications (bring your own)

BaxCloud does not ship Firebase or Apple certificates. Configure optional credentials under Project → Features → Call push in the dashboard, then wire your app:

  1. Register device tokens after BaxCalls.initialize:
await BaxCalls.registerPushToken(
  fcmToken,
  platform: BaxPushPlatform.android,
);

await BaxCalls.registerPushToken(
  voipToken,
  platform: BaxPushPlatform.iosVoip,
);
  1. On a call invite push (type == bax_call_invite), call BaxCalls.showIncoming(...).
  2. Check what the server can deliver:
final caps = await BaxCalls.refreshCapabilities();
// caps.fcmConfigured / caps.apnsVoipConfigured
// caps.coldStartAndroid / caps.coldStartIos

Without project FCM / APNs VoIP credentials, invites still return a pushPayload — your app or backend must deliver it. Full walkthrough: Push setup.

Foreground incoming poll (no-push fallback)

When FCM / PushKit is not configured (or for dev / two-device testing while the app is open), the kit can poll for ringing invites:

// Auto-starts after initialize() only when the project has no server push.
// Force on for dev / example apps:
BaxCalls.setIncomingPollEnabled(true);

Production apps with push should disable poll after registering tokens:

await BaxCalls.registerPushToken(fcmToken, platform: BaxPushPlatform.android);
BaxCalls.setIncomingPollEnabled(false);

Push and poll both call showIncoming — the kit deduplicates by invitationId so you won't get double rings. Poll does not replace push for background/killed state. Details: Incoming poll.

Platform setup

iOS

  • Camera & microphone usage descriptions in Info.plist
  • Background modes: audio, voip, remote-notification
  • PushKit + VoIP capability for killed-state ringing

Android

  • CAMERA, RECORD_AUDIO, INTERNET, POST_NOTIFICATIONS
  • Full-screen intent + foreground service (PHONE_CALL, CAMERA, MICROPHONE) / MANAGE_OWN_CALLS as required by your target SDK

See the platform section in the docs.

API surface (high level)

App API Purpose
BaxCalls.initialize Config + local user + native ringing bridge
BaxCalls.startOutgoing Invite 1:1 or group
BaxCalls.showIncoming Ring from push / in-app
BaxCalls.accept / decline / cancel / end Call actions
BaxCalls.registerPushToken Register FCM / APNs / VoIP tokens
BaxCalls.setIncomingPollEnabled Enable / disable foreground invite poll
BaxCalls.refreshCapabilities Server push capability flags
BaxCallView Default in-call media UI
BaxCalls.instance.events Lifecycle stream
BaxCallTheme / BaxCallStrings Native ring chrome + i18n copy

See the Calls guide for the full lifecycle. Prefer kit methods over calling HTTP yourself.

Example app

cd example
flutter run \
  --dart-define=BAXCLOUD_PROJECT_ID=… \
  --dart-define=BAXCLOUD_API_KEY=bax_pk_…

Support

License

See LICENSE.

Libraries

baxcloud_calls_uikit_sdk
BaxCloud Call UI Kit.
version