From 10069a1800cb491d5cec8c72575e8a820a18bae1 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Mon, 8 Sep 2025 14:32:28 +0900 Subject: [PATCH] perf(ui): enable KeepAlive on subscription list, tune prefetch, and reduce list/gesture animations --- lib/widgets/staggered_list_animation.dart | 32 ++++++++++++-------- lib/widgets/subscription_list_widget.dart | 2 ++ lib/widgets/swipeable_subscription_card.dart | 26 ++++++++++++---- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/lib/widgets/staggered_list_animation.dart b/lib/widgets/staggered_list_animation.dart index ed6068f..e1a2071 100644 --- a/lib/widgets/staggered_list_animation.dart +++ b/lib/widgets/staggered_list_animation.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'dart:math' as math; +import '../utils/reduce_motion.dart'; /// 스태거 애니메이션이 적용된 리스트 위젯 class StaggeredListAnimation extends StatefulWidget { @@ -95,13 +96,14 @@ class _StaggeredListAnimationState extends State @override Widget build(BuildContext context) { + if (ReduceMotion.platform()) { + return widget.direction == Axis.vertical + ? Column(children: widget.children) + : Row(children: widget.children); + } return widget.direction == Axis.vertical - ? Column( - children: _buildAnimatedChildren(), - ) - : Row( - children: _buildAnimatedChildren(), - ); + ? Column(children: _buildAnimatedChildren()) + : Row(children: _buildAnimatedChildren()); } List _buildAnimatedChildren() { @@ -156,8 +158,9 @@ class _StaggeredAnimationItemState extends State @override void initState() { super.initState(); + final reduced = ReduceMotion.platform(); _controller = AnimationController( - duration: widget.duration, + duration: reduced ? Duration.zero : widget.duration, vsync: this, ); @@ -170,7 +173,9 @@ class _StaggeredAnimationItemState extends State )); _slideAnimation = Tween( - begin: const Offset(0, 0.3), + begin: ReduceMotion.platform() + ? const Offset(0, 0.05) + : const Offset(0, 0.3), end: Offset.zero, ).animate(CurvedAnimation( parent: _controller, @@ -185,11 +190,11 @@ class _StaggeredAnimationItemState extends State curve: widget.curve, )); - // 지연 후 애니메이션 시작 - Future.delayed(widget.delay * widget.index, () { - if (mounted) { - _controller.forward(); - } + // 지연 후 애니메이션 시작 (모션 축소 시 지연 없음) + final startDelay = + ReduceMotion.platform() ? Duration.zero : widget.delay * widget.index; + Future.delayed(startDelay, () { + if (mounted) _controller.forward(); }); } @@ -201,6 +206,7 @@ class _StaggeredAnimationItemState extends State @override Widget build(BuildContext context) { + if (ReduceMotion.platform()) return widget.child; return AnimatedBuilder( animation: _controller, builder: (context, child) { diff --git a/lib/widgets/subscription_list_widget.dart b/lib/widgets/subscription_list_widget.dart index b569298..5af84a0 100644 --- a/lib/widgets/subscription_list_widget.dart +++ b/lib/widgets/subscription_list_widget.dart @@ -71,6 +71,7 @@ class SubscriptionListWidget extends StatelessWidget { physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, padding: const EdgeInsets.symmetric(horizontal: 16), + cacheExtent: 500, prototypeItem: const SizedBox(height: 156), itemCount: subscriptions.length, itemBuilder: (context, subIndex) { @@ -102,6 +103,7 @@ class SubscriptionListWidget extends StatelessWidget { child: RepaintBoundary( child: SwipeableSubscriptionCard( subscription: subscriptions[subIndex], + keepAlive: true, onTap: () { Log.d( '[SubscriptionListWidget] SwipeableSubscriptionCard onTap 호출됨'); diff --git a/lib/widgets/swipeable_subscription_card.dart b/lib/widgets/swipeable_subscription_card.dart index b0934b0..ebc10f1 100644 --- a/lib/widgets/swipeable_subscription_card.dart +++ b/lib/widgets/swipeable_subscription_card.dart @@ -2,12 +2,14 @@ import 'package:flutter/material.dart'; import '../models/subscription_model.dart'; import '../utils/haptic_feedback_helper.dart'; import 'subscription_card.dart'; +import '../utils/reduce_motion.dart'; class SwipeableSubscriptionCard extends StatefulWidget { final SubscriptionModel subscription; final VoidCallback? onEdit; final Future Function()? onDelete; final VoidCallback? onTap; + final bool keepAlive; const SwipeableSubscriptionCard({ super.key, @@ -15,6 +17,7 @@ class SwipeableSubscriptionCard extends StatefulWidget { this.onEdit, this.onDelete, this.onTap, + this.keepAlive = false, }); @override @@ -23,7 +26,7 @@ class SwipeableSubscriptionCard extends StatefulWidget { } class _SwipeableSubscriptionCardState extends State - with SingleTickerProviderStateMixin { + with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { // 상수 정의 static const double _tapTolerance = 20.0; // 탭 허용 범위 static const double _actionThresholdPercent = 0.15; @@ -49,7 +52,9 @@ class _SwipeableSubscriptionCardState extends State void initState() { super.initState(); _controller = AnimationController( - duration: const Duration(milliseconds: 300), + duration: ReduceMotion.platform() + ? const Duration(milliseconds: 0) + : const Duration(milliseconds: 300), vsync: this, ); _animation = Tween( @@ -215,10 +220,14 @@ class _SwipeableSubscriptionCardState extends State right: isLeft ? 0 : 24, ), child: AnimatedOpacity( - duration: const Duration(milliseconds: 200), + duration: ReduceMotion.platform() + ? const Duration(milliseconds: 0) + : const Duration(milliseconds: 200), opacity: showIcon ? 1.0 : 0.0, child: AnimatedScale( - duration: const Duration(milliseconds: 200), + duration: ReduceMotion.platform() + ? const Duration(milliseconds: 0) + : const Duration(milliseconds: 200), scale: showIcon ? 1.0 : 0.5, child: Icon( isDeleteThreshold @@ -236,9 +245,10 @@ class _SwipeableSubscriptionCardState extends State return Transform.translate( offset: Offset(_currentOffset, 0), child: Transform.scale( - scale: 1.0 - (_currentOffset.abs() / 2000), + scale: + ReduceMotion.platform() ? 1.0 : 1.0 - (_currentOffset.abs() / 2000), child: Transform.rotate( - angle: _currentOffset / 2000, + angle: ReduceMotion.platform() ? 0.0 : _currentOffset / 2000, child: SubscriptionCard( subscription: widget.subscription, onTap: widget @@ -251,6 +261,7 @@ class _SwipeableSubscriptionCardState extends State @override Widget build(BuildContext context) { + super.build(context); // 웹과 모바일 모두 동일한 스와이프 기능 제공 return Stack( children: [ @@ -266,4 +277,7 @@ class _SwipeableSubscriptionCardState extends State ], ); } + + @override + bool get wantKeepAlive => widget.keepAlive; }