UI Kit quickstart

Updated

A highly reliable global communication platform where users can chat one-to-one, in groups or in chat rooms.

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.

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
    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
  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"/>
    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.**

Implementation

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

WidgetFunctionDescription
ChatUIKitRoot widgetThe root of all widgets in Chat UI Kit.
ChatConversationsViewConversation listPresents avatar, nickname, last message, unread count, and message time.
ChatConversationsViewDelete conversationDeletes the conversation from the conversation list.
ChatMessagesViewMessage senderSends text, emoji, image, file, and voice messages.
ChatMessagesViewDelete messagesDeletes messages.
ChatMessagesViewRecall messageRecalls messages sent within 120 seconds.
ChatMessagesViewRead markReceives a read receipt after a message is retrieved.
ChatMessagesViewMessage sent stateDisplays the status after a message is sent.
ChatMessagesViewDisplay messageDisplays one-to-one and group messages with sender, content, time, and status information.

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,
});

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,
});

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,
});

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
  2. Add the UI Kit dependency:

    dependencies:
        agora_chat_uikit:
            path: `<#uikit path#>`
  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;
      }
    }
  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,
            ),
          ),
        );
      }
    }

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 = '';
    }
  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'),
    );
  }
}

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,
            );
          },
        ),
      ),
    );
  }
}

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);
          },
        ),
      ),
    );
  }
}

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');
  }
}

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);
  }
}

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'))
        ],
      ),
    );
  }
}

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,
        ),
      ),
    );
  }
}

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();
  }
}