Kayla Agora Voice Call
A highly customizable and flexible Agora voice call package for Flutter applications.
Features
- 📱 Complete voice call solution with Agora RTC Engine
- 🎨 Highly customizable UI
- 🔌 Flexible integration with any state management solution
- 💾 Support for various database backends (Firebase, REST API..)
- 📞 CallKit integration for native call UI
- 🔄 Call state management and events
- 📊 Call history tracking
- 🛠️ Extensive configuration options
Installation
dependencies:
kayla_agora_voice_call: ^0.1.0
Getting Started
1. Initialize the package
First, you need to configure and initialize the call service.
import 'package:kayla_agora_voice_call/kayla_agora_voice_call.dart';
// Configure the call service
final callConfig = KaylaCallConfig(
agoraAppId: 'YOUR_AGORA_APP_ID',
callTimeoutDuration: 60, // seconds
theme: KaylaCallTheme(
primaryColor: Colors.blue,
backgroundColor: Colors.black,
textColor: Colors.white,
acceptCallColor: Colors.green,
declineCallColor: Colors.red,
),
tokenGenerator: (channelId, userId) async {
// Generate or fetch your Agora token here
// This could be a call to your backend server
final response = await http.get(
Uri.parse('https://your-api.com/generate-token?channelId=$channelId&userId=$userId'),
);
return response.body;
},
);
// Create and initialize the call service
final callService = KaylaCallService(
config: callConfig,
// Optional: provide a database implementation
database: MyFirebaseCallDatabase(),
);
await callService.initialize();
2. Start a call
To start a call, use the startCall
method:
final call = await callService.startCall(
callerId: currentUser.id,
callerName: currentUser.name,
receiverId: otherUser.id,
receiverName: otherUser.name,
callerProfilePic: currentUser.profilePic,
receiverProfilePic: otherUser.profilePic,
);
// Show the call screen
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => KaylaCallScreen(
callService: callService,
call: call!,
uiConfig: CallUIConfig(
showCallerProfilePicture: true,
showCallerName: true,
showCallDuration: true,
useBlurredBackground: true,
useBackgroundGradient: true,
backgroundGradientColors: [
Colors.blue.shade900,
Colors.black,
],
),
onCallEnded: () {
Navigator.pop(context);
},
),
),
);
3. Handle incoming calls
To handle incoming calls, show the incoming call UI:
callService.callEvents.listen((event) {
if (event == CallEvent.callKitAccept) {
// Get the call from the event data and navigate to the call screen
final callId = callService.currentCall?.callId;
if (callId != null) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => KaylaCallScreen(
callService: callService,
call: callService.currentCall!,
),
),
);
}
}
});
// Show incoming call UI (usually triggered by a push notification)
await callService.showIncomingCallUI(
callId: incomingCallData.callId,
callerName: incomingCallData.callerName,
callerId: incomingCallData.callerId,
callerImageUrl: incomingCallData.callerProfilePic,
);
4. Implement a database
For call history and state persistence, implement a database:
class MyFirebaseCallDatabase implements CallDatabaseInterface {
final FirebaseFirestore _firestore;
MyFirebaseCallDatabase({FirebaseFirestore? firestore})
: _firestore = firestore ?? FirebaseFirestore.instance;
@override
Future<void> saveCall(CallModel call) async {
await _firestore.collection('calls').doc(call.callId).set(call.toMap());
}
@override
Future<void> updateCall(CallModel call) async {
await _firestore.collection('calls').doc(call.callId).update(call.toMap());
}
@override
Future<void> updateCallStatus(String callId, CallStatus status) async {
await _firestore.collection('calls').doc(callId).update({
'status': status.name,
if (status == CallStatus.completed ||
status == CallStatus.missed ||
status == CallStatus.rejected)
'endTime': DateTime.now().toIso8601String(),
});
}
@override
Future<CallModel?> getCall(String callId) async {
final doc = await _firestore.collection('calls').doc(callId).get();
if (doc.exists && doc.data() != null) {
return CallModel.fromMap(doc.data()!);
}
return null;
}
@override
Future<List<CallModel>> getCallsForUser(String userId, {int limit = 20}) async {
final query = await _firestore.collection('calls')
.where(Filter.or(
Filter.equalTo('callerId', userId),
Filter.equalTo('receiverId', userId),
))
.orderBy('startTime', descending: true)
.limit(limit)
.get();
return query.docs
.map((doc) => CallModel.fromMap(doc.data()))
.toList();
}
@override
Future<List<CallModel>> getCallsBetweenUsers(String user1Id, String user2Id, {int limit = 20}) async {
final query = await _firestore.collection('calls')
.where(Filter.or(
Filter.and(
Filter.equalTo('callerId', user1Id),
Filter.equalTo('receiverId', user2Id),
),
Filter.and(
Filter.equalTo('callerId', user2Id),
Filter.equalTo('receiverId', user1Id),
),
))
.orderBy('startTime', descending: true)
.limit(limit)
.get();
return query.docs
.map((doc) => CallModel.fromMap(doc.data()))
.toList();
}
}
Customizing the UI
You can fully customize the call screen UI:
KaylaCallScreen(
callService: callService,
call: call,
uiConfig: CallUIConfig(
// Basic configuration
showCallerProfilePicture: true,
showCallerName: true,
showCallDuration: true,
useBlurredBackground: true,
showMuteButton: true,
showSpeakerButton: true,
// Custom UI elements
avatarBuilder: (profileUrl, name) {
return CircleAvatar(
backgroundImage: profileUrl != null ? NetworkImage(profileUrl) : null,
backgroundColor: Colors.purple,
radius: 60,
child: profileUrl == null ? Text(name[0], style: TextStyle(fontSize: 40)) : null,
);
},
callerInfoBuilder: (name, status) {
return Column(
children: [
Text(name, style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold)),
Text(status ?? '', style: TextStyle(fontSize: 18)),
],
);
},
// Custom action buttons
customActionButtons: [
CallActionButton(
icon: Icons.message,
color: Colors.white,
label: 'Message',
onPressed: () {
// Handle message button press
},
),
],
// Custom control buttons
customControlButtons: [
CallControlButton(
icon: Icons.mic_off,
color: Colors.white,
backgroundColor: Colors.grey,
label: 'Mute',
onPressed: () {
callService.toggleMute();
},
),
CallControlButton(
icon: Icons.call_end,
color: Colors.white,
backgroundColor: Colors.red,
label: 'End',
onPressed: () {
callService.endCall();
},
),
],
// Background styling
useBackgroundGradient: true,
backgroundGradientColors: [
Colors.blue.shade900,
Colors.black,
],
),
),
CallKit Integration
The package integrates with CallKit on iOS and the equivalent on Android for a native call experience:
// Show native call UI for incoming calls
await callService.showIncomingCallUI(
callId: callId,
callerName: callerName,
callerId: callerId,
callerImageUrl: callerImageUrl,
);
// Listen for CallKit events
callService.callEvents.listen((event) {
switch (event) {
case CallEvent.callKitAccept:
// Handle call accept from CallKit
break;
case CallEvent.callKitDecline:
// Handle call decline from CallKit
break;
case CallEvent.callKitEnd:
// Handle call end from CallKit
break;
case CallEvent.callKitTimeout:
// Handle call timeout from CallKit
break;
default:
break;
}
});
Managing Calls
Control ongoing calls with these methods:
// Mute/unmute microphone
await callService.toggleMute();
// Enable/disable speaker
await callService.toggleSpeaker();
// End the current call
await callService.endCall();
// Accept an incoming call
await callService.acceptCall(callId);
// Decline an incoming call
await callService.declineCall(callId);
// Check call status and properties
final isMuted = callService.isMuted;
final isSpeakerOn = callService.isSpeakerOn;
final callDuration = callService.callDuration;
final isCallInProgress = callService.isCallInProgress;
final callStatus = callService.callStatus;
Permissions
The package automatically handles microphone permissions, but you can also check them manually:
final hasPermissions = await callService.checkPermissions();
if (!hasPermissions) {
// Handle permission denied
}
Events
Listen to call events:
callService.callEvents.listen((event) {
switch (event) {
case CallEvent.initialized:
print('Call service initialized');
break;
case CallEvent.initiating:
print('Initiating call');
break;
case CallEvent.started:
print('Call started');
break;
case CallEvent.joined:
print('Joined call');
break;
case CallEvent.remoteUserJoined:
print('Remote user joined');
break;
case CallEvent.remoteUserLeft:
print('Remote user left');
break;
case CallEvent.ended:
print('Call ended');
break;
case CallEvent.error:
print('Call error');
break;
default:
break;
}
});
License
This package is licensed under the MIT License.
Credits
Developed by Kayla.