Back to Blog

Build a Speed Dating App using the Agora Flutter SDK

“Did it hurt when you fell from heaven?” — Anonymous

Just like that pickup line, dating apps are getting insipid. Dating apps do an amazing job of matching users based on their interests, hobbies, taste, etc. But to keep the user engaged, it takes more than just finding them a match.

User interaction is the key to user engagement. If “less is more,” then less text means more interaction. In order to get to know someone, you need more than texting and looking at their photos (assuming that they in fact use their own photos).

In this tutorial, I will walk you through a use case where video calling can be integrated into a dating app to increase user engagement — and thus enable the user to find a good match.

Prerequisites

If you’re new to Flutter, install the Flutter SDK from here.

Building the User Authentication Page

To begin with, I have created a simple login/signup form that takes two inputs: email ID and password. You can customize this interface for your needs.

Login/Sign-up page

Here, I am using Firebase for user authentication and to save the user profile. The user profile consists of:

  • Email ID
  • UID
  • List of interests

Matching Users Based on Their Interests

For the sake of this demo, I have taken a small example of six categories from which people can select and complete their profile. These interests are then matched with all the users in the database. If two users have shared interests, they are added to the same list.

Select Your Interests

To find a match for a user, I map their interests with the interests of the other users. Users whose interests overlap are matched and added to a matchedUsers list with their UID and availability status.

void matchUserInterests() async {
int count = 0;
var user = FirebaseAuth.instance.currentUser;
var db = await FirebaseFirestore.instance.collection('user').get();
int index = db.docs.indexWhere((element) => element.data()['email'] == user.email);
await FirebaseFirestore.instance.collection('user').get().then((val) {
for (var i = 0; i < val.docs.length; i++) {
for (var j = 0; j < val.docs[i].data()['interests'].length; j++) {
if (i != index) {
if (val.docs[i].data()['interests'].contains(val.docs[index].data()['interests'][j])) {
count++;
}
}
}
if (count >= 1) {
value = value.copyWith(matchedUsers: [...value.matchedUsers, AgoraUser(uid: val.docs[i].data()['uid'], isAvailable: true)]);
}
count = 0;
}
});
}

Camera and Microphone Test

Before joining the video call with the matched users, I have added a screen to test your device camera and microphone.

Camera and mic test

For testing the camera, we initialize the Agora RtcEngine and then use the startPreview() method.

void startVideoPreview() async {
await value.engine.enableVideo();
await value.engine.startPreview();
}

For testing the microphone, I use another plug-in, mic_stream, which returns the level of audio received by the microphone. Using this, we make audio bars to represent the state of the microphone.

void volumeListener() async {
// Init a new Stream
Stream<List<int>> stream = await MicStream.microphone(sampleRate: 44100);
// Start listening to the stream
listener = stream.listen((samples) {
double tempVolume1 = 0;
double tempVolume2 = 0;
double tempVolume3 = 0;
setState(() {
tempVolume1 = volumeHeight1;
volumeHeight1 = samples[0].toDouble();
tempVolume2 = volumeHeight2;
volumeHeight2 = tempVolume1;
tempVolume3 = volumeHeight3;
volumeHeight3 = tempVolume2;
volumeHeight4 = tempVolume3;
});
});
}
view raw mic_test.dart hosted with ❤ by GitHub

Call Page

For the call page, I have gone with a UI similar to Tinder, where the users can swipe right or left depending on whether they like the person on the screen or not.

So we will begin by setting up the video view for the local and remote users. Here, we have gone with a simple UI where the local user screen is stacked on top of the remote user view.

/// Helper function to get list of native views
List<Widget> _getRenderViews() {
final List<StatefulWidget> list = [];
list.add(rtc_local_view.SurfaceView());
widget.controller.value.matchedUsers.forEach((AgoraUser agoraUser) {
if (agoraUser.isAvailable) {
list.add(rtc_remote_view.SurfaceView(uid: agoraUser.uid));
agoraUser.copyWith(isAvailable: false);
}
});
return list;
}
/// Video view wrapper
Widget _videoView(view) {
return Expanded(child: Container(child: view));
}
Widget _localVideoView(view) {
return Container(
height: 150,
width: 120,
child: view,
);
}

Video View

This video call between the two users occurs in a Tinder UI fashion. So as soon as a person swipes the other user right or left, they join a call with another matched user. For the Tinder-like UI, we are using a plug-in called tcard.

List<Widget> tinderCards() {
final List<Widget> tcards = List.generate(widget.controller.value.matchedUsers.length, (index) {
final views = _getRenderViews();
return Container(
child: Stack(
children: <Widget>[
_videoView(rtc_remote_view.SurfaceView(uid: widget.controller.value.matchedUsers[index].uid)),
Align(alignment: Alignment(0.95, -0.95), child: _localVideoView(views[0])),
],
));
});
return tcards;
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: TCard(
controller: _controller,
lockYAxis: true,
cards: tinderCards(),
onBack: (index, info) async {
await widget.controller.leaveVideoChannel(index);
},
onForward: (index, info) async {
info.direction == SwipDirection.Left
? await widget.controller.leaveVideoChannel(index)
: await widget.controller.onForwardAction(index);
},
)
);
}

Agora Video Call Cards

Here’s what happens when a user swipes right or left:

  • Left swipe: The user leaves the current channel and then joins a new channel to interact with another match.
Future<void> leaveVideoChannel(int index) async {
await value.engine.leaveChannel();
int remoteUserIndex = value.allUsers.indexWhere((element) => element.uid == value.matchedUsers[0].uid);
List<AgoraUser> tempList = value.allUsers;
tempList[remoteUserIndex] = tempList[remoteUserIndex].copyWith(isAvailable: true);
value = value.copyWith(allUsers: tempList);
List<AgoraUser> tempList2 = value.matchedUsers;
tempList2.removeAt(0);
value = value.copyWith(matchedUsers: tempList2);
joinVideoChannel(0);
}

Left Swipe

  • Right swipe: The user leaves the current channel and saves their UID in a list of liked users. This list of UIDs is stored so that two people who liked each other can send each other messages once the call has disconnected. The user then joins a random channel with another matched person.
Future<void> onForwardAction(int index) async {
await value.engine.leaveChannel();
int remoteUid = value.matchedUsers[0].uid;
value = value.copyWith(likedUsers: [...value.likedUsers, remoteUid]);
value.likedUsers.forEach((element) {
print('Liked User: $element');
});
List<AgoraUser> tempList = value.matchedUsers;
tempList.removeAt(0);
value = value.copyWith(matchedUsers: tempList);
joinVideoChannel(0);
}

Right Swipe

Messaging Page

For all the users that have been swiped right, we maintain a likedUsers list that holds the UID of all these users. This UID helps us join an RTM channel with a user so that we can send them messages after the call has ended.

To set up the RTM SDK follow these steps:

  • Initialize the SDK. In this step, we create RtmChannel and RtmClient objects and then initialize them using an App ID, a token, and a channel name.
void createClient() async {
String username;
await FirebaseFirestore.instance.collection('user').get().then((value) {
username = value.docs[3].data()['email'];
});
value = value.copyWith(client: await AgoraRtmClient.createInstance(appId));
value.client.onMessageReceived = (AgoraRtmMessage message, String peerId) {
_logPeer(message.text);
};
value.client.onConnectionStateChanged = (int state, int reason) {
print('Connection state changed: ' + state.toString() + ', reason: ' + reason.toString());
if (state == 5) {
value.client.logout();
}
};
toggleLogin(username);
}

Initializing the RTM SDK

  • Log in using a username. Here we are using the email stored in Firebase as a username. Since it is a primary key in our model, this will ensure that the username is unique for every user.
void toggleLogin(String username) async {
try {
await value.client.login(null, username);
print('Login success: ' + username);
} catch (errorCode) {
print('Login error: ' + errorCode.toString());
}
}
view raw rtm_login.dart hosted with ❤ by GitHub

RTM Login

  • Join a channel. Once we have logged in to the RTM channel, we can join a channel. This requires a unique channel name that is used by both the users in the channel. In this example, I use the UIDs of the local and remote users to create a unique channel name.
Future<void> toggleJoinChannel(int remoteChannelName) async {
String channelName;
if (remoteChannelName < value.localUid) {
channelName = value.localUid.toString() + remoteChannelName.toString();
} else {
channelName = remoteChannelName.toString() + value.localUid.toString();
}
try {
value = value.copyWith(channel: await _createChannel(channelName));
await value.channel.join();
print('Join channel success.');
} catch (errorCode) {
print('Join channel error: ' + errorCode.toString());
}
}
Future<AgoraRtmChannel> _createChannel(String name) async {
AgoraRtmChannel channel = await value.client.createChannel(name);
channel.onMemberJoined = (AgoraRtmMember member) {
print("Member joined: " + member.userId + ', channel: ' + member.channelId);
};
channel.onMemberLeft = (AgoraRtmMember member) {
print("Member left: " + member.userId + ', channel: ' + member.channelId);
};
channel.onMessageReceived = (AgoraRtmMessage message, AgoraRtmMember member) {
print('Chanel Message Received : ' + message.text);
_logPeer(message.text);
};
return channel;
}

Join a RTM channel

  • Send a message. Once you have joined a channel, you can call the sendMessage method to send a message to a channel.
void toggleSendChannelMessage(String text) async {
if (text.isEmpty) {
print('Please input text to send.');
return;
}
try {
await value.channel.sendMessage(AgoraRtmMessage.fromText(text));
_log(text);
} catch (errorCode) {
print('Send channel message error: ' + errorCode.toString());
}
}

Send a RTM message

  • Leave channel. When a user exits the chat page, it is important to leave the channel so that the user can join any other channel if they want to.
void leaveRtmChannel() async {
await value.channel.leave();
value = value.copyWith(messages: []);
}

Leave RTM Channel

Testing

Before you build the app, make sure that you did the following:

  • You added your App Id and token to initialize your Agora SDK
  • You linked your app with firebase to register all the users
  • After you build the app, you should see something like this:

Conclusion

Combining RTC and RTM creates a lot of possibilities. In this tutorial, we have seen one of the biggest engagement apps: speed dating, where two random people are matched based on their interests and are added to an RTC channel.

You can find the complete code for this application here.

Other Resources

To learn more about the Agora Flutter SDK and other use cases, see the developer guide here.

You can also have a look at the complete documentation for the functions discussed above and many more here.

And I invite you to join the Agora Developer Slack Community.

RTE Telehealth 2023
Join us for RTE Telehealth - a virtual webinar where we’ll explore how AI and AR/VR technologies are shaping the future of healthcare delivery.

Learn more about Agora's video and voice solutions

Ready to chat through your real-time video and voice needs? We're here to help! Current Twilio customers get up to 2 months FREE.

Complete the form, and one of our experts will be in touch.

Try Agora for Free

Sign up and start building! You don’t pay until you scale.
Try for Free