Skip to content

Commit 332dfab

Browse files
committed
feat: implement two-sided pagination
1 parent 1297071 commit 332dfab

File tree

11 files changed

+630
-268
lines changed

11 files changed

+630
-268
lines changed

examples/flyer_chat/lib/hive_chat_controller.dart

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,33 @@ class HiveChatController
1313
List<Message>? _cachedMessages;
1414

1515
@override
16-
Future<void> insertMessage(Message message, {int? index}) async {
16+
Future<void> insertMessage(
17+
Message message, {
18+
int? index,
19+
bool animated = true,
20+
}) async {
1721
if (_box.containsKey(message.id)) return;
1822

1923
// Index is ignored because Hive does not maintain order
2024
await _box.put(message.id, message.toJson());
2125
_invalidateCache();
22-
_operationsController.add(ChatOperation.insert(message, _box.length - 1));
26+
_operationsController.add(
27+
ChatOperation.insert(message, _box.length - 1, animated: animated),
28+
);
2329
}
2430

2531
@override
26-
Future<void> removeMessage(Message message) async {
32+
Future<void> removeMessage(Message message, {bool animated = true}) async {
2733
final sortedMessages = List.from(messages);
2834
final index = sortedMessages.indexWhere((m) => m.id == message.id);
2935

3036
if (index != -1) {
3137
final messageToRemove = sortedMessages[index];
3238
await _box.delete(messageToRemove.id);
3339
_invalidateCache();
34-
_operationsController.add(ChatOperation.remove(messageToRemove, index));
40+
_operationsController.add(
41+
ChatOperation.remove(messageToRemove, index, animated: animated),
42+
);
3543
}
3644
}
3745

@@ -56,11 +64,14 @@ class HiveChatController
5664
}
5765

5866
@override
59-
Future<void> setMessages(List<Message> messages) async {
67+
Future<void> setMessages(
68+
List<Message> messages, {
69+
bool animated = true,
70+
}) async {
6071
await _box.clear();
6172
if (messages.isEmpty) {
6273
_invalidateCache();
63-
_operationsController.add(ChatOperation.set([]));
74+
_operationsController.add(ChatOperation.set([], animated: false));
6475
return;
6576
} else {
6677
await _box.putAll(
@@ -70,12 +81,18 @@ class HiveChatController
7081
.reduce((acc, map) => {...acc, ...map}),
7182
);
7283
_invalidateCache();
73-
_operationsController.add(ChatOperation.set(messages));
84+
_operationsController.add(
85+
ChatOperation.set(messages, animated: animated),
86+
);
7487
}
7588
}
7689

7790
@override
78-
Future<void> insertAllMessages(List<Message> messages, {int? index}) async {
91+
Future<void> insertAllMessages(
92+
List<Message> messages, {
93+
int? index,
94+
bool animated = true,
95+
}) async {
7996
if (messages.isEmpty) return;
8097

8198
// Index is ignored because Hive does not maintain order
@@ -88,7 +105,7 @@ class HiveChatController
88105
);
89106
_invalidateCache();
90107
_operationsController.add(
91-
ChatOperation.insertAll(messages, originalLength),
108+
ChatOperation.insertAll(messages, originalLength, animated: animated),
92109
);
93110
}
94111

examples/flyer_chat/lib/main.dart

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import 'api_get_initial_messages.dart';
1212
import 'basic.dart';
1313
import 'gemini.dart';
1414
import 'local.dart';
15-
import 'pagination.dart';
15+
import 'pagination_newer.dart';
16+
import 'pagination_older.dart';
1617

1718
void main() async {
1819
WidgetsFlutterBinding.ensureInitialized();
@@ -268,11 +269,22 @@ class _FlyerChatHomePageState extends State<FlyerChatHomePage> {
268269
),
269270
ElevatedButton(
270271
onPressed: () {
271-
Navigator.of(
272-
context,
273-
).push(MaterialPageRoute(builder: (context) => Pagination()));
272+
Navigator.of(context).push(
273+
MaterialPageRoute(
274+
builder: (context) => const PaginationOlder(),
275+
),
276+
);
274277
},
275-
child: const Text('pagination'),
278+
child: const Text('pagination (get older)'),
279+
),
280+
const SizedBox(height: 8),
281+
ElevatedButton(
282+
onPressed: () => Navigator.of(context).push(
283+
MaterialPageRoute(
284+
builder: (context) => const PaginationNewer(),
285+
),
286+
),
287+
child: const Text('pagination (get newer)'),
276288
),
277289
const SizedBox(height: 8),
278290
ElevatedButton(
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import 'dart:math';
2+
3+
import 'package:flutter_chat_core/flutter_chat_core.dart';
4+
5+
const _mockDatabaseDelay = Duration(milliseconds: 500);
6+
7+
class MockDatabase {
8+
static final _messages = List.generate(100, (i) {
9+
final random = Random();
10+
final numLines = random.nextInt(4) + 1;
11+
final text = List.generate(
12+
numLines,
13+
(lineIndex) => 'Message ${i + 1} - Line ${lineIndex + 1}',
14+
).join('\n');
15+
return Message.text(
16+
id: (i + 1).toString(),
17+
authorId: 'me',
18+
createdAt: DateTime.fromMillisecondsSinceEpoch(
19+
1736893310000 - ((100 - i) * 1000),
20+
isUtc: true,
21+
),
22+
text: text,
23+
);
24+
});
25+
26+
static final List<Message> initialNewerMessages = _messages
27+
.take(20)
28+
.toList()
29+
.reversed
30+
.toList();
31+
32+
static final List<Message> initialOlderMessages = _messages
33+
.skip(80)
34+
.take(20)
35+
.toList()
36+
.reversed
37+
.toList();
38+
39+
static Future<List<Message>> getOlderMessages({
40+
required int limit,
41+
MessageID? lastMessageId,
42+
}) async {
43+
await Future.delayed(_mockDatabaseDelay);
44+
45+
final start = lastMessageId == null
46+
? 20
47+
: _messages.indexWhere((m) => m.id == lastMessageId) + 1;
48+
49+
if (start >= _messages.length) return [];
50+
51+
return _messages.skip(start).take(limit).toList().reversed.toList();
52+
}
53+
54+
static Future<List<Message>> getNewerMessages({
55+
required int limit,
56+
MessageID? newestMessageId,
57+
}) async {
58+
await Future.delayed(_mockDatabaseDelay);
59+
final end = newestMessageId == null
60+
? 80
61+
: _messages.indexWhere((m) => m.id == newestMessageId);
62+
63+
if (end <= 0) return [];
64+
65+
final start = (end - limit).clamp(0, end);
66+
67+
return _messages.sublist(start, end).reversed.toList();
68+
}
69+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_chat_core/flutter_chat_core.dart';
3+
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
4+
5+
import 'pagination_mock_database.dart';
6+
7+
class PaginationNewer extends StatefulWidget {
8+
const PaginationNewer({super.key});
9+
10+
@override
11+
PaginationNewerState createState() => PaginationNewerState();
12+
}
13+
14+
class PaginationNewerState extends State<PaginationNewer> {
15+
final _chatController = InMemoryChatController(
16+
messages: List.from(MockDatabase.initialOlderMessages),
17+
);
18+
final _currentUser = const User(id: 'me');
19+
20+
bool _hasMore = true;
21+
bool _isLoading = false;
22+
23+
@override
24+
void dispose() {
25+
_chatController.dispose();
26+
super.dispose();
27+
}
28+
29+
@override
30+
Widget build(BuildContext context) {
31+
final theme = Theme.of(context);
32+
33+
return Scaffold(
34+
appBar: AppBar(title: const Text('Pagination (get newer)')),
35+
body: Chat(
36+
builders: Builders(
37+
chatAnimatedListBuilder: (context, itemBuilder) {
38+
return ChatAnimatedList(
39+
itemBuilder: itemBuilder,
40+
onStartReached: _loadNewerMessages,
41+
initialScrollToEndMode: InitialScrollToEndMode.none,
42+
);
43+
},
44+
),
45+
chatController: _chatController,
46+
currentUserId: _currentUser.id,
47+
resolveUser: (id) => Future.value(switch (id) {
48+
'me' => _currentUser,
49+
_ => null,
50+
}),
51+
theme: ChatTheme.fromThemeData(theme),
52+
),
53+
);
54+
}
55+
56+
Future<void> _loadNewerMessages() async {
57+
if (!_hasMore || _isLoading) return;
58+
59+
_isLoading = true;
60+
61+
// The visually bottommost message is the last in our data list.
62+
final newestMessageId = _chatController.messages.last.id;
63+
final messages = await MockDatabase.getNewerMessages(
64+
limit: 20,
65+
newestMessageId: newestMessageId,
66+
);
67+
68+
if (messages.isEmpty) {
69+
_hasMore = false;
70+
} else {
71+
// Append newer messages to the visual bottom of the list.
72+
await _chatController.insertAllMessages(
73+
messages,
74+
index: _chatController.messages.length,
75+
// Important: we don't want to animate the insertion of the messages
76+
// because pagination logic relies on messages to be inserted instantly.
77+
// There is no need for animation anyway as all newer messages are out of
78+
// visible range.
79+
animated: false,
80+
);
81+
}
82+
83+
_isLoading = false;
84+
}
85+
}

0 commit comments

Comments
 (0)