Skip to main content

UI Kit quickstart

Instant messaging connects people wherever they are and allows them to communicate with others in real time. Agora offers an open-source Chat UI Kit project on GitHub. With built-in user interfaces for key Chat features, the Agora Chat UI Kit enables you to quickly embed real-time messaging into your app without requiring extra effort on the UI.

info

For the latest Agora Chat UIKit documentation, refer to the UIKit 2.x Documentation GitHub repository.

Legacy UIkit 1.x documentation

This page shows sample code to add peer-to-peer messaging into your app by using the Agora Chat UI Kit.

Understand the tech

The following figure shows the workflow of how clients send and receive peer-to-peer messages:

Chat UI kit workflow

  1. Clients retrieve a token from your app server.
  2. Client A and Client B log in to Agora Chat.
  3. Client A sends a message to Client B. The message is sent to the Agora Chat server, and the server delivers the message to Client B. When Client B receives the message, the SDK triggers an event. Client B listens for the event and gets the message.

Prerequisites

  • Flutter 2.10 or higher.

  • Dart 2.16 or higher.

  • If your target platform is iOS:

    • macOS
    • Xcode 12.4 or higher with Xcode Command Line Tools
    • CocoaPods
    • An iOS emulator or a physical iOS device running iOS 10.0 or higher
  • If your target platform is Android:

    • macOS or Windows
    • Android Studio 4.0 or higher with JDK 1.8 or higher
    • An Android emulator or a physical Android device running Android SDK API 21 or higher

Project setup

Follow these steps to create the environment necessary to integrate Chat UI Kit into your app:

  1. Create a new Flutter project for iOS and Android

    1. In the terminal, run the following command:

      flutter create uikit_demo --platforms=ios,android -i objc -a java
      Copy
    2. Integrate the UI Kit using pub.dev (Recommended)

      Execute the following commands in the uikit_demo directory:

      cd uikit_demo

      flutter pub add agora_chat_uikit
      flutter pub get
      Copy
  2. Add permissions for network and device access

    1. For Android, in /app/Manifests/AndroidManifest.xml, add the following permissions after </application>:

      <uses-permission android:name="android.permission.INTERNET" />
      <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
      <uses-permission android:name="android.permission.WAKE_LOCK"/>
      <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
      <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
      <uses-permission android:name="android.permission.CAMERA"/>
      <uses-permission android:name="android.permission.RECORD_AUDIO"/>
      Copy
    2. For iOS, open info in the project navigation panel, and add the following properties to the Property List:

      KeyTypeValue
      Privacy - Microphone Usage DescriptionStringFor microphone access
      Privacy - Camera Usage DescriptionStringFor camera access
      Privacy - Photo Library Usage DescriptionStringFor photo library access
  3. Prevent code obfuscation

    In the example/android/app/proguard-rules.pro file, add the following lines:

    -keep class com.hyphenate.** {*;}
    -dontwarn com.hyphenate.**
    Copy

This section shows how to use the Chat UI Kit to implement peer-to-peer messaging in your app.

The agora_chat_uikit library provides the logic and UI to implement the following Chat functions:

  • Send, receive, and display messages
  • Shows the unread message count, and clear messages. Text, image, emoji, file, and audio messages are supported
  • Delete conversations and messages
  • Customize the UI

Chat UI Kit widgets

Widget Function Description
ChatUIKit The root of all widgets in Chat UI Kit.
ChatConversationsView Conversation list Presents the conversation information, including the user's avatar, nickname, content of the last message, unread message count, and the time when the last message was sent or received.
Delete conversation Deletes the conversation from the conversation list.
ChatMessagesView Message sender Sends text, emoji, image, file, and voice messages.
Delete messages Deletes messages.
Recall message Recalls message that were sent within 120 seconds.
Read mark You receive a read receipt after retrieving your message.
Message sent state Display the status after the message is sent.
Display message Displays one-to-one messages and group messages, including the user's avatar, nickname, the message content, sending or reception time, sending status, and read status. Text, image, emoji, file, voice, and video messages can be displayed.

Use the following UI Kit widgets to implement Chat functionality with the associated UI in your projects.

ChatUIKit

Place a ChatUIKit widget at the top of your widget tree to help refresh the ChatConversationsView when returning from ChatMessagesView.

ChatUIKit({
super.key,
this.child,
ChatUIKitTheme? theme,
});
Copy

The ChatUIKit widget also enables you to customize the theme settings.

PropertyDescription
themeChat UI Kit theme for setting component styles. If this property is not set, the default style is used.

ChatMessagesView

You use ChatMessagesView to manage text, images, emojis, files, and voice messages:

  • Send and receive messages
  • Delete messages
  • Recall messages
PropertyDescription
conversationThe ChatConversation to which the messages belong.
inputBarTextEditingControllerText input widget text editing controller.
backgroundThe background widget.
inputBarText input component. If you don't pass in this property, ChatInputBar is used by default.
onTapMessage bubble click callback.
onBubbleLongPressCallback for holding a message bubble.
onBubbleDoubleTapCallback for double-clicking a message bubble.
avatarBuilderAvatar component builder.
nicknameBuilderNickname component builder.
itemBuilderMessage bubble. If you don't set this property, the default bubble is used.
moreItemsAction items displayed after a message bubble is held down. If you return null in onBubbleLongPress, moreItems is used. This property involves three default actions: copy, delete, and recall.
messageListViewControllerMessage list controller. You are advised to use the default value. For details, see ChatMessageListController.
willSendMessageText message pre-sending callback. This callback needs to return a ChatMessage object.
onErrorError callback, such as no permissions.
enableScrollBarWhether to enable the scroll bar. The scroll bar is enabled by default.
needDismissInputWidgetCallback for dismissing the input widget. If you use a custom input widget, dismiss the input widget when you receive this callback, for example, by calling FocusNode.unfocus. See ChatInputBar.
inputBarMoreActionsOnTapThe callback for clicking the plus symbol next to the input box. You need to return the ChatBottomSheetItems list.
ChatMessagesView({
required this.conversation,
this.inputBarTextEditingController,
this.background,
this.inputBar,
this.onTap,
this.onBubbleLongPress,
this.onBubbleDoubleTap,
this.avatarBuilder,
this.nicknameBuilder,
this.itemBuilder,
this.moreItems,
ChatMessageListController? messageListViewController,
this.willSendMessage,
this.onError,
this.enableScrollBar = true,
this.needDismissInputWidget,
this.inputBarMoreActionsOnTap,
super.key,
});
Copy

ChatConversationsView

The 'ChatConversationsView' allows you to quickly display and manage the current conversations.

PropertyDescription
controllerThe ChatConversationsView controller.
itemBuilderConversation list item builder. Return a widget if you need to customize it.
avatarBuilderAvatar builder. If this property is not implemented or you return null, the default avatar is used.
nicknameBuilderNickname builder. If you don't set this property or return null, the conversation ID is displayed.
onItemTapThe callback of the click event of the conversation list item.
backgroundWidgetWhenListEmptyBackground widget when the list is empty.
enablePullReloadWhether to enable drop-down refresh.
ChatConversationsView({
super.key,
this.onItemTap,
ChatConversationsViewController? controller,
this.itemBuilder,
this.avatarBuilder,
this.nicknameBuilder,
this.backgroundWidgetWhenListEmpty,
this.enablePullReload = false,
this.scrollController,
this.reverse = false,
this.primary,
this.physics,
this.shrinkWrap = false,
this.cacheExtent,
this.dragStartBehavior = DragStartBehavior.down,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
});
Copy

Local integration

The recommended integration method for the Chat UI Kit is using pub.dev. You can also download the project manually for integration. To do this, open the uikit_demo project in your IDE and take the following steps:

  1. Clone the Chat UIKit for Flutter repository.

    git clone https://github.com/AgoraIO-Usecase/AgoraChat-UIKit-flutter.git
    Copy
  2. Add the UI Kit dependency:

    dependencies:
    agora_chat_uikit:
    path: `<#uikit path#>`
    Copy
  3. Replace the code in lib/main.dart with the following:

    import 'package:flutter/material.dart';
    import 'package:agora_chat_uikit/agora_chat_uikit.dart';

    import 'messages_page.dart';

    class ChatConfig {
    static const String appKey = "";
    static const String userId = "";
    static const String agoraToken = '';
    }

    void main() async {
    assert(ChatConfig.appKey.isNotEmpty,
    "You need to configure AppKey information first.");
    WidgetsFlutterBinding.ensureInitialized();
    final options = ChatOptions(
    appKey: ChatConfig.appKey,
    autoLogin: false,
    );
    await ChatClient.getInstance.init(options);
    runApp(const MyApp());
    }

    class MyApp extends StatelessWidget {
    const MyApp({super.key});
    @override
    Widget build(BuildContext context) {
    return MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
    primarySwatch: Colors.blue,
    ),
    builder: (context, child) {
    // ChatUIKit widget at the top of the widget
    return ChatUIKit(child: child!);
    },
    localizationsDelegates: AppLocalizations.localizationsDelegates,
    supportedLocales: AppLocalizations.supportedLocales,
    home: const MyHomePage(title: 'Flutter Demo'),
    );
    }
    }

    class MyHomePage extends StatefulWidget {
    const MyHomePage({super.key, required this.title});

    final String title;

    @override
    State<MyHomePage> createState() => _MyHomePageState();
    }

    class _MyHomePageState extends State<MyHomePage> {
    ScrollController scrollController = ScrollController();
    ChatConversation? conversation;
    String _chatId = "";
    final List<String> _logText = [];
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text(widget.title),
    ),
    body: Container(
    padding: const EdgeInsets.only(left: 10, right: 10),
    child: Column(
    crossAxisAlignment: CrossAxisAlignment.stretch,
    mainAxisSize: MainAxisSize.max,
    children: [
    const SizedBox(height: 10),
    const Text("login userId: ${ChatConfig.userId}"),
    const Text("agoraToken: ${ChatConfig.agoraToken}"),
    const SizedBox(height: 10),
    Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
    Expanded(
    flex: 1,
    child: ElevatedButton(
    onPressed: () {
    _signIn();
    },
    child: const Text("SIGN IN"),
    ),
    ),
    const SizedBox(width: 10),
    Expanded(
    child: ElevatedButton(
    onPressed: () {
    _signOut();
    },
    child: const Text("SIGN OUT"),
    ),
    ),
    ],
    ),
    const SizedBox(height: 10),
    Row(
    children: [
    Expanded(
    child: TextField(
    decoration: const InputDecoration(
    hintText: "Enter recipient's userId",
    ),
    onChanged: (chatId) => _chatId = chatId,
    ),
    ),
    const SizedBox(height: 10),
    ElevatedButton(
    onPressed: () {
    pushToChatPage(_chatId);
    },
    child: const Text("START CHAT"),
    ),
    ],
    ),
    Flexible(
    child: ListView.builder(
    controller: scrollController,
    itemBuilder: (_, index) {
    return Text(_logText[index]);
    },
    itemCount: _logText.length,
    ),
    ),
    ],
    ),
    ),
    );
    }

    void pushToChatPage(String userId) async {
    if (userId.isEmpty) {
    _addLogToConsole('UserId is null');
    return;
    }
    if (ChatClient.getInstance.currentUserId == null) {
    _addLogToConsole('user not login');
    return;
    }
    ChatConversation? conv =
    await ChatClient.getInstance.chatManager.getConversation(userId);
    Future(() {
    Navigator.of(context).push(MaterialPageRoute(builder: (_) {
    return MessagesPage(conv!);
    }));
    });
    }

    void _signIn() async {
    _addLogToConsole('begin sign in...');
    if (ChatConfig.agoraToken.isNotEmpty) {
    try {
    await ChatClient.getInstance.loginWithAgoraToken(
    ChatConfig.userId,
    ChatConfig.agoraToken,
    );
    } on ChatError catch (e) {
    _addLogToConsole('sign in fail: ${e.description}');
    }
    } else {
    _addLogToConsole('sign in fail: The agoraToken cannot both be null.');
    }
    }

    void _signOut() async {
    _addLogToConsole('begin sign out...');
    try {
    await ChatClient.getInstance.logout();
    _addLogToConsole('sign out success');
    } on ChatError catch (e) {
    _addLogToConsole('sign out fail: ${e.description}');
    }
    }

    void _addLogToConsole(String log) {
    _logText.add("$_timeString: $log");
    setState(() {
    scrollController.jumpTo(scrollController.position.maxScrollExtent);
    });
    }

    String get _timeString {
    return DateTime.now().toString().split(".").first;
    }
    }
    Copy
  4. To create a MessagesPage refer to the following code:

    import 'package:agora_chat_uikit/agora_chat_uikit.dart';
    import 'package:flutter/material.dart';

    class MessagesPage extends StatefulWidget {
    const MessagesPage(this.conversation, {super.key});

    final ChatConversation conversation;

    @override
    State<MessagesPage> createState() => _MessagesPageState();
    }

    class _MessagesPageState extends State<MessagesPage> {
    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(title: Text(widget.conversation.id)),
    body: SafeArea(
    // Message page in uikit.
    child: ChatMessagesView(
    conversation: widget.conversation,
    ),
    ),
    );
    }
    }
    Copy

Test your app

To test the UI Kit Chat features example:

  1. In example/lib/main.dart, set the appKey, userId, and agoraToken to valid values.

    class ChatConfig {
    static const String appKey = "";
    static const String userId = "";
    static const String agoraToken = '';
    }
    Copy
  2. Click Sign In. You see a log message confirming sign-in success.

  3. Run the app on another device or simulator. Ensure that the userId values are unique.

  4. On the first device or simulator, enter the user id you just created and click Start Chat. You can now start chatting between the two clients.

Reference

Refer to the fully featured AgoraChat-UIKit-flutter demo app as an implementation reference. To customize the UI Kit, refer to the following information.

Customize colors

You can set the color when adding ChatUIKit.

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

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
builder: (context, child) {
// ChatUIKit widget at the top of the widget
return ChatUIKit(
// ChatUIKitTheme
theme: ChatUIKitTheme(),
child: child!,
);
},
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
Copy

Add an avatar

class _MessagesPageState extends State<MessagesPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.conversation.id)),
body: SafeArea(
// Message page in uikit.
child: ChatMessagesView(
conversation: widget.conversation,
avatarBuilder: (context, userId) {
// Returns the avatar widget that you want to display.
return Container(
width: 30,
height: 30,
color: Colors.red,
);
},
),
),
);
}
}
Copy

Add a nickname

class _MessagesPageState extends State<MessagesPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.conversation.id)),
body: SafeArea(
// Message page in uikit.
child: ChatMessagesView(
conversation: widget.conversation,
// Returns the nickname widget that you want to display.
nicknameBuilder: (context, userId) {
return Text(userId);
},
),
),
);
}
}
Copy

Add the bubble click event

class _MessagesPageState extends State<MessagesPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.conversation.id)),
body: SafeArea(
// Message page in uikit.
child: ChatMessagesView(
conversation: widget.conversation,
// item tap event
onTap: (context, message) {
bubbleClicked(message);
return true;
},
),
),
);
}

void bubbleClicked(ChatMessage message) {
debugPrint('bubble clicked');
}
}
Copy

Customize the message item widget

class _MessagesPageState extends State<MessagesPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.conversation.id)),
body: SafeArea(
// Message page in uikit.
child: ChatMessagesView(
conversation: widget.conversation,
itemBuilder: (context, model) {
if (model.message.body.type == MessageType.TXT) {
// Custom message bubble
return CustomTextItemWidget(
model: model,
onTap: (context, message) {
bubbleClicked(message);
return true;
},
);
}
},
),
),
);
}

void bubbleClicked(ChatMessage message) {
debugPrint('bubble clicked');
}
}

// Custom message bubble
class CustomTextItemWidget extends ChatMessageListItem {
const CustomTextItemWidget({super.key, required super.model, super.onTap});

@override
Widget build(BuildContext context) {
ChatTextMessageBody body = model.message.body as ChatTextMessageBody;

Widget content = Text(
body.content,
style: const TextStyle(
color: Colors.black,
fontSize: 50,
fontWeight: FontWeight.w400,
),
);
return getBubbleWidget(content);
}
}
Copy

Customize the input widget

class _MessagesPageState extends State<MessagesPage> {
late ChatMessageListController _msgController;
final TextEditingController _textController = TextEditingController();
final FocusNode _focusNode = FocusNode();
@override
void initState() {
super.initState();
_msgController = ChatMessageListController(widget.conversation);
}

@override
void dispose() {
_focusNode.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.conversation.id)),
body: SafeArea(
// Message page in uikit.
child: ChatMessagesView(
conversation: widget.conversation,
messageListViewController: _msgController,
inputBar: customInputWidget(),
needDismissInputWidget: () {
_focusNode.unfocus();
},
),
),
);
}

// custom input widget
Widget customInputWidget() {
return SizedBox(
height: 50,
child: Row(
children: [
Expanded(
child: TextField(
focusNode: _focusNode,
controller: _textController,
),
),
ElevatedButton(
onPressed: () {
final msg = ChatMessage.createTxtSendMessage(
targetId: widget.conversation.id,
content: _textController.text);
_textController.text = '';
_msgController.sendMessage(msg);
},
child: const Text('Send'))
],
),
);
}
}
Copy

Delete all Messages in the current conversation

class _MessagesPageState extends State<MessagesPage> {
late ChatMessageListController _msgController;

@override
void initState() {
super.initState();
_msgController = ChatMessageListController(widget.conversation);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.conversation.id),
actions: [
TextButton(
onPressed: () {
_msgController.deleteAllMessages();
},
child: const Text(
'Clear',
style: TextStyle(color: Colors.white),
))
],
),
body: SafeArea(
// Message page in uikit.
child: ChatMessagesView(
conversation: widget.conversation,
messageListViewController: _msgController,
),
),
);
}
}
Copy

Customize actions displayed upon a click of the plus symbol in the page

class _MessagesPageState extends State<MessagesPage> {
@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.conversation.id),
),
body: SafeArea(
// Message page in uikit.
child: ChatMessagesView(
conversation: widget.conversation,
// Returns a list of custom events
inputBarMoreActionsOnTap: (items) {
ChatBottomSheetItem item = ChatBottomSheetItem(
type: ChatBottomSheetItemType.normal,
onTap: customMoreAction,
label: 'more',
);

return items + [item];
},
),
),
);
}

Future<void> customMoreAction() async {
debugPrint('custom action');
Navigator.of(context).pop();
}
}
Copy