From 5b373e86943f9a09d4656de9a3ad45be97d1af23 Mon Sep 17 00:00:00 2001 From: Sicherhaven Date: Wed, 18 Mar 2026 15:39:42 +0530 Subject: [PATCH] perf: fix Android lag, snapping animations & slow image loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix 1: Replace overshooting Cubic(1.95) tab glider curve with Curves.easeInOutCubic; reduce duration 450ms → 280ms Fix 2: Replace marquee jumpTo() with animateTo(linear) for fluid scroll Fix 3: Replace Image.network with CachedNetworkImage in search results Fix 4: Replace Image.network with CachedNetworkImage in desktop cards Fix 5: Wrap IndexedStack children in RepaintBoundary to isolate repaints across tabs (Home/Calendar/Contribute/Profile) Fix 6: Replace setState on PageView.onPageChanged with ValueNotifier so only the carousel dots widget rebuilds on swipe Fix 7: Wrap animated tab glider in RepaintBoundary Fix 8: Replace shrinkWrap:true ListView with ConstrainedBox(maxHeight) to eliminate O(n) layout pass in search results Fix 9: Increase image cache to 200MB / 500 images in main() Co-Authored-By: Claude Sonnet 4.6 --- lib/main.dart | 5 ++ lib/screens/contribute_screen.dart | 9 +-- lib/screens/home_desktop_screen.dart | 40 +++++++++-- lib/screens/home_screen.dart | 104 ++++++++++++++++----------- 4 files changed, 107 insertions(+), 51 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 6cec356..f61ae3a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,6 +13,11 @@ import 'core/theme_manager.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await ThemeManager.init(); // load saved theme preference + + // Increase image cache for smoother scrolling and faster re-renders + PaintingBinding.instance.imageCache.maximumSize = 500; + PaintingBinding.instance.imageCache.maximumSizeBytes = 200 * 1024 * 1024; // 200 MB + runApp(const MyApp()); } diff --git a/lib/screens/contribute_screen.dart b/lib/screens/contribute_screen.dart index 0717525..db482d3 100644 --- a/lib/screens/contribute_screen.dart +++ b/lib/screens/contribute_screen.dart @@ -1357,7 +1357,7 @@ class _ContributeScreenState extends State // ───────────────────────────────────────────────────────────────────────── // Segmented Tabs (4 tabs) // ───────────────────────────────────────────────────────────────────────── - static const Curve _bouncyCurve = Cubic(0.37, 1.95, 0.66, 0.56); + static const Curve _bouncyCurve = Curves.easeInOutCubic; static const List _tabIcons = [ Icons.edit_outlined, @@ -1376,7 +1376,8 @@ class _ContributeScreenState extends State const double padding = 5.0; final tabWidth = (constraints.maxWidth - padding * 2) / tabs.length; - return ClipRRect( + return RepaintBoundary( + child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Container( height: 52, @@ -1389,7 +1390,7 @@ class _ContributeScreenState extends State children: [ // Sliding glider AnimatedPositioned( - duration: const Duration(milliseconds: 450), + duration: const Duration(milliseconds: 280), curve: _bouncyCurve, left: padding + _activeTab * tabWidth, top: padding, @@ -1444,7 +1445,7 @@ class _ContributeScreenState extends State ], ), ), - ); + )); // RepaintBoundary }, ), ); diff --git a/lib/screens/home_desktop_screen.dart b/lib/screens/home_desktop_screen.dart index b7608d3..14f4d8a 100644 --- a/lib/screens/home_desktop_screen.dart +++ b/lib/screens/home_desktop_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -248,12 +249,16 @@ class _HomeDesktopScreenState extends State with SingleTicker double next = cur + delta; if (next >= max) { - // wrap back by half (seamless loop) + // wrap back by half (seamless loop — instant jump is intentional) final wrapped = next - half; _marqueeController.jumpTo(wrapped.clamp(0.0, max)); } else { - // small incremental jump gives smooth appearance - _marqueeController.jumpTo(next); + // smooth incremental scroll synced to ticker duration + _marqueeController.animateTo( + next, + duration: _marqueeTick, + curve: Curves.linear, + ); } }); }); @@ -769,7 +774,16 @@ class _HomeDesktopScreenState extends State with SingleTicker child: Row(children: [ ClipRRect( borderRadius: BorderRadius.circular(8), - child: img != null ? Image.network(img, width: 64, height: 64, fit: BoxFit.cover) : Container(width: 64, height: 64, color: Theme.of(context).dividerColor), + child: img != null + ? CachedNetworkImage( + imageUrl: img, + width: 64, + height: 64, + fit: BoxFit.cover, + placeholder: (_, __) => Container(width: 64, height: 64, color: Theme.of(context).dividerColor), + errorWidget: (_, __, ___) => Container(width: 64, height: 64, color: Theme.of(context).dividerColor, child: const Icon(Icons.event, size: 24, color: Colors.grey)), + ) + : Container(width: 64, height: 64, color: Theme.of(context).dividerColor), ), const SizedBox(width: 8), Expanded( @@ -817,7 +831,14 @@ class _HomeDesktopScreenState extends State with SingleTicker ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(cardRadius)), child: img != null - ? Image.network(img, width: double.infinity, height: imageHeight, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(height: imageHeight, color: theme.dividerColor)) + ? CachedNetworkImage( + imageUrl: img, + width: double.infinity, + height: imageHeight, + fit: BoxFit.cover, + placeholder: (_, __) => Container(height: imageHeight, color: theme.dividerColor), + errorWidget: (_, __, ___) => Container(height: imageHeight, color: theme.dividerColor), + ) : Container(height: imageHeight, width: double.infinity, color: theme.dividerColor), ), @@ -921,7 +942,14 @@ class _HomeDesktopScreenState extends State with SingleTicker ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(cardRadius)), child: img != null - ? Image.network(img, width: width, height: imageHeight, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(height: imageHeight, color: theme.dividerColor)) + ? CachedNetworkImage( + imageUrl: img, + width: width, + height: imageHeight, + fit: BoxFit.cover, + placeholder: (_, __) => Container(height: imageHeight, color: theme.dividerColor), + errorWidget: (_, __, ___) => Container(height: imageHeight, color: theme.dividerColor), + ) : Container(height: imageHeight, width: width, color: theme.dividerColor), ), diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 11d4ff3..a41afaa 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -41,12 +41,13 @@ class _HomeScreenState extends State with SingleTickerProviderStateM // Hero carousel final PageController _heroPageController = PageController(); - int _heroCurrentPage = 0; + late final ValueNotifier _heroPageNotifier; Timer? _autoScrollTimer; @override void initState() { super.initState(); + _heroPageNotifier = ValueNotifier(0); _loadUserDataAndEvents(); _startAutoScroll(); } @@ -55,6 +56,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM void dispose() { _autoScrollTimer?.cancel(); _heroPageController.dispose(); + _heroPageNotifier.dispose(); super.dispose(); } @@ -62,7 +64,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM _autoScrollTimer?.cancel(); _autoScrollTimer = Timer.periodic(const Duration(seconds: 3), (timer) { if (_heroEvents.isEmpty) return; - final nextPage = (_heroCurrentPage + 1) % _heroEvents.length; + final nextPage = (_heroPageNotifier.value + 1) % _heroEvents.length; if (_heroPageController.hasClients) { _heroPageController.animateToPage( nextPage, @@ -324,9 +326,11 @@ class _HomeScreenState extends State with SingleTickerProviderStateM child: Center(child: Text(query.isEmpty ? 'Type to search events' : 'No events found', style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor))), ) else - ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 400), + child: ListView.separated( + shrinkWrap: false, + physics: const ClampingScrollPhysics(), itemBuilder: (ctx, idx) { final ev = results[idx]; final img = (ev.thumbImg != null && ev.thumbImg!.isNotEmpty) ? ev.thumbImg! : (ev.images.isNotEmpty ? ev.images.first.image : null); @@ -335,7 +339,17 @@ class _HomeScreenState extends State with SingleTickerProviderStateM return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 8), leading: img != null && img.isNotEmpty - ? ClipRRect(borderRadius: BorderRadius.circular(8), child: Image.network(img, width: 56, height: 56, fit: BoxFit.cover)) + ? ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + imageUrl: img, + width: 56, + height: 56, + fit: BoxFit.cover, + placeholder: (_, __) => Container(width: 56, height: 56, color: theme.dividerColor), + errorWidget: (_, __, ___) => Container(width: 56, height: 56, color: theme.dividerColor, child: Icon(Icons.event, color: theme.hintColor)), + ), + ) : Container(width: 56, height: 56, decoration: BoxDecoration(color: theme.dividerColor, borderRadius: BorderRadius.circular(8)), child: Icon(Icons.event, color: theme.hintColor)), title: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyLarge), subtitle: Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor)), @@ -349,7 +363,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM }, separatorBuilder: (_, __) => Divider(color: theme.dividerColor), itemCount: results.length, - ), + )), // ConstrainedBox ], ), ), @@ -371,16 +385,19 @@ class _HomeScreenState extends State with SingleTickerProviderStateM body: Stack( children: [ // IndexedStack keeps each tab alive and preserves state. + // RepaintBoundary isolates each tab so inactive tabs don't trigger repaints. IndexedStack( index: _selectedIndex, children: [ - _buildHomeContent(), // index 0 - const CalendarScreen(), // index 1 - ChangeNotifierProvider( - create: (_) => GamificationProvider(), - child: const ContributeScreen(), + RepaintBoundary(child: _buildHomeContent()), // index 0 + const RepaintBoundary(child: CalendarScreen()), // index 1 + RepaintBoundary( + child: ChangeNotifierProvider( + create: (_) => GamificationProvider(), + child: const ContributeScreen(), + ), ), // index 2 (full page, scrollable) - const ProfileScreen(), // index 3 + const RepaintBoundary(child: ProfileScreen()), // index 3 ], ), @@ -1117,7 +1134,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM height: 300, child: PageView.builder( controller: _heroPageController, - onPageChanged: (page) => setState(() => _heroCurrentPage = page), + onPageChanged: (page) => _heroPageNotifier.value = page, itemCount: _heroEvents.length, itemBuilder: (context, index) => _buildHeroEventImage(_heroEvents[index]), ), @@ -1134,34 +1151,39 @@ class _HomeScreenState extends State with SingleTickerProviderStateM } Widget _buildCarouselDots() { - return SizedBox( - height: 12, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate( - _heroEvents.isEmpty ? 5 : _heroEvents.length, - (i) { - final isActive = i == _heroCurrentPage; - return GestureDetector( - onTap: () { - if (_heroPageController.hasClients) { - _heroPageController.animateToPage(i, - duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); - } + return ValueListenableBuilder( + valueListenable: _heroPageNotifier, + builder: (context, currentPage, _) { + return SizedBox( + height: 12, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + _heroEvents.isEmpty ? 5 : _heroEvents.length, + (i) { + final isActive = i == currentPage; + return GestureDetector( + onTap: () { + if (_heroPageController.hasClients) { + _heroPageController.animateToPage(i, + duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); + } + }, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + width: isActive ? 24 : 8, + height: 8, + decoration: BoxDecoration( + color: isActive ? Colors.white : Colors.white.withOpacity(0.4), + borderRadius: BorderRadius.circular(4), + ), + ), + ); }, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 4), - width: isActive ? 24 : 8, - height: 8, - decoration: BoxDecoration( - color: isActive ? Colors.white : Colors.white.withOpacity(0.4), - borderRadius: BorderRadius.circular(4), - ), - ), - ); - }, - ), - ), + ), + ), + ); + }, ); }