Flutter for OpenHarmony:迈向专业:购物APP的架构演进与未来蓝图
引言
经过前两篇文章的努力,我们的"淘淘购物"APP已经具备了基本的形态和交互能力。然而,审视当前的代码,我们会发现一些潜在的隐患:数据硬编码在UI文件中、状态管理逻辑与UI紧密耦合、缺乏对网络请求和持久化存储的支持。这些问题在小型Demo中或许无伤大雅,但在一个真实的、需要长期维护和迭代的商业项目中,它们将成为巨大的技术债务。
本文将作为本系列的收官之作,带领大家跳出代码细节,从更高的维度思考应用的架构设计。我们将对现有代码进行一次彻底的重构,引入清晰的分层架构,并探讨如何集成现代Flutter开发的最佳实践,为APP的未来发展绘制一幅清晰的蓝图。
完整效果展示


完整代码
dart
import 'package:flutter/material.dart';
void main() {
runApp(const ShoppingApp());
}
class ShoppingApp extends StatelessWidget {
const ShoppingApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '淘淘购物',
theme: ThemeData(
primaryColor: Colors.orange,
scaffoldBackgroundColor: Colors.grey[100],
useMaterial3: true,
),
home: const MainScreen(),
debugShowCheckedModeBanner: false,
);
}
}
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _currentIndex = 0;
final List<Widget> _screens = [
const HomeScreen(),
const CategoryScreen(),
const CartScreen(),
const ProfileScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _screens[_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),
label: '首页',
),
BottomNavigationBarItem(
icon: Icon(Icons.category),
label: '分类',
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart),
label: '购物车',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: '我的',
),
],
),
);
}
}
// 首页
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('淘淘购物'),
backgroundColor: Colors.orange,
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.message),
onPressed: () {},
),
],
),
body: ListView(
children: [
// 搜索框
Container(
padding: const EdgeInsets.all(10),
color: Colors.orange,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: const TextField(
decoration: InputDecoration(
hintText: '搜索宝贝',
border: InputBorder.none,
prefixIcon: Icon(Icons.search, color: Colors.grey),
),
),
),
),
// 轮播图区域
Container(
height: 150,
margin: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.orange[300],
borderRadius: BorderRadius.circular(10),
),
child: const Center(
child: Text(
'轮播图区域',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
),
// 快捷入口
Container(
padding: const EdgeInsets.all(15),
color: Colors.white,
child: GridView.count(
crossAxisCount: 5,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: const [
QuickEntry(Icons.shopping_bag, '天猫'),
QuickEntry(Icons.card_giftcard, '聚划算'),
QuickEntry(Icons.local_offer, '优惠券'),
QuickEntry(Icons.phone_android, '数码'),
QuickEntry(Icons.style, '服饰'),
QuickEntry(Icons.home, '家居'),
QuickEntry(Icons.restaurant, '美食'),
QuickEntry(Icons.flight, '旅行'),
QuickEntry(Icons.sports_basketball, '运动'),
QuickEntry(Icons.book, '图书'),
],
),
),
const SizedBox(height: 10),
// 推荐商品
Container(
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.all(15),
child: Text(
'为你推荐',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(10),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.75,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: _recommendedProducts.length,
itemBuilder: (context, index) {
return ProductCard(product: _recommendedProducts[index]);
},
),
],
),
),
],
),
);
}
}
// 快捷入口组件
class QuickEntry extends StatelessWidget {
final IconData icon;
final String label;
const QuickEntry(this.icon, this.label, {super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
Icon(icon, color: Colors.orange, size: 30),
const SizedBox(height: 5),
Text(label, style: const TextStyle(fontSize: 12)),
],
);
}
}
// 分类页
class CategoryScreen extends StatelessWidget {
const CategoryScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('商品分类'),
backgroundColor: Colors.orange,
),
body: Row(
children: [
// 左侧分类列表
Container(
width: 100,
color: Colors.grey[200],
child: ListView.builder(
itemCount: _categories.length,
itemBuilder: (context, index) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 20),
decoration: BoxDecoration(
color: index == 0 ? Colors.white : null,
border: index == 0
? const Border(
left: BorderSide(color: Colors.orange, width: 3))
: null,
),
child: Center(
child: Text(
_categories[index],
style: TextStyle(
color: index == 0 ? Colors.orange : Colors.black87,
fontWeight:
index == 0 ? FontWeight.bold : FontWeight.normal,
),
),
),
);
},
),
),
// 右侧商品列表
Expanded(
child: GridView.builder(
padding: const EdgeInsets.all(10),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 0.8,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
itemCount: _subCategories.length,
itemBuilder: (context, index) {
return SubCategoryCard(category: _subCategories[index]);
},
),
),
],
),
);
}
}
// 子分类卡片
class SubCategoryCard extends StatelessWidget {
final String category;
const SubCategoryCard({super.key, required this.category});
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.orange[100],
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.image, color: Colors.orange, size: 30),
),
const SizedBox(height: 8),
Text(
category,
style: const TextStyle(fontSize: 12),
textAlign: TextAlign.center,
),
],
);
}
}
// 购物车页
class CartScreen extends StatefulWidget {
const CartScreen({super.key});
@override
State<CartScreen> createState() => _CartScreenState();
}
class _CartScreenState extends State<CartScreen> {
double _totalPrice = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('购物车'),
backgroundColor: Colors.orange,
),
body: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: _cartItems.length,
itemBuilder: (context, index) {
return CartItemCard(
item: _cartItems[index],
onQuantityChanged: (quantity) {
setState(() {
_totalPrice = _calculateTotal();
});
},
);
},
),
),
// 底部结算栏
Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.3),
blurRadius: 5,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
const Text(
'合计: ',
style: TextStyle(fontSize: 16),
),
Text(
'¥$_totalPrice',
style: const TextStyle(
fontSize: 24,
color: Colors.orange,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
padding: const EdgeInsets.symmetric(
horizontal: 40,
vertical: 12,
),
),
child: const Text('结算', style: TextStyle(fontSize: 16)),
),
],
),
),
],
),
);
}
double _calculateTotal() {
return _cartItems.fold(
0.0, (sum, item) => sum + item.price * item.quantity);
}
}
// 我的页面
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('我的淘淘'),
backgroundColor: Colors.orange,
),
body: ListView(
children: [
// 用户信息
Container(
padding: const EdgeInsets.all(20),
color: Colors.orange,
child: Column(
children: [
Container(
width: 80,
height: 80,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child:
const Icon(Icons.person, size: 50, color: Colors.orange),
),
const SizedBox(height: 10),
const Text(
'用户昵称',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 5),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 5),
decoration: BoxDecoration(
border: Border.all(color: Colors.white),
borderRadius: BorderRadius.circular(15),
),
child: const Text(
'登录/注册',
style: TextStyle(color: Colors.white),
),
),
],
),
),
const SizedBox(height: 10),
// 订单状态
Container(
padding: const EdgeInsets.all(15),
color: Colors.white,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'我的订单',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Row(
children: const [
Text('全部订单', style: TextStyle(color: Colors.grey)),
Icon(Icons.chevron_right, color: Colors.grey),
],
),
],
),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: const [
OrderStatusIcon(Icons.payment, '待付款'),
OrderStatusIcon(Icons.inventory, '待发货'),
OrderStatusIcon(Icons.local_shipping, '待收货'),
OrderStatusIcon(Icons.star, '待评价'),
OrderStatusIcon(Icons.replay, '退款/售后'),
],
),
],
),
),
const SizedBox(height: 10),
// 功能列表
Container(
color: Colors.white,
child: Column(
children: const [
ListTile(
leading: Icon(Icons.favorite_border, color: Colors.orange),
title: Text('我的收藏'),
trailing: Icon(Icons.chevron_right),
),
Divider(height: 1),
ListTile(
leading: Icon(Icons.location_on, color: Colors.orange),
title: Text('收货地址'),
trailing: Icon(Icons.chevron_right),
),
Divider(height: 1),
ListTile(
leading: Icon(Icons.help_outline, color: Colors.orange),
title: Text('联系客服'),
trailing: Icon(Icons.chevron_right),
),
Divider(height: 1),
ListTile(
leading: Icon(Icons.settings, color: Colors.orange),
title: Text('设置'),
trailing: Icon(Icons.chevron_right),
),
],
),
),
],
),
);
}
}
// 订单状态图标
class OrderStatusIcon extends StatelessWidget {
final IconData icon;
final String label;
const OrderStatusIcon(this.icon, this.label, {super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
Icon(icon, color: Colors.orange, size: 28),
const SizedBox(height: 5),
Text(label, style: const TextStyle(fontSize: 12)),
],
);
}
}
// 商品卡片
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({super.key, required this.product});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.orange[100],
borderRadius:
const BorderRadius.vertical(top: Radius.circular(10)),
),
child: const Center(
child: Icon(Icons.image, color: Colors.orange, size: 50),
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: const TextStyle(fontSize: 14),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 5),
Row(
children: [
Text(
'¥${product.price.toStringAsFixed(2)}',
style: const TextStyle(
color: Colors.orange,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 5),
Text(
'${product.sales}人付款',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
],
),
),
],
),
);
}
}
// 购物车商品卡片
class CartItemCard extends StatefulWidget {
final CartItem item;
final Function(int) onQuantityChanged;
const CartItemCard({
super.key,
required this.item,
required this.onQuantityChanged,
});
@override
State<CartItemCard> createState() => _CartItemCardState();
}
class _CartItemCardState extends State<CartItemCard> {
bool _isChecked = true;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Checkbox(
value: _isChecked,
onChanged: (value) {
setState(() {
_isChecked = value ?? false;
});
},
),
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.orange[100],
borderRadius: BorderRadius.circular(5),
),
child: const Icon(Icons.image, color: Colors.orange),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.item.name,
style: const TextStyle(fontSize: 14),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'¥${widget.item.price.toStringAsFixed(2)}',
style: const TextStyle(
color: Colors.orange,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Row(
children: [
IconButton(
icon: const Icon(Icons.remove, size: 18),
onPressed: () {
if (widget.item.quantity > 1) {
widget.item.quantity--;
widget.onQuantityChanged(widget.item.quantity);
}
},
),
Text('${widget.item.quantity}'),
IconButton(
icon: const Icon(Icons.add, size: 18),
onPressed: () {
widget.item.quantity++;
widget.onQuantityChanged(widget.item.quantity);
},
),
],
),
],
),
],
),
),
],
),
);
}
}
// 数据模型
class Product {
final String name;
final double price;
final int sales;
Product({required this.name, required this.price, required this.sales});
}
class CartItem {
final String name;
final double price;
int quantity;
CartItem({
required this.name,
required this.price,
this.quantity = 1,
});
}
// 数据
final List<Product> _recommendedProducts = [
Product(name: '新款智能手机', price: 2999.00, sales: 1000),
Product(name: '无线蓝牙耳机', price: 199.00, sales: 5000),
Product(name: '智能手表', price: 899.00, sales: 2000),
Product(name: '运动鞋', price: 399.00, sales: 3000),
Product(name: '男士休闲裤', price: 159.00, sales: 1500),
Product(name: '女士连衣裙', price: 299.00, sales: 2500),
];
final List<String> _categories = [
'热门推荐',
'手机数码',
'家用电器',
'电脑办公',
'家居家装',
'服装服饰',
'鞋靴箱包',
'运动户外',
'美妆个护',
'食品生鲜',
];
final List<String> _subCategories = [
'手机',
'平板',
'耳机',
'充电宝',
'数据线',
'保护壳',
'笔记本',
'键盘',
'鼠标',
'显示器',
'音响',
'耳机',
'冰箱',
'洗衣机',
'空调',
'电视',
'微波炉',
'电饭煲',
];
final List<CartItem> _cartItems = [
CartItem(name: '新款智能手机', price: 2999.00, quantity: 1),
CartItem(name: '无线蓝牙耳机', price: 199.00, quantity: 2),
CartItem(name: '智能手表', price: 899.00, quantity: 1),
];
第一步:痛点分析------当前架构的局限性
让我们先明确当前代码存在的主要问题:
- 数据与UI混杂 :所有的模拟数据(
_recommendedProducts,_cartItems等)都定义在UI文件(如main.dart)的顶层。这导致UI文件臃肿不堪,且无法方便地替换为真实API数据。- 状态管理脆弱 :购物车的总价计算依赖于
CartScreen内部的_cartItems列表。这意味着,如果其他页面(如商品详情页)需要向购物车添加商品,就必须找到一种方式去修改CartScreen内部的状态,这破坏了组件的封装性。- 缺乏可测试性 :业务逻辑(如总价计算)嵌入在
StatefulWidget中,难以进行单元测试。- 扩展性差:新增一个功能(如商品收藏)可能需要在多个地方修改代码,容易引入bug。
第二步:引入分层架构------Clean Architecture的简化版
为了解决上述问题,我们将采用一种简化的分层架构,将代码划分为三个核心层次:
presentation(表现层) :即UI层。包含Screens、Widgets和Providers(状态管理)。它只负责展示数据和响应用户事件,不包含任何业务逻辑。domain(领域层) :包含核心的业务逻辑和数据模型。例如,Product、CartItem模型,以及CartService(负责计算总价、管理购物车项等)。data(数据层) :负责数据的获取和存储。包括repositories(仓库)和datasources(数据源,如API、本地数据库Hive/SQLite)。
第三步:拥抱现代化状态管理------Riverpod
要实现分层架构,我们需要一个强大的状态管理工具来连接各层。Riverpod 是目前Flutter社区公认的最佳选择之一,它相比setState和Provider有诸多优势:
- 解耦:Provider的定义完全独立于UI树。
- 安全:编译时检查,避免运行时错误。
- 强大:支持异步、家族(Family)、监听等多种高级用法。
- 可测试:Provider可以轻松地在测试中被覆盖。
重构购物车状态
-
定义
CartService:dart// domain/services/cart_service.dart class CartService { final List<CartItem> _items = []; List<CartItem> get items => _items; double get total => _items.fold(0, (sum, item) => sum + item.price * item.quantity); void addItem(CartItem item) { _items.add(item); } void removeItem(String productName) { _items.removeWhere((i) => i.name == productName); } void updateQuantity(String productName, int newQuantity) { /* ... */ } } -
创建
cartProvider:dart// presentation/providers/cart_provider.dart final cartServiceProvider = Provider((ref) => CartService()); final cartItemsProvider = Provider<List<CartItem>>((ref) { final service = ref.watch(cartServiceProvider); return service.items; }); final cartTotalProvider = Provider<double>((ref) { final service = ref.watch(cartServiceProvider); return service.total; }); -
在UI中使用:
dart// 在CartScreen中 final cartItems = ref.watch(cartItemsProvider); final totalPrice = ref.watch(cartTotalProvider); // 在商品详情页添加到购物车 ref.read(cartServiceProvider).addItem(newItem);
现在,购物车的状态被集中管理在CartService中。任何页面都可以通过Provider安全地读取或修改购物车,彻底解决了状态共享的难题。
第四步:集成真实数据源
有了清晰的架构,集成真实数据变得水到渠成。
- 网络请求 :在
data/datasources中创建ProductRemoteDataSource,使用http或dio库从RESTful
API获取商品列表。- 本地缓存 :使用
Hive或shared_preferences在data/datasources中创建CartLocalDataSource,在应用关闭后也能保存购物车数据。- 仓库模式 :
data/repositories/ProductRepository将协调远程和本地数据源,对外提供统一的Future<List<Product>> getAllProducts()接口。
表现层只需要调用ProductRepository,完全不用关心数据是从网络还是本地获取的。
第五步:未来功能展望
基于这套健壮的架构,我们可以轻松地扩展以下功能:
- 用户认证 :引入
AuthService和authProvider,管理用户登录状态。 - 商品搜索与筛选 :在
ProductRepository中增加带参数的查询方法。 - 订单系统 :创建新的
Order实体和OrderService,处理下单、支付等复杂流程。 - 推送通知:集成Firebase Cloud Messaging (FCM),向用户发送促销信息或订单状态更新。
- 性能优化 :使用
ListView.builder的itemExtent、图片懒加载(cached_network_image)等技术提升列表滚动性能。
结语
从一个简单的UI Demo,到一个具备清晰架构、可维护、可扩展的专业应用,我们走过了完整的演进之路。Flutter本身提供了强大的工具集,但如何组织和运用这些工具,才是区分业余与专业的关键。
通过引入分层架构和Riverpod,我们不仅解决了当前的问题,更重要的是为未来的所有可能性敞开了大门。代码不再是束缚,而是助力我们快速迭代、应对变化的坚实基石。
🌐 加入社区
欢迎加入 开源鸿蒙跨平台开发者社区 ,获取最新资源与技术支持:
技术因分享而进步,生态因共建而繁荣 。
------ 晚霞的不甘 · 与您共赴鸿蒙跨平台开发之旅