从 0 打造一个高质感的 Flutter 聊天页面:自定义气泡、滚动体验与状态管理全解析

204 阅读5分钟

本文完整呈现一个可落地的聊天页面:自定义渐变气泡顺滑的滚动回到底部体验基于 BLoC 的消息流转,以及 RenderObject 级别的文字/状态排版优化。代码已适配中文环境(zh_CN)并提供轻量主题方案(FlexColorScheme)。(文末代码下载链接),


目录


在线效果预览


项目亮点

  • 🎨 高质感聊天气泡:我的消息使用多段渐变(四色),对方为白底,圆角根据上下相邻消息动态变化。
  • 🧮 RenderObject 级布局TextMessageWidget 将“发送时间/已读状态”与最后一行文本进行智能贴合,减少冗余空白。
  • 🧱 BLoC 状态管理:消息获取与发送事件分离,UI 与业务解耦,便于扩展(上拉加载/消息回执/撤回等)。
  • 🧭 专业滚动体验ScrollablePositionedList + 「回到底部」浮动按钮,支持反向列表与平滑滚动。
  • 🌐 国际化时间intl + jiffy,统一中文环境时间格式。
  • 🧰 工程化主题FlexColorScheme 提供可扩展的浅色主题;系统状态栏/导航栏按平台分别适配。

依赖与环境

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.1.0
  equatable: ^2.0.5
  intl: ^0.19.0
  jiffy: ^6.2.1
  scrollable_positioned_list: ^0.3.8
  flex_color_scheme: ^7.3.1
  uuid: ^4.5.1
  • 需要在 pubspec.yaml 中声明图片资源(背景图、头像占位图、分享图等)。

目录结构

lib/
├─ main.dart
├─ app_ui/
│  └─ app_theme.dart
└─ chat/
   ├─ bloc/
   │  ├─ chat_bloc.dart
   │  ├─ chat_event.dart
   │  └─ chat_state.dart
   ├─ model/
   │  ├─ message.dart
   │  └─ User.dart
   ├─ view/
   │  └─ chat_page.dart
   └─ widgets/
      ├─ message_bubble.dart
      ├─ message_bubble_background.dart
      ├─ message_conent.dart
      ├─ message_text.dart
      ├─ message_input_controller.dart
      └─ message_text_field.dart

整体架构

MaterialApp (AppTheme)
  └── ChatPage (BlocProvider<ChatBloc>)
      └── ChatView (Stateful)
          ├── AppBar: ChatAppBar
          ├── Expanded: ChatMessagesListView
          │    └── ScrollablePositionedList (reverse=true)
          │         └── MessageBubble
          │              ├── MessageBubbleBackground (CustomPainter 渐变)
          │              └── MessageBubbleContent
          │                   ├── TextMessageWidget (RenderObject 文本+状态)
          │                   └── MessageSharePost (分享卡片)
          └── ChatMessageTextField (输入框/发送按钮)

页面与状态流转

入口与主题

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  initializeDateFormatting('zh_CN', null).then((_) => runApp(const MyApp()));
}

class MyApp extends StatelessWidget { ... }
  • 要点:提前初始化 intlzh_CN 本地化,再启动应用。

BLoC:消息获取与发送

class ChatBloc extends Bloc<ChatEvent, ChatState> {
  ChatBloc() : super(const ChatState.initial()) {
    on<ChatMessagesRequested>(_onMessagesRequested);
    on<ChatSendMessageRequested>(_onSendMessageRequested);
  }
  ...
}
  • ChatMessagesRequested:初始化拉取 Message.messageList(演示数据)。
  • ChatSendMessageRequested:将输入框生成的 Message 追加到状态。

自定义聊天气泡:渐变背景 & 状态布局

渐变背景 MessageBubbleBackground

  • 通过 CustomPainter + ui.Gradient.linear 实现 随滚动位置变化的线性渐变
class BubblePainter extends CustomPainter {
  BubblePainter({ required ScrollableState scrollable, ... })
    : super(repaint: scrollable.position); // 随滚动重绘

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..shader = ui.Gradient.linear(
        scrollableRect.topCenter,
        scrollableRect.bottomCenter,
        _colors,
        _colors.length == 4 ? [0.0, 0.5, 0.75, 1.0] : [1.0],
        TileMode.clamp,
        Matrix4.translationValues(-origin.dx, -origin.dy, 0).storage,
      );
    canvas.drawRect(Offset.zero & size, paint);
  }
}
  • 关键点repaint: scrollable.position 能够在滚动时驱动渐变刷新,且每条气泡外层套了 RepaintBoundary,避免全列表重绘。

文本与状态的排版 TextMessageWidget

  • 自定义 RenderBox,将发送时间/已读图标作为 child 贴在最后一行文本旁边:
class RenderTextMessageWidget extends RenderBox
    with RenderObjectWithChildMixin<RenderBox> {
  @override
  Size _performLayout({required BoxConstraints constraints, required bool dry}) {
    textPainter = TextPainter(...); textPainter.layout(maxWidth: constraints.maxWidth);
    final lines = textPainter.computeLineMetrics();
    final lastLineWidth = lines.last.width;

    // 若最后一行 + 状态宽度超出一行,则整体高度加半行作为“折行区”
    if (child != null) {
      final childSize = dry
        ? child!.getDryLayout(BoxConstraints(maxWidth: constraints.maxWidth))
        : (child!..layout(BoxConstraints(maxWidth: constraints.maxWidth), parentUsesSize: true)).size;

      if (lastLineWidth + spacing > constraints.maxWidth - childSize.width) {
        height += childSize.height * 0.5;
      } else if (lines.length == 1) {
        width += childSize.width + spacing;
      }
    }
    return Size(width, height);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    textPainter.paint(context.canvas, offset);
    final parentData = child!.parentData! as BoxParentData;
    context.paintChild(child!, offset + parentData.offset);
  }
}
  • 效果:单行文本时,时间/已读直接跟在文字后;多行时,尽量贴近最后一行,超出就下沉半行。

分享卡片样式 MessageSharePost

  • 头像 + 用户名 + 图片 + 文字说明,右下角悬浮消息状态:
Positioned.fill(
  right: 12, bottom: 4,
  child: Align(
    alignment: Alignment.bottomRight,
    child: MessageStatuses(message: message),
  ),
)

输入框系统:MessageInputController

  • 使用 ValueNotifier<Message> 管理输入态,与 TextEditingController 双向同步:
class MessageInputController extends ValueNotifier<Message> {
  MessageInputController._(...) : super(initialMessage) {
    _textFieldController.addListener(_textFieldListener);
  }

  void _textFieldListener() {
    message = message.copyWith(message: _textFieldController.text);
  }

  void reset({bool resetId = true}) { ... }
}
  • 发送逻辑:回车/点击发送触发 _onSend(),派发 ChatSendMessageRequested,然后重置输入并滚动回底部。

消息列表与滚动体验

  • 使用 ScrollablePositionedList.separated,并设置 reverse: true最新消息在底部
ScrollablePositionedList.separated(
  itemCount: messages.length,
  reverse: true,
  itemScrollController: itemScrollController,
  itemPositionsListener: itemPositionsListener,
  ...
)
  • 「回到底部」按钮根据 UserScrollNotification 的方向切换显示:
if (notification.direction == ScrollDirection.forward) {
  _showScrollToBottom.value = false;
} else if (notification.direction == ScrollDirection.reverse) {
  _showScrollToBottom.value = true;
}
  • 点击后滚动:itemScrollController.scrollTo(index: 0, duration: 150ms, curve: easeIn)

主题与系统栏适配

  • 采用 FlexColorScheme 定义浅色主题,并通过扩展方法为 Android/iOS 设置不同的系统栏样式:
extension SystemNavigationBarTheme on Widget {
  Widget withSystemNavigationBarTheme(BuildContext context) =>
      AnnotatedRegion<SystemUiOverlayStyle>(value: ... , child: this);
}

国际化与时间格式

  • main() 初始化:initializeDateFormatting('zh_CN', null)
  • 显示格式:DateFormat.Hm('zh_CN').format(message.createAt)

注意 API:DateFormat.Hm('zh_CN') 更直观;如需 24 小时制、日期拼接,可换用 DateFormat('yyyy-MM-dd HH:mm')


可用性与性能优化

  • RepaintBoundary 包裹气泡内容,减少渐变重绘影响。
  • ✅ 列表项 key: ValueKey(message.id) 提升复用稳定性。
  • ✅ 控制 maxLinesoverflow,避免极端长文本卡顿。
  • ✅ 所有异步滚动与动画时长使用 毫秒 级(可读性更好)。
  • ✅ 图片缩略图通过 ResizeImage.resizeIfNeeded 控制分辨率。

结语

本文完整拆解了一个高质感的 Flutter 聊天页面:从 UI 细节到状态管理,再到滚动与绘制优化。你可以在此基础上扩展 图片/语音/撤回/已读回执/引用回复/消息搜索 等高级能力。


仓库地址

GitHub