import 'package:flutter/material.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart' as chat_core;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
void main() {
runApp(const MinimalReproApp());
}
class MinimalReproApp extends StatelessWidget {
const MinimalReproApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Scroll Bug Repro',
debugShowCheckedModeBanner: false,
home: const ChatPage(),
);
}
}
class ChatPage extends StatefulWidget {
const ChatPage({super.key});
@override
State<ChatPage> createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
final chat_core.ChatController _chatController = chat_core.InMemoryChatController();
final _user = const chat_core.User(id: 'user-1');
final _otherUser = const chat_core.User(id: 'user-2', name: 'Other User');
int _messageCounter = 0;
@override
void initState() {
super.initState();
_loadInitialMessages();
}
void _loadInitialMessages() {
_addMessage('Message ${_messageCounter + 1}');
}
void _addMessage(String text, {String? authorId}) {
final message = chat_core.TextMessage(
createdAt: DateTime.now(),
id: 'msg-${_messageCounter++}',
text: text,
authorId: authorId ?? _user.id,
);
setState(() {
_chatController.insertMessage(message);
});
}
void _addTallMessage() {
// Create a message with lots of text that will be taller than the viewport
final tallText = List.generate(70, (i) => 'Line ${i + 1} of a very tall message.')
.join('\n');
// Send from the other user to simulate receiving a tall message
_addMessage(tallText, authorId: _otherUser.id);
}
Future<chat_core.User> _resolveUser(String userId) async {
if (userId == _otherUser.id) {
return _otherUser;
}
return _user;
}
late final chat_core.Builders _chatBuilders = chat_core.Builders(
chatAnimatedListBuilder: (context, itemBuilder) {
return ChatAnimatedList(
itemBuilder: itemBuilder,
shouldScrollToEndWhenAtBottom: false,
shouldScrollToEndWhenSendingMessage: false,
initialScrollToEndMode: InitialScrollToEndMode.none,
);
},
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Scroll Bug Reproduction'),
actions: [
TextButton(
onPressed: _addTallMessage,
style: TextButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text(
'Add Tall Message',
style: TextStyle(color: Colors.black),
),
),
],
),
body: Column(
children: [
Expanded(
child: Chat(
chatController: _chatController,
currentUserId: _user.id,
resolveUser: _resolveUser,
builders: _chatBuilders,
theme: chat_core.ChatTheme(
colors: chat_core.ChatColors(
primary: Colors.blue,
onPrimary: Colors.white,
surface: Colors.white,
onSurface: Colors.black,
surfaceContainer: Colors.grey.shade100,
surfaceContainerLow: Colors.grey.shade50,
surfaceContainerHigh: Colors.grey.shade200,
),
typography: chat_core.ChatTypography(
bodyLarge: const TextStyle(fontSize: 16),
bodyMedium: const TextStyle(fontSize: 14),
bodySmall: const TextStyle(fontSize: 12),
labelLarge: const TextStyle(fontSize: 16),
labelMedium: const TextStyle(fontSize: 14),
labelSmall: const TextStyle(fontSize: 12),
),
shape: const BorderRadiusGeometry.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
bottomLeft: Radius.circular(20),
),
),
),
),
],
),
);
}
}
When the
ChatAnimatedListtransitions from non-scrollable to scrollable, for example, when a message that does not fit the viewport is received, it automatically scrolls to the end. As a result, the user needs to scroll up again to see the beginning of the message.Steps to reproduce:
Expected behavior:
The list should not scroll
Actual behavior:
The list scrolls
Screen.Recording.2026-01-22.at.08.59.33.mov
Code sample