Skip to main content

Easy Way to Implement Chat App with Flutter and ZEGOCLOUD Kit

· 9 min read

thumbnail

After we learn to create a real-time data usage scheme using the SDK Kit for streaming applications. Now we can add or create new feature that is "in-app-chat" for your app. You only need to focus on your bussiness or main feature and let zegocloud chat kit handle your "in-app-chat".

Prepare

Register new account, and you can get 10,000 minutes for free from zegocloud. Next open zegocloud dashboard to access project.

  1. Create your zegocloud project.

alt

  1. Select In-app Chat.

alt

  1. Input project name, e.g., "Chat_App".

alt

  1. Open Detail Project ZEGOCLOUD in Project Information Tab, you will find AppID and AppSign. Kepp this secret and don't share to public.

alt

  1. Activate the In-app Chat service at Service Management.

alt

After create ZEGOCLOUD project for Chat App. Move on to creating a Flutter App.

My Environment
  • Flutter: 3.29.0
  • IDE: Visual Studio Code
  • OS: Microsoft Windows 11
  • Android Studio: 2024.2
  1. Create Flutter project.

alt

alt

alt

alt

  1. Run this command to add uikit in-app chat package from zegocloud.
flutter pub add zego_zimkit
  1. Create new Class to store ZEGOCLOUD Secret keys.

alt

lib/config/zegocloud_keys.dart
class ZegocloudKeys {
static const appId = 0;
static const appSign = '';
}
  1. Check here to setup Android Gradle which are using Android Studio latest version and Flutter latest version.

Configure Project

Android

Add these permission to Android manifest inside manifest tag.

android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.VIBRATE"/>

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEOS"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>

alt

Prevent code obfuscation for Android.

android/app/proguard-rules.pro
-keep class **.zego.** { *; }
# to shrink your code with R8 add below
-dontwarn com.itgsa.opensdk.mediaunit.KaraokeMediaHelper
android/app/build.gradle.kts
...
android {
...

buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
}
}

...

iOS

Add permission to Info.plist inside dict tag.

ios/Runner/Info.plist
<key>NSCameraUsageDescription</key>
<string>We require camera access to connect</string>
<key>NSMicrophoneUsageDescription</key>
<string>We require microphone access to connect</string>

alt

To build ios app correctly, set the following option for target app in xCode.

alt

Refer to and set the following build options:

  • In the Runner Target:

    • Build Libraries for Distribution -> NO
    • Only safe API extensions -> NO
    • iOS Deployment Target -> 11 or greater
  • In other Targets:

    • Build Libraries for Distribution -> NO
    • Only safe API extensions -> YES

Login Page

First we create an entry point page to get user id and username. Simply, we can use auth template from Flutter Delux.

Open auth sample, copy & paste & edit like below.

lib/pages/login_page.dart
import 'package:flutter/material.dart';
import 'package:d_input/d_input.dart';
import 'package:gap/gap.dart';

class LoginPage extends StatefulWidget {
const LoginPage({super.key});


State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
final _userIdController = TextEditingController();
final _userNameController = TextEditingController();

void _executeLogin() {
final userId = _userIdController.text;
if (userId == '') return _showInvalidMessage('User Id must be filled');
final userName = _userNameController.text;
if (userName == '') return _showInvalidMessage('User Name must be filled');

Navigator.pushNamed(
context,
'/conversation',
arguments: {'userId': userId, 'userName': userName},
);
}


void dispose() {
_userIdController.dispose();
_userNameController.dispose();
super.dispose();
}


Widget build(BuildContext context) {
return Scaffold(
body: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Padding(
padding: const EdgeInsets.all(30),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(radius: 80, child: Icon(Icons.chat, size: 80)),
Gap(30),
Text(
'In-App Chat',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 30,
color: Theme.of(context).primaryColor,
),
),
Gap(70),
DInputMix(controller: _userIdController, hint: 'User Id'),
Gap(20),
DInputMix(
controller: _userNameController,
hint: 'User Name',
),
Gap(20),
SizedBox(
width: double.infinity,
height: 50,
child: FilledButton(
style: ButtonStyle(
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
onPressed: () => _executeLogin(),
child: Text('LOGIN'),
),
),
],
),
),
),
);
},
),
);
}

void _showInvalidMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message, style: TextStyle(color: Colors.white)),
duration: Duration(seconds: 2),
backgroundColor: Colors.red,
),
);
}
}

Run flutter pub add d_input gap to add required package. Inside Material App, setup launch route to Login page.

lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_zegocloud_chat/pages/login_page.dart';

void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MainApp());
}

class MainApp extends StatelessWidget {
const MainApp({super.key});


Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
routes: {'/': (context) => LoginPage()},
);
}
}

Login Page will be like this:

ZIMKIT

We will use 2 ukit from ZIMKIT (ZEGOCLOUD In-App Chat):

  • Conversation Page
    We will create custom page, then setup conversation list or list of people which is chatting with us.
  • Chat Room
    Full Kit from ZEGOCLOUD.

Firt, initialize ZIMKIT.

lib/main.dart
import 'package:zego_zimkit/zego_zimkit.dart';

void main() {
WidgetsFlutterBinding.ensureInitialized();
ZIMKit().init(appID: ZegocloudKeys.appId, appSign: ZegocloudKeys.appSign);
runApp(const MainApp());
}
...

If you get an error like this when run/build app, use the following alternative package (for now).

pubspec.yaml
dependencies:
d_input: ^0.5.1
flutter:
sdk: flutter
gap: ^3.0.1
# zego_zimkit: ^1.18.14
zego_zimkit:
git:
url: https://github.com/indratrisnar/zego_zimkit_flutter/
ref: indra

Conversation Page

Arguments

Create Conversation Page class with Statefull Widget and create 2 argument to receive data were sent from Login Page.

lib/pages/live_page.dart
...

const ConversationPage({
super.key,
required this.userId,
required this.userName,
});
final String userId;
final String userName;

...

Register conversation page to route in MaterialApp.

lib/main.dart
...
return MaterialApp(
debugShowCheckedModeBanner: false,
routes: {
'/': (context) => LoginPage(),
'/conversation': (context) {
final data = ModalRoute.settingsOf(context)?.arguments as Map?;
if (data == null) {
return Scaffold(appBar: AppBar(title: Text('Invalid Argument')));
}
return ConversationPage(
userId: data['userId'],
userName: data['userName'],
);
},
},
);
...

Init Conversation List

Prepare to get Conversation List. Users must log in at least once to get the list. So, if you get infinit loading at first, you just have to re-login.

late final Future<int> initializedConnectUser;


void initState() {
initializedConnectUser = ZIMKit().connectUser(
id: widget.userId,
name: widget.userName,
);
super.initState();
}


Widget build(BuildContext context) {
return FutureBuilder(
future: initializedConnectUser,
builder: (context, snapshot) {
log(snapshot.data.toString());
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
return {...};
},
);
}

Start New Chat

Input User Id
void showStartChatWith() {
showModalBottomSheet(
context: context,
showDragHandle: true,
isScrollControlled: true,
builder: (context) {
return Padding(
padding: EdgeInsets.fromLTRB(
20,
20,
20,
MediaQuery.viewInsetsOf(context).bottom + 40,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DInputMix(
controller: conversationIdController,
title: 'Chat with UserId:',
hint: 'userId',
suffixIcon: IconSpec(icon: Icons.send, onTap: startChat),
),
],
),
);
},
);
}
Goto Chat Room
void startChat() {
final conversationId = conversationIdController.text;
if (conversationId == '') return;

Navigator.pushNamed(
context,
'/chat-room',
arguments: {
'conversationId': conversationId,
'conversationType': ZIMConversationType.peer,
},
);
}

Conversation Full Code

Inside Conversation List, there is function to navigate to Detail Conversation or List Message.

lib/pages/conversation_page.dart

import 'dart:developer';

import 'package:d_input/d_input.dart';
import 'package:flutter/material.dart';
import 'package:zego_zimkit/zego_zimkit.dart';

class ConversationPage extends StatefulWidget {
const ConversationPage({
super.key,
required this.userId,
required this.userName,
});
final String userId;
final String userName;


State<ConversationPage> createState() => _ConversationPageState();
}

class _ConversationPageState extends State<ConversationPage> {
late final Future<int> initializedConnectUser;
final conversationIdController = TextEditingController();

void startChat() {
final conversationId = conversationIdController.text;
if (conversationId == '') return;

Navigator.pushNamed(
context,
'/chat-room',
arguments: {
'conversationId': conversationId,
'conversationType': ZIMConversationType.peer,
},
);
}


void initState() {
initializedConnectUser = ZIMKit().connectUser(
id: widget.userId,
name: widget.userName,
);
super.initState();
}


void dispose() {
conversationIdController.dispose();
super.dispose();
}


Widget build(BuildContext context) {
return FutureBuilder(
future: initializedConnectUser,
builder: (context, snapshot) {
log(snapshot.data.toString());
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold(
appBar: AppBar(title: Text('Conversation')),
floatingActionButton: FloatingActionButton(
onPressed: () => showStartChatWith(),
child: Icon(Icons.chat),
),
body: ZIMKitConversationListView(
onPressed: (context, conversation, defaultAction) {
Navigator.pushNamed(
context,
'/chat-room',
arguments: {
'conversationId': conversation.id,
'conversationType': conversation.type,
},
);
},
),
);
},
);
}

void showStartChatWith() {
showModalBottomSheet(
context: context,
showDragHandle: true,
isScrollControlled: true,
builder: (context) {
return Padding(
padding: EdgeInsets.fromLTRB(
20,
20,
20,
MediaQuery.viewInsetsOf(context).bottom + 40,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DInputMix(
controller: conversationIdController,
title: 'Chat with UserId:',
hint: 'userId',
suffixIcon: IconSpec(icon: Icons.send, onTap: startChat),
),
],
),
);
},
);
}
}

Chat Room Page

Create new route to Chat Room Page in main.dart, where will return UI Kit from ZIMKIT.

lib/main.dart
...
return MaterialApp(
debugShowCheckedModeBanner: false,
routes: {
'/': (context) => LoginPage(),
'/conversation': (context) {...},
'/chat-room': (context) {
final data = ModalRoute.settingsOf(context)?.arguments as Map?;
if (data == null) {
return Scaffold(appBar: AppBar(title: Text('Invalid Argument')));
}
return ZIMKitMessageListPage(
conversationID: data['conversationId'],
conversationType: data['conversationType'],
);
},
},
);
...

Flow

Sequence
User Open App
Redirect to Login Page
Conversation Page
Choose ConversationNew Person
Chat RoomChat Room
Read and or replyStart send message

Final Result

Here are some views of the final results. Test using OS Android 10 and 15.

Luffy LoginLuffy ConversationZoro LoginZoro Conversation
altaltaltalt
Luffy Send to ZoroLuffy Send New MessageZoro Receive new MessageZoro Open Chat Room
altaltaltalt
Zoro ReplyLuffy's Chat RoomZoro's Chat Room
altaltalt