💫 List loading state switch animation

This commit is contained in:
2025-12-06 19:54:34 +08:00
parent ea93aa144e
commit 2ff60fc4ff

View File

@@ -39,6 +39,8 @@ class PaginationList<T> extends HookConsumerWidget {
final data = ref.watch(provider);
final noti = ref.watch(notifier);
if (isSliver) {
// For slivers, return widgets directly without animation
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
final content = List<Widget>.generate(
10,
@@ -54,9 +56,7 @@ class PaginationList<T> extends HookConsumerWidget {
child: footerSkeletonChild ?? const _DefaultSkeletonChild(),
),
);
return isSliver
? SliverList.list(children: content)
: ListView(children: content);
return SliverList.list(children: content);
}
if (data.hasError) {
@@ -64,27 +64,10 @@ class PaginationList<T> extends HookConsumerWidget {
error: data.error,
onRetry: noti.refresh,
);
return isSliver ? SliverFillRemaining(child: content) : content;
return SliverFillRemaining(child: content);
}
final listView = isSliver
? SuperSliverList.builder(
itemCount: (data.value?.length ?? 0) + 1,
itemBuilder: (context, idx) {
if (idx == data.value?.length) {
return PaginationListFooter(
noti: noti,
data: data,
skeletonChild: footerSkeletonChild,
);
}
final entry = data.value?[idx];
if (entry != null) return itemBuilder(context, idx, entry);
return null;
},
)
: SuperListView.builder(
padding: padding,
final listView = SuperSliverList.builder(
itemCount: (data.value?.length ?? 0) + 1,
itemBuilder: (context, idx) {
if (idx == data.value?.length) {
@@ -104,6 +87,68 @@ class PaginationList<T> extends HookConsumerWidget {
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: listView)
: listView;
}
// For non-slivers, use AnimatedSwitcher for smooth transitions
Widget buildContent() {
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
final content = List<Widget>.generate(
10,
(_) => Skeletonizer(
enabled: true,
effect: ShimmerEffect(
baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
highlightColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
child: footerSkeletonChild ?? const _DefaultSkeletonChild(),
),
);
return SizedBox(
key: const ValueKey('loading'),
child: ListView(children: content),
);
}
if (data.hasError) {
final content = ResponseErrorWidget(
error: data.error,
onRetry: noti.refresh,
);
return SizedBox(key: const ValueKey('error'), child: content);
}
final listView = SuperListView.builder(
padding: padding,
itemCount: (data.value?.length ?? 0) + 1,
itemBuilder: (context, idx) {
if (idx == data.value?.length) {
return PaginationListFooter(
noti: noti,
data: data,
skeletonChild: footerSkeletonChild,
);
}
final entry = data.value?[idx];
if (entry != null) return itemBuilder(context, idx, entry);
return null;
},
);
return SizedBox(
key: const ValueKey('data'),
child: isRefreshable
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: listView)
: listView,
);
}
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: buildContent(),
);
}
}
class PaginationWidget<T> extends HookConsumerWidget {
@@ -130,6 +175,8 @@ class PaginationWidget<T> extends HookConsumerWidget {
final data = ref.watch(provider);
final noti = ref.watch(notifier);
if (isSliver) {
// For slivers, return widgets directly without animation
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
final content = List<Widget>.generate(
10,
@@ -145,9 +192,7 @@ class PaginationWidget<T> extends HookConsumerWidget {
child: footerSkeletonChild ?? const _DefaultSkeletonChild(),
),
);
return isSliver
? SliverList.list(children: content)
: ListView(children: content);
return SliverList.list(children: content);
}
if (data.hasError) {
@@ -155,7 +200,7 @@ class PaginationWidget<T> extends HookConsumerWidget {
error: data.error,
onRetry: noti.refresh,
);
return isSliver ? SliverFillRemaining(child: content) : content;
return SliverFillRemaining(child: content);
}
final footer = PaginationListFooter(
@@ -169,6 +214,58 @@ class PaginationWidget<T> extends HookConsumerWidget {
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: content)
: content;
}
// For non-slivers, use AnimatedSwitcher for smooth transitions
Widget buildContent() {
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
final content = List<Widget>.generate(
10,
(_) => Skeletonizer(
enabled: true,
effect: ShimmerEffect(
baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
highlightColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
child: footerSkeletonChild ?? const _DefaultSkeletonChild(),
),
);
return SizedBox(
key: const ValueKey('loading'),
child: ListView(children: content),
);
}
if (data.hasError) {
final content = ResponseErrorWidget(
error: data.error,
onRetry: noti.refresh,
);
return SizedBox(key: const ValueKey('error'), child: content);
}
final footer = PaginationListFooter(
noti: noti,
data: data,
skeletonChild: footerSkeletonChild,
);
final content = contentBuilder(data.value ?? [], footer);
return SizedBox(
key: const ValueKey('data'),
child: isRefreshable
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: content)
: content,
);
}
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: buildContent(),
);
}
}
class PaginationListFooter<T> extends HookConsumerWidget {