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:
cogwheel0
2025-10-10 13:19:11 +05:30
parent 9fff4d49ea
commit 4e64b6f32a

View File

@@ -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)),