Easy Way to Implement Chat App with Flutter and ZEGOCLOUD Kit
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.
- Create your zegocloud project.
- Select In-app Chat.
- Input project name, e.g., "Chat_App".
- Open Detail Project ZEGOCLOUD in Project Information Tab, you will find AppID and AppSign. Kepp this secret and don't share to public.
- Activate the In-app Chat service at Service Management.
After create ZEGOCLOUD project for Chat App. Move on to creating a Flutter App.
- Flutter: 3.29.0
- IDE: Visual Studio Code
- OS: Microsoft Windows 11
- Android Studio: 2024.2
- Create Flutter project.
- Run this command to add uikit in-app chat package from zegocloud.
flutter pub add zego_zimkit
- Create new Class to store ZEGOCLOUD Secret keys.
class ZegocloudKeys {
static const appId = 0;
static const appSign = '';
}
- 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.
<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"/>
Prevent code obfuscation for Android.
-keep class **.zego.** { *; }
# to shrink your code with R8 add below
-dontwarn com.itgsa.opensdk.mediaunit.KaraokeMediaHelper
- .gradle.kts
- .gradle
...
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")
}
}
}
...
...
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.
<key>NSCameraUsageDescription</key>
<string>We require camera access to connect</string>
<key>NSMicrophoneUsageDescription</key>
<string>We require microphone access to connect</string>
To build ios app correctly, set the following option for target app in xCode.
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
- Build Libraries for Distribution ->
-
In other Targets:
- Build Libraries for Distribution ->
NO
- Only safe API extensions ->
YES
- Build Libraries for Distribution ->
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.
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.
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).
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.
...
const ConversationPage({
super.key,
required this.userId,
required this.userName,
});
final String userId;
final String userName;
...
Register conversation page to route in MaterialApp.
...
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
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),
),
],
),
);
},
);
}
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.
...
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 Conversation | New Person | |
↓ | ↓ | |
Chat Room | Chat Room | |
↓ | ↓ | |
Read and or reply | Start send message |
Final Result
Here are some views of the final results. Test using OS Android 10 and 15.
Luffy Login | Luffy Conversation | Zoro Login | Zoro Conversation |
---|---|---|---|
![]() | ![]() | ![]() | ![]() |
Luffy Send to Zoro | Luffy Send New Message | Zoro Receive new Message | Zoro Open Chat Room |
![]() | ![]() | ![]() | ![]() |
Zoro Reply | Luffy's Chat Room | Zoro's Chat Room | |
![]() | ![]() | ![]() |