Flutter: 项目设计 — ShopApp 架构规划

建楼先画蓝图——架构设计决定了代码的"承重墙"在哪。

📋 前置知识:需要先掌握以下内容

1. 你将学到


2. 一个没有架构的项目的故事

(1) 痛点:需求模糊 + 架构混乱

Bob 接到 ShopApp 项目时只有一句话需求:"做一个跨境电商应用"。他直接开始写代码,3 个月后发现:用户故事不明确(商家管理功能漏了)、数据模型频繁变更(3 次重写 Order)、架构耦合严重(改支付要动商品列表)。项目延期 2 个月。

(2) 先设计后编码的解法

项目设计阶段把需求、架构、数据模型、UI 规划全部想清楚,编码只是"照图施工"。

(3) 收益:编码时间减半 + 变更成本降 80%

Bob 用 2 周做设计后,编码 4 周完成(而非之前的 3+2 个月),需求变更只需改对应层,不影响其他模块。


3. 需求分析

(1) 用户故事

100%
graph TD
    subgraph User Stories
        ALICE[Alice: Browse → Order → Pay $299.99]
        BOB[Bob: Manage Products → View Orders]
        CHARLIE[Charlie: Cross-border → JPY → International Payment]
    end
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
角色 用户故事 优先级
Alice (消费者) 浏览千万级商品列表 P0
Alice 搜索/筛选/分类浏览商品 P0
Alice 查看商品详情 + 评价 P0
Alice 加入购物车 + 管理数量 P0
Alice USD/CNY/JPY 多币种结算 P1
Alice 查看订单状态 + 物流追踪 P1
Bob (商家) 管理商品上下架 + 库存 P1
Bob 查看销售数据报表 P2
Charlie (跨境用户) 海淘商品 + 跨境支付 P2
Charlie 多语言切换(中/英/日) P1

4. 技术选型

(1) 技术栈决策

领域 选型 理由
UI 框架 Flutter 3.x (Material 3) 跨平台 + 自绘引擎
状态管理 Riverpod 2.x (@riverpod) 类型安全 + 代码生成
路由 GoRouter 声明式 + Web URL 支持
网络 Dio + 拦截器 拦截器 + Token 刷新
后端 Firebase (Auth+FS+Storage) 零运维 + 实时同步
缓存 Hive 轻量 NoSQL + 离线
加密存储 flutter_secure_storage Token 安全
序列化 json_serializable + freezed 类型安全 + 不可变
国际化 flutter_localizations + intl ARB 工作流
测试 flutter_test + mocktail 单元/Widget/集成
CI/CD GitHub Actions 免费额度 + 多矩阵

(2) Riverpod vs BLoC 对比

维度 Riverpod BLoC
学习曲线
样板代码 少 (@riverpod) 多 (Event/State/Bloc)
类型安全
测试 简单 简单
代码生成
适用场景 中小型项目 大型团队项目
💡 提示: ShopApp 选 Riverpod,因为代码量少、代码生成减少样板、学习曲线适中。


5. Clean Architecture 分层

100%
graph TD
    subgraph Presentation
        PAGE[Pages/Widgets]
        NOTI[Notifiers/Providers]
    end
    subgraph Domain
        ENT[Entities]
        REPO_I[Repository Interfaces]
        USECASE[Use Cases]
    end
    subgraph Data
        REPO_IMPL[Repository Impl]
        DS[Data Sources]
        DTO2[DTOs / Models]
    end
    PAGE --> NOTI
    NOTI --> USECASE
    USECASE --> REPO_I
    REPO_I -.-> REPO_IMPL
    REPO_IMPL --> DS
    DS --> DTO2
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(1) 分层职责

目录 职责 依赖方向
Presentation screens/, widgets/ UI + 用户交互 → Application
Application notifiers/, usecases/ 业务逻辑 + 状态 → Domain
Domain entities/, repositories/ 核心业务规则 无外部依赖
Data repositories/impl/, datasources/ 数据获取 + 持久化 → Domain

(2) 依赖规则


6. 数据建模

(1) 核心实体

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

:Domain Entity 定义

DART
// domain/entities/product.dart
// 纯 Dart 类,无外部依赖

class Product {
  final int id;
  final String name;
  final double price;
  final String imageUrl;
  final String category;
  final double rating;
  final int reviewCount;
  final int stock;
  final String currency;

  const Product({
    required this.id,
    required this.name,
    required this.price,
    required this.imageUrl,
    this.category = 'General',
    this.rating = 0.0,
    this.reviewCount = 0,
    this.stock = 0,
    this.currency = 'USD',
  });

  bool get inStock => stock > 0;
  bool get hasDiscount => false; // Extended in sale scenario
}

// domain/entities/cart_item.dart
class CartItem {
  final Product product;
  final int quantity;
  final String? selectedSize;
  final String? selectedColor;

  const CartItem({
    required this.product,
    required this.quantity,
    this.selectedSize,
    this.selectedColor,
  });

  double get lineTotal => product.price * quantity;

  CartItem copyWith({int? quantity, String? selectedSize, String? selectedColor}) =>
      CartItem(product: product, quantity: quantity ?? this.quantity,
        selectedSize: selectedSize ?? this.selectedSize,
        selectedColor: selectedColor ?? this.selectedColor);
}

// domain/entities/address.dart
class Address {
  final String name;
  final String street;
  final String city;
  final String state;
  final String zip;
  final String country;

  const Address({
    required this.name,
    required this.street,
    required this.city,
    required this.state,
    required this.zip,
    this.country = 'US',
  });

  String get fullAddress => '$street, $city, $state $zip, $country';

  Map<String, dynamic> toJson() => {
    'name': name, 'street': street, 'city': city,
    'state': state, 'zip': zip, 'country': country,
  };
}

// domain/entities/order.dart
enum OrderStatus { pending, confirmed, shipped, delivered, cancelled }

class Order {
  final String id;
  final String userId;
  final List<CartItem> items;
  final double total;
  final String currency;
  final OrderStatus status;
  final DateTime createdAt;
  final Address shippingAddress;

  const Order({
    required this.id,
    required this.userId,
    required this.items,
    required this.total,
    this.currency = 'USD',
    this.status = OrderStatus.pending,
    required this.createdAt,
    required this.shippingAddress,
  });
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

(2) Firestore Schema 设计

TEXT 📖 仅展示
firestore/
├── products/{productId}
│   ├── name: string
│   ├── price: number
│   ├── stock: number
│   ├── category: string
│   ├── rating: number
│   ├── imageUrl: string
│   └── currency: string
├── users/{userId}
│   ├── email: string
│   ├── name: string
│   ├── preferences: map
│   │   ├── theme: string
│   │   ├── currency: string
│   │   └── locale: string
│   └── addresses: array
├── orders/{orderId}
│   ├── userId: string
│   ├── items: array<{productId, quantity, price}>
│   ├── total: number
│   ├── currency: string
│   ├── status: string
│   └── createdAt: timestamp
└── reviews/{reviewId}
    ├── productId: string
    ├── userId: string
    ├── rating: number
    └── comment: string
```text


```text
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

7. UI/UX 规划

(1) 页面清单

页面 路由 核心组件
首页 / SliverAppBar + GridView + CategoryChips
搜索 /search SearchBar + FilterSheet + ProductList
商品详情 /product/:id Hero + SliverAppBar + BottomSheet
购物车 /cart CartList + QuantitySelector + TotalBar
结账 /checkout AddressForm + PaymentSelector + OrderSummary
订单确认 /order/:id OrderTimeline + TrackingMap
个人中心 /profile UserCard + OrderHistory + Settings
登录 /login EmailForm + GoogleButton + AppleButton

(2) 设计令牌

DART
import 'package:flutter/material.dart';

// 设计令牌:统一颜色/间距/圆角/字体常量,供 ThemeExtension 承载

class ShopDesignTokens {
  // Colors
  static const primary = Color(0xFF0066CC);
  static const secondary = Color(0xFFFF6B35);
  static const sale = Color(0xFF4CAF50);
  static const discount = Color(0xFFEF5350);

  // Spacing
  static const xs = 4.0;
  static const sm = 8.0;
  static const md = 16.0;
  static const lg = 24.0;
  static const xl = 32.0;

  // Radius
  static const cardRadius = 12.0;
  static const buttonRadius = 8.0;
  static const inputRadius = 8.0;

  // Typography
  static const headlineSize = 24.0;
  static const titleSize = 18.0;
  static const bodySize = 14.0;
  static const captionSize = 12.0;
}
TEXT 📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

8. 完整示例:项目目录结构

TEXT 📖 仅展示
shop_app/
├── lib/
│   ├── main.dart
│   ├── app.dart
│   ├── core/
│   │   ├── router/
│   │   │   └── app_router.dart
│   │   ├── network/
│   │   │   ├── dio_client.dart
│   │   │   └── api_exception.dart
│   │   ├── storage/
│   │   │   ├── secure_storage.dart
│   │   │   ├── hive_cache.dart
│   │   │   └── shared_prefs.dart
│   │   ├── theme/
│   │   │   ├── app_theme.dart
│   │   │   └── brand_tokens.dart
│   │   └── platform/
│   │       └── platform_helper.dart
│   ├── domain/
│   │   ├── entities/
│   │   │   ├── product.dart
│   │   │   ├── cart_item.dart
│   │   │   ├── order.dart
│   │   │   └── user.dart
│   │   └── repositories/
│   │       ├── product_repository.dart
│   │       ├── auth_repository.dart
│   │       └── order_repository.dart
│   ├── data/
│   │   ├── repositories/
│   │   │   ├── product_repository_impl.dart
│   │   │   ├── auth_repository_impl.dart
│   │   │   └── order_repository_impl.dart
│   │   ├── datasources/
│   │   │   ├── firestore_datasource.dart
│   │   │   └── hive_datasource.dart
│   │   └── models/
│   │       ├── product_dto.dart
│   │       └── order_dto.dart
│   ├── application/
│   │   ├── notifiers/
│   │   │   ├── auth_notifier.dart
│   │   │   ├── product_notifier.dart
│   │   │   └── cart_notifier.dart
│   │   └── providers/
│   │       └── infrastructure_providers.dart
│   ├── presentation/
│   │   ├── screens/
│   │   │   ├── home_page.dart
│   │   │   ├── detail_page.dart
│   │   │   ├── cart_page.dart
│   │   │   ├── checkout_page.dart
│   │   │   └── login_page.dart
│   │   └── widgets/
│   │       ├── product_card.dart
│   │       ├── quantity_selector.dart
│   │       ├── cart_badge.dart
│   │       └── price_tag.dart
│   └── l10n/
│       ├── app_en.arb
│       ├── app_zh.arb
│       └── app_ja.arb
├── test/
│   ├── domain/
│   ├── application/
│   └── presentation/
├── integration_test/
├── android/
├── ios/
├── web/
├── windows/
├── macos/
├── pubspec.yaml
├── l10n.yaml
└── .github/workflows/
```text


```text
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。

❓ 常见问题

Q Clean Architecture 每层都需要吗?
A 中小型项目 Application 和 Domain 可以合并为 Application 层。Data 层始终需要(数据源抽象)。
Q Entity 和 DTO 有什么区别?
A Entity 是领域概念(业务逻辑用),DTO 是数据传输对象(API/数据库用)。Entity 不依赖外部,DTO 可能包含 JSON 序列化注解。
Q Repository 接口放 Domain 层有意义吗?
A 有。这是依赖反转原则:Domain 定义接口,Data 层实现。这样 Domain 层不依赖 Firebase/Dio 等外部库。
Q 项目设计阶段要花多久?
A 中小型项目 1-2 周。大型项目 2-4 周。设计投入 20% 时间,节省 50% 开发时间。
Q Firestore Schema 怎么设计最优?
A 1) 按 1:1 或 1:N 设计文档;2) 避免深层嵌套;3) 读频繁的数据反范式化(冗余存名称);4) 写频繁的数据范式化。
Q 怎么保证架构设计落地?
A 1) 用 lint 规则强制依赖方向;2) Code Review 检查分层合规;3) 单元测试覆盖每层边界。

📖 小节


📝 作业

  1. 基础题(难度⭐):为 ShopApp 编写 10 条用户故事,按 P0/P1/P2 优先级分类。
  2. 进阶题(难度⭐⭐):设计 Clean Architecture 四层目录结构,定义 Product/Cart/Order 三个 Entity 和对应 Repository 接口。
  3. 挑战题(难度⭐⭐⭐):完成完整的项目设计文档:用户故事 + 技术选型对比表 + Clean Architecture 分层图 + Firestore Schema + 页面清单 + 设计令牌。

← 上一课 | 下一课 →

(2) 示例:ShopApp 模块依赖图

DART
final Map<String, List<String>> moduleDeps = {
  'core': [],
  'cart': ['core'],
  'checkout': ['core', 'cart'],
  'profile': ['core'],
  'catalog': ['core'],
};

输出:

TEXT 📖 仅展示
core: 0 直接依赖
cart: 依赖 core
checkout: 依赖 core, cart
profile: 依赖 core
catalog: 依赖 core

(3) 示例:路由表

DART
final Map<String, String> routes = {
  '/': 'CatalogPage',
  '/cart': 'CartPage',
  '/checkout': 'CheckoutPage',
  '/profile': 'ProfilePage',
};

输出:

TEXT 📖 仅展示
/         -> CatalogPage
/cart     -> CartPage
/checkout -> CheckoutPage
/profile  -> ProfilePage
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏