refactor: update ChatsDrawer to use slivers for improved performance and layout
- Replaced ListView with CustomScrollView and SliverList for better lazy loading of conversation tiles. - Removed legacy helper methods and optimized the layout structure using SliverPadding and SliverToBoxAdapter. - Enhanced the refreshable scrollable functionality to support slivers, improving overall performance and responsiveness. - Streamlined the handling of pinned, folder, and archived conversations within the drawer.
This commit is contained in:
@@ -78,42 +78,42 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRefreshableScrollable({required List<Widget> children}) {
|
// Build a lazily-constructed sliver list of conversation tiles.
|
||||||
// Common padding used in both scrollable variants
|
Widget _conversationsSliver(List<dynamic> items, {bool inFolder = false}) {
|
||||||
const padding = EdgeInsets.fromLTRB(0, Spacing.sm, 0, Spacing.md);
|
return SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) => _buildTileFor(items[index], inFolder: inFolder),
|
||||||
|
childCount: items.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy helper removed: drawer now uses slivers with lazy delegates.
|
||||||
|
|
||||||
|
Widget _buildRefreshableScrollableSlivers({required List<Widget> slivers}) {
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
// Use Cupertino-style pull-to-refresh on iOS
|
|
||||||
final scroll = CustomScrollView(
|
final scroll = CustomScrollView(
|
||||||
key: const PageStorageKey<String>('chats_drawer_scroll'),
|
key: const PageStorageKey<String>('chats_drawer_scroll'),
|
||||||
controller: _listController,
|
controller: _listController,
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
slivers: [
|
slivers: [
|
||||||
CupertinoSliverRefreshControl(onRefresh: _refreshChats),
|
CupertinoSliverRefreshControl(onRefresh: _refreshChats),
|
||||||
SliverPadding(
|
...slivers,
|
||||||
padding: padding,
|
|
||||||
sliver: SliverList(delegate: SliverChildListDelegate(children)),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
return CupertinoScrollbar(controller: _listController, child: scroll);
|
return CupertinoScrollbar(controller: _listController, child: scroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Material pull-to-refresh elsewhere
|
final scroll = CustomScrollView(
|
||||||
|
key: const PageStorageKey<String>('chats_drawer_scroll'),
|
||||||
|
controller: _listController,
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
cacheExtent: 800,
|
||||||
|
slivers: slivers,
|
||||||
|
);
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: _refreshChats,
|
onRefresh: _refreshChats,
|
||||||
child: Scrollbar(
|
child: Scrollbar(controller: _listController, child: scroll),
|
||||||
controller: _listController,
|
|
||||||
child: ListView(
|
|
||||||
key: const PageStorageKey<String>('chats_drawer_scroll'),
|
|
||||||
controller: _listController,
|
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
// Precache a bit ahead for perceived smoothness when scrolling.
|
|
||||||
cacheExtent: 800,
|
|
||||||
padding: padding,
|
|
||||||
children: children,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,35 +285,34 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
|
|
||||||
final archived = list.where((c) => c.archived == true).toList();
|
final archived = list.where((c) => c.archived == true).toList();
|
||||||
|
|
||||||
final children = <Widget>[
|
final slivers = <Widget>[
|
||||||
if (pinned.isNotEmpty) ...[
|
if (pinned.isNotEmpty) ...[
|
||||||
Padding(
|
SliverPadding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
left: Spacing.md,
|
sliver: SliverToBoxAdapter(
|
||||||
right: Spacing.md,
|
child: _buildSectionHeader(
|
||||||
),
|
AppLocalizations.of(context)!.pinned,
|
||||||
child: _buildSectionHeader(
|
pinned.length,
|
||||||
AppLocalizations.of(context)!.pinned,
|
),
|
||||||
pinned.length,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)),
|
||||||
...pinned.map((conv) => _buildTileFor(conv)),
|
_conversationsSliver(pinned),
|
||||||
const SizedBox(height: Spacing.md),
|
const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)),
|
||||||
],
|
],
|
||||||
|
|
||||||
// Folders section (shown even if empty)
|
// Folders section (shown even if empty)
|
||||||
Padding(
|
SliverPadding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
left: Spacing.md,
|
sliver: SliverToBoxAdapter(child: _buildFoldersSectionHeader()),
|
||||||
right: Spacing.md,
|
|
||||||
),
|
|
||||||
child: _buildFoldersSectionHeader(),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)),
|
||||||
if (_isDragging && _draggingHasFolder) ...[
|
if (_isDragging && _draggingHasFolder) ...[
|
||||||
_buildUnfileDropTarget(),
|
SliverPadding(
|
||||||
const SizedBox(height: Spacing.sm),
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
|
sliver: SliverToBoxAdapter(child: _buildUnfileDropTarget()),
|
||||||
|
),
|
||||||
|
const SliverToBoxAdapter(child: SizedBox(height: Spacing.sm)),
|
||||||
],
|
],
|
||||||
...ref
|
...ref
|
||||||
.watch(foldersProvider)
|
.watch(foldersProvider)
|
||||||
@@ -327,8 +326,8 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
|
|
||||||
final expandedMap = ref.watch(_expandedFoldersProvider);
|
final expandedMap = ref.watch(_expandedFoldersProvider);
|
||||||
|
|
||||||
// Show all folders (including empty)
|
final out = <Widget>[];
|
||||||
final sections = folders.map((folder) {
|
for (final folder in folders) {
|
||||||
final existing = grouped[folder.id] ?? const <dynamic>[];
|
final existing = grouped[folder.id] ?? const <dynamic>[];
|
||||||
final convs = _resolveFolderConversations(
|
final convs = _resolveFolderConversations(
|
||||||
folder,
|
folder,
|
||||||
@@ -337,59 +336,80 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
final isExpanded =
|
final isExpanded =
|
||||||
expandedMap[folder.id] ?? folder.isExpanded;
|
expandedMap[folder.id] ?? folder.isExpanded;
|
||||||
final hasItems = convs.isNotEmpty;
|
final hasItems = convs.isNotEmpty;
|
||||||
return Column(
|
out.add(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
SliverPadding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(
|
||||||
_buildFolderHeader(
|
horizontal: Spacing.md,
|
||||||
folder.id,
|
|
||||||
folder.name,
|
|
||||||
convs.length,
|
|
||||||
defaultExpanded: folder.isExpanded,
|
|
||||||
),
|
),
|
||||||
if (isExpanded && hasItems) ...[
|
sliver: SliverToBoxAdapter(
|
||||||
const SizedBox(height: Spacing.xs),
|
child: _buildFolderHeader(
|
||||||
...convs.map(
|
folder.id,
|
||||||
(c) => _buildTileFor(c, inFolder: true),
|
folder.name,
|
||||||
|
convs.length,
|
||||||
|
defaultExpanded: folder.isExpanded,
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
),
|
||||||
],
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}).toList();
|
if (isExpanded && hasItems) {
|
||||||
return sections.isEmpty
|
out.add(
|
||||||
? [const SizedBox.shrink()]
|
const SliverToBoxAdapter(
|
||||||
: sections;
|
child: SizedBox(height: Spacing.xs),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
out.add(_conversationsSliver(convs, inFolder: true));
|
||||||
|
out.add(
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(height: Spacing.xs),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
out.add(
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(height: Spacing.xs),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return out.isEmpty
|
||||||
|
? <Widget>[
|
||||||
|
const SliverToBoxAdapter(child: SizedBox.shrink()),
|
||||||
|
]
|
||||||
|
: out;
|
||||||
},
|
},
|
||||||
loading: () => [const SizedBox.shrink()],
|
loading: () => [
|
||||||
error: (e, st) => [const SizedBox.shrink()],
|
const SliverToBoxAdapter(child: SizedBox.shrink()),
|
||||||
|
],
|
||||||
|
error: (e, st) => [
|
||||||
|
const SliverToBoxAdapter(child: SizedBox.shrink()),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.md),
|
const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)),
|
||||||
|
|
||||||
if (regular.isNotEmpty) ...[
|
if (regular.isNotEmpty) ...[
|
||||||
Padding(
|
SliverPadding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
left: Spacing.md,
|
sliver: SliverToBoxAdapter(
|
||||||
right: Spacing.md,
|
child: _buildSectionHeader(
|
||||||
),
|
AppLocalizations.of(context)!.recent,
|
||||||
child: _buildSectionHeader(
|
regular.length,
|
||||||
AppLocalizations.of(context)!.recent,
|
),
|
||||||
regular.length,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)),
|
||||||
...regular.map(_buildTileFor),
|
_conversationsSliver(regular),
|
||||||
],
|
],
|
||||||
|
|
||||||
if (archived.isNotEmpty) ...[
|
if (archived.isNotEmpty) ...[
|
||||||
const SizedBox(height: Spacing.md),
|
const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)),
|
||||||
Padding(
|
SliverPadding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
child: _buildArchivedSection(archived),
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: _buildArchivedSection(archived),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
return _buildRefreshableScrollable(children: children);
|
return _buildRefreshableScrollableSlivers(slivers: slivers);
|
||||||
},
|
},
|
||||||
loading: () =>
|
loading: () =>
|
||||||
const Center(child: CircularProgressIndicator(strokeWidth: 2.0)),
|
const Center(child: CircularProgressIndicator(strokeWidth: 2.0)),
|
||||||
@@ -453,97 +473,145 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
|||||||
|
|
||||||
final archived = list.where((c) => c.archived == true).toList();
|
final archived = list.where((c) => c.archived == true).toList();
|
||||||
|
|
||||||
final children = <Widget>[
|
final slivers = <Widget>[
|
||||||
Padding(
|
SliverPadding(
|
||||||
padding: const EdgeInsets.only(left: Spacing.md, right: Spacing.md),
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
child: _buildSectionHeader('Results', list.length),
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: _buildSectionHeader('Results', list.length),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)),
|
||||||
if (pinned.isNotEmpty) ...[
|
];
|
||||||
Padding(
|
|
||||||
|
if (pinned.isNotEmpty) {
|
||||||
|
slivers.addAll([
|
||||||
|
SliverPadding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
child: _buildSectionHeader(
|
sliver: SliverToBoxAdapter(
|
||||||
AppLocalizations.of(context)!.pinned,
|
child: _buildSectionHeader(
|
||||||
pinned.length,
|
AppLocalizations.of(context)!.pinned,
|
||||||
|
pinned.length,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)),
|
||||||
...pinned.map((conv) => _buildTileFor(conv)),
|
_conversationsSliver(pinned),
|
||||||
const SizedBox(height: Spacing.md),
|
const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)),
|
||||||
],
|
]);
|
||||||
// Folders section (shown even if empty)
|
}
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: Spacing.md, right: Spacing.md),
|
slivers.addAll([
|
||||||
child: _buildFoldersSectionHeader(),
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
|
sliver: SliverToBoxAdapter(child: _buildFoldersSectionHeader()),
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)),
|
||||||
if (_isDragging && _draggingHasFolder) ...[
|
]);
|
||||||
_buildUnfileDropTarget(),
|
|
||||||
const SizedBox(height: Spacing.sm),
|
|
||||||
],
|
|
||||||
...ref
|
|
||||||
.watch(foldersProvider)
|
|
||||||
.when(
|
|
||||||
data: (folders) {
|
|
||||||
final grouped = <String, List<dynamic>>{};
|
|
||||||
for (final c in foldered) {
|
|
||||||
final id = c.folderId!;
|
|
||||||
grouped.putIfAbsent(id, () => []).add(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
final expandedMap = ref.watch(_expandedFoldersProvider);
|
if (_isDragging && _draggingHasFolder) {
|
||||||
|
slivers.add(
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
|
sliver: SliverToBoxAdapter(child: _buildUnfileDropTarget()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
slivers.add(
|
||||||
|
const SliverToBoxAdapter(child: SizedBox(height: Spacing.sm)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final sections = folders.map((folder) {
|
final folderSlivers = ref
|
||||||
final existing = grouped[folder.id] ?? const <dynamic>[];
|
.watch(foldersProvider)
|
||||||
final convs = _resolveFolderConversations(folder, existing);
|
.when(
|
||||||
final isExpanded =
|
data: (folders) {
|
||||||
expandedMap[folder.id] ?? folder.isExpanded;
|
final grouped = <String, List<dynamic>>{};
|
||||||
final hasItems = convs.isNotEmpty;
|
for (final c in foldered) {
|
||||||
return Column(
|
final id = c.folderId!;
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
grouped.putIfAbsent(id, () => []).add(c);
|
||||||
children: [
|
}
|
||||||
_buildFolderHeader(
|
final expandedMap = ref.watch(_expandedFoldersProvider);
|
||||||
|
final out = <Widget>[];
|
||||||
|
for (final folder in folders) {
|
||||||
|
final existing = grouped[folder.id] ?? const <dynamic>[];
|
||||||
|
final convs = _resolveFolderConversations(folder, existing);
|
||||||
|
final isExpanded =
|
||||||
|
expandedMap[folder.id] ?? folder.isExpanded;
|
||||||
|
final hasItems = convs.isNotEmpty;
|
||||||
|
|
||||||
|
out.add(
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.md,
|
||||||
|
),
|
||||||
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: _buildFolderHeader(
|
||||||
folder.id,
|
folder.id,
|
||||||
folder.name,
|
folder.name,
|
||||||
convs.length,
|
convs.length,
|
||||||
defaultExpanded: folder.isExpanded,
|
defaultExpanded: folder.isExpanded,
|
||||||
),
|
),
|
||||||
if (isExpanded && hasItems) ...[
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
),
|
||||||
...convs.map((c) => _buildTileFor(c, inFolder: true)),
|
);
|
||||||
const SizedBox(height: Spacing.sm),
|
if (isExpanded && hasItems) {
|
||||||
],
|
out.add(
|
||||||
],
|
const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(height: Spacing.xs),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}).toList();
|
out.add(_conversationsSliver(convs, inFolder: true));
|
||||||
return sections.isEmpty
|
out.add(
|
||||||
? [const SizedBox.shrink()]
|
const SliverToBoxAdapter(
|
||||||
: sections;
|
child: SizedBox(height: Spacing.sm),
|
||||||
},
|
),
|
||||||
loading: () => [const SizedBox.shrink()],
|
);
|
||||||
error: (e, st) => [const SizedBox.shrink()],
|
}
|
||||||
),
|
}
|
||||||
const SizedBox(height: Spacing.md),
|
return out.isEmpty
|
||||||
if (regular.isNotEmpty) ...[
|
? <Widget>[
|
||||||
Padding(
|
const SliverToBoxAdapter(child: SizedBox.shrink()),
|
||||||
|
]
|
||||||
|
: out;
|
||||||
|
},
|
||||||
|
loading: () => <Widget>[
|
||||||
|
const SliverToBoxAdapter(child: SizedBox.shrink()),
|
||||||
|
],
|
||||||
|
error: (e, st) => <Widget>[
|
||||||
|
const SliverToBoxAdapter(child: SizedBox.shrink()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
slivers.addAll(folderSlivers);
|
||||||
|
|
||||||
|
if (regular.isNotEmpty) {
|
||||||
|
slivers.addAll([
|
||||||
|
const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)),
|
||||||
|
SliverPadding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
child: _buildSectionHeader(
|
sliver: SliverToBoxAdapter(
|
||||||
AppLocalizations.of(context)!.recent,
|
child: _buildSectionHeader(
|
||||||
regular.length,
|
AppLocalizations.of(context)!.recent,
|
||||||
|
regular.length,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.xs),
|
const SliverToBoxAdapter(child: SizedBox(height: Spacing.xs)),
|
||||||
...regular.map(_buildTileFor),
|
_conversationsSliver(regular),
|
||||||
],
|
]);
|
||||||
if (archived.isNotEmpty) ...[
|
}
|
||||||
const SizedBox(height: Spacing.md),
|
|
||||||
Padding(
|
if (archived.isNotEmpty) {
|
||||||
|
slivers.addAll([
|
||||||
|
const SliverToBoxAdapter(child: SizedBox(height: Spacing.md)),
|
||||||
|
SliverPadding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
padding: const EdgeInsets.symmetric(horizontal: Spacing.md),
|
||||||
child: _buildArchivedSection(archived),
|
sliver: SliverToBoxAdapter(
|
||||||
|
child: _buildArchivedSection(archived),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
]);
|
||||||
];
|
}
|
||||||
return _buildRefreshableScrollable(children: children);
|
|
||||||
|
return _buildRefreshableScrollableSlivers(slivers: slivers);
|
||||||
},
|
},
|
||||||
loading: () =>
|
loading: () =>
|
||||||
const Center(child: CircularProgressIndicator(strokeWidth: 2.0)),
|
const Center(child: CircularProgressIndicator(strokeWidth: 2.0)),
|
||||||
|
|||||||
Reference in New Issue
Block a user