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

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐