Add calls, chat and a prebuilt call UI to your Flutter app. Wire the whole call flow in two lines with SocritsCallUI, or drop down to the client callbacks for full control.
Install
1flutter pub add socrits livekit_client permission_handlerAdd platform permissions (Android needs min SDK 21):
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.
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');Option A — zero-glue calling (recommended)
SocritsCallUI wires the entire call flow for you — the ZEGO ZegoUIKitPrebuiltCallInvitationService equivalent. Attach it once after connect() and incoming calls ring with a prebuilt screen, navigate to the call UI on accept, and tear down on hang-up / decline / timeout — no orchestration code.
1import 'package:socrits/socrits.dart';23// 1) Give MaterialApp a navigator key so the SDK can show the ring + call UI.4final navKey = GlobalKey<NavigatorState>();5// MaterialApp(navigatorKey: navKey, home: ...)67// 2) After connect(), wire the whole call flow — ONCE.8SocritsCallUI.instance.attach(9 client: client,10 navigatorKey: navKey,11 selfName: 'Alice',12);13// That's it. Incoming calls now ring with a prebuilt screen, navigate to the14// call UI on accept, and tear down on hang-up / decline / timeout — automatically.Start a call with one method or a drop-in button:
1// Start a call from anywhere:2SocritsCallUI.instance.call(userId: 'bob', displayName: 'Bob', mode: CallMode.video);34// …or drop a button next to a user (the ZegoSendCallInvitationButton equivalent):5SocritsCallButton(inviteeUserId: 'bob', inviteeName: 'Bob', isVideo: true)SocritsCallUI.instance.attach(client, navigatorKey)— one-time setup; also routes offline (native CallKit) Accept/Decline into the same flow..call(userId:, mode:)/SocritsCallButton— place an outgoing call; the prebuiltSocritsRingScreenhandles the rest.accent— theme the ring + call screens to your brand colour.- Need custom billing, a reconnection grace window, or your own call screen? Use Option B below (or the Express tier).
SocritsCallUI you can skip the SocritsCallKit.listen(...) step; it’s wired for you.Option B — manual control
Prefer to drive navigation and billing yourself? Wire the client callbacks directly. Everything below is what SocritsCallUI does under the hood.
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) { /* ring ended before connecting: timeout | cancelled */ };12client.onCallHungUp = (callId) { /* peer hung up a LIVE call (explicit call:end) */ };1314// End a live call (tells the peer immediately, vs a silent drop):15// client.endCall(callId);1617// Offline ringing: register the device's FCM token so Socrits can push a ring18client.registerDevice(fcmToken, platform: 'android');Make & receive a call
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.Render the prebuilt call screen
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.
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 allowVideo: mode == CallMode.video, // voice call → hides Video/Flip, stays audio-only10 startedAt: client.lastStartedAt, // same duration on both sides11 initialCam: mode == CallMode.video,12 initialMic: true,13 initialSpeaker: mode == CallMode.video, // video → speaker, voice → earpiece14 onHangUp: () => Navigator.of(context).pop(),15 ),16 ),17 ));18};Widget parameters
room— the connectedlk.Roomfromaccept()/onCallConnected. Required.title— name shown for the other party.allowVideo— setfalsefor a voice call to hide the Video/Flip controls and keep it audio-only (can’t switch to video). Defaulttrue. Added in 0.1.8.startedAt— server start time (epoch ms) so both parties show the same duration; passclient.lastStartedAt. Added in 0.1.8.initialMic/initialCam— starting mic/camera state (default true).initialSpeaker— start on loudspeaker (true) or earpiece. Defaulttrue; 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 C — 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:
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:
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}If you use SocritsCallUI (Option A), the native Accept/Decline routing below is already done for you — skip to step 4 (registerDevice). Wiring it manually (Option B):
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;onAcceptreconnects (if cold-started) then callsacceptCallById.client.acceptCallById(callId)/rejectCallById(callId)— answer a push-delivered call that had no prioronIncomingCall.registerDevice(fcmToken)— call afterconnect()so Socrits knows where to push.
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.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
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);