Skip to content
Client SDK

Flutter 

Add calls, chat and a prebuilt call UI to your Flutter app. The SDK wraps signalling (ring/accept), media (LiveKit) and chat behind one small client — with a drop-in UI-Kit, or bring your own.

Install

terminal
1flutter pub add socrits livekit_client permission_handler

Add platform permissions (Android needs min SDK 21):

permissions
1<!-- android/app/src/main/AndroidManifest.xml -->2<uses-permission android:name="android.permission.INTERNET"/>3<uses-permission android:name="android.permission.CAMERA"/>4<uses-permission android:name="android.permission.RECORD_AUDIO"/>5<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>6<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>78<!-- ios/Runner/Info.plist -->9<key>NSCameraUsageDescription</key><string>Video calls</string>10<key>NSMicrophoneUsageDescription</key><string>Voice calls</string>

Initialize & connect

Create the client with your Socrits base URL and connect the signalling socket. The app authenticates with a session token your backend mints — see Server & tokens.

lib/socrits.dart
1import 'package:socrits/socrits.dart';2import 'package:livekit_client/livekit_client.dart' as lk;34final client = Socrits(baseUrl: 'https://api.socrits.com');56// Connect the signalling socket. Your backend issues the user's session token7// (POST /v1/session with your sk_live_ key) — never embed the key in the app.8await client.connect(9  sessionEndpoint: 'https://your-backend.com/socrits/session', // returns { token }10  userId: 'alice',11  userName: 'Alice',12);13// (or, if you already have a token) await client.connect(userToken: token, userId: 'alice');

Set up callbacks

lib/socrits.dart
1// Wire these once, right after connect()2client.onIncomingCall = (IncomingCall call) {3  // show your ringing screen, then accept or reject:4  // final room = await client.accept(call);5  // client.reject(call);6};7client.onCallConnected = (lk.Room room, CallMode mode) {8  // both sides are in the room now — show the call screen (see below)9};10client.onCallRejected = (callId) { /* callee declined */ };11client.onCallEnded    = (reason) { /* other side hung up / left */ };1213// Offline ringing: register the device's FCM token so Socrits can push a ring14client.registerDevice(fcmToken, platform: 'android');

Make & receive a call

lib/socrits.dart
1// Caller: start a call. Returns a callId; the callee gets onIncomingCall.2final callId = await client.invite(3  toUserId: 'bob',4  mode: CallMode.video, // or CallMode.voice5);67// Callee: inside onIncomingCall8final room = await client.accept(call);   // joins the media room9// onCallConnected fires on BOTH sides with the connected lk.Room.

Option A — with the UI-Kit

SocritsCallView is a complete WhatsApp-style call screen: remote video full-screen, your camera in a PiP, a live timer, and Speaker / Flip / Video / Mute / End controls (voice calls show a pulsing avatar). Hand it the connected room and you’re done.

lib/call_screen.dart
1import 'package:socrits/socrits.dart';23client.onCallConnected = (room, mode) {4  Navigator.of(context).push(MaterialPageRoute(5    builder: (_) => Scaffold(6      body: SocritsCallView(7        room: room,8        title: 'Bob',9        initialCam: mode == CallMode.video,10        initialMic: true,11        initialSpeaker: mode == CallMode.video, // video → speaker, voice → earpiece12        onHangUp: () => Navigator.of(context).pop(),13      ),14    ),15  ));16};

Widget parameters

  • room — the connected lk.Room from accept() / onCallConnected. Required.
  • title — name shown for the other party.
  • initialMic / initialCam — starting mic/camera state (default true).
  • initialSpeaker — start on loudspeaker (true) or earpiece. Default true; set it from your ring screen (video → speaker, voice → earpiece). Added in 0.1.7.
  • onHangUp — called when the user taps End or the call ends; pop your screen here.

Option B — without the UI-Kit

Prefer your own design? accept() and onCallConnected give you the raw LiveKit Room. Control tracks and render video yourself with livekit_client:

lib/custom_call.dart
1// You get the raw LiveKit Room from accept() / onCallConnected — build any UI you want.2final lk.Room room = await client.accept(call);34// controls5await room.localParticipant?.setMicrophoneEnabled(true);6await room.localParticipant?.setCameraEnabled(mode == CallMode.video);78// render remote video9final listener = room.createListener();10listener.on<lk.TrackSubscribedEvent>((e) {11  if (e.track is lk.VideoTrack) {12    setState(() => remote = e.track as lk.VideoTrack);13  }14});15// in build():  if (remote != null) lk.VideoTrackRenderer(remote!)1617// end the call18await room.disconnect();

Offline ringing

When the callee’s app is backgrounded or killed its socket is paused, so a socketonIncomingCall never arrives. Socrits instead sends a high-priority FCM data push, and SocritsCallKit turns it into the native full-screen incoming-call screen (over the lock screen on Android, CallKit on iOS). Registering the FCM token alone is not enough — wire these four steps:

lib/main.dart
1// socrits bundles flutter_callkit_incoming; you also need firebase_core +2// firebase_messaging for the push itself.3import 'package:firebase_messaging/firebase_messaging.dart';4import 'package:socrits/socrits.dart';56// 1) Top-level FCM background handler — runs when the app is killed/backgrounded.7@pragma('vm:entry-point')8Future<void> _bg(RemoteMessage m) async {9  await SocritsCallKit.onPushData(m.data); // raises the native full-screen ring10}1112void main() {13  FirebaseMessaging.onBackgroundMessage(_bg);14  // 2) Foreground pushes (harmless if your live socket already rang):15  FirebaseMessaging.onMessage.listen((m) => SocritsCallKit.onPushData(m.data));16  runApp(const MyApp());17}
lib/main.dart
1// 3) Route the OS Accept / Decline — register ONCE, high in the widget tree.2SocritsCallKit.appName = 'MyApp';3SocritsCallKit.listen(4  onAccept: (callId, extra) async {5    // reconnect first if the app was cold-started by the ring:6    await client.connect(userToken: await mintToken(), userId: me);7    await client.acceptCallById(callId); // joins the room → onCallConnected fires8  },9  onDecline: (callId) => client.rejectCallById(callId),10);1112// 4) After connect(), register this device's FCM token so Socrits can ring it:13client.registerDevice(14  await FirebaseMessaging.instance.getToken(),15  platform: 'android',16);
  • SocritsCallKit.onPushData(data) — turns a Socrits FCM data message into the native ring. Safe to call from the background isolate.
  • SocritsCallKit.listen(onAccept, onDecline) — routes the OS Accept/Decline back to your client; onAccept reconnects (if cold-started) then calls acceptCallById.
  • client.acceptCallById(callId) / rejectCallById(callId) — answer a push-delivered call that had no prior onIncomingCall.
  • registerDevice(fcmToken) — call after connect() so Socrits knows where to push.
Upload your Firebase key in the console (required):Socrits can only push to a device token that belongs to your Firebase project, so upload your service-account key once: open your project in the console → Service Management → In-app Chat → Notification configuration → Offline push → Add credentials, and paste your Admin SDK JSON (Firebase Console → Project settings → Service accounts → Generate new private key). Drop that same project’s google-services.json into your app too. Without this, registerDevice() stores a token Socrits can’t reach. Fire a test ring from Development Tools → Push debug.
Tip:flutter_callkit_incoming (bundled with socrits) needs its full-screen-intent activity + broadcast receiver and the call permissions in your Android manifest — see its README. And on Xiaomi / Oppo / Vivo, enable Autostart and set battery to “No restrictions” for your app, or the OS blocks the wake-up push while the app is killed.

Chat

lib/chat.dart
1// 1:1 or group chat rides the same connection2client.onMessage = (ChatMessage m) { /* append to the thread */ };34await client.sendMessage(toUserId: 'bob', text: 'Hey!');5final history = await client.messageHistory(conversationId, limit: 30);6final convos  = await client.conversations();7client.setTyping(conversationId, true);8client.markRead(conversationId);