Skip to content

3、Chat Observer

LinXunFeng edited this page May 20, 2024 · 3 revisions

3.1、Basic usage

We only need three steps to implement the chat session page effect.

  • 1、All chat data are displayed at the top of the listView when there is less than one screen of chat data.
  • 2、When inserting a chat data
    • If the latest message is close to the bottom of the list, the listView will be pushed up.
    • Otherwise, the listView will be fixed to the current chat position.

Step 1: Initialize the necessary ListObserverController and ChatScrollObserver.

/// Initialize ListObserverController
observerController = ListObserverController(controller: scrollController)
  ..cacheJumpIndexOffset = false;

/// Initialize ChatScrollObserver
chatObserver = ChatScrollObserver(observerController)
  // Greater than this offset will be fixed to the current chat position.
  ..fixedPositionOffset = 5
  ..toRebuildScrollViewCallback = () {
    // Here you can use other way to rebuild the specified listView instead of [setState]
    setState(() {});
  };

Step 2: Configure ListView as follows and wrap it with ListViewObserver.

Widget _buildListView() {
  Widget resultWidget = ListView.builder(
    physics: ChatObserverClampingScrollPhysics(observer: chatObserver),
    shrinkWrap: chatObserver.isShrinkWrap,
    reverse: true,
    controller: scrollController,
    ...
  );

  resultWidget = ListViewObserver(
    controller: observerController,
    child: resultWidget,
  );
  return resultWidget;
}

Step 3: Call the [standby] method of ChatScrollObserver before inserting or removing chat data.

onPressed: () {
  chatObserver.standby();
  setState(() {
    chatModels.insert(0, ChatDataHelper.createChatModel());
  });
},
...
onRemove: () {
  chatObserver.standby(isRemove: true);
  setState(() {
    chatModels.removeAt(index);
  });
},

This feature only handles inserting one message by default. If you need to insert multiple messages at once, you can pass the changeCount parameter to the standby method.

_addMessage(int count) {
  chatObserver.standby(changeCount: count);
  setState(() {
    needIncrementUnreadMsgCount = true;
    for (var i = 0; i < count; i++) {
      chatModels.insert(0, ChatDataHelper.createChatModel());
    }
  });
}

Note: This feature relies on the latest message view before the message is inserted as a reference to calculate the offset, so if too many messages are inserted at once and the reference message view cannot be rendered, this feature will fail, and you need to try to avoid this problem by setting a reasonable value for cacheExtent of ScrollView by yourself!

The size calculation of cacheExtent can refer to the following formula:

cacheExtent = [maximum height of a message] * [number of messages inserted at one time] + 200

3.2、The result callback for processing chat position.

chatObserver = ChatScrollObserver(observerController)
  ..onHandlePositionResultCallback = (result) {
    switch (result.type) {
      case ChatScrollObserverHandlePositionType.keepPosition:
        // Keep the current chat position.
        // updateUnreadMsgCount(changeCount: result.changeCount);
        break;
      case ChatScrollObserverHandlePositionType.none:
        // Do nothing about the chat position.
        // updateUnreadMsgCount(isReset: true);
        break;
    }
  };

This callback is mainly used to display the unread bubbles of new messages when adding chat messages.

3.3、Generative message keeps position

The generative messages like ChatGPT also need to keep the message position when looking through old messages, you only need to adjust the processing mode in the standby method.

chatObserver.standby(
  mode: ChatScrollObserverHandleMode.generative,
  // changeCount: 1,
);

Note: The referenced item will be determined internally based on changeCount, and this mode only supports the case where generative messages are continuous.

3.4、Specifies the referenced item

If your generative messages are discontinuous, or there are generative message updates and the behavior of adding and deleting messages at the same time, in this complex case, you need to specify the referenced item by yourself, and This processing mode is more flexible.

chatObserver.standby(
  mode: ChatScrollObserverHandleMode.specified,
  refIndexType: ChatScrollObserverRefIndexType.relativeIndexStartFromCacheExtent,
  refItemIndex: 2,
  refItemIndexAfterUpdate: 2,
);
  1. Set mode to .specified.
  2. Set refItemIndex to relative index of the referenced item before the update.
  3. Set refItemIndexAfterUpdate to relative index of the referenced item after the update.

Note: The functions of refItemIndex and refItemIndexAfterUpdate differ based on the value of refIndexType, as shown below

enum ChatScrollObserverRefIndexType {
  ///     relativeIndex        trailing
  ///
  ///           6         |     item16    | cacheExtent
  ///   ----------------- -----------------
  ///           5         |     item15    |
  ///           4         |     item14    |
  ///           3         |     item13    | displaying
  ///           2         |     item12    |
  ///           1         |     item11    |
  ///   ----------------- -----------------
  ///           0         |     item10    | cacheExtent <---- start
  ///
  ///                          leading
  relativeIndexStartFromCacheExtent,

  ///     relativeIndex        trailing
  ///
  ///           5         |     item16    | cacheExtent
  ///   ----------------- -----------------
  ///           4         |     item15    |
  ///           3         |     item14    |
  ///           2         |     item13    | displaying
  ///           1         |     item12    |
  ///           0         |     item11    |             <---- start
  ///   ----------------- -----------------
  ///          -1         |     item10    | cacheExtent
  ///
  ///                          leading
  relativeIndexStartFromDisplaying,

  /// Directly specify the index of item.
  itemIndex,
}

Remember, your refItemIndex and refItemIndexAfterUpdate should point to the same message object whatever you set!