Flutter: Project Design — ShopApp Architecture Planning

Draw the blueprint before building — architecture design determines where the "load-bearing walls" of your code are.

📋 Prerequisites: You should have mastered the following first

1. What You Will Learn


2. A Story of a Project Without Architecture

(1) The Pain: Vague Requirements + Chaotic Architecture

When Bob received the ShopApp project, the only requirement was: "Build a cross-border e-commerce app." He started coding immediately, and 3 months later discovered: user stories were unclear (merchant management features were missing), data models kept changing (Order was rewritten 3 times), and architecture was heavily coupled (changing payment affected the product list). The project was delayed by 2 months.

(2) Design First, Code Later

The project design phase clarifies requirements, architecture, data models, and UI planning upfront — coding then becomes "building from the blueprint."

(3) The Result: Coding Time Halved + Change Cost Reduced by 80%

After spending 2 weeks on design, Bob completed coding in 4 weeks (instead of the previous 3+2 months), and requirement changes only required modifying the corresponding layer without affecting other modules.


3. Requirements Analysis

(1) User Stories

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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.
Role User Story Priority
Alice (Consumer) Browse million-level product list P0
Alice Search/filter/browse by category P0
Alice View product details + reviews P0
Alice Add to cart + manage quantities P0
Alice USD/CNY/JPY multi-currency settlement P1
Alice View order status + logistics tracking P1
Bob (Merchant) Manage product listing/delisting + inventory P1
Bob View sales data reports P2
Charlie (Cross-border User) Cross-border shopping + international payment P2
Charlie Multi-language switching (EN/ZH/JA) P1

4. Technology Selection

(1) Technology Stack Decisions

Domain Selection Rationale
UI Framework Flutter 3.x (Material 3) Cross-platform + custom paint engine
State Management Riverpod 2.x (@riverpod) Type-safe + code generation
Routing GoRouter Declarative + Web URL support
Networking Dio + Interceptors Interceptors + Token refresh
Backend Firebase (Auth+FS+Storage) Zero ops + real-time sync
Cache Hive Lightweight NoSQL + offline
Encrypted Storage flutter_secure_storage Secure token storage
Serialization json_serializable + freezed Type-safe + immutable
Internationalization flutter_localizations + intl ARB workflow
Testing flutter_test + mocktail Unit/Widget/Integration
CI/CD GitHub Actions Free tier + multi-matrix

(2) Riverpod vs BLoC Comparison

Dimension Riverpod BLoC
Learning curve Medium High
Boilerplate Less (@riverpod) More (Event/State/Bloc)
Type safety Strong Strong
Testing Simple Simple
Code generation
Best for Small-to-medium projects Large team projects
💡 Tip: ShopApp chose Riverpod because it has less code, code generation reduces boilerplate, and the learning curve is moderate.


5. Clean Architecture Layering

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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

(1) Layer Responsibilities

Layer Directory Responsibility Dependency Direction
Presentation screens/, widgets/ UI + user interaction → Application
Application notifiers/, usecases/ Business logic + state → Domain
Domain entities/, repositories/ Core business rules No external dependencies
Data repositories/impl/, datasources/ Data fetching + persistence → Domain

(2) Dependency Rules


6. Data Modeling

(1) Core Entities

▶ Example

TEXT
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

: Domain Entity definitions

DART
// domain/entities/product.dart
// Pure Dart class, no external dependencies

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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

(2) Firestore Schema Design

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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

7. UI/UX Planning

(1) Page Inventory

Page Route Core Components
Home / SliverAppBar + GridView + CategoryChips
Search /search SearchBar + FilterSheet + ProductList
Product Detail /product/:id Hero + SliverAppBar + BottomSheet
Cart /cart CartList + QuantitySelector + TotalBar
Checkout /checkout AddressForm + PaymentSelector + OrderSummary
Order Confirmation /order/:id OrderTimeline + TrackingMap
Profile /profile UserCard + OrderHistory + Settings
Login /login EmailForm + GoogleButton + AppleButton

(2) Design Tokens

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

// Design tokens: unified color/spacing/radius/font constants, carried by 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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

8. Complete Example: Project Directory Structure

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
> Output: Run on a local Flutter SDK (Flutter 3.x / Dart 3.x). The Piston server does not have Flutter installed; please use `flutter run` on your local machine for comparison. Actual UI/state may vary slightly by platform.

❓ FAQ

Q Do I need every layer of Clean Architecture?
A For small-to-medium projects, the Application and Domain layers can be merged into a single Application layer. The Data layer is always needed (data source abstraction).
Q What's the difference between Entity and DTO?
A An Entity is a domain concept (used in business logic); a DTO is a data transfer object (used for API/database). Entities don't depend on externals; DTOs may include JSON serialization annotations.
Q Is it meaningful to put Repository interfaces in the Domain layer?
A Yes. This is the Dependency Inversion Principle: Domain defines the interface, Data layer implements it. This way the Domain layer doesn't depend on external libraries like Firebase/Dio.
Q How long should the project design phase take?
A Small-to-medium projects: 1-2 weeks. Large projects: 2-4 weeks. Investing 20% of time in design saves 50% of development time.
Q How should I optimally design a Firestore Schema?
A 1) Design documents by 1:1 or 1:N relationships; 2) Avoid deep nesting; 3) Denormalize frequently-read data (redundantly store names); 4) Normalize frequently-written data.
Q How do I ensure the architecture design is actually followed?
A 1) Use lint rules to enforce dependency direction; 2) Code Review checks layering compliance; 3) Unit tests cover each layer boundary.

📖 Summary


📝 Exercises

  1. Basic (Difficulty ⭐): Write 10 user stories for ShopApp, categorized by P0/P1/P2 priority.
  2. Intermediate (Difficulty ⭐⭐): Design a Clean Architecture four-layer directory structure, defining Product/Cart/Order entities and corresponding Repository interfaces.
  3. Challenge (Difficulty ⭐⭐⭐): Complete a full project design document: user stories + technology selection comparison table + Clean Architecture layering diagram + Firestore Schema + page inventory + design tokens.

← Previous Lesson | Next Lesson →

(2) Example: ShopApp Module Dependency Graph

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

Output:

TEXT
core: 0 direct dependencies
cart: depends on core
checkout: depends on core, cart
profile: depends on core
catalog: depends on core

(3) Example: Route Table

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

Output:

TEXT
/         -> CatalogPage
/cart     -> CartPage
/checkout -> CheckoutPage
/profile  -> ProfilePage
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

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

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