【OpenHarmonyOS】DAY5:Flutter × OpenHarmony:电商首页完整开发指南(组件化设计 + 数据驱动 + 最佳实践)
// 轮播图数据模型linkUrl;// 跳转链接});?'',?'',/// 分类数据模型children;});?'',?'',?'',?null.toList(),/// 商品数据模型desc;?'',?'',?'0',?'',?0,/// 商品列表模型});?0,?0,?0,?0,?.toList()??[],/// 特惠推荐结果模型});?'',?'',?
Flutter × OpenHarmony:电商首页完整开发指南(组件化设计 + 数据驱动 + 最佳实践)
前言
本文将带你从零开始,使用 Flutter 构建一个生产级的电商应用首页。我们将深入探讨组件化设计、数据驱动开发、状态管理、以及在实际开发中遇到的各种问题和解决方案。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
项目环境:
- Flutter SDK 3.24.0+
- Dart 3.5.0+
- DevEco Studio 5.0+
- HarmonyOS API 9+
你将学到:
- ✅ 完整的组件化设计模式
- ✅ 数据驱动的开发思维
- ✅ MVVM 架构在 Flutter 中的应用
- ✅ CustomScrollView 高级用法
- ✅ 真实场景的踩坑与解决方案
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、项目架构设计
1.1 整体架构
采用 MVVM(Model-View-ViewModel) 架构:
┌─────────────────────────────────────────────────────┐
│ View (页面) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 首页视图 │ │ 分类视图 │ │购物车视图 │ ... │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
├───────┴─────────────┴─────────────┴────────────────┤
│ ViewModel (视图模型) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │首页VM │ │数据状态 │ │业务逻辑 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
├───────┴─────────────┴─────────────┴────────────────┤
│ Model (数据模型) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │BannerItem│ │Category │ │GoodsItem │ │
│ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────┤
│ API (网络层) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Dio封装 │ │ 接口定义 │ │ 错误处理 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────┘
1.2 项目目录结构
lib/
├── main.dart # 应用入口
├── constants/ # 常量定义
│ └── index.dart # 全局常量、接口地址
├── utils/ # 工具类
│ └── DioRequest.dart # 网络请求封装
├── viewmodels/ # 视图模型(数据模型)
│ └── home.dart # 首页数据模型
├── api/ # API接口
│ └── home.dart # 首页接口
├── components/ # 可复用组件
│ └── Home/ # 首页组件
│ ├── HmSlider.dart # 轮播图组件
│ ├── HmCategory.dart # 分类组件
│ ├── HmSuggestion.dart # 推荐组件
│ ├── HmHot.dart # 爆款推荐组件
│ └── HmMoreList.dart # 商品列表组件
└── pages/ # 页面
├── home/
│ └── index.dart # 首页
├── category/
│ └── index.dart # 分类页面
├── cart/
│ └── index.dart # 购物车页面
├── profile/
│ └── index.dart # 我的页面
└── main/
└── index.dart # 底部导航主框架
二、核心数据模型设计
2.1 数据模型定义
文件:lib/viewmodels/home.dart
/// 轮播图数据模型
class BannerItem {
final String id;
final String imgUrl;
final String? linkUrl; // 跳转链接
BannerItem({
required this.id,
required this.imgUrl,
this.linkUrl,
});
factory BannerItem.fromJSON(Map<String, dynamic> json) {
return BannerItem(
id: json['id']?.toString() ?? '',
imgUrl: json['imgUrl'] ?? '',
linkUrl: json['linkUrl'],
);
}
}
/// 分类数据模型
class CategoryItem {
final String id;
final String name;
final String picture;
final List<CategoryItem>? children;
CategoryItem({
required this.id,
required this.name,
required this.picture,
this.children,
});
factory CategoryItem.fromJSON(Map<String, dynamic> json) {
return CategoryItem(
id: json['id']?.toString() ?? '',
name: json['name'] ?? '',
picture: json['picture'] ?? '',
children: json['children'] == null
? null
: (json['children'] as List)
.map((e) => CategoryItem.fromJSON(e as Map<String, dynamic>))
.toList(),
);
}
}
/// 商品数据模型
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']?.toString() ?? '',
name: json['name'] ?? '',
desc: json['desc'],
price: json['price']?.toString() ?? '0',
picture: json['picture'] ?? '',
orderNum: json['orderNum'] ?? 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'] ?? 0,
pageSize: json['pageSize'] ?? 0,
pages: json['pages'] ?? 0,
page: json['page'] ?? 0,
items: (json['items'] as List?)
?.map((e) => GoodsItem.fromJSON(e as Map<String, dynamic>))
.toList() ?? [],
);
}
}
/// 特惠推荐结果模型
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']?.toString() ?? '',
title: json['title'] ?? '',
subTypes: (json['subTypes'] as List?)
?.map((e) => SubType.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']?.toString() ?? '',
title: json['title'] ?? '',
goodsItems: GoodsItems.fromJSON(json['goodsItems'] ?? {}),
);
}
}
2.2 常量定义
文件:lib/constants/index.dart
/// 全局状态常量
class GlobalConstants {
/// 基础地址
static const String BASE_URL = "https://meikou-api.itheima.net/";
/// 超时时间(秒)
static const int TIME_OUT = 10;
/// 成功状态码
static const String SUCCESS_CODE = "1";
}
/// 接口地址常量
class HttpConstants {
/// 轮播图接口
static const String BANNER_LIST = "/home/banner";
/// 分类列表接口
static const String CATEGORY_LIST = "/home/category/head";
/// 特惠推荐接口
static const String PRODUCT_LIST = "/hot/preference";
}
三、网络层封装
3.1 Dio 请求封装
文件:lib/utils/DioRequest.dart
import 'package:dio/dio.dart';
import 'package:harmonyos_day_four/constants/index.dart';
/// Dio 单例
final dioRequest = DioRequest._internal();
class DioRequest {
late final Dio _dio;
// 私有构造函数
DioRequest._internal() {
_dio = Dio(BaseOptions(
baseUrl: GlobalConstants.BASE_URL,
connectTimeout: Duration(seconds: GlobalConstants.TIME_OUT),
receiveTimeout: Duration(seconds: GlobalConstants.TIME_OUT),
headers: {
'Content-Type': 'application/json',
},
));
// 添加拦截器
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
print('请求: ${options.method} ${options.uri}');
return handler.next(options);
},
onResponse: (response, handler) {
print('响应: ${response.statusCode} ${response.data}');
return handler.next(response);
},
onError: (DioException error, handler) {
print('错误: ${error.message}');
return handler.next(error);
},
));
}
/// GET 请求
Future get(String path, {Map<String, dynamic>? queryParameters}) async {
try {
final response = await _dio.get(
path,
queryParameters: queryParameters,
);
return response.data['data'] ?? response.data;
} catch (e) {
rethrow;
}
}
/// POST 请求
Future post(String path, {dynamic data}) async {
try {
final response = await _dio.post(path, data: data);
return response.data['data'] ?? response.data;
} catch (e) {
rethrow;
}
}
}
3.2 API 接口定义
文件:lib/api/home.dart
import 'package:harmonyos_day_four/utils/DioRequest.dart';
import 'package:harmonyos_day_four/viewmodels/home.dart';
import 'package:harmonyos_day_four/constants/index.dart';
/// 获取轮播图列表
Future<List<BannerItem>> getBannerListAPI() async {
final result = await dioRequest.get(HttpConstants.BANNER_LIST) as List;
return result.map((e) => BannerItem.fromJSON(e as Map<String, dynamic>)).toList();
}
/// 获取分类列表
Future<List<CategoryItem>> getCategoryListAPI() async {
final result = await dioRequest.get(HttpConstants.CATEGORY_LIST) as List;
return result.map((e) => CategoryItem.fromJSON(e as Map<String, dynamic>)).toList();
}
/// 获取特惠推荐
Future<SpecialOfferResult> getProductListAPI() async {
final result = await dioRequest.get(HttpConstants.PRODUCT_LIST);
return SpecialOfferResult.fromJSON(result);
}
四、首页组件实现(数据驱动)
4.1 轮播图组件(支持自动播放)
文件:lib/components/Home/HmSlider.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:harmonyos_day_four/viewmodels/home.dart';
/// 轮播图组件
class HmSlider extends StatefulWidget {
final List<BannerItem> bannerList;
const HmSlider({super.key, required this.bannerList});
State<HmSlider> createState() => _HmSliderState();
}
class _HmSliderState extends State<HmSlider> {
final PageController _pageController = PageController();
int _currentIndex = 0;
Timer? _timer;
void initState() {
super.initState();
_startAutoPlay();
}
void didUpdateWidget(HmSlider oldWidget) {
super.didUpdateWidget(oldWidget);
// 数据更新时重置轮播
if (widget.bannerList != oldWidget.bannerList) {
setState(() {
_currentIndex = 0;
});
_pageController.jumpToPage(0);
}
}
/// 开始自动播放
void _startAutoPlay() {
_timer?.cancel();
if (widget.bannerList.length <= 1) return;
_timer = Timer.periodic(const Duration(seconds: 3), (timer) {
if (_currentIndex < widget.bannerList.length - 1) {
_currentIndex++;
} else {
_currentIndex = 0;
}
if (_pageController.hasClients) {
_pageController.animateToPage(
_currentIndex,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
});
}
void dispose() {
_pageController.dispose();
_timer?.cancel();
super.dispose();
}
Widget build(BuildContext context) {
if (widget.bannerList.isEmpty) {
return _buildPlaceholder();
}
final screenWidth = MediaQuery.of(context).size.width;
return SizedBox(
height: 180,
child: Stack(
children: [
// 轮播图
PageView.builder(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentIndex = index;
});
},
itemCount: widget.bannerList.length,
itemBuilder: (context, index) {
return _buildBannerItem(widget.bannerList[index], screenWidth);
},
),
// 指示器
_buildIndicator(),
],
),
);
}
/// 构建单个轮播图
Widget _buildBannerItem(BannerItem item, double screenWidth) {
return GestureDetector(
onTap: () {
if (item.linkUrl != null) {
// TODO: 处理跳转
print('点击轮播图: ${item.linkUrl}');
}
},
child: Image.network(
item.imgUrl,
width: screenWidth,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[200],
child: const Center(
child: Icon(Icons.broken_image, size: 50, color: Colors.grey),
),
);
},
),
);
}
/// 构建指示器
Widget _buildIndicator() {
return Positioned(
left: 0,
right: 0,
bottom: 10,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(widget.bannerList.length, (index) {
final isActive = index == _currentIndex;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.symmetric(horizontal: 4),
width: isActive ? 20 : 8,
height: 6,
decoration: BoxDecoration(
color: isActive ? Colors.orange : Colors.white,
borderRadius: BorderRadius.circular(3),
),
);
}),
),
);
}
/// 占位图
Widget _buildPlaceholder() {
return SizedBox(
height: 180,
child: Container(
color: Colors.grey[200],
child: const Center(
child: CircularProgressIndicator(),
),
),
);
}
}
4.2 分类组件(横向滚动)
文件:lib/components/Home/HmCategory.dart
import 'package:flutter/material.dart';
import 'package:harmonyos_day_four/viewmodels/home.dart';
/// 分类组件
class HmCategory extends StatelessWidget {
final List<CategoryItem> categoryList;
const HmCategory({super.key, required this.categoryList});
Widget build(BuildContext context) {
if (categoryList.isEmpty) {
return _buildPlaceholder();
}
return SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 10),
itemCount: categoryList.length,
itemBuilder: (context, index) {
return _buildCategoryItem(categoryList[index]);
},
),
);
}
/// 构建分类项
Widget _buildCategoryItem(CategoryItem item) {
return Container(
width: 70,
margin: const EdgeInsets.only(right: 15),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 图标
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.orange[50],
borderRadius: BorderRadius.circular(25),
),
child: ClipOval(
child: Image.network(
item.picture,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Icon(Icons.category, color: Colors.orange[300]);
},
),
),
),
const SizedBox(height: 8),
// 名称
Text(
item.name,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF333333),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
/// 占位图
Widget _buildPlaceholder() {
return SizedBox(
height: 100,
child: Center(
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(25),
),
child: const CircularProgressIndicator(strokeWidth: 2),
),
),
);
}
}
4.3 特惠推荐组件
文件:lib/components/Home/HmSuggestion.dart
import 'package:flutter/material.dart';
import 'package:harmonyos_day_four/viewmodels/home.dart';
/// 特惠推荐组件
class HmSuggestion extends StatelessWidget {
final SpecialOfferResult specialOfferResult;
const HmSuggestion({super.key, required this.specialOfferResult});
Widget build(BuildContext context) {
if (specialOfferResult.subTypes.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
specialOfferResult.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
const SizedBox(height: 12),
// 子类型列表
...specialOfferResult.subTypes.map((subType) {
return _buildSubTypeSection(subType);
}).toList(),
],
),
);
}
/// 构建子类型区域
Widget _buildSubTypeSection(SubType subType) {
final goods = subType.goodsItems;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 子类型标题
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 4,
height: 16,
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
subType.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF333333),
),
),
],
),
),
// 商品横向列表
SizedBox(
height: 180,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: goods.items.length,
itemBuilder: (context, index) {
return _buildGoodsItem(goods.items[index]);
},
),
),
],
);
}
/// 构建商品项
Widget _buildGoodsItem(GoodsItem item) {
return Container(
width: 120,
margin: const EdgeInsets.only(right: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 商品图片
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
item.picture,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[200],
child: const Center(
child: Icon(Icons.image_not_supported),
),
);
},
),
),
),
const SizedBox(height: 8),
// 商品名称
Text(
item.name,
style: const TextStyle(
fontSize: 13,
color: Color(0xFF333333),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
// 价格
Text(
'¥${item.price}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.orange,
),
),
],
),
);
}
}
4.4 爆款推荐组件
文件:lib/components/Home/HmHot.dart
import 'package:flutter/material.dart';
/// 爆款推荐组件
class HmHot extends StatelessWidget {
const HmHot({super.key});
Widget build(BuildContext context) {
return Container(
height: 160,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.orange[400]!, Colors.orange[300]!],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
),
alignment: Alignment.center,
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.local_fire_department, color: Colors.white, size: 40),
SizedBox(height: 8),
Text(
'爆款推荐',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
}
4.5 商品列表组件(无限滚动)
文件:lib/components/Home/HmMoreList.dart
import 'package:flutter/material.dart';
import 'package:harmonyos_day_four/viewmodels/home.dart';
/// 商品列表组件
class HmMorelist extends StatefulWidget {
const HmMorelist({super.key});
State<HmMorelist> createState() => _HmMorelistState();
}
class _HmMorelistState extends State<HmMorelist> {
// 模拟数据
final List<Map<String, String>> _goods = List.generate(
20,
(index) => {
'name': '商品 ${index + 1}',
'price': '${(index + 1) * 99}.00',
'image': 'https://via.placeholder.com/150',
},
);
// 是否正在加载
bool _isLoading = false;
Widget build(BuildContext context) {
return SliverGrid.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 0.75,
),
itemCount: _goods.length + (_isLoading ? 2 : 0),
itemBuilder: (context, index) {
// 加载更多
if (index == _goods.length) {
_loadMore();
return _buildLoadingItem();
}
return _buildGoodsItem(_goods[index]);
},
);
}
/// 构建商品项
Widget _buildGoodsItem(Map<String, String> goods) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[200]!),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 商品图片
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
child: Image.network(
goods['image']!,
width: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[100],
child: const Center(
child: Icon(Icons.image, color: Colors.grey),
),
);
},
),
),
),
// 商品信息
Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
goods['name']!,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF333333),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'¥${goods['price']}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.orange,
),
),
],
),
),
],
),
);
}
/// 构建加载项
Widget _buildLoadingItem() {
return Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: CircularProgressIndicator(strokeWidth: 2),
),
);
}
/// 加载更多
Future<void> _loadMore() async {
if (_isLoading) return;
setState(() {
_isLoading = true;
});
// 模拟网络请求
await Future.delayed(const Duration(seconds: 1));
if (!mounted) return;
setState(() {
_isLoading = false;
});
}
}
五、首页页面组装
文件:lib/pages/home/index.dart
import 'package:flutter/material.dart';
import 'package:harmonyos_day_four/components/Home/HmCategory.dart';
import 'package:harmonyos_day_four/components/Home/HmHot.dart';
import 'package:harmonyos_day_four/components/Home/HmMoreList.dart';
import 'package:harmonyos_day_four/components/Home/HmSlider.dart';
import 'package:harmonyos_day_four/components/Home/HmSuggestion.dart';
import 'package:harmonyos_day_four/viewmodels/home.dart';
import 'package:harmonyos_day_four/api/home.dart';
/// 首页视图
class HomeView extends StatefulWidget {
const HomeView({super.key});
State<HomeView> createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
// 数据状态
List<BannerItem> _bannerList = [];
List<CategoryItem> _categoryList = [];
SpecialOfferResult _specialOfferResult = SpecialOfferResult(
id: '',
title: '',
subTypes: [],
);
// 加载状态
bool _isLoading = true;
void initState() {
super.initState();
_loadData();
}
/// 加载首页数据
Future<void> _loadData() async {
try {
// 并行请求所有接口
final results = await Future.wait([
getBannerListAPI(),
getCategoryListAPI(),
getProductListAPI(),
]);
if (!mounted) return;
setState(() {
_bannerList = results[0] as List<BannerItem>;
_categoryList = results[1] as List<CategoryItem>;
_specialOfferResult = results[2] as SpecialOfferResult;
_isLoading = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_isLoading = false;
});
// 显示错误提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('数据加载失败: $e')),
);
}
}
/// 获取滚动容器内容
List<Widget> _getScrollChildren() {
return [
// 轮播图
SliverToBoxAdapter(
child: _isLoading
? _buildLoadingPlaceholder()
: HmSlider(bannerList: _bannerList),
),
const SliverToBoxAdapter(child: SizedBox(height: 10)),
// 分类
SliverToBoxAdapter(
child: _isLoading
? _buildLoadingPlaceholder(height: 100)
: HmCategory(categoryList: _categoryList),
),
const SliverToBoxAdapter(child: SizedBox(height: 10)),
// 特惠推荐
SliverToBoxAdapter(
child: _isLoading
? const SizedBox.shrink()
: HmSuggestion(specialOfferResult: _specialOfferResult),
),
const SliverToBoxAdapter(child: SizedBox(height: 10)),
// 爆款推荐(两个)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
children: [
Expanded(child: HmHot()),
const SizedBox(width: 10),
Expanded(child: HmHot()),
],
),
),
),
const SliverToBoxAdapter(child: SizedBox(height: 10)),
// 商品列表
const HmMorelist(),
];
}
/// 加载占位符
Widget _buildLoadingPlaceholder({double height = 180}) {
return SizedBox(
height: height,
child: Container(
color: Colors.grey[100],
child: const Center(
child: CircularProgressIndicator(strokeWidth: 2),
),
),
);
}
Widget build(BuildContext context) {
return CustomScrollView(slivers: _getScrollChildren());
}
}
六、底部导航栏实现
文件:lib/pages/main/index.dart
import 'package:flutter/material.dart';
import 'package:harmonyos_day_four/pages/home/index.dart';
import 'package:harmonyos_day_four/pages/category/index.dart';
import 'package:harmonyos_day_four/pages/cart/index.dart';
import 'package:harmonyos_day_four/pages/profile/index.dart';
/// 底部导航主框架
class MainContainer extends StatefulWidget {
const MainContainer({super.key});
State<MainContainer> createState() => _MainContainerState();
}
class _MainContainerState extends State<MainContainer> {
// 当前选中的页面索引
int _currentIndex = 0;
// 页面列表(使用 const 优化性能)
static const List<Widget> _pages = [
HomeView(),
CategoryView(),
CartView(),
ProfileView(),
];
Widget build(BuildContext context) {
return Scaffold(
body: _pages[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
type: BottomNavigationBarType.fixed,
selectedItemColor: Colors.orange,
unselectedItemColor: Colors.grey,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: '首页',
),
BottomNavigationBarItem(
icon: Icon(Icons.category_outlined),
activeIcon: Icon(Icons.category),
label: '分类',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
activeIcon: Icon(Icons.shopping_cart),
label: '购物车',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
activeIcon: Icon(Icons.person),
label: '我的',
),
],
),
);
}
}
七、常见问题与解决方案
问题 1:CustomScrollView 报错 “All children must be Sliver widgets”
错误信息:
FlutterError: All children of a CustomScrollView must be Sliver widgets.
A Container was found, but Slivers are expected.
原因分析:CustomScrollView 的 slivers 参数只接受 Sliver 家族组件,不能直接放入普通 Widget。
解决方案:
使用 SliverToBoxAdapter 包裹普通组件:
// ❌ 错误写法
CustomScrollView(
slivers: [
Container(...), // 报错
],
)
// ✅ 正确写法
CustomScrollView(
slivers: [
SliverToBoxAdapter(child: Container(...)), // 正确
],
)
问题 2:ListView 横向滚动高度失效
问题描述:
将 ListView.scrollDirection 设置为 Axis.horizontal 后,容器高度变为 0。
原因分析:ListView 在横向滚动时,无法自动推断高度。
解决方案:
使用 SizedBox 指定高度:
SizedBox(
height: 100, // 必须指定高度
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) => Container(...),
),
)
问题 3:底部导航栏图标不显示
问题描述:
超过 3 个 Tab 时,图标显示异常。
解决方案:
设置 type: BottomNavigationBarType.fixed:
BottomNavigationBar(
type: BottomNavigationBarType.fixed, // 关键
items: [...],
)
问题 4:轮播图自动播放内存泄漏
问题描述:
退出页面后 Timer 仍在运行。
解决方案:
在 dispose 中取消 Timer:
void dispose() {
_timer?.cancel(); // 必须取消
_pageController.dispose();
super.dispose();
}
问题 5:网络图片加载失败显示空白
解决方案:
使用 errorBuilder 处理错误:
Image.network(
url,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[200],
child: const Icon(Icons.broken_image),
);
},
)
八、项目运行
运行步骤
-
启动项目
cd harmonyos_day_four flutter run -
选择设备
- Android 模拟器/真机
- iOS 模拟器/真机
- HarmonyOS 设备
-
查看效果
- 首页:完整的电商首页布局
- 底部导航:4 个 Tab 可切换
- 数据加载:真实的 API 数据
效果展示
| 页面 | 说明 |
|---|---|
| 首页 | 轮播图自动播放、分类横向滚动、特惠推荐、商品列表 |
| 分类 | 分类展示页面 |
| 购物车 | 购物车页面 |
| 我的 | 个人中心页面 |
九、总结
通过本文,我们实现了一个生产级的电商首页:
| 模块 | 技术要点 |
|---|---|
| 数据层 | 完整的数据模型 + API 封装 |
| 组件层 | 可复用的组件 + 数据驱动 |
| 页面层 | MVVM 架构 + 状态管理 |
| 网络层 | Dio 封装 + 错误处理 |
关键要点:
- ✅ 使用 MVVM 架构分离业务逻辑
- ✅ 组件化设计提高代码复用性
- ✅ 数据驱动开发,组件接收数据参数
- ✅ 完整的错误处理和加载状态
- ✅ 生命周期管理,防止内存泄漏
项目源码:
https://atomgit.com/lbbxmx111/haromyos_day_four
欢迎交流:
如有疑问,欢迎评论区讨论!
更多推荐
所有评论(0)