Flutter 鸿蒙电商开发:特惠推荐模块完整实现

在这里插入图片描述

摘要:本文详细介绍在 Flutter/HarmonyOS 项目中实现特惠推荐模块的完整流程。该模块涉及四层嵌套数据结构的解析、渐变 UI 设计、图片加载优化以及 HTTP/HTTPS 兼容处理,是电商应用中提升转化率的核心功能。


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

一、功能分析

1.1 业务价值

特惠推荐模块是电商应用的流量转化核心,通过限时促销信息刺激用户点击购买。其设计需要兼顾:

  • 视觉吸引力:通过渐变色和图标营造促销氛围
  • 信息密度:在有限空间展示商品图片和价格
  • 加载性能:优化图片加载,避免白屏等待
  • 容错能力:网络失败时保留用户数据

1.2 UI 设计规范

┌────────────────────────────────────────────────────────────┐
│  特惠推荐          精选省攻略                              │
│  ┌────────────────────┐  ┌─────┐ ┌─────┐ ┌─────┐         │
│  │                    │  │图   │ │图   │ │图   │         │
│  │    🎁              │  │片   │ │片   │ │片   │         │
│  │   特惠             │  │     │ │     │ │     │         │
│  │   限时             │  │¥xx │ │¥xx │ │¥xx │         │
│  │                    │  └─────┘ └─────┘ └─────┘         │
│  │  橙红→橙黄渐变     │                                  │
│  └────────────────────┘                                  │
└────────────────────────────────────────────────────────────┘

1.3 开发流程

① 定义接口常量 → ② 构建数据模型 → ③ 封装 API
                                    ↓
⑦ 完善展示 ← ⑥ 传递数据 ← ⑤ 初始化数据 ← ④ 更新组件

二、数据结构设计

2.1 API 规范

接口地址GET /hot/preference

响应结构

{
  "code": "1",
  "msg": "操作成功",
  "result": {
    "id": "897682543",
    "title": "特惠推荐",
    "subTypes": [
      {
        "id": "912000341",
        "title": "抢先尝鲜",
        "goodsItems": {
          "counts": 459,
          "pageSize": 10,
          "pages": 46,
          "page": 1,
          "items": [
            {
              "id": "1750713979950333956",
              "name": "Balva 日本制高级时尚太阳镜",
              "desc": "抵挡99%紫外线",
              "price": "1213.00",
              "picture": "https://...",
              "orderNum": 17
            }
          ]
        }
      }
    ]
  }
}

2.2 数据模型层次

SpecialOfferResult (特惠推荐结果)
 └─ subTypes: List<SubType> (子类型列表)
     └─ goodsItems: GoodsItems (商品集合)
         └─ items: List<GoodsItem> (商品列表)
             └─ [id, name, desc, price, picture, orderNum]

2.3 数据模型代码

/// 商品项
class GoodsItem {
  final String id;
  final String name;
  final String? desc;      // 可空字段
  final String price;
  final String picture;
  final int orderNum;

  GoodsItem({
    required this.id,
    required this.name,
    this.desc,
    required this.price,
    required this.picture,
    required this.orderNum,
  });

  factory GoodsItem.fromJSON(Map<String, dynamic> json) {
    return GoodsItem(
      id: json["id"] as String? ?? "",
      name: json["name"] as String? ?? "",
      desc: json["desc"] as String?, // 保持可空
      price: json["price"] as String? ?? "0.00",
      picture: json["picture"] as String? ?? "",
      orderNum: json["orderNum"] as int? ?? 0,
    );
  }
}

/// 商品集合(分页信息)
class GoodsItems {
  final int counts;
  final int pageSize;
  final int pages;
  final int page;
  final List<GoodsItem> items;

  GoodsItems({
    required this.counts,
    required this.pageSize,
    required this.pages,
    required this.page,
    required this.items,
  });

  factory GoodsItems.fromJSON(Map<String, dynamic> json) {
    return GoodsItems(
      counts: json["counts"] as int? ?? 0,
      pageSize: json["pageSize"] as int? ?? 0,
      pages: json["pages"] as int? ?? 0,
      page: json["page"] as int? ?? 0,
      items: (json["items"] as List<dynamic>?)
              ?.map((e) => GoodsItem.fromJSON(e as Map<String, dynamic>))
              .toList() ?? [],
    );
  }
}

/// 子类型
class SubType {
  final String id;
  final String title;
  final GoodsItems goodsItems;

  SubType({
    required this.id,
    required this.title,
    required this.goodsItems,
  });

  factory SubType.fromJSON(Map<String, dynamic> json) {
    return SubType(
      id: json["id"] as String? ?? "",
      title: json["title"] as String? ?? "",
      // 空对象兜底
      goodsItems: GoodsItems.fromJSON(
        json["goodsItems"] as Map<String, dynamic>? ?? {}
      ),
    );
  }
}

/// 特惠推荐结果(根对象)
class SpecialOfferResult {
  final String id;
  final String title;
  final List<SubType> subTypes;

  SpecialOfferResult({
    required this.id,
    required this.title,
    required this.subTypes,
  });

  factory SpecialOfferResult.fromJSON(Map<String, dynamic> json) {
    return SpecialOfferResult(
      id: json["id"] as String? ?? "",
      title: json["title"] as String? ?? "",
      subTypes: (json["subTypes"] as List<dynamic>?)
                ?.map((e) => SubType.fromJSON(e as Map<String, dynamic>))
                .toList() ?? [],
    );
  }
}

三、UI 组件实现

3.1 组件结构

class HmSuggestion extends StatefulWidget {
  final SpecialOfferResult specialOfferResult;

  const HmSuggestion({
    super.key,
    required this.specialOfferResult,
  });

  
  State<HmSuggestion> createState() => _HmSuggestionState();
}

class _HmSuggestionState extends State<HmSuggestion> {
  // 获取前3条商品数据
  List<GoodsItem> get _displayItems {
    if (widget.specialOfferResult.subTypes.isEmpty) {
      return [];
    }
    return widget.specialOfferResult.subTypes.first.goodsItems.items
        .take(3)
        .toList();
  }

  
  Widget build(BuildContext context) {
    if (_displayItems.isEmpty) {
      return const SizedBox.shrink();
    }

    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
      padding: const EdgeInsets.all(12),
      decoration: BoxDecoration(
        color: const Color(0xFFFFF6EE),
        borderRadius: BorderRadius.circular(12),
      ),
      child: Column(
        children: [
          _buildHeader(),
          const SizedBox(height: 12),
          Row(
            children: [
              _buildPromoCard(),
              const SizedBox(width: 8),
              Expanded(child: _buildProductList()),
            ],
          ),
        ],
      ),
    );
  }

  // 标题栏
  Widget _buildHeader() {
    return Row(
      children: [
        const Text(
          '特惠推荐',
          style: TextStyle(
            color: Color(0xFF561814),
            fontSize: 18,
            fontWeight: FontWeight.w700,
          ),
        ),
        const SizedBox(width: 8),
        Text(
          widget.specialOfferResult.title.isNotEmpty
              ? widget.specialOfferResult.title
              : '精选省攻略',
          style: const TextStyle(
            fontSize: 12,
            color: Color(0xFF7C3F3A),
          ),
        ),
      ],
    );
  }

  // 左侧促销卡片
  Widget _buildPromoCard() {
    return Container(
      width: 75,
      height: 140,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(10),
        gradient: const LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [Color(0xFFFF7832), Color(0xFFFFB43C)],
        ),
        boxShadow: [
          BoxShadow(
            color: const Color(0xFFFF8C32).withOpacity(0.3),
            blurRadius: 8,
            offset: const Offset(2, 4),
          ),
        ],
      ),
      child: const Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.card_giftcard, color: Colors.white, size: 36),
          SizedBox(height: 8),
          Text(
            '特惠',
            style: TextStyle(
              color: Colors.white,
              fontSize: 16,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 4),
          Text(
            '限时',
            style: TextStyle(
              color: Colors.white,
              fontSize: 11,
            ),
          ),
        ],
      ),
    );
  }

  // 右侧商品列表
  Widget _buildProductList() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: _displayItems.map((item) => _ProductCard(item: item)).toList(),
    );
  }
}

3.2 商品卡片组件

class _ProductCard extends StatelessWidget {
  final GoodsItem item;

  const _ProductCard({required this.item});

  
  Widget build(BuildContext context) {
    return Expanded(
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 4),
        child: Column(
          children: [
            // 商品图片
            ClipRRect(
              borderRadius: BorderRadius.circular(8),
              child: _OptimizedImage(url: item.picture, height: 140),
            ),
            const SizedBox(height: 10),
            // 价格标签
            Container(
              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(12),
                color: const Color(0xFFF0600C),
              ),
              child: Text(
                '¥${item.price}',
                style: const TextStyle(
                  color: Colors.white,
                  fontSize: 11,
                  fontWeight: FontWeight.w500,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

3.3 图片优化组件

/// 优化的网络图片组件
/// 支持 HTTP/HTTPS 自动转换、加载进度、错误占位
class _OptimizedImage extends StatelessWidget {
  final String url;
  final double height;

  const _OptimizedImage({
    required this.url,
    this.height = 140,
  });

  
  Widget build(BuildContext context) {
    if (url.isEmpty) {
      return _ErrorPlaceholder(height: height);
    }

    // HTTP 转 HTTPS
    String secureUrl = url.startsWith('http://')
        ? url.replaceFirst('http://', 'https://')
        : url;

    return SizedBox(
      width: double.infinity,
      height: height,
      child: Image.network(
        secureUrl,
        fit: BoxFit.cover,
        loadingBuilder: (context, child, loadingProgress) {
          if (loadingProgress == null) return child;
          return _LoadingIndicator(
            height: height,
            progress: loadingProgress,
          );
        },
        errorBuilder: (context, error, stackTrace) {
          // HTTPS 失败则尝试原 HTTP URL
          if (secureUrl != url) {
            return SizedBox(
              width: double.infinity,
              height: height,
              child: Image.network(
                url,
                fit: BoxFit.cover,
                errorBuilder: (_, __, ___) => _ErrorPlaceholder(height: height),
              ),
            );
          }
          return _ErrorPlaceholder(height: height);
        },
      ),
    );
  }
}

/// 加载中指示器
class _LoadingIndicator extends StatelessWidget {
  final double height;
  final ImageChunkEvent? progress;

  const _LoadingIndicator({
    required this.height,
    this.progress,
  });

  
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      height: height,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(8),
        color: Colors.grey[200],
      ),
      child: Center(
        child: CircularProgressIndicator(
          value: progress?.expectedTotalBytes != null
              ? progress!.cumulativeBytesLoaded / progress!.expectedTotalBytes!
              : null,
          strokeWidth: 2,
        ),
      ),
    );
  }
}

/// 错误占位组件
class _ErrorPlaceholder extends StatelessWidget {
  final double height;

  const _ErrorPlaceholder({required this.height});

  
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      height: height,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(8),
        color: Colors.grey[300],
      ),
      child: const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.image_not_supported, size: 32, color: Colors.grey),
            SizedBox(height: 4),
            Text(
              '图片加载失败',
              style: TextStyle(fontSize: 10, color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }
}

四、页面集成

4.1 API 封装

class HomeAPI {
  /// 获取特惠推荐数据
  static Future<SpecialOfferResult> getSpecialOffers() async {
    try {
      final response = await dioRequest.get(HttpConstants.PRODUCT_LIST);
      return SpecialOfferResult.fromJSON(response as Map<String, dynamic>);
    } catch (e) {
      debugPrint('获取特惠推荐失败: $e');
      rethrow;
    }
  }
}

4.2 首页集成

class _HomeViewState extends State<HomeView> {
  SpecialOfferResult _specialOfferResult = SpecialOfferResult(
    id: '',
    title: '',
    subTypes: [],
  );

  
  void initState() {
    super.initState();
    _initData();
  }

  Future<void> _initData() async {
    try {
      // 并发请求
      await Future.wait([
        _getBannerList(),
        _getCategoryList(),
        _getSpecialOffers(),
      ]);
    } catch (e) {
      debugPrint('数据加载失败: $e');
    }
  }

  Future<void> _getSpecialOffers() async {
    try {
      _specialOfferResult = await HomeAPI.getSpecialOffers();
      setState(() {});
    } catch (e) {
      // 可选择使用默认数据
    }
  }

  
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        SliverToBoxAdapter(child: HmSlider(bannerList: _bannerList)),
        const SliverToBoxAdapter(child: SizedBox(height: 12)),
        SliverToBoxAdapter(child: HmCategory(categoryList: _categoryList)),
        const SliverToBoxAdapter(child: SizedBox(height: 12)),
        SliverToBoxAdapter(
          child: HmSuggestion(specialOfferResult: _specialOfferResult),
        ),
      ],
    );
  }
}

五、问题排查记录

5.1 问题清单

问题 原因 解决方案
空列表崩溃 subTypes.isEmpty 时调用 first 添加空值检查
HTTP 图片失败 HarmonyOS 禁止明文流量 自动转换 HTTPS
白屏等待 无加载反馈 添加 loadingBuilder
desc 字段空指针 API 返回 null 使用 String?
配置 Schema 错误 module.json5 不允许 network 配置 使用代码解决方案

5.2 空数据保护

// 错误写法
List<GoodsItem> get items => widget.result.subTypes.first.goodsItems.items;

// 正确写法
List<GoodsItem> get displayItems {
  if (widget.result.subTypes.isEmpty) return [];
  return widget.result.subTypes.first.goodsItems.items.take(3).toList();
}

5.3 HTTP/HTTPS 处理

String _normalizeUrl(String url) {
  if (url.isEmpty) return '';
  return url.startsWith('http://')
      ? url.replaceFirst('http://', 'https://')
      : url;
}

六、UI 设计规范

元素 规格
容器背景 #FFF6EE 浅米白
渐变色 #FF7832#FFB43C 橙红到橙黄
阴影颜色 #FF8C32 30% 透明度
价格标签 #F0600C 橙色背景
卡片圆角 10-12 dp
内边距 12 dp

七、总结

技术要点

  1. 四层嵌套模型GoodsItem → GoodsItems → SubType → SpecialOfferResult
  2. 图片优化:HTTP/HTTPS 自动转换 + 加载进度 + 错误占位
  3. 空安全处理:可空类型 + 默认值兜底
  4. 渐变设计LinearGradient 营造促销氛围

扩展建议

  • 添加倒计时组件显示限时信息
  • 支持点击商品跳转到详情页
  • 实现下拉刷新和加载更多
  • 添加骨架屏提升首屏体验

源码地址:https://atomgit.com/lbbxmx111/haromyos_day_four

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐