How we make Flutter work with CallKit Call Directory
Have an idea to make an iOS voice or video chat app with Flutter? Then you will need Apple’s CallKit. It is a universal framework for displaying the system-calling UI for the app’s VoIP services and handling your calling services with other apps. However, today’s talk will not be solely about CallKit, but rather about our project to create a Flutter plugin and make Flutter work with CallKit.
What is CallKit?
CallKit, introduced in 2016, is a component of the iOS SDK that allows third-party apps to integrate video and voice calls. It means the framework helps display the call UI as native. Besides, it can handle audio interruptions and “do not disturb” mode.
CallKit provides a set of features to make the app more user-centric and deliver a positive user experience.
- Locked and Unlocked status when receiving calls: the incoming calls appear over the Home and Lock screen instead of push notifications.
- Calls within the app, directly in the Apple phone app, or via a link.
- System-level integration features — mute, do-not-disturb, vibration-only modes.
- Call-blocking features such as matching the sender’s number with the iPhone user blocked list and inserting a custom list of blocked numbers.
- Caller identification through extracting his name from the contact list.
- External interruption notifications and correct termination of calls, for example, when the user has a bad connection or switches to another call.
- User security and privacy.
What’s with CallKit on Flutter?
Being part of the iOS SDK, CallKit can still be accessed from Flutter through interaction with native code. The framework’s functionality cannot be utilized unless you connect a third-party plugin that wraps Flutter’s interaction with iOS. But you can also go the other way, as shown in the example below.
CallKit Flutter solutions
So how did we come up with this Flutter plugin for the integration of our Flutter application for VoIP calls with the system?
Any existing third-party solution to this point had certain issues. Those plugins used the CallKit API in their own API, losing the native feel, flexibility, and some features. Such plugins, which mostly have little to no documentation, couldn’t work with modern and fast-growing Flutter.
There was a clear goal to preserve CallKit interfaces and architecture, have all the flexibility, and use the original documentation. The solution was to move CallKit to Dart, keeping all the mechanisms of interaction and the hierarchy of classes.
Since communication between Flutter and iOS is asynchronous, it was challenging and time-consuming to implement some details that required synchronous communication on one side or the other.
For example, the CXProviderDelegate.provider(_:execute:) native CallKit API requires synchronously returning a Bool value. Every new CXTransaction calls this method. To process the transaction, you can return true. By returning false, we get the default behavior, in which for each CXAction contained in the transaction, the corresponding handler method in the CXProviderDelegate will be called.
We declared this API in the Dart code, returning true in the native code, and moving transaction control to the Dart code, performing manual or automatic CXTransaction processing.
One of the key achievements is the Call Directory App Extension support. This system extension, a separate iOS app target, runs independently at the request of the system and enables identifying and blocking numbers. When the system receives an incoming call, it looks for the caller’s number in the contact or blocked list. If the number is missing, the system requests data from Call Directory.
Call Directory Extension in Flutter
The problem with Call Directory Extension is writing native code, which Flutter doesn’t support. This feature works in the extension, launched by the system, and runs for a short time, without depending on Flutter. Therefore, in order to enable this feature in the plugin, the user needs to create an app extension and data storage on his own.
To execute this functionality, here are the things to consider:
- Transfer CXCallDirectoryManager class interface;
- Decide on the app extension and its numbers storage;
- Decide on the data transfer method from the Dart to native code and back to manage the numbers from the Flutter app.
Transfer CX Call Directory Manager interfaces to Flutter
Here is what is needed to be transferred.
Recreate CXCallDirectoryManager.EnabledStatus enum with Dart.
Next, declare the class and methods.
For communication between Flutter and iOS, use FlutterMethodChannel .
Create a MethodChannel object on Flutter
Subscribe the iOS plugin class to the FlutterPlugin protocol.
Create a FlutterMethodChannel with the same identifier to initialize the plugin.
Use this channel to call iOS methods from Flutter.
Now, check out the Dart methods’ implementation and the plugin’s native part using the getEnabledStatus .
Call MethodChannel.invokeMethod on Flutter with arguments and process the result.
Pass the method name and the extensionIdentifier argument to MethodChannel.invokeMethod. Convert the result from the int type to FCXCallDirectoryManagerEnabledStatus. Also, handle PlatformException for an error in native code.
On the iOS side, move to the platform code and implement the backend.
Calls through FlutterMethodChannel go to the handleMethodCall:result: method.
By using the previously passed identifier, it is possible to determine the method called, get the arguments from it, and execute the main part of the code.
Implement the rest two FCXCallDirectoryManager methods
Well done! CallDirectoryManager has been successfully implemented.
App Extension and number storage
For a simple storage version use UserDefaults wrapped in propertyWrapper .
Pass numbers from Flutter to iOS
Once the extension is connected to the storage and CallDirectoryManager methods are implemented, it’s time to take numbers from Flutter and put them in the platform storage or vice versa.
The best way is to make an API to pass numbers through the framework.
Here is what you need:
- Add blocked/identifiable numbers to the storage
- Delete blocked/identifiable numbers from the repository
- Request blocked/identifiable numbers from the repository
Connect a database of numbers with the plugin to enable Flutter access to the storage on the iOS side.
Get identifiable numbers
To sum it up
This article was created to show you how to use and identify numbers through the native Call Directory in Flutter.
Follow the steps to move the CallDirectoryManager interface and pass the numbers from Flutter to iOS, all presented with visuals for easy implementation.