perf(ui): enable KeepAlive on subscription list, tune prefetch, and reduce list/gesture animations

This commit is contained in:
JiWoong Sul
2025-09-08 14:32:28 +09:00
parent b034f60510
commit 10069a1800
3 changed files with 41 additions and 19 deletions

View File

@@ -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<StaggeredListAnimation>
@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<Widget> _buildAnimatedChildren() {
@@ -156,8 +158,9 @@ class _StaggeredAnimationItemState extends State<StaggeredAnimationItem>
@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<StaggeredAnimationItem>
));
_slideAnimation = Tween<Offset>(
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<StaggeredAnimationItem>
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<StaggeredAnimationItem>
@override
Widget build(BuildContext context) {
if (ReduceMotion.platform()) return widget.child;
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {

View File

@@ -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 호출됨');

View File

@@ -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<void> 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<SwipeableSubscriptionCard>
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<SwipeableSubscriptionCard>
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<double>(
@@ -215,10 +220,14 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
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<SwipeableSubscriptionCard>
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<SwipeableSubscriptionCard>
@override
Widget build(BuildContext context) {
super.build(context);
// 웹과 모바일 모두 동일한 스와이프 기능 제공
return Stack(
children: [
@@ -266,4 +277,7 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
],
);
}
@override
bool get wantKeepAlive => widget.keepAlive;
}