💫 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,70 +39,115 @@ class PaginationList<T> extends HookConsumerWidget {
final data = ref.watch(provider); final data = ref.watch(provider);
final noti = ref.watch(notifier); final noti = ref.watch(notifier);
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) { if (isSliver) {
final content = List<Widget>.generate( // For slivers, return widgets directly without animation
10, if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
(_) => Skeletonizer( final content = List<Widget>.generate(
enabled: true, 10,
effect: ShimmerEffect( (_) => Skeletonizer(
baseColor: Theme.of(context).colorScheme.surfaceContainerHigh, enabled: true,
highlightColor: Theme.of( effect: ShimmerEffect(
context, baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
).colorScheme.surfaceContainerHighest, highlightColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
child: footerSkeletonChild ?? const _DefaultSkeletonChild(),
), ),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow, );
child: footerSkeletonChild ?? const _DefaultSkeletonChild(), return SliverList.list(children: content);
), }
if (data.hasError) {
final content = ResponseErrorWidget(
error: data.error,
onRetry: noti.refresh,
);
return SliverFillRemaining(child: content);
}
final listView = 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;
},
); );
return isSliver
? SliverList.list(children: content) return isRefreshable
: ListView(children: content); ? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: listView)
: listView;
} }
if (data.hasError) { // For non-slivers, use AnimatedSwitcher for smooth transitions
final content = ResponseErrorWidget( Widget buildContent() {
error: data.error, if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
onRetry: noti.refresh, 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 isSliver ? SliverFillRemaining(child: content) : content;
} }
final listView = isSliver return AnimatedSwitcher(
? SuperSliverList.builder( duration: const Duration(milliseconds: 300),
itemCount: (data.value?.length ?? 0) + 1, child: buildContent(),
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,
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 isRefreshable
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: listView)
: listView;
} }
} }
@@ -130,44 +175,96 @@ class PaginationWidget<T> extends HookConsumerWidget {
final data = ref.watch(provider); final data = ref.watch(provider);
final noti = ref.watch(notifier); final noti = ref.watch(notifier);
if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) { if (isSliver) {
final content = List<Widget>.generate( // For slivers, return widgets directly without animation
10, if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
(_) => Skeletonizer( final content = List<Widget>.generate(
enabled: true, 10,
effect: ShimmerEffect( (_) => Skeletonizer(
baseColor: Theme.of(context).colorScheme.surfaceContainerHigh, enabled: true,
highlightColor: Theme.of( effect: ShimmerEffect(
context, baseColor: Theme.of(context).colorScheme.surfaceContainerHigh,
).colorScheme.surfaceContainerHighest, highlightColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow,
child: footerSkeletonChild ?? const _DefaultSkeletonChild(),
), ),
containersColor: Theme.of(context).colorScheme.surfaceContainerLow, );
child: footerSkeletonChild ?? const _DefaultSkeletonChild(), return SliverList.list(children: content);
), }
if (data.hasError) {
final content = ResponseErrorWidget(
error: data.error,
onRetry: noti.refresh,
);
return SliverFillRemaining(child: content);
}
final footer = PaginationListFooter(
noti: noti,
data: data,
skeletonChild: footerSkeletonChild,
); );
return isSliver final content = contentBuilder(data.value ?? [], footer);
? SliverList.list(children: content)
: ListView(children: content); return isRefreshable
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: content)
: content;
} }
if (data.hasError) { // For non-slivers, use AnimatedSwitcher for smooth transitions
final content = ResponseErrorWidget( Widget buildContent() {
error: data.error, if ((data.isLoading || noti.isLoading) && data.value?.isEmpty == true) {
onRetry: noti.refresh, 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 isSliver ? SliverFillRemaining(child: content) : content;
} }
final footer = PaginationListFooter( return AnimatedSwitcher(
noti: noti, duration: const Duration(milliseconds: 300),
data: data, child: buildContent(),
skeletonChild: footerSkeletonChild,
); );
final content = contentBuilder(data.value ?? [], footer);
return isRefreshable
? ExtendedRefreshIndicator(onRefresh: noti.refresh, child: content)
: content;
} }
} }