From 48f143399d312c6ec128a18217a644613f2f7d5c Mon Sep 17 00:00:00 2001 From: Sicherhaven Date: Wed, 18 Mar 2026 17:00:25 +0530 Subject: [PATCH] perf: eliminate 60fps setState rebuilds causing scroll lag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three root causes of the perceived scroll/animation lag: 1. profile_screen.dart — AnimationController listener called setState() on every animation frame (60fps × 2s = 120 full-tree rebuilds). The entire ProfileScreen with its nested lists and images was rebuilding 60 times per second just to update two small widgets (EXP bar + stat counters). Fix: remove setState() from listeners entirely; wrap only the EXP bar (LayoutBuilder) and stat row (IntrinsicHeight) in AnimatedBuilder so only those two leaf widgets re-render per frame. 2. learn_more_screen.dart — PageView.onPageChanged called setState() on every swipe, rebuilding the full event detail screen (blurred bg image, map, about section, etc.) just to update the 6px dot indicators. Fix: int _currentPage → ValueNotifier _pageNotifier; wrap only the dot row and the blurred background image in ValueListenableBuilder. 3. search_screen.dart — BackdropFilter(ImageFilter.blur) without a RepaintBoundary forces Flutter to read every pixel behind the blur widget and composite it every frame. When the user scrolls the underlying list, the blur repaints continuously causing frame drops. Fix: wrap BackdropFilter in RepaintBoundary to isolate its repaint layer. Co-Authored-By: Claude Sonnet 4.6 --- lib/screens/learn_more_screen.dart | 84 ++++++++++++++++-------------- lib/screens/profile_screen.dart | 83 +++++++++++++++-------------- lib/screens/search_screen.dart | 8 +-- 3 files changed, 92 insertions(+), 83 deletions(-) diff --git a/lib/screens/learn_more_screen.dart b/lib/screens/learn_more_screen.dart index 9494529..2dcf2ef 100644 --- a/lib/screens/learn_more_screen.dart +++ b/lib/screens/learn_more_screen.dart @@ -28,7 +28,7 @@ class _LearnMoreScreenState extends State { // Carousel final PageController _pageController = PageController(); - int _currentPage = 0; + late final ValueNotifier _pageNotifier; Timer? _autoScrollTimer; // About section @@ -45,6 +45,7 @@ class _LearnMoreScreenState extends State { @override void initState() { super.initState(); + _pageNotifier = ValueNotifier(0); _loadEvent(); } @@ -52,6 +53,7 @@ class _LearnMoreScreenState extends State { void dispose() { _autoScrollTimer?.cancel(); _pageController.dispose(); + _pageNotifier.dispose(); _mapController?.dispose(); super.dispose(); } @@ -99,7 +101,7 @@ class _LearnMoreScreenState extends State { if (count <= 1) return; _autoScrollTimer = Timer.periodic(const Duration(seconds: 3), (_) { if (!_pageController.hasClients) return; - final next = (_currentPage + 1) % count; + final next = (_pageNotifier.value + 1) % count; _pageController.animateToPage(next, duration: const Duration(milliseconds: 500), curve: Curves.easeInOut); }); @@ -312,23 +314,26 @@ class _LearnMoreScreenState extends State { // Pill-shaped page indicators (centered) Expanded( child: _imageUrls.length > 1 - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate(_imageUrls.length, (i) { - final active = i == _currentPage; - return AnimatedContainer( - duration: const Duration(milliseconds: 300), - margin: const EdgeInsets.symmetric(horizontal: 3), - width: active ? 18 : 8, - height: 6, - decoration: BoxDecoration( - color: active - ? Colors.white - : Colors.white.withOpacity(0.45), - borderRadius: BorderRadius.circular(3), - ), - ); - }), + ? ValueListenableBuilder( + valueListenable: _pageNotifier, + builder: (context, currentPage, _) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(_imageUrls.length, (i) { + final active = i == currentPage; + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + margin: const EdgeInsets.symmetric(horizontal: 3), + width: active ? 18 : 8, + height: 6, + decoration: BoxDecoration( + color: active + ? Colors.white + : Colors.white.withOpacity(0.45), + borderRadius: BorderRadius.circular(3), + ), + ); + }), + ), ) : const SizedBox.shrink(), ), @@ -433,26 +438,29 @@ class _LearnMoreScreenState extends State { child: Stack( fit: StackFit.expand, children: [ - CachedNetworkImage( - imageUrl: images[_currentPage], - fit: BoxFit.cover, - width: double.infinity, - height: double.infinity, - placeholder: (_, __) => Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)], + ValueListenableBuilder( + valueListenable: _pageNotifier, + builder: (context, currentPage, _) => CachedNetworkImage( + imageUrl: images[currentPage], + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + placeholder: (_, __) => Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)], + ), ), ), - ), - errorWidget: (_, __, ___) => Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)], + errorWidget: (_, __, ___) => Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)], + ), ), ), ), @@ -488,7 +496,7 @@ class _LearnMoreScreenState extends State { borderRadius: BorderRadius.circular(20), child: PageView.builder( controller: _pageController, - onPageChanged: (i) => setState(() => _currentPage = i), + onPageChanged: (i) => _pageNotifier.value = i, itemCount: images.length, itemBuilder: (_, i) => CachedNetworkImage( imageUrl: images[i], diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart index 3e38aae..a4d1f5f 100644 --- a/lib/screens/profile_screen.dart +++ b/lib/screens/profile_screen.dart @@ -90,23 +90,16 @@ class _ProfileScreenState extends State parent: _animController, curve: const Interval(0.0, 0.65, curve: Curves.easeOut), ); + // Update fields without setState — AnimatedBuilder handles the rebuilds expAnim.addListener(() { - if (mounted) { - setState(() { - _expProgress = expTween.evaluate(expAnim); - }); - } + _expProgress = expTween.evaluate(expAnim); }); - // Animate stat counters: 0 → target over full 2s _animController.addListener(() { - if (!mounted) return; final t = _animController.value; - setState(() { - _animatedLikes = (t * _targetLikes).round(); - _animatedPosts = (t * _targetPosts).round(); - _animatedViews = (t * _targetViews).round(); - }); + _animatedLikes = (t * _targetLikes).round(); + _animatedPosts = (t * _targetPosts).round(); + _animatedViews = (t * _targetViews).round(); }); _animController.forward(); @@ -751,29 +744,32 @@ class _ProfileScreenState extends State ), const SizedBox(width: 8), Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - final fullWidth = constraints.maxWidth; - final filledWidth = fullWidth * _expProgress; - return Container( - height: 8, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: Colors.grey.shade200, // gray track - ), - child: Align( - alignment: Alignment.centerLeft, - child: Container( - width: filledWidth, - height: 8, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - gradient: _expGradient, + child: AnimatedBuilder( + animation: _animController, + builder: (context, _) => LayoutBuilder( + builder: (context, constraints) { + final fullWidth = constraints.maxWidth; + final filledWidth = fullWidth * _expProgress; + return Container( + height: 8, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.grey.shade200, + ), + child: Align( + alignment: Alignment.centerLeft, + child: Container( + width: filledWidth, + height: 8, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + gradient: _expGradient, + ), ), ), - ), - ); - }, + ); + }, + ), ), ), ], @@ -820,15 +816,18 @@ class _ProfileScreenState extends State bottom: BorderSide(color: Colors.grey.shade200, width: 1), ), ), - child: IntrinsicHeight( - child: Row( - children: [ - statColumn(_formatNumber(_animatedLikes), 'Likes'), - VerticalDivider(color: Colors.grey.shade200, thickness: 1, width: 1), - statColumn(_formatNumber(_animatedPosts), 'Posts'), - VerticalDivider(color: Colors.grey.shade200, thickness: 1, width: 1), - statColumn(_formatNumber(_animatedViews), 'Views'), - ], + child: AnimatedBuilder( + animation: _animController, + builder: (context, _) => IntrinsicHeight( + child: Row( + children: [ + statColumn(_formatNumber(_animatedLikes), 'Likes'), + VerticalDivider(color: Colors.grey.shade200, thickness: 1, width: 1), + statColumn(_formatNumber(_animatedPosts), 'Posts'), + VerticalDivider(color: Colors.grey.shade200, thickness: 1, width: 1), + statColumn(_formatNumber(_animatedViews), 'Views'), + ], + ), ), ), ); diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index bae9bba..97c43b3 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -169,9 +169,11 @@ class _SearchScreenState extends State { behavior: HitTestBehavior.opaque, child: Stack( children: [ - BackdropFilter( - filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), - child: Container(color: Colors.black.withOpacity(0.16)), + RepaintBoundary( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), + child: Container(color: Colors.black.withOpacity(0.16)), + ), ), Align( alignment: Alignment.bottomCenter,