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 'package:flutter/material.dart';
import 'dart:math' as math; import 'dart:math' as math;
import '../utils/reduce_motion.dart';
/// 스태거 애니메이션이 적용된 리스트 위젯 /// 스태거 애니메이션이 적용된 리스트 위젯
class StaggeredListAnimation extends StatefulWidget { class StaggeredListAnimation extends StatefulWidget {
@@ -95,13 +96,14 @@ class _StaggeredListAnimationState extends State<StaggeredListAnimation>
@override @override
Widget build(BuildContext context) { 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 return widget.direction == Axis.vertical
? Column( ? Column(children: _buildAnimatedChildren())
children: _buildAnimatedChildren(), : Row(children: _buildAnimatedChildren());
)
: Row(
children: _buildAnimatedChildren(),
);
} }
List<Widget> _buildAnimatedChildren() { List<Widget> _buildAnimatedChildren() {
@@ -156,8 +158,9 @@ class _StaggeredAnimationItemState extends State<StaggeredAnimationItem>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final reduced = ReduceMotion.platform();
_controller = AnimationController( _controller = AnimationController(
duration: widget.duration, duration: reduced ? Duration.zero : widget.duration,
vsync: this, vsync: this,
); );
@@ -170,7 +173,9 @@ class _StaggeredAnimationItemState extends State<StaggeredAnimationItem>
)); ));
_slideAnimation = Tween<Offset>( _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, end: Offset.zero,
).animate(CurvedAnimation( ).animate(CurvedAnimation(
parent: _controller, parent: _controller,
@@ -185,11 +190,11 @@ class _StaggeredAnimationItemState extends State<StaggeredAnimationItem>
curve: widget.curve, curve: widget.curve,
)); ));
// 지연 후 애니메이션 시작 // 지연 후 애니메이션 시작 (모션 축소 시 지연 없음)
Future.delayed(widget.delay * widget.index, () { final startDelay =
if (mounted) { ReduceMotion.platform() ? Duration.zero : widget.delay * widget.index;
_controller.forward(); Future.delayed(startDelay, () {
} if (mounted) _controller.forward();
}); });
} }
@@ -201,6 +206,7 @@ class _StaggeredAnimationItemState extends State<StaggeredAnimationItem>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (ReduceMotion.platform()) return widget.child;
return AnimatedBuilder( return AnimatedBuilder(
animation: _controller, animation: _controller,
builder: (context, child) { builder: (context, child) {

View File

@@ -71,6 +71,7 @@ class SubscriptionListWidget extends StatelessWidget {
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
cacheExtent: 500,
prototypeItem: const SizedBox(height: 156), prototypeItem: const SizedBox(height: 156),
itemCount: subscriptions.length, itemCount: subscriptions.length,
itemBuilder: (context, subIndex) { itemBuilder: (context, subIndex) {
@@ -102,6 +103,7 @@ class SubscriptionListWidget extends StatelessWidget {
child: RepaintBoundary( child: RepaintBoundary(
child: SwipeableSubscriptionCard( child: SwipeableSubscriptionCard(
subscription: subscriptions[subIndex], subscription: subscriptions[subIndex],
keepAlive: true,
onTap: () { onTap: () {
Log.d( Log.d(
'[SubscriptionListWidget] SwipeableSubscriptionCard onTap 호출됨'); '[SubscriptionListWidget] SwipeableSubscriptionCard onTap 호출됨');

View File

@@ -2,12 +2,14 @@ import 'package:flutter/material.dart';
import '../models/subscription_model.dart'; import '../models/subscription_model.dart';
import '../utils/haptic_feedback_helper.dart'; import '../utils/haptic_feedback_helper.dart';
import 'subscription_card.dart'; import 'subscription_card.dart';
import '../utils/reduce_motion.dart';
class SwipeableSubscriptionCard extends StatefulWidget { class SwipeableSubscriptionCard extends StatefulWidget {
final SubscriptionModel subscription; final SubscriptionModel subscription;
final VoidCallback? onEdit; final VoidCallback? onEdit;
final Future<void> Function()? onDelete; final Future<void> Function()? onDelete;
final VoidCallback? onTap; final VoidCallback? onTap;
final bool keepAlive;
const SwipeableSubscriptionCard({ const SwipeableSubscriptionCard({
super.key, super.key,
@@ -15,6 +17,7 @@ class SwipeableSubscriptionCard extends StatefulWidget {
this.onEdit, this.onEdit,
this.onDelete, this.onDelete,
this.onTap, this.onTap,
this.keepAlive = false,
}); });
@override @override
@@ -23,7 +26,7 @@ class SwipeableSubscriptionCard extends StatefulWidget {
} }
class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard> class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
// 상수 정의 // 상수 정의
static const double _tapTolerance = 20.0; // 탭 허용 범위 static const double _tapTolerance = 20.0; // 탭 허용 범위
static const double _actionThresholdPercent = 0.15; static const double _actionThresholdPercent = 0.15;
@@ -49,7 +52,9 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
void initState() { void initState() {
super.initState(); super.initState();
_controller = AnimationController( _controller = AnimationController(
duration: const Duration(milliseconds: 300), duration: ReduceMotion.platform()
? const Duration(milliseconds: 0)
: const Duration(milliseconds: 300),
vsync: this, vsync: this,
); );
_animation = Tween<double>( _animation = Tween<double>(
@@ -215,10 +220,14 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
right: isLeft ? 0 : 24, right: isLeft ? 0 : 24,
), ),
child: AnimatedOpacity( child: AnimatedOpacity(
duration: const Duration(milliseconds: 200), duration: ReduceMotion.platform()
? const Duration(milliseconds: 0)
: const Duration(milliseconds: 200),
opacity: showIcon ? 1.0 : 0.0, opacity: showIcon ? 1.0 : 0.0,
child: AnimatedScale( child: AnimatedScale(
duration: const Duration(milliseconds: 200), duration: ReduceMotion.platform()
? const Duration(milliseconds: 0)
: const Duration(milliseconds: 200),
scale: showIcon ? 1.0 : 0.5, scale: showIcon ? 1.0 : 0.5,
child: Icon( child: Icon(
isDeleteThreshold isDeleteThreshold
@@ -236,9 +245,10 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
return Transform.translate( return Transform.translate(
offset: Offset(_currentOffset, 0), offset: Offset(_currentOffset, 0),
child: Transform.scale( child: Transform.scale(
scale: 1.0 - (_currentOffset.abs() / 2000), scale:
ReduceMotion.platform() ? 1.0 : 1.0 - (_currentOffset.abs() / 2000),
child: Transform.rotate( child: Transform.rotate(
angle: _currentOffset / 2000, angle: ReduceMotion.platform() ? 0.0 : _currentOffset / 2000,
child: SubscriptionCard( child: SubscriptionCard(
subscription: widget.subscription, subscription: widget.subscription,
onTap: widget onTap: widget
@@ -251,6 +261,7 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
// 웹과 모바일 모두 동일한 스와이프 기능 제공 // 웹과 모바일 모두 동일한 스와이프 기능 제공
return Stack( return Stack(
children: [ children: [
@@ -266,4 +277,7 @@ class _SwipeableSubscriptionCardState extends State<SwipeableSubscriptionCard>
], ],
); );
} }
@override
bool get wantKeepAlive => widget.keepAlive;
} }