在这里插入图片描述

每次逛手办店,总会被琳琅满目的手办吸引。有时候不知道该买哪个,就想先看看有什么新品,或者看看最近流行什么。所以在App里做了个发现页面,用网格布局展示推荐的手办,就像逛实体店一样,可以慢慢浏览。

网格布局的选择

发现页面和收藏页面不同,它更注重视觉展示。手办本身就是视觉产品,用网格布局能同时展示多个手办,让用户快速浏览。这种布局在电商App中很常见,比如淘宝、京东的商品列表都是这样。

页面的基本结构:

class DiscoverPage extends StatelessWidget {
  const DiscoverPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('发现')),
      body: GridView.builder(
        padding: EdgeInsets.all(16.w),

Scaffold 提供了基本的页面框架,AppBar 显示标题"发现"。这里没有用Consumer,因为发现页面展示的是推荐数据,不需要实时响应Provider的变化。

GridView.builder 是网格布局的核心组件,它和ListView.builder类似,也是按需渲染,性能很好。padding: EdgeInsets.all(16.w) 给整个网格添加内边距,让内容不会紧贴屏幕边缘。

为什么用GridView.builder而不是GridView?因为builder支持懒加载,只渲染可见区域的网格项。如果手办数量很多,builder的性能优势就很明显。而且builder可以动态生成网格项,不需要提前创建所有组件。

网格参数配置

网格的关键是 SliverGridDelegate,它控制网格的列数、间距等参数:

        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          crossAxisSpacing: 12.w,
          mainAxisSpacing: 12.h,
        ),
        itemCount: 20,

crossAxisCount: 2 表示每行显示2个网格项,这是手机上最常见的布局。如果设为3,网格项会太小,图片看不清。如果设为1,就变成列表了,不够直观。

crossAxisSpacing: 12.w 是横向间距,mainAxisSpacing: 12.h 是纵向间距。这两个间距让网格项之间有呼吸感,不会挤在一起。12这个数值是经过多次调试得出的,既不会太挤也不会太空。

itemCount: 20 表示总共有20个网格项。实际项目中,这个数值应该从服务器获取,或者从Provider读取。这里为了演示,写死了20个。

为什么不用 SliverGridDelegateWithMaxCrossAxisExtent?因为FixedCrossAxisCount更简单直接,指定列数就行。MaxCrossAxisExtent需要指定最大宽度,然后自动计算列数,适合响应式布局,但对于手机应用,固定列数更常用。

网格项的构建

每个网格项的内容:

        itemBuilder: (context, index) {
          return Card(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.image, size: 80.sp),
                Text('手办 ${index + 1}'),
                Text(${(index + 1) * 100}', 
                  style: TextStyle(color: Colors.red)),
              ],
            ),
          );
        },
      ),
    );
  }
}

Card 给每个网格项添加了卡片效果,自带阴影和圆角,看起来更有层次感。Column 垂直排列三个元素:图标、名称、价格。

mainAxisAlignment: MainAxisAlignment.center 让内容在卡片中垂直居中,视觉上更平衡。如果不加这个,内容会靠顶部,下方会有大片空白。

Icon 代表手办图片,实际项目中应该用Image.network或Image.asset加载真实图片。这里用图标是为了演示,避免网络请求的复杂性。图标大小 80.sp 比较大,占据卡片的主要空间。

名称和价格的显示:

Text('手办 ${index + 1}')

这里用了字符串插值,${index + 1} 会被替换成实际的数字。index从0开始,所以要加1,让显示从"手办 1"开始,而不是"手办 0"。

Text(${(index + 1) * 100}', style: TextStyle(color: Colors.red))

价格用红色显示,这是电商App的常见做法。红色能吸引注意力,让用户快速看到价格。(index + 1) * 100 是模拟价格,实际项目中应该从数据模型读取。

网格布局的优势

相比列表布局,网格布局有几个明显优势:

空间利用率高。手机屏幕宽度有限,列表布局一次只能显示一个商品,而网格布局可以同时显示两个,用户一屏能看到更多内容。这对于浏览型页面特别重要,用户可以快速扫视,找到感兴趣的手办。

视觉冲击力强。多个手办图片同时展示,形成视觉矩阵,比单个图片更有吸引力。就像逛手办店,满墙的手办比单个展示柜更吸引人。

适合图片展示。手办是视觉产品,图片是最重要的信息。网格布局给每个图片分配了足够的空间,不会像列表那样图片太小看不清。

滚动效率高。用户向下滚动时,每次可以看到两个新手办,而不是一个。这意味着用相同的滚动距离,能浏览更多内容。

实际使用体验

打开发现页面,立即看到一个2列的网格,每个格子里都有手办的图片、名称、价格。向下滚动,新的手办不断出现,滚动很流畅,没有卡顿。

点击某个手办,可以跳转到详情页面(这个功能后续会实现)。整个浏览过程很自然,就像在实体店里逛一样,可以慢慢看,慢慢选。

网格的间距设置得刚刚好,既能区分不同的手办,又不会显得稀疏。卡片的阴影效果让每个手办都有独立的空间感,不会混在一起。

性能优化考虑

使用builder模式。GridView.builder只渲染可见区域的网格项,不可见的不会渲染。这对性能很重要,特别是手办数量很多的时候。如果用GridView,所有网格项都会一次性创建,内存占用会很大。

图片懒加载。实际项目中,手办图片应该用Image.network加载,并且配置缓存。Flutter的Image组件自带缓存机制,加载过的图片会缓存在内存中,下次显示时直接从缓存读取,不需要重新下载。

避免过度渲染。每个网格项用const构造函数,比如 const Icon(Icons.image)。const对象在编译时就创建好了,运行时不需要重新创建,可以提高性能。

合理设置itemCount。如果手办数量很多,可以考虑分页加载。比如第一次加载20个,滚动到底部时再加载20个。这样可以减少初始加载时间,提升用户体验。

后续功能扩展

发现页面还可以添加很多功能:

筛选和排序。在AppBar添加筛选按钮,可以按价格区间、系列、厂商等条件筛选。添加排序按钮,可以按价格、热度、发售日期等排序。

搜索功能。在AppBar添加搜索框,支持按名称搜索手办。搜索结果也用网格布局展示,保持一致的视觉体验。

下拉刷新。用RefreshIndicator包裹GridView,支持下拉刷新。用户可以手动刷新数据,获取最新的推荐手办。

加载更多。监听滚动位置,当滚动到底部时自动加载更多数据。可以用ScrollController实现,或者用第三方库如pull_to_refresh。

收藏功能。在每个网格项添加收藏按钮,点击后添加到心愿单。可以用一个心形图标表示,点击后变成实心红色,表示已收藏。

详情跳转。点击网格项跳转到手办详情页面,显示更多信息,比如详细描述、多张图片、用户评价等。

网格布局的变体

除了固定2列的网格,还可以尝试其他布局:

瀑布流布局。用flutter_staggered_grid_view库实现,每个网格项的高度可以不同,根据内容自适应。这种布局更灵活,但实现复杂一些。

横向滚动网格。把scrollDirection设为Axis.horizontal,网格就变成横向滚动。适合展示推荐手办,用户可以左右滑动浏览。

混合布局。顶部用横幅展示精选手办,中间用网格展示推荐手办,底部用列表展示最新手办。这种混合布局信息密度更高,但要注意不要太复杂,影响用户体验。

响应式网格。根据屏幕宽度动态调整列数,手机上显示2列,平板上显示3列或4列。可以用MediaQuery获取屏幕宽度,然后计算合适的列数。

开发心得

网格布局要注意比例。crossAxisCount和childAspectRatio要配合好,确保网格项不会太扁或太高。一般来说,手办图片是竖向的,childAspectRatio设为0.7-0.8比较合适。

间距要统一。crossAxisSpacing和mainAxisSpacing最好设为相同的值,这样网格看起来更整齐。如果两个间距不同,会显得不协调。

卡片效果要适度。Card的elevation(阴影高度)不要设太高,默认值就够了。阴影太重会显得浮夸,阴影太轻又看不出层次感。

颜色要和谐。价格用红色是惯例,但不要用太鲜艳的红色,会刺眼。用Colors.red就够了,它是Material Design的标准红色,视觉上比较舒服。

总结

发现页面用网格布局展示推荐手办,视觉效果好,空间利用率高。GridView.builder的性能优化让滚动流畅,Card的卡片效果让每个手办都有独立的空间感。

整个页面的实现很简洁,核心代码不到50行,但效果很好。这得益于Flutter强大的布局组件,GridView.builder封装了网格布局的复杂逻辑,开发者只需要关注网格项的内容。

Flutter for OpenHarmony的GridView组件和原生Flutter完全一样,没有兼容性问题。如果你也在开发OpenHarmony应用,可以放心使用GridView实现网格布局。

下一篇文章会介绍搜索页面的实现,包括搜索框、热门标签、搜索结果展示等内容。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

߼ չ

1. ݻ

ʵ ܻ ƣ ٶȣ

class CacheManager {
  static final Map<String, CachedData> _cache = {};
  
  static Future<T?> get<T>(String key) async {
    if (_cache.containsKey(key)) {
      final cached = _cache[key]!;
      if (!cached.isExpired) {
        return cached.data as T;
      }
    }
    return null;
  }
  
  static void set(String key, dynamic data, {Duration duration = const Duration(hours: 1)}) {
    _cache[key] = CachedData(
      data: data,
      expireTime: DateTime.now().add(duration),
    );
  }
}

class CachedData {
  final dynamic data;
  final DateTime expireTime;
  
  CachedData({required this.data, required this.expireTime});
  
  bool get isExpired => DateTime.now().isAfter(expireTime);
}

2.

ƵĴ û 飺

class ErrorHandler {
  static void handle(BuildContext context, dynamic error) {
    String message = '    ʧ ܣ       ';
    
    if (error is SocketException) {
      message = '        ʧ ܣ             ';
    } else if (error is TimeoutException) {
      message = '    ʱ     Ժ     ';
    } else if (error is FormatException) {
      message = '   ݸ ʽ    ';
    }
    
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        action: SnackBarAction(
          label: '    ',
          onPressed: () {
            //      ߼ 
          },
        ),
      ),
    );
  }
}

3. ״̬

ŵļ ״̬չʾ

class LoadingState<T> {
  final bool isLoading;
  final T? data;
  final String? error;
  
  LoadingState({
    this.isLoading = false,
    this.data,
    this.error,
  });
  
  LoadingState<T> copyWith({
    bool? isLoading,
    T? data,
    String? error,
  }) {
    return LoadingState<T>(
      isLoading: isLoading ?? this.isLoading,
      data: data ?? this.data,
      error: error ?? this.error,
    );
  }
}

// ʹ  ʾ  
class DataProvider extends ChangeNotifier {
  LoadingState<List<Item>> _state = LoadingState();
  
  LoadingState<List<Item>> get state => _state;
  
  Future<void> loadData() async {
    _state = _state.copyWith(isLoading: true, error: null);
    notifyListeners();
    
    try {
      final data = await fetchData();
      _state = _state.copyWith(isLoading: false, data: data);
    } catch (e) {
      _state = _state.copyWith(isLoading: false, error: e.toString());
    }
    notifyListeners();
  }
}

4. ܼ

   ܼ أ  Ż  û    飺
class PerformanceMonitor {
  static final Map<String, Stopwatch> _timers = {};
  
  static void start(String operation) {
    _timers[operation] = Stopwatch()..start();
  }
  
  static void end(String operation) {
    if (_timers.containsKey(operation)) {
      final elapsed = _timers[operation]!.elapsedMilliseconds;
      print('[]   ʱ: \ms');
      _timers.remove(operation);
      
      if (elapsed > 1000) {
        print('    : [\] ִ  ʱ     ');
      }
    }
  }
}

// ʹ  ʾ  
Future<void> loadData() async {
  PerformanceMonitor.start('loadData');
  try {
    await fetchData();
  } finally {
    PerformanceMonitor.end('loadData');
  }
}

5. Ԥ

ʵ Ԥ أ Ӧ ٶȣ

class PreloadManager {
  static final Set<String> _preloaded = {};
  
  static Future<void> preload(List<String> imageUrls) async {
    for (var url in imageUrls) {
      if (!_preloaded.contains(url)) {
        try {
          await precacheImage(NetworkImage(url), context);
          _preloaded.add(url);
        } catch (e) {
          print('Ԥ    ʧ  : \');
        }
      }
    }
  }
  
  static void clear() {
    _preloaded.clear();
  }
}

6. ֧

ʵ ģʽ ԣ

class OfflineManager {
  static bool _isOnline = true;
  
  static bool get isOnline => _isOnline;
  
  static void init() {
    Connectivity().onConnectivityChanged.listen((result) {
      _isOnline = result != ConnectivityResult.none;
    });
  }
  
  static Future<T> execute<T>(
    Future<T> Function() onlineAction,
    Future<T> Function() offlineAction,
  ) async {
    if (_isOnline) {
      try {
        return await onlineAction();
      } catch (e) {
        return await offlineAction();
      }
    } else {
      return await offlineAction();
    }
  }
}

Ż ʵ

1. б Ż

ʹ AutomaticKeepAliveClientMixin б ״̬

class OptimizedListItem extends StatefulWidget {
  
  _OptimizedListItemState createState() => _OptimizedListItemState();
}

class _OptimizedListItemState extends State<OptimizedListItem> 
    with AutomaticKeepAliveClientMixin {
  
  bool get wantKeepAlive => true;
  
  
  Widget build(BuildContext context) {
    super.build(context);
    return ListTile(
      //  б       
    );
  }
}

2. ͼƬ Ż

ʵ ֽ ʽͼƬ أ

FadeInImage.memoryNetwork(
  placeholder: kTransparentImage,
  image: imageUrl,
  fadeInDuration: Duration(milliseconds: 300),
  fit: BoxFit.cover,
)

3. ڴ

ʱ ͷ Դ ڴ й©


void dispose() {
  _scrollController.dispose();
  _textController.dispose();
  _focusNode.dispose();
  super.dispose();
}

ʵս ܽ

ͨ ĵ 뽲 ⣬ Ѿ ˣ

  • ** Ĺ ʵ ** ҳ 沼 ֡ Ⱦ

  • **״̬ ** ʹ Provider ״̬

  • ** Ż ** 桢Ԥ ء صȼ

  • ** ** Ƶ 쳣 û ʾ

  • ** ֧ ** ״̬ ݷ

  • ** ʵ ** ֯ Դ ܼ

    Щ ɲ ڵ ǰҳ 棬 Ӧ õ Flutter for OpenHarmony Ŀ У 㹹 Ӧ á


ӭ 뿪Դ ɿ ƽ̨ https://openharmonycrossplatform.csdn.net

深度功能扩展

1. 智能内容推荐

基于机器学习的内容推荐引擎:

class ContentRecommendation {
  static List<DiscoveryItem> getPersonalizedContent(
    User user,
    List<DiscoveryItem> allContent,
  ) {
    final userVector = _buildUserVector(user);
    
    return allContent.map((item) {
      final itemVector = _buildItemVector(item);
      final similarity = _cosineSimilarity(userVector, itemVector);
      return MapEntry(item, similarity);
    }).toList()
      ..sort((a, b) => b.value.compareTo(a.value))
      ..map((e) => e.key).toList();
  }
  
  static List<double> _buildUserVector(User user) {
    return [
      user.preferences.categoryWeights,
      user.interactionHistory.frequency,
      user.ratingPatterns.average,
    ].expand((x) => x).toList();
  }
  
  static double _cosineSimilarity(List<double> a, List<double> b) {
    double dotProduct = 0.0;
    double normA = 0.0;
    double normB = 0.0;
    
    for (int i = 0; i < a.length; i++) {
      dotProduct += a[i] * b[i];
      normA += a[i] * a[i];
      normB += b[i] * b[i];
    }
    
    return dotProduct / (sqrt(normA) * sqrt(normB));
  }
}

推荐引擎使用余弦相似度算法,计算用户偏好向量与内容特征向量的相似度。通过分析用户的浏览历史、收藏记录和评分数据,为每位用户提供个性化的发现内容。

2. 社交互动功能

增强用户间的互动体验:

class SocialInteraction {
  static Future<void> likeContent(String contentId) async {
    await _updateInteraction(contentId, InteractionType.like);
    await _notifyAuthor(contentId, '有人喜欢了你的内容');
  }
  
  static Future<void> shareContent(String contentId, String platform) async {
    final content = await _getContent(contentId);
    final shareUrl = await _generateShareUrl(content);
    
    await Share.share(
      '发现了一个很棒的手办内容:${content.title}\n$shareUrl',
      subject: content.title,
    );
    
    await _recordShare(contentId, platform);
  }
  
  static Future<void> followUser(String userId) async {
    await http.post(
      Uri.parse('$baseUrl/follow'),
      body: jsonEncode({'userId': userId}),
    );
    
    await _updateFollowList(userId);
  }
}

社交功能支持点赞、分享和关注等互动操作。用户可以关注感兴趣的创作者,及时获取他们发布的新内容。分享功能支持多个社交平台,扩大内容传播范围。

3. 内容质量评估

自动评估和筛选高质量内容:

class ContentQualityAssessor {
  static double assessQuality(DiscoveryItem item) {
    double score = 0.0;
    
    // 图片质量评分
    score += _assessImageQuality(item.images) * 0.3;
    
    // 文本质量评分
    score += _assessTextQuality(item.description) * 0.2;
    
    // 用户互动评分
    score += _assessEngagement(item.stats) * 0.3;
    
    // 时效性评分
    score += _assessTimeliness(item.publishDate) * 0.2;
    
    return score;
  }
  
  static double _assessImageQuality(List<String> images) {
    if (images.isEmpty) return 0.0;
    
    double totalScore = 0.0;
    for (var image in images) {
      final resolution = _getImageResolution(image);
      final clarity = _getImageClarity(image);
      totalScore += (resolution + clarity) / 2;
    }
    
    return totalScore / images.length;
  }
  
  static double _assessEngagement(ContentStats stats) {
    final likeRate = stats.likes / max(stats.views, 1);
    final commentRate = stats.comments / max(stats.views, 1);
    final shareRate = stats.shares / max(stats.views, 1);
    
    return (likeRate * 0.5 + commentRate * 0.3 + shareRate * 0.2) * 100;
  }
}

质量评估系统从多个维度自动评估内容质量,包括图片清晰度、文本丰富度、用户互动率和内容时效性。高质量内容会获得更多曝光机会,提升用户的浏览体验。

4. 个性化标签系统

动态生成和管理内容标签:

class TagManager {
  static List<String> extractTags(DiscoveryItem item) {
    final tags = <String>[];
    
    // 从标题提取关键词
    tags.addAll(_extractKeywords(item.title));
    
    // 从描述提取关键词
    tags.addAll(_extractKeywords(item.description));
    
    // 添加分类标签
    tags.add(item.category);
    
    // 添加品牌标签
    if (item.brand != null) tags.add(item.brand!);
    
    return tags.toSet().toList();
  }
  
  static List<String> _extractKeywords(String text) {
    final words = text.split(RegExp(r'\s+'));
    return words.where((word) => 
      word.length > 2 && !_isStopWord(word)
    ).toList();
  }
  
  static Future<void> updateUserTagPreferences(
    String userId,
    List<String> tags,
  ) async {
    for (var tag in tags) {
      await _incrementTagWeight(userId, tag);
    }
  }
}

标签系统自动从内容中提取关键词,生成相关标签。系统会记录用户对不同标签的偏好权重,用于优化后续的内容推荐。标签云功能帮助用户快速发现感兴趣的主题。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐