@@ -4,6 +4,7 @@ import 'package:url_launcher/url_launcher.dart';
44import '../services/auth_service.dart' ;
55import 'package:installed_apps/installed_apps.dart' ;
66import 'package:flutter/services.dart' ;
7+ import 'package:flutter/foundation.dart' ;
78import 'dart:convert' ;
89import '../utils/logger.dart' ;
910
@@ -25,6 +26,8 @@ class _AppsScreenState extends State<AppsScreen> with AutomaticKeepAliveClientMi
2526
2627 // MethodChannel for native Android communication
2728 static const platform = MethodChannel ('com.example.classaware/launcher' );
29+ final Map <String , Uint8List ?> _iconCache = {};
30+ final Map <String , Future <Uint8List ?>> _iconFutures = {};
2831
2932 @override
3033 bool get wantKeepAlive => true ; // 保持页面状态,避免重复加载应用列表
@@ -68,8 +71,12 @@ class _AppsScreenState extends State<AppsScreen> with AutomaticKeepAliveClientMi
6871
6972 for (var appData in launchableApps) {
7073 final Map <String , dynamic > app = Map <String , dynamic >.from (appData);
71-
72- // 原生Android代码已经包含图标数据
74+ final iconStr = (app['icon' ] ?? '' ).toString ();
75+ if (iconStr.isNotEmpty) {
76+ try {
77+ app['iconBytes' ] = base64Decode (iconStr);
78+ } catch (_) {}
79+ }
7380 apps.add (app);
7481 }
7582
@@ -238,9 +245,12 @@ class _AppsScreenState extends State<AppsScreen> with AutomaticKeepAliveClientMi
238245 SizedBox (height: 12. h),
239246 Expanded (
240247 child: ListView .builder (
248+ cacheExtent: 200 ,
241249 itemCount: quickApps.length,
242250 itemBuilder: (context, index) {
243251 final app = quickApps[index];
252+ final dpr = MediaQuery .of (context).devicePixelRatio;
253+ final cacheSize = (48. w * dpr).round ();
244254 return RepaintBoundary ( // 添加重绘边界优化
245255 child: GestureDetector (
246256 onTap: () => _launchApp (app),
@@ -257,27 +267,7 @@ class _AppsScreenState extends State<AppsScreen> with AutomaticKeepAliveClientMi
257267 ),
258268 child: ClipRRect (
259269 borderRadius: BorderRadius .circular (12. r),
260- child: app['icon' ] != null && app['icon' ].toString ().isNotEmpty
261- ? Image .memory (
262- base64Decode (app['icon' ]),
263- width: 48. w,
264- height: 48. w,
265- fit: BoxFit .contain,
266- cacheWidth: (48. w * MediaQuery .of (context).devicePixelRatio).round (),
267- cacheHeight: (48. w * MediaQuery .of (context).devicePixelRatio).round (),
268- errorBuilder: (context, error, stackTrace) {
269- return Icon (
270- Icons .apps,
271- size: 24. sp,
272- color: Theme .of (context).colorScheme.onSurfaceVariant,
273- );
274- },
275- )
276- : Icon (
277- Icons .apps,
278- size: 24. sp,
279- color: Theme .of (context).colorScheme.onSurfaceVariant,
280- ),
270+ child: _buildAppIcon (app['packageName' ] as String , 48. w, cacheSize, fallbackColor: Theme .of (context).colorScheme.onSurfaceVariant, fallbackSize: 24. sp),
281271 ),
282272 ),
283273 SizedBox (height: 4. h),
@@ -402,9 +392,12 @@ class _AppsScreenState extends State<AppsScreen> with AutomaticKeepAliveClientMi
402392 final textHeight = fontSize * 1.2 * 2 ;
403393 final tileHeight = iconSize + 4. h + textHeight + 12.0 ;
404394 final aspect = (minTile / tileHeight).clamp (0.65 , 1.0 );
395+ final dpr = MediaQuery .of (context).devicePixelRatio;
396+ final cacheSize = (iconSize * dpr).round ();
405397 return GridView .builder (
406398 physics: const BouncingScrollPhysics (),
407399 shrinkWrap: true ,
400+ cacheExtent: tileHeight * 2 ,
408401 gridDelegate: SliverGridDelegateWithFixedCrossAxisCount (
409402 crossAxisCount: cols,
410403 childAspectRatio: aspect,
@@ -434,27 +427,7 @@ class _AppsScreenState extends State<AppsScreen> with AutomaticKeepAliveClientMi
434427 ),
435428 child: ClipRRect (
436429 borderRadius: BorderRadius .circular (12. r),
437- child: app['icon' ] != null && app['icon' ].toString ().isNotEmpty
438- ? Image .memory (
439- base64Decode (app['icon' ]),
440- width: 48. w,
441- height: 48. w,
442- fit: BoxFit .contain,
443- cacheWidth: (48. w * MediaQuery .of (context).devicePixelRatio).round (),
444- cacheHeight: (48. w * MediaQuery .of (context).devicePixelRatio).round (),
445- errorBuilder: (context, error, stackTrace) {
446- return Icon (
447- Icons .apps,
448- color: Theme .of (context).colorScheme.primary,
449- size: 28. w,
450- );
451- },
452- )
453- : Icon (
454- Icons .apps,
455- color: Theme .of (context).colorScheme.primary,
456- size: 28. w,
457- ),
430+ child: _buildAppIcon (app['packageName' ] as String , 48. w, cacheSize, fallbackColor: Theme .of (context).colorScheme.primary, fallbackSize: 28. w),
458431 ),
459432 ),
460433 SizedBox (height: 4. h),
@@ -499,4 +472,58 @@ class _AppsScreenState extends State<AppsScreen> with AutomaticKeepAliveClientMi
499472 return false ;
500473 }
501474 }
475+
476+ Widget _buildAppIcon (String packageName, double size, int cacheSize, {required Color fallbackColor, required double fallbackSize}) {
477+ final cached = _iconCache[packageName];
478+ if (cached != null && cached.isNotEmpty) {
479+ return Image .memory (
480+ cached,
481+ width: size,
482+ height: size,
483+ fit: BoxFit .contain,
484+ cacheWidth: cacheSize,
485+ cacheHeight: cacheSize,
486+ filterQuality: FilterQuality .none,
487+ gaplessPlayback: true ,
488+ );
489+ }
490+ final future = _iconFutures[packageName] ?? _requestIcon (packageName);
491+ _iconFutures[packageName] = future;
492+ return FutureBuilder <Uint8List ?>(
493+ future: future,
494+ builder: (context, snap) {
495+ if (snap.connectionState == ConnectionState .done && snap.data != null && snap.data! .isNotEmpty) {
496+ return Image .memory (
497+ snap.data! ,
498+ width: size,
499+ height: size,
500+ fit: BoxFit .contain,
501+ cacheWidth: cacheSize,
502+ cacheHeight: cacheSize,
503+ filterQuality: FilterQuality .none,
504+ gaplessPlayback: true ,
505+ );
506+ }
507+ return Icon (
508+ Icons .apps,
509+ color: fallbackColor,
510+ size: fallbackSize,
511+ );
512+ },
513+ );
514+ }
515+
516+ Future <Uint8List ?> _requestIcon (String packageName) async {
517+ try {
518+ final bytes = await platform.invokeMethod <Uint8List >('getAppIcon' , {
519+ 'packageName' : packageName,
520+ });
521+ if (bytes == null || bytes.isEmpty) return null ;
522+ _iconCache[packageName] = bytes;
523+ return bytes;
524+ } catch (_) {
525+ _iconCache[packageName] = null ;
526+ return null ;
527+ }
528+ }
502529}
0 commit comments