All call lifecycle events are delivered through the FlutterCallkitIncoming.onEvent broadcast stream. Subscribe to it as early as possible — ideally in main() or your root widget — so no events are missed while the app is running.
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';FlutterCallkitIncoming.onEvent.listen((CallEvent? event) { switch (event!.event) { case Event.actionCallIncoming: // Received an incoming call break; case Event.actionCallStart: // Started an outgoing call // Show your calling screen in Flutter break; case Event.actionCallAccept: // User accepted the incoming call // Show your calling screen in Flutter break; case Event.actionCallDecline: // User declined the incoming call break; case Event.actionCallEnded: // Call ended (incoming or outgoing) break; case Event.actionCallTimeout: // Incoming call timed out (missed) break; case Event.actionCallCallback: // User tapped "Call back" in the missed call notification (Android) break; case Event.actionCallConnected: // Call connected (WebRTC/P2P established) break; case Event.actionCallToggleHold: // iOS only: hold state toggled break; case Event.actionCallToggleMute: // iOS only: mute state toggled break; case Event.actionCallToggleDmtf: // iOS only: DTMF tone sent break; case Event.actionCallToggleGroup: // iOS only: call group toggled break; case Event.actionCallToggleAudioSession: // iOS only: audio session state changed break; case Event.actionDidUpdateDevicePushTokenVoip: // iOS only: VoIP push token has been updated break; case Event.actionCallCustom: // Custom action fired from native code break; }});
Every CallEvent carries a body map containing the call parameters that were passed to showCallkitIncoming or startCall, including any extra data.
FlutterCallkitIncoming.onEvent.listen((CallEvent? event) { if (event == null) return; // Access call data from the event body final body = event.body as Map<String, dynamic>; final callId = body['id'] as String?; final callerName = body['nameCaller'] as String?; final extra = body['extra'] as Map<String, dynamic>?; print('Event: ${event.event}, call id: $callId, caller: $callerName');});
When using Firebase Cloud Messaging to trigger calls, the background message handler runs in an isolate. Annotate it with @pragma('vm:entry-point') to prevent the Dart VM from tree-shaking the function.
@pragma('vm:entry-point')Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(); // Show the incoming call — onEvent listeners in other isolates // will not receive these events, handle logic here directly. final uuid = const Uuid().v4(); await FlutterCallkitIncoming.showCallkitIncoming(CallKitParams( id: uuid, nameCaller: message.data['nameCaller'] ?? '', handle: message.data['handle'] ?? '', type: int.tryParse(message.data['type'] ?? '0') ?? 0, ));}
Event stream listeners registered in the main isolate do not receive events fired in a background isolate. Handle any required logic (API calls, notifications) directly inside the background handler.