FSA全栈行动 FSA全栈行动
首页
  • 移动端文章

    • Android
    • iOS
    • Flutter
  • 学习笔记

    • 《Kotlin快速入门进阶》笔记
    • 《Flutter从入门到实战》笔记
    • 《Flutter复习》笔记
前端
后端
  • 学习笔记

    • 《深入浅出设计模式Java版》笔记
  • 逆向
  • 分类
  • 标签
  • 归档
  • LinXunFeng
  • GitLqr

公众号:FSA全栈行动

记录学习过程中的知识
首页
  • 移动端文章

    • Android
    • iOS
    • Flutter
  • 学习笔记

    • 《Kotlin快速入门进阶》笔记
    • 《Flutter从入门到实战》笔记
    • 《Flutter复习》笔记
前端
后端
  • 学习笔记

    • 《深入浅出设计模式Java版》笔记
  • 逆向
  • 分类
  • 标签
  • 归档
  • LinXunFeng
  • GitLqr
  • AndroidUI

  • Android第三方SDK

  • Android混淆

  • Android仓库

  • Android新闻

  • Android系统开发

  • Android源码

  • Android注解AOP

  • Android脚本

  • AndroidTv开发

  • AndroidNDK

  • Android音视频

  • Android热修复

  • Android性能优化

  • Android云游戏

  • Android插件化

  • iOSUI

  • iOS工具

  • iOS底层原理与应用

  • iOS组件化

  • iOS音视频

  • iOS疑难杂症

  • iOS之Swift

  • iOS之RxSwift

  • iOS开源项目

  • iOS逆向

  • Flutter开发

    • Dart - 抽象类的实例化
    • Flutter - 打印好用的Debug日志
    • Flutter - 混合开发
    • Flutter - 解决混合开发iOS脚本打包遇到的问题
    • Flutter - 低版本在iOS14上遇到的问题与解决方案
    • Flutter - 解决原生弹窗的触摸事件被Flutter响应的问题
    • Flutter - 实现列表上下拉切换header
    • Flutter - 获取ListView当前正在显示的Widget信息
    • Flutter - 列表滚动定位超强辅助库,墙裂推荐!🔥
    • Flutter - 快速实现聊天会话列表的效果,完美💯
    • Flutter - 聊天输入框更新文本时的必备优化点🔖
    • Flutter - 我给官方提PR,解决run命令卡住问题 😃
    • Flutter - 探索run命令到底做了什么 🤔
    • Flutter - 引擎调试(iOS篇)🛠
    • Flutter - 引擎调试bug到提交PR实战 🐞
    • Flutter - 船新升级😱支持观察第三方构建的滚动视图💪
    • Flutter - 瀑布流交替播放视频 🎞
    • Flutter - IM保持消息位置大升级(支持ChatGPT生成式消息) 🤖
      • 一、概述
      • 二、布局
        • 1、ChatObserver 配置
        • 2、ListView 配置
      • 三、实战
      • 四、用法解析
        • 1、常规模式
        • 2、生成式模式(仿ChatGPT)
        • 3、指定模式
      • 五、最后
    • Flutter - 滚动视图中的表单防遮挡 🗒
    • Flutter - 秒杀1/2曝光统计 📊
    • 一天内加入 Flutter 和 FlutterCandies 两大组织是什么体验 🧐
    • Flutter - 如何快速搓一个微信通讯录列表(azlist) 📓
    • Flutter - 混编项目集成Shorebird热更新🐦(安卓篇)
    • Flutter - 混编项目集成Shorebird热更新🐦(iOS篇)
    • Flutter - 解决返回原生页面时dispose方法未被触发的问题 🐞
    • Flutter - 升级3.19之后页面多次rebuild?🤨
    • Flutter - 热更新 Shorebird 1.0 正式版来了 🐦
    • Flutter - 使用Pigeon实现视频缓存插件 🐌
    • Flutter - 轻松搞定屏幕旋转功能 😎
    • Flutter - 解决Connection closed before full header was received
    • Flutter - 实现聊天键盘与功能面板的丝滑切换 🍻
    • Flutter - 支持观察NestedScrollView,兼容性更强 😈
    • Flutter - 聊天键盘与面板丝滑切换的强势升级 🍻
    • Flutter - 升级到3.24后页面还会多次rebuild吗?🧐
    • Flutter - 轻松实现PageView卡片偏移效果
    • Flutter - 轻松搞定炫酷视差(Parallax)效果
    • Flutter - 危!3.24版本苹果审核被拒!
    • Flutter - 子部件任意位置观察滚动数据
    • Flutter - iOS编译加速
    • Flutter - Xcode16 还原编译速度
  • 移动端
  • Flutter开发
LinXunFeng
2023-06-18
目录

Flutter - IM保持消息位置大升级(支持ChatGPT生成式消息) 🤖

欢迎关注微信公众号:[FSA全栈行动 👋]

# 一、概述

在 【Flutter - 快速实现聊天会话列表的效果,完美💯】 一文中介绍了常规聊天场景下如何保持消息位置及其实现原理,本篇将在此基础上完成 ChatGPT 这种生成式消息保持位置的功能。

如上图所示,操作与现象:

  1. 点击右上角的按钮后,会插入一条消息,模拟向 ChatGPT 发出问题
  2. 然后间隔 1s 会自动插入一条生成式消息,模拟 ChatGPT 在不断回复我们的问题

# 二、布局

布局非常简单,就一个 ListView,不过需要结合 ChatObserver 做一些配置

# 1、ChatObserver 配置

// 实例化 ListObserverController
// 由于滚动视图使用的是 ListView,所以选择 ListObserverController
// 否则请选择相应的 ObserverController
observerController = ListObserverController(controller: scrollController)
  // 关闭模块定位的偏移缓存功能,因为IM经常添加或删除消息,偏移的缓存容易过时
  // 如果你不会使用到模块定位功能,则可以不用理会
  ..cacheJumpIndexOffset = false;

// 实例化 ChatScrollObserver
chatObserver = ChatScrollObserver(observerController)
  // 滚动视图的偏移量超过 5 时才会启用保持IM消息位置的功能
  ..fixedPositionOffset = 5
  // 内部在切换 isShrinkWrap 的值时会触发该回调,
  // 触发时局部刷新列表视图即可,这里因为是 Demo 的缘故,做法比较简单粗暴
  ..toRebuildScrollViewCallback = () {
    setState(() {});
  };

# 2、ListView 配置

Widget _buildListView() {
  Widget resultWidget = ListView.builder(
    // 内含处理保持IM消息位置的核心逻辑
    physics: ChatObserverClampingScrollPhysics(observer: chatObserver),
    padding: const EdgeInsets.only(left: 10, right: 10, top: 15, bottom: 15),
    // 切换滚动视图的高度模式,当消息不满一屏时,该值为 true,使消息在顶部展示,
    // 消息超一屏时为 false,消息在底部展示,chatObserver 内部会适时变更该值
    // 如果你希望一直都是底部展示,可以注释该行
    shrinkWrap: chatObserver.isShrinkWrap,
    // 消息从底部往上开始排序,所以来新消息时应该插入到 0 的位置
    reverse: true,
    controller: scrollController,
    itemBuilder: ((context, index) {
      return ChatItemWidget(...);
    }),
    itemCount: chatModels.length,
  );
  
  // 对滚动视图监听
  resultWidget = ListViewObserver(
    controller: observerController,
    child: resultWidget,
  );
  
  // 重点,如果你希望不满一屏时在顶部展示消息,而又不生效时,
  // 记得如下所示设置 alignment 为 Alignment.topCenter
  resultWidget = Align(
    child: resultWidget,
    alignment: Alignment.topCenter,
  );
  return resultWidget;
}

基本的配置到这就完成了,接下来看看如何为生成式消息保持位置吧

# 三、实战

AppBar 右边的按钮,点击时模拟发出一条问题消息,然后等待1s后 ChatGPT 以不断更新消息的方式回答问题

IconButton(
  onPressed: () async {
    // 关闭上一条更新生成式消息
    stopMsgUpdateStream();
    // 模拟发送一条问题消息
    _addMessage(isOwn: true);
    // 等待1s
    await Future.delayed(const Duration(seconds: 1));
    // 插入生成式消息
    insertGenerativeMsg();
  },
  icon: const Icon(Icons.add_comment),
)

停止旧生成式消息的更新,控制同一时间只允许存在一条生成式消息

stopMsgUpdateStream() {
  timer?.cancel();
  timer = null;
}

插入一条新消息方法

_addMessage({
  required bool isOwn,
}) {
  // 在符合条件的情况下保持当前的IM消息位置
  chatObserver.standby(changeCount: 1);
  setState(() {
    chatModels.insert(0, ChatDataHelper.createChatModel(isOwn: isOwn));
  });
}

插入生成式消息,模拟 ChatGPT 正在回答问题

insertGenerativeMsg() {
  // 停止之前的生成式消息
  stopMsgUpdateStream();
  // 插入一条
  _addMessage(isOwn: false);
  // 开始模拟接收更新的消息数据
  int count = 0;
  timer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
    // 接收完毕
    if (count >= 60) {
      stopMsgUpdateStream();
      return;
    }
    count++;
    // 取出最新的生成式消息
    final model = chatModels.first;
    // 更新消息内容
    final newString = '${model.content}-1+1';
    // 替换消息
    final newModel = ChatModel(isOwn: model.isOwn, content: newString);
    chatModels[0] = newModel;
    // 准备保持位置
    chatObserver.standby(
      // 重点,使用生成式消息的处理模式
      mode: ChatScrollObserverHandleMode.generative,
      // changeCount: 1,
    );
    // 后面会解释该模式的使用
    // chatObserver.standby(
    //   changeCount: 1,
    //   mode: ChatScrollObserverHandleMode.specified,
    //   refItemRelativeIndex: 2,
    //   refItemRelativeIndexAfterUpdate: 2,
    // );
    // 刷新滚动视图,可局部刷新,Demo 缘故,做法比较简单粗暴
    setState(() {});
  });
}

# 四、用法解析

其实上面那一堆代码中,重点在于 standby 方法的使用

standby({
  // 滚动视图的 BuildContext
  // 当布局复杂使 scrollview_observer 无法顺利自动找到滚动视图的 BuildContext 时才需要传入
  // 如 CustomScrollView,或 ObserverWidget 包裹的 child 中有多个 ListView、GridView
  BuildContext? sliverContext,
  // 是否是删除消息的操作,它不会启用保持位置功能,而是可能切换 isShrinkWrap 的值
  bool isRemove = false,
  // 变更的IM消息数
  int changeCount = 1,
  // 处理模式
  ChatScrollObserverHandleMode mode = ChatScrollObserverHandleMode.normal,
  // 刷新滚动视图前,参照的相对item的下标
  int refItemRelativeIndex = 0,
  // 刷新滚动视图后,参照的相对item的下标
  int refItemRelativeIndexAfterUpdate = 0,
})

处理模式的定义如下

enum ChatScrollObserverHandleMode {
  /// 常规模式
  /// 插入或删除IM消息的时候使用
  normal,

  /// 生成式模式
  /// 专门处理 ChatGPT 这种不断更新消息的情况
  generative,

  /// 指定模式
  /// 在该模式下我们可以指定参照的IM消息的相对下标
  specified,
}

# 1、常规模式

常规模式为默认的处理模式,应用于日常的 IM 添加和删除消息的场景,比较简单

插入多条消息

_addMessage(int count) {
  // 进入待命状态,保持IM消息位置
  chatObserver.standby(changeCount: count);
  // 一次性插入多条消息,并刷新滚动视图
  setState(() {
    needIncrementUnreadMsgCount = true;
    for (var i = 0; i < count; i++) {
      chatModels.insert(0, ChatDataHelper.createChatModel());
    }
  });
}

删除消息

chatObserver.standby(isRemove: true);
setState(() {
  chatModels.removeAt(index);
});

# 2、生成式模式(仿ChatGPT)

专门处理 ChatGPT 这种生成式消息的场景

final model = chatModels.first;
final newString = '${model.content}-1+1';
final newModel = ChatModel(isOwn: model.isOwn, content: newString);
// 更新生成式消息的数据
chatModels[0] = newModel;
// 进入待命状态
chatObserver.standby(
  mode: ChatScrollObserverHandleMode.generative,
  // changeCount: 1,
);
// 刷新列表
setState(() {});
  • 指定处理模式 mode 为 .generative
  • changeCount 默认值为 1,传与不传一样

内部会根据 changeCount 来计算并记录参照消息的相对下标(关于相对下标的内容会在 指定模式 一节中说明),这意味着该模式支持连续的多条生成式消息保持位置,比如最新的两条消息都是生成式的情况是支持。

而如果最新的三条消息,0 和 2 都是生成式消息而 1 不是,又或者同一时间插入消息和更新生成式消息,此时该模式是无法很好的支持保持消息位置这一功能的,那有什么办法呢?你可以使用 指定模式

# 3、指定模式

顾名思义,可以指定参照的 IM 消息的相对下标,从而可以自由的使用保持消息位置的功能,当然了,自由也意味着传入的数据和需要了解的会更多!

我们先来了解一下什么是参照的 IM 消息的相对下标?

注: 滚动视图内已渲染的 item 不一定会被显示出来,如果下文中提及的滚动视图渲染的 item 让你不好理解,你可以直接认为是屏幕中正在展示的 item。

假如当前是正在看最新消息的情况,滚动视图内渲染了 item0 到 item4,其相对下标为 0 到 4。

     trailing        relativeIndex
-----------------  -----------------
|     item4     |          4
|     item3     |          3
|     item2     |          2
|     item1     |          1
|     item0     |          0 
-----------------  -----------------
     leading

如果此时你在翻看历史消息,滚动视图内渲染了 item10 到 item14,其相对下标也为 0 到 4。

     trailing        relativeIndex
-----------------  -----------------
|     item14    |          4
|     item13    |          3
|     item12    |          2
|     item11    |          1
|     item10    |          0 
-----------------  -----------------
     leading

这里 0 到 4 的下标即为相对下标,我们来使用该模式(.specified)来完成 .generative 模式的功能

在上述示例中,离发出问题间隔了1秒后,ChatGPT 会开始回答问题,此时我们插入生成式的消息,并不断更新该消息内容。

注意: 插入消息的方法内已经做了保持消息位置的处理,所以我们的关注点在于更新消息

     trailing        relativeIndex
-----------------  -----------------
|     item4     |          4
|     item3     |          3
|     item2     |          2
|     item1     |          1
|     item0     |          0 
-----------------  -----------------
     leading

假设此时 item0 为生成式消息,它的消息内容在不断变多,如果放任不管,item0 以上的消息会逐渐被往上顶,因此在这里我们的目的是不管 item0 如何变化,都需要保持 item1 及以上消息的位置,所以 item1 就成为了我们的参照消息,它此刻的下标为 1,而生成式消息的变化前后,item1 的下标一直都是 1!

改造的代码如下:

chatObserver.standby(
  changeCount: 1,
  // 设置处理模式为 .specified
  mode: ChatScrollObserverHandleMode.specified,
  // 滚动视图更新前,参照消息的相对下标
  refItemRelativeIndex: 1,
  // 滚动视图更新后,参照消息的相对下标
  refItemRelativeIndexAfterUpdate: 1,
);

注意,refItemRelativeIndex 和 refItemRelativeIndexAfterUpdate 应该指向同一条消息!

其实参照消息在理论上只要是 item0 以上的已经渲染的消息即可,即上述的参照消息的相对下标也可以是 2、 3 和 4。

注: 如果更新滚动视图后参照的消息无法得到渲染,则该功能就会失效,所以建议还是选择当前发生变化的消息的上一条消息的相对下标~

比较有意思的是,假设我们在发出问题后,往上翻页了,1秒后滚动视图渲染的消息是 item10 到 item14

     trailing        relativeIndex
-----------------  -----------------
|     item14    |          4
|     item13    |          3
|     item12    |          2
|     item11    |          1
|     item10    |          0 
-----------------  -----------------
     leading

此时你的参照消息的相对下标就可以为 0 了,但是你需要判断当前已渲染的第一个 item 是否为生成式消息,这就很麻烦,没有必要。

总结来说,参照的消息不可以为发生变化的消息本身,而是为滚动视图在更新前后都会被渲染的 item 即可!

# 五、最后

通过上述示例的讲解,相信你对 scrollview_observer 的使用又更加清楚,如果你也觉得这个库好用,请不吝给个 Star 👍

GitHub: https://github.com/LinXunFeng/flutter_scrollview_observer (opens new window)

#Dart#Flutter
Flutter - 瀑布流交替播放视频 🎞
Flutter - 滚动视图中的表单防遮挡 🗒

← Flutter - 瀑布流交替播放视频 🎞 Flutter - 滚动视图中的表单防遮挡 🗒→

最近更新
01
Flutter - Xcode16 还原编译速度
04-05
02
AI - 免费的 Cursor 平替方案
03-30
03
Android - 2025年安卓真的闭源了吗
03-28
更多文章>
Theme by Vdoing | Copyright © 2020-2025 FSA全栈行动
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×