普通视图

发现新文章,点击刷新页面。
昨天以前首页

Flutter Clean 架构中的 Riverpod 在哪里

作者 tangzzzfan
2025年6月25日 08:49

本篇文章与你深入探讨在 Flutter 中如何运用 Clean Architecture,并结合 Riverpod、Go_Router 和 Retrofit 这套现代化工具栈,打造一个高内聚、低耦合、可扩展、易维护的顶级应用架构。

我们将从“道”的层面(高屋建瓴的架构哲学)入手,再深入到“术”的层面(具体分层、组件和痛点解决方案),并最终通过一个实例来将理论落地。


第一部分:高屋建瓴的架构统领 (The "Why" and "How" at 30,000 Feet)

在引入任何具体技术之前,我们必须先确立架构的核心指导思想。对于 Clean Architecture + Riverpod + Go_Router + Retrofit 这套组合拳,其核心思想是:

“通过依赖倒置(Dependency Inversion),实现业务逻辑与外部世界的彻底解耦,并利用现代化的状态管理和路由框架,优雅地将它们粘合起来。”

这个思想可以拆解为三个关键原则:

  1. 同心圆法则 (The Dependency Rule): 这是 Clean Architecture 的灵魂。内层(Domain)绝对不能依赖外层(Infrastructure, Presentation)。所有依赖关系都指向内部。这意味着,你的核心业务逻辑(比如商品折扣如何计算)不应该知道它是被一个 Flutter App 使用,数据是来自一个 REST API,还是存储在 Hive 数据库里。

  2. 职责分离原则 (Separation of Concerns):

    • Clean Architecture 负责 “结构” 的分离:它定义了代码应该放在哪里(Domain, Application, Presentation, Infrastructure)。
    • Riverpod 负责 “状态与依赖” 的分离:它作为依赖注入(DI)容器和服务定位器(SL),将各层实现“注入”到需要它们的地方;同时,它作为状态管理(SM)框架,将 UI 状态与 UI 渲染分离。
    • Go_Router 负责 “UI 与导航” 的分离:它将页面导航逻辑从 UI 组件的调用中抽离出来,变为中心化的、基于路由地址的声明式导航,极大降低了页面间的耦合。
    • Retrofit/Dio 负责 “业务与数据获取” 的分离:它将 HTTP 请求的构造和解析细节封装起来,让我们的数据层实现更专注于“获取什么数据”,而不是“如何获取”。
  3. 面向接口编程 (Programming to an Interface): 这是实现依赖倒置的具体手段。应用层(Application)和基础设施层(Infrastructure)都依赖于领域层(Domain)中定义的抽象接口(Repository Interfaces),而不是具体的实现。这使得我们可以轻松地替换数据来源(比如从网络 API切换到 Mock 数据或本地数据库)而无需修改任何业务逻辑或 UI 代码。

一言以蔽之:我们的目标是构建一个稳定的“业务核心”(Domain + Application),外部的变化(UI框架升级、数据库更换、API变更)不会轻易撼动这个核心。


第二部分:深入阐述各关键点与痛点分析

现在,我们深入到具体的“术”。

2.1 Clean 架构分层详解

一个典型的 Flutter Clean Architecture 项目结构如下:

  • Domain Layer (领域层):

    • 职责: 包含最核心、最纯粹的业务逻辑和业务实体,完全独立于任何框架。
    • 包含内容:
      • Entities (实体): 代表业务对象的类,如 User, Product, Order。它们可以包含只依赖自身属性的业务逻辑(例如,一个 Order 实体可以有一个 isCompleted() 的 getter 方法)。
      • Repositories (仓库接口): 抽象接口,定义了获取和操作业务实体的数据契约。例如,abstract class ProductRepository { Future<Product> getProductById(String id); }。它只定义“做什么”,不定义“怎么做”。
      • Value Objects (值对象):Email, Password 等,用于封装验证逻辑,保证数据的有效性。
    • 特点: 无任何 Flutter/Dart 外部库依赖(除了可能的 equatable 等辅助库)。这是最稳定、最可移植的一层。
  • Application Layer (应用层 / Use Cases):

    • 职责: 编排和调度 Domain 层和 Infrastructure 层,执行具体的应用功能。它代表了“用户想要做什么”。
    • 包含内容:
      • Use Cases (或称 Interactors): 每个 Use Case 代表一个单一的应用功能点。例如 GetProductDetailsUseCase, AddToCartUseCase。它们会调用一个或多个 Repository 接口来完成任务。
    • 特点: 依赖于 Domain 层。它不知道 UI,也不知道数据来自网络还是本地。它只负责协调。例如,AddToCartUseCaseexecute 方法可能会先调用 ProductRepository 获取商品信息,再调用 UserRepository 检查用户资格,最后调用 CartRepository 将商品加入购物车。
  • Presentation Layer (表现层):

    • 职责: 显示 UI,响应用户交互,并将用户行为传递给 Application 层。
    • 包含内容:
      • Widgets/Pages (UI): Flutter 的 StatelessWidgetConsumerWidget。它们应该尽可能“笨”,只负责根据状态渲染 UI 和转发用户事件。
      • State Management (Providers/ViewModels): Riverpod Providers 就在这里扮演核心角色。通常我们会为每个页面或复杂组件创建一个 StateNotifierProviderAsyncNotifierProvider,它扮演着 ViewModel/Controller 的角色。这个 Provider 会调用相应的 Use Case,处理返回结果(成功、失败、加载中),并管理 UI 所需的状态。
      • Navigation (Go_Router): 路由配置和导航逻辑。
  • Infrastructure Layer (基础设施层):

    • 职责: 实现 Domain 层定义的接口,处理所有与外部世界的交互。
    • 包含内容:
      • Repository Implementations: 对 Domain 层 Repository 接口的具体实现。例如,ProductRepositoryImpl 会实现 ProductRepository 接口,其内部会调用一个或多个数据源。
      • Data Sources:
        • Remote Data Source: 使用 RetrofitDio 来访问网络 API。
        • Local Data Source: 使用 Hive, Isar, shared_preferences 等进行本地数据持久化。
      • Other Services: 封装其他平台相关的功能,如设备信息、权限管理、推送通知、分析服务等。

2.2 Riverpod 的角色与层次归属

这是一个常见困惑点。Riverpod 不属于任何单一层,而是贯穿各层的“粘合剂”和“电力系统”

  • 在 Presentation 层:

    • UI 组件 (ConsumerWidget) 通过 ref.watch 来订阅 Provider 暴露的 UI 状态,实现响应式刷新。
    • 用户事件(如按钮点击)通过 ref.readref.notifier 来调用 Provider 中的方法,从而触发业务逻辑。
  • 作为依赖注入 (DI) 容器:

    • 核心作用:main.dart 或应用启动时,我们通过 ProviderScopeoverride 来“组装”我们的应用。
    • 示例:
      // Domain Layer (product_repository.dart)
      abstract class ProductRepository { ... }
      
      // Infrastructure Layer (product_repository_impl.dart)
      class ProductRepositoryImpl implements ProductRepository { ... }
      
      // Application Layer (get_product_usecase.dart)
      class GetProductDetailsUseCase {
        final ProductRepository _repo;
        GetProductDetailsUseCase(this._repo);
        ...
      }
      
      // Riverpod Providers (providers.dart)
      // 1. 提供基础设施层的具体实现
      final productRepositoryProvider = Provider<ProductRepository>((ref) {
        // 在这里可以根据环境返回 Mock 或真实实现
        return ProductRepositoryImpl(ref.watch(dioProvider));
      });
      
      // 2. 提供应用层的 UseCase,并自动注入依赖
      final getProductDetailsUseCaseProvider = Provider<GetProductDetailsUseCase>((ref) {
        // Riverpod 自动处理依赖关系!
        final repository = ref.watch(productRepositoryProvider);
        return GetProductDetailsUseCase(repository);
      });
      
      // 3. 提供表现层的 StateNotifier/ViewModel
      final productDetailsViewModelProvider = StateNotifierProvider.family<...>((ref, productId) {
        // ViewModel 可以访问 UseCase
        final useCase = ref.watch(getProductDetailsUseCaseProvider);
        return ProductDetailsViewModel(useCase, productId);
      });
      
    • 通过这种方式,ProductDetailsViewModel 依赖 GetProductDetailsUseCase,后者又依赖 ProductRepository 接口。但在运行时,Riverpod 悄悄地将 ProductRepositoryImpl 这个具体实现注入了进来,完美实现了依赖倒置。

2.3 项目组件划分:以“四级存储”为例

在实际项目中,一个功能(比如获取用户信息)可能涉及多级数据源。我们可以设计一个经典的四级存储策略:

  1. 一级缓存 (Memory Cache):

    • 实现: Riverpod Provider 本身。使用 AsyncNotifierProvider.autoDispose 或普通的 AsyncNotifierProvider 配合 keepAlive 就可以实现。数据存在内存中,速度最快,但随 Provider 销毁而失效。
    • 场景: 页面内或短时间内的重复数据请求。
  2. 二级缓存 (Local Persistence Cache):

    • 实现: 在 Repository 实现中引入本地数据源(如 Hive/Isar)。
    • 场景: 跨应用会话的数据缓存,如用户配置、不常变动的列表数据。启动应用时可以先从这里加载,给用户即时反馈。
  3. 三级存储 (Network):

    • 实现: 远程数据源,通过 Retrofit 调用 API。
    • 场景: 数据的最终来源(Source of Truth)。
  4. 四级存储 (Bundled/Pre-populated Data):

    • 实现: 打包在 App assets 里的 JSON 或数据库文件。
    • 场景: 初始配置、地区列表、默认数据等。

RepositoryImpl 中的逻辑流可能是这样的:

Future<User> getUser(String userId) async {
  // 1. 尝试从内存缓存获取 (由 Riverpod 的 Provider 缓存策略管理)
  // Riverpod 自身就处理了这部分,如果 provider 还在,就不会重新执行 fetch 逻辑

  // 2. 尝试从本地数据库获取
  final localUser = await _localDataSource.getUser(userId);
  if (localUser != null && !isStale(localUser.timestamp)) {
    return localUser;
  }

  // 3. 从网络获取
  try {
    final remoteUser = await _remoteDataSource.fetchUser(userId);
    // 成功后,更新本地数据库
    await _localDataSource.saveUser(remoteUser);
    return remoteUser;
  } catch (e) {
    // 网络失败,如果本地有旧数据,也可以考虑返回
    if (localUser != null) return localUser;
    // 实在没有,就抛出异常
    throw e;
  }
}

第三部分:痛点识别与解决方案

这是架构实践中最具挑战性的部分。

痛点1:如何区分业务逻辑?(Domain Logic vs. Application Logic)

  • 困惑: 一段逻辑,是应该放在 Entity 里,还是放在 Use Case 里?
  • 解决方案与心法:
    • 问自己一个问题:“这个逻辑是否具有普适性,并且只依赖于实体自身的数据?”

      • 是 -> 放入 Domain Entity。 例如:Order 实体有一个 totalPrice 属性,一个 calculateTotalPrice() 方法根据其 lineItems 列表计算总价。这个计算逻辑是 Order 固有的,不依赖外部服务。
      • 否 -> 放入 Application Use Case。 例如:ApplyCouponToOrderUseCase。这个逻辑需要:1. 获取 Order;2. 获取 Coupon 信息(可能要调 CouponRepository);3. 验证优惠券是否适用于该订单和用户(可能要调 UserRepository);4. 最后计算出新的价格。这个过程是在协调多个实体和仓库,它是一个应用级别的操作,因此属于 Use Case。
    • 简单法则:

      • Domain Logic: 规则和计算。
      • Application Logic: 流程和编排。

痛点2:如何划分模块?(Monolith vs. Feature-based Modules)

  • 困惑: 项目变大后,lib 文件夹变得臃肿不堪。所有功能的代码混在一起,commonshared 文件夹成为“垃圾场”。
  • 解决方案:功能驱动的垂直切分 (Vertical Slicing by Feature)
    • 放弃按层级划分顶级目录 (data, domain, presentation)。 这种方式在小项目里还行,大项目里会导致你为了修改一个功能,不得不在三个相距甚远的文件夹里跳来跳去。
    • 拥抱按功能划分模块。 这是现代大型应用架构的趋势。

第四部分:以一个“可扩展的电商App”为例进行实战演练

项目背景: 一个电商 App,初期只有商品浏览、购物车、用户中心。未来需要快速迭代,加入直播带货、社区分享等新功能。

目标: 设计一个能够支撑这种演进的架构。

项目结构 (采用功能驱动模块化):

flutter_ecommerce_app/
├── lib/
│   ├── **features/**                   # 核心:按功能划分的模块
│   │   ├── **auth/**                   # 认证模块
│   │   │   ├── presentation/
│   │   │   │   ├── screens/login_screen.dart
│   │   │   │   └── providers/auth_providers.dart
│   │   │   ├── application/
│   │   │   │   └── usecases/login_usecase.dart
│   │   │   ├── domain/
│   │   │   │   ├── entities/auth_token.dart
│   │   │   │   └── repositories/auth_repository.dart (interface)
│   │   │   └── infrastructure/
│   │   │       ├── datasources/auth_remote_datasource.dart
│   │   │       └── repositories/auth_repository_impl.dart
│   │   ├── **products/**               # 商品模块 (列表、详情)
│   │   │   └── ... (同样遵循 presentation/app/domain/infra 结构)
│   │   ├── **cart/**                   # 购物车模块
│   │   │   └── ...
│   │   └── **profile/**                # 用户中心模块
│   │       └── ...
│   │
│   ├── **core/**                     # 跨功能共享的核心代码
│   │   ├── **domain/**                 # 共享的 Domain (e.g., User, AppError)
│   │   │   └── entities/user_entity.dart
│   │   ├── **ui/**                     # 共享的 UI 组件 (e.g., PrimaryButton, LoadingIndicator)
│   │   ├── **network/**                # 共享的网络配置 (Dio instance, interceptors)
│   │   │   └── dio_client.dart
│   │   ├── **navigation/**             # 共享的导航配置
│   │   │   └── app_router.dart (Go_Router 配置)
│   │   ├── **storage/**                # 共享的存储封装 (e.g., Hive helper)
│   │   └── **utils/**                  # 共享的工具类 (e.g., formatters, validators)
│   │
│   ├── **main.dart**                   # 应用入口,组装 ProviderScope 和 GoRouter
│   └── **injection_container.dart**    # (可选) 集中管理所有顶层 Provider 的声明
│
└── pubspec.yaml

这个结构的优势:

  1. 高内聚: auth 相关的所有代码都在 features/auth 目录下,从 UI 到数据源一目了然。
  2. 低耦合:
    • products 模块不直接依赖 cart 模块。如果需要交互(比如“添加到购物车”),products 模块的 AddToCartUseCase 会调用 CartRepository 接口。这个接口的实现在 cart 模块中,但依赖关系是 products -> cart/domain,而不是 products -> cart/infrastructure,耦合度很低。
    • 可移除性: 如果老板说“我们不要购物车功能了”,理论上你可以直接删除 features/cart 文件夹,修复一下路由和调用点,应用主体依然能运行。
  3. 可扩展性:
    • 当需要加入“直播带货”功能时,只需新建一个 features/live_streaming 模块,按照同样的结构进行开发。
    • 新模块可以复用 core 里的所有组件,如 core/network 的 Dio 实例,core/ui 的按钮等。
  4. 团队协作: 不同的团队可以并行开发不同的 feature 模块,冲突仅限于 corepubspec.yaml,大大提高了开发效率。

Go_Router 在此结构中的作用:core/navigation/app_router.dart 中,你会定义所有路由:

final GoRouter router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => ProductsListScreen(), // from products feature
    ),
    GoRoute(
      path: '/product/:id',
      builder: (context, state) => ProductDetailsScreen(id: state.pathParameters['id']!), // from products feature
    ),
    GoRoute(
      path: '/login',
      builder: (context, state) => LoginScreen(), // from auth feature
    ),
    GoRoute(
      path: '/cart',
      builder: (context, state) => CartScreen(), // from cart feature
    ),
  ],
);

这样,ProductsListScreen 只需调用 context.go('/product/123'),而无需知道 ProductDetailsScreen 的存在,实现了导航解耦。

总结

作为架构师,我们的工作不是选择最“时髦”的技术,而是构建一个能够应对未来不确定性的、有弹性的系统。

  • Clean Architecture 提供了抵御变化的“防火墙”。
  • 功能模块化 提供了应对业务增长的“扩展坞”。
  • Riverpod 提供了连接一切的、灵活高效的“能源和通信网络”。
  • Go_RouterRetrofit 则是这个网络中负责特定任务(导航、网络)的、可替换的“标准化插件”。

下一篇, 将在此基础上, 讲解如何开发 User 模块.

Flutter Clean 架构下的用户画像系统与各模块的协同工作

作者 tangzzzfan
2025年6月25日 09:14

第一部分:[交互设计] 用户画像系统与各模块的协同工作

用户画像模块不是一个孤岛,它是一个数据枢纽,与其他模块进行着双向的数据交换。其交互的核心原则是:"单向依赖,面向接口"。即,具体功能模块(如 Onboarding)可以依赖 user_profile 模块的 applicationdomain 层,但反之不行。

1. 与 Onboarding 模块的交互

  • 目的: 收集用户初始的、显式的偏好 (UserPreferences)。这是用户画像的冷启动数据。
  • 数据流向: Onboarding (UI) -> UpdateUserPreferencesUseCase (user_profile/application) -> UserProfileRepository (user_profile/domain)。
  • 架构实现:
    1. Onboarding 屏幕是一个 ConsumerWidget,包含一系列让用户选择兴趣标签的 UI。
    2. 当用户点击“完成”按钮时,Onboarding 屏幕的 Provider (或直接在 onPressed 回调中) 会执行以下操作:
      // In Onboarding screen's logic
      void onComplete(WidgetRef ref) {
        // 1. 从 UI 状态中收集用户的选择
        final selectedCategories = ref.read(selectedCategoriesProvider);
        final userPreferences = UserPreferences(likedCategories: selectedCategories, ...);
      
        // 2. 调用 user_profile 模块的 UseCase
        //    注意:Onboarding 模块只知道 UseCase 的存在,不知道其内部实现
        ref.read(updateUserPreferencesUseCaseProvider).execute(userPreferences);
        
        // 3. 导航到主页
        context.go('/home');
      }
      
    3. updateUserPreferencesUseCaseProvider 是在 user_profile 模块中定义的 Provider。Onboarding 模块通过 pubspec.yaml 依赖 user_profile 模块来访问它。

2. 与 Auth 模块的交互

  • 目的: 关联用户身份。UserProfile 必须与一个唯一的 userId 绑定。
  • 数据流向: Auth 模块在登录/注册成功后,会产生一个 userId。其他模块(包括 user_profile)需要能够获取到这个 userId,以便在进行数据操作时进行关联。
  • 架构实现:
    1. Auth 模块的核心产出是一个 AuthProvider,它管理着用户的认证状态(未登录、已登录、加载中)和认证信息(如 AuthToken,其中包含 userId)。
      // In auth/providers
      final authStateProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) => ...);
      
      // AuthState could be a sealed class
      // sealed class AuthState {}
      // class Authenticated extends AuthState { final AuthToken token; }
      // class Unauthenticated extends AuthState {}
      
    2. UserProfileRepositoryImpl 在执行任何操作前,都需要获取当前的 userId。它会通过 ref 来读取 authStateProvider
      // In user_profile/infrastructure/repositories/user_profile_repository_impl.dart
      class UserProfileRepositoryImpl implements UserProfileRepository {
        final Ref _ref;
        // ...其他依赖...
      
        UserProfileRepositoryImpl(this._ref, ...);
      
        String _getCurrentUserId() {
          final authState = _ref.read(authStateProvider);
          if (authState is Authenticated) {
            return authState.token.userId;
          }
          throw AuthException('User not authenticated');
        }
      
        @override
        Future<UserProfile> getUserProfile() async {
          final userId = _getCurrentUserId();
          // ...后续所有本地或远程操作都带上 userId
          return _localDataSource.getProfile(userId);
        }
        // ...其他方法同样处理
      }
      
    3. 关键点: user_profile 模块依赖 auth 模块的状态输出 (authStateProvider),而不是其内部实现。这是一种松耦合的依赖关系。

3. 与 个人中心 (Profile Display) 模块的交互

  • 目的: 展示用户画像数据,并提供修改入口。
  • 数据流向: GetUserProfileUseCase (user_profile/application) -> 个人中心 (UI)。同时,用户在个人中心修改偏好时,流向与 Onboarding 类似。
  • 架构实现:
    1. 个人中心 屏幕的 ViewModel (或 StateNotifierProvider) 会调用 GetUserProfileUseCase 来获取 UserProfile 对象。
      // In profile_display/providers
      @riverpod
      class ProfileViewModel extends _$ProfileViewModel {
        @override
        Future<UserProfile> build() {
          // 调用 UseCase 获取数据
          return ref.watch(getUserProfileUseCaseProvider).execute();
        }
      
        // 提供修改入口,例如修改昵称或偏好
        Future<void> updatePreferences(UserPreferences newPrefs) async {
          // ... 更新状态为 loading
          await ref.read(updateUserPreferencesUseCaseProvider).execute(newPrefs);
          // ... 刷新页面数据
          ref.invalidateSelf();
        }
      }
      
    2. UI (ConsumerWidget) watch 这个 ProfileViewModelProvider,并根据其 AsyncValue (loading, data, error) 来构建界面。

第二部分:[分级存储] 基于数据敏感性的安全设计

这是一个至关重要的部分,直接关系到用户隐私和应用安全。我们不能将所有数据都以相同的方式存储在同一个地方。必须设计一个分级的本地存储策略。

核心思想:Infrastructure 层的 DataSource 拆分为多个,每个 DataSource 负责一个安全级别的数据,并使用不同的存储技术。Repository 层作为外观(Façade),负责整合这些 DataSource,对上层(Application 层)屏蔽这些复杂性。

数据敏感性分级

级别 级别名称 数据例子 存储技术建议 特点
L3 (最高) 高度敏感数据 (Highly Sensitive) accessToken, refreshToken, 第三方平台 token flutter_secure_storage (使用 Keychain/Keystore) 系统级加密,App卸载后数据清除。仅在需要时读入内存。
L2 (中等) 个人身份信息 (PII - Personally Identifiable Info) 用户名, 邮箱, 手机号, 用户自己填写的地址 Hive / Isar 加密盒子 (Encrypted Box) App级别加密,性能较高,适合结构化数据。密钥存储在 L3。
L1 (较低) 推断与行为数据 (Inferred & Behavioral) inferredTopCategories, engagementScore, 点击/浏览记录 Hive / Isar 普通盒子 (Regular Box) 无需加密或轻量级加密。追求读写性能,数据量可能很大。
L0 (无敏感) 应用配置 (App Config) isDarkMode, language shared_preferences 简单键值对,性能高,无安全要求。

架构实现

  1. 拆分 DataSource 接口和实现:

    // core/storage/
    // L3
    abstract class SecureAuthDataSource { Future<void> saveToken(AuthToken token); ... }
    class SecureAuthDataSourceImpl implements SecureAuthDataSource { ... } // 使用 flutter_secure_storage
    
    // L2
    abstract class EncryptedPiiDataSource { Future<void> saveUserInfo(UserInfo info); ... }
    class EncryptedPiiDataSourceImpl implements EncryptedPiiDataSource { ... } // 使用加密的 Hive Box
    
    // L1
    abstract class BehavioralDataSource { Future<void> logBehavior(UserBehaviorEvent event); ... }
    class BehavioralDataSourceImpl implements BehavioralDataSource { ... } // 使用普通的 Hive Box
    

    注意:这些 DataSource 可以分散在各自的 feature 模块中,例如 SecureAuthDataSourceauth 模块的 infrastructure 里,而另外两个在 user_profile 模块里。

  2. 重构 UserProfileRepositoryImpl 作为数据整合者:

    // In user_profile/infrastructure/repositories/user_profile_repository_impl.dart
    class UserProfileRepositoryImpl implements UserProfileRepository {
      final Ref _ref;
      // 注入多个不同级别的 DataSource
      final EncryptedPiiDataSource _piiDataSource;
      final BehavioralDataSource _behavioralDataSource;
      final ProfileGeneratorService _profileGenerator;
    
      UserProfileRepositoryImpl(this._ref, this._piiDataSource, this._behavioralDataSource, this._profileGenerator);
      
      String _getCurrentUserId() { /* ... */ }
    
      @override
      Future<UserProfile> getUserProfile() async {
        final userId = _getCurrentUserId();
    
        // 1. 并行从不同安全级别的数据源获取数据
        final results = await Future.wait([
            _piiDataSource.getUserInfo(userId),
            _behavioralDataSource.getRecentBehaviors(userId),
            _piiDataSource.getPreferences(userId) // 假设偏好也存在 L2
        ]);
        
        final userInfo = results[0] as UserInfo;
        final behaviors = results[1] as List<UserBehaviorEvent>;
        final preferences = results[2] as UserPreferences;
    
        // 2. 将原始数据喂给画像生成服务 (ML 或规则引擎)
        final profile = await _profileGenerator.generateProfile(
          preferences: preferences,
          recentBehaviors: behaviors,
          // ...可能还需要 userInfo
        );
        
        // 3. 组合最终返回给 UI 的对象 (可能需要合并 userInfo 和 profile)
        //    对上层屏蔽了数据来自多个源头的复杂性
        return profile.copyWith(
            // 如果 UserProfile 实体也包含 PII 信息
            userName: userInfo.name,
            email: userInfo.email
        );
      }
    
      @override
      Future<void> updateUserPreferences(UserPreferences preferences) async {
        final userId = _getCurrentUserId();
        // 存储到 L2 加密数据源
        await _piiDataSource.savePreferences(userId, preferences);
      }
    
      @override
      Future<void> logBehavior(UserBehaviorEvent event) async {
        final userId = _getCurrentUserId();
        // 存储到 L1 普通数据源
        await _behavioralDataSource.logBehavior(userId, event);
      }
    }
    
  3. 在 Riverpod 中组装依赖:

    // providers.dart
    // L3
    final secureAuthDataSourceProvider = Provider((ref) => SecureAuthDataSourceImpl());
    // L2
    final encryptedPiiDataSourceProvider = Provider((ref) => EncryptedPiiDataSourceImpl(/* hive encryption key */));
    // L1
    final behavioralDataSourceProvider = Provider((ref) => BehavioralDataSourceImpl());
    
    final userProfileRepositoryProvider = Provider<UserProfileRepository>((ref) {
      return UserProfileRepositoryImpl(
        ref,
        ref.watch(encryptedPiiDataSourceProvider),
        ref.watch(behavioralDataSourceProvider),
        ref.watch(profileGeneratorProvider)
      );
    });
    

总结

通过这样的设计,我们构建了一个既健壮又安全的系统:

  1. 交互清晰: 各模块职责单一,通过 UseCase 和 Provider 进行松耦合交互,数据流向明确。
  2. 安全分级: 敏感数据(Token、PII)和非敏感数据(行为、推断)被物理隔离在不同的存储介质和加密等级中,极大地增强了应用的安全性,并为满足 GDPR、数据安全法等隐私法规打下了坚实基础。
  3. 封装良好: UserProfileRepository 完美地扮演了外观模式的角色,它将底层复杂的多源、分级存储细节完全封装起来,对 Application 层和 Presentation 层只暴露一个统一、干净的 UserProfileRepository 接口。这使得上层业务逻辑可以完全不关心数据到底是怎么存的,从而保持了 Clean Architecture 的核心优势。

下一篇文章会给出 Flutter 开发中常见模块划分范例。

Flutter Clean 架构下的用户画像系统的设计与实现

作者 tangzzzfan
2025年6月25日 09:08

第一部分:[高屋建瓴] Mobile User Profile 系统的战略重要性

在现代 App 中,“用户画像”系统不再仅仅是存储用户信息的数据库条目,它是一个动态的、可演进的、智能的资产。其战略重要性体M现于:

  1. 个性化体验的基石: 它是实现“千人千面”的根本。无论是推荐系统(商品、内容)、UI 动态调整(展示用户偏好的模块)、还是营销推送,都依赖于一个准确、实时的用户画像。

  2. 提升用户粘性和留存: 一个“懂你”的 App 能极大地提升用户满意度和使用时长。当用户感觉 App 是为他量身定做时,其迁移成本会显著增高。

  3. 数据驱动决策的引擎: 聚合的用户画像数据可以揭示用户群体的行为模式,为产品迭代、运营策略提供精准的数据支持。

  4. 未来智能化的入口: 它是端侧机器学习的“燃料”。在本地处理用户数据进行模型训练和推理,可以实现:

    • 更高响应速度: 无需网络请求,实时反馈。
    • 更强隐私保护: 敏感数据不出设备,符合越来越严格的隐私法规。
    • 离线可用性: 在无网络环境下依然可以提供部分智能服务。

因此,我们在架构设计之初就必须将其视为一个一级功能模块,而不是“个人中心”或“设置”的附属品。


第二部分:[痛点辨析] 概念边界的划分:Auth vs. Settings vs. Preferences vs. Profile

这是最容易混淆,也是导致后续架构腐化的重灾区。必须在设计之初就用“快刀斩乱麻”的方式清晰界定。

概念 职责 (Responsibility) 数据例子 所属模块 特点
认证信息 (Auth Info) “你是谁?” - 用于验证用户身份和授权访问。 userId, accessToken, refreshToken, loginMethod (手机/微信) features/auth 高敏感性,安全性要求最高,数据量小,相对稳定。
应用设置 (App Settings) “App 该如何为你工作?” - 用户对 App 功能行为的控制。 isDarkMode, notificationEnabled, language, fontSize features/settings (或 core/settings) 功能性,非个人化,与用户兴趣无关,通常是布尔/枚举值。
用户偏好 (User Preferences) “你告诉我们你喜欢什么?” - 用户显式提供的主观兴趣和选择。 喜欢的分类: [科幻, 悬疑],不感兴趣的标签: [体育],价格敏感度: features/user_profile (作为输入) 主观性,是用户画像的原始输入数据之一,用户可直接修改。
用户画像 (User Profile/Portrait) “我们认为你是怎样的人?” - 系统通过分析显式偏好隐式行为后,得出的推断性、结构化的用户模型。 inferredPersona: "价格敏感的科幻迷", engagementScore: 0.85, topCategories: ["科幻", "数码"], nextBestOffer: "XX 耳机" features/user_profile (作为核心产出) 推断性、动态演进,是算法和规则的产物,通常不直接对用户暴露全部内容,是所有个性化服务的直接数据源

核心痛点解决方案:

  • 物理隔离: 在项目结构上,authuser_profile 必须是两个独立的 feature 模块。它们之间可以通过 userId 关联,但数据模型和仓库(Repository)完全分离。
  • 逻辑分离: User PreferencesUser Profile 的一部分,是其输入。UserProfile 实体则包含了 Preferences 以及更多推断出的数据。

第三部分:[架构设计] "用户画像" 模块的落地

基于我们之前的项目结构,我们新增一个 features/user_profile 模块。

flutter_ecommerce_app/
├── lib/
│   ├── features/
│   │   ├── auth/
│   │   ├── products/
│   │   ├── ...
│   │   └── **user_profile/**             # << 新增用户画像模块
│   │       ├── presentation/
│   │       │   ├── screens/onboarding_screen.dart   # 收集偏好
│   │       │   ├── screens/profile_display_screen.dart # “个人中心”的一部分
│   │       │   └── providers/user_profile_providers.dart
│   │       ├── application/
│   │       │   ├── usecases/get_user_profile_usecase.dart
│   │       │   ├── usecases/update_user_preferences_usecase.dart
│   │       │   └── usecases/track_user_behavior_usecase.dart # 用于记录隐式行为
│   │       ├── domain/
│   │       │   ├── entities/user_preferences.dart
│   │       │   ├── entities/user_profile.dart       # << 核心实体
│   │       │   ├── entities/user_behavior_event.dart
│   │       │   └── repositories/user_profile_repository.dart (interface)
│   │       └── infrastructure/
│   │           ├── datasources/profile_local_datasource.dart
│   │           ├── datasources/profile_remote_datasource.dart
│   │           ├── repositories/user_profile_repository_impl.dart
│   │           └── **services/ml_profile_generator.dart** # << 为 Core ML 预留的钩子
│   ├── core/
│   └── main.dart

Domain 层设计 (稳定内核):

// domain/entities/user_preferences.dart
class UserPreferences extends Equatable {
  final List<String> likedCategories;
  final List<String> dislikedTags;
  // ... 其他显式偏好
}

// domain/entities/user_profile.dart
class UserProfile extends Equatable {
  final UserPreferences preferences; // 包含显式偏好
  final List<String> inferredTopCategories; // 推断出的 Top 分类
  final String personaTag; // 推断出的画像标签,如 "TechEnthusiast"
  final double engagementScore; // 用户活跃度得分
  // ... 其他推断性数据
}

// domain/repositories/user_profile_repository.dart
abstract class UserProfileRepository {
  // 获取最终整合后的用户画像
  Future<UserProfile> getUserProfile();
  
  // 更新用户显式偏好
  Future<void> updateUserPreferences(UserPreferences preferences);
  
  // 记录用户隐式行为(如点击、浏览、购买)
  Future<void> logBehavior(UserBehaviorEvent event);
}

Application 层设计 (业务流程):

  • GetUserProfileUseCase: 调用 UserProfileRepository.getUserProfile(),供个人中心页面或推荐系统使用。
  • UpdateUserPreferencesUseCase: 在 Onboarding 或设置页调用,参数是 UserPreferences,内部调用 UserProfileRepository.updateUserPreferences()
  • TrackUserBehaviorUseCase: 这是关键。App 内的各种用户行为(点击商品、观看视频)都会调用这个 UseCase,它会调用 UserProfileRepository.logBehavior(),将行为数据记录下来。

第四部分:[核心难点] 为 Core ML 预留接口

这正是 Clean Architecture 发挥威力的地方。ML 模型是实现细节,属于基础设施层。

设计思路:

  1. 数据收集: UserProfileRepositoryImpl 负责从各个数据源收集原始数据。
  2. 模型调用: 将收集到的原始数据喂给一个“画像生成服务”。
  3. 结果存储: 将服务返回的 UserProfile 缓存并提供给上层。

Infrastructure 层实现:

1. 定义一个抽象的“画像生成服务”接口:

// infrastructure/services/ml_profile_generator.dart

// 这是未来 CoreML 实现需要遵循的契约
abstract class ProfileGeneratorService {
  Future<UserProfile> generateProfile({
    required UserPreferences currentPreferences,
    required List<UserBehaviorEvent> recentBehaviors,
  });
}

2. 实现 Repository,并注入这个服务:

// infrastructure/repositories/user_profile_repository_impl.dart
class UserProfileRepositoryImpl implements UserProfileRepository {
  final ProfileLocalDataSource _localDataSource;
  final ProfileGeneratorService _profileGenerator; // << 注入 ML 服务

  UserProfileRepositoryImpl(this._localDataSource, this._profileGenerator);

  @override
  Future<void> logBehavior(UserBehaviorEvent event) async {
    // 将行为事件存储到本地 (e.g., a separate Hive box or SQLite table)
    await _localDataSource.saveBehavior(event);
  }
  
  @override
  Future<void> updateUserPreferences(UserPreferences preferences) async {
    await _localDataSource.savePreferences(preferences);
  }

  @override
  Future<UserProfile> getUserProfile() async {
    // 1. 检查是否有有效的本地缓存画像
    final cachedProfile = await _localDataSource.getProfile();
    if (cachedProfile != null && !isStale(cachedProfile.timestamp)) {
      return cachedProfile;
    }

    // 2. 如果没有或已过期,则重新生成
    //    a. 从本地数据源获取原始数据
    final preferences = await _localDataSource.getPreferences();
    final behaviors = await _localDataSource.getRecentBehaviors(limit: 100);

    //    b. 调用 ML 服务进行计算和推断
    //       这里就是与 Core ML 交互的“口子”
    final newProfile = await _profileGenerator.generateProfile(
      currentPreferences: preferences,
      recentBehaviors: behaviors,
    );

    //    c. 缓存新生成的画像
    await _localDataSource.saveProfile(newProfile);

    return newProfile;
  }
}

3. 如何为 Core ML 预留口子?

  • 当前阶段: 我们可以提供一个基于规则的 RuleBasedProfileGenerator 实现,它不依赖 ML,只是简单地根据行为计数等来生成画像。
    class RuleBasedProfileGenerator implements ProfileGeneratorService {
      @override
      Future<UserProfile> generateProfile(...) {
        // 简单的逻辑:浏览次数最多的分类就是 Top 分类
        // ...
        return Future.value(UserProfile(...));
      }
    }
    
  • 未来阶段 (引入 Core ML): 你只需要创建一个新的实现 CoreMLProfileGenerator
    class CoreMLProfileGenerator implements ProfileGeneratorService {
      // 内部会通过 platform channels 调用原生的 Core ML SDK
      final MethodChannel _channel = MethodChannel('com.yourapp/coreml');
    
      @override
      Future<UserProfile> generateProfile({ ... }) async {
        // 1. 将 preferences 和 behaviors 序列化为 ML 模型需要的格式 (e.g., JSON or Map)
        final inputData = serializeForML(preferences, behaviors);
        
        // 2. 通过平台通道调用原生 Core ML 模型进行推理
        final resultJson = await _channel.invokeMethod('predictUserProfile', inputData);
        
        // 3. 将返回的 JSON 解析为 UserProfile 实体
        return UserProfile.fromJson(resultJson);
      }
    }
    

最后,在 Riverpod 的 Provider 中切换实现即可,对 App 的其他部分完全透明!

// providers.dart
final profileGeneratorProvider = Provider<ProfileGeneratorService>((ref) {
  // 在这里决定使用哪个实现
  // 可以通过环境变量、A/B Test 等方式动态切换
  if (featureFlags.isCoreMLEnabled) {
    return CoreMLProfileGenerator();
  } else {
    return RuleBasedProfileGenerator();
  }
});

final userProfileRepositoryProvider = Provider<UserProfileRepository>((ref) {
  return UserProfileRepositoryImpl(
    ref.watch(profileLocalDataSourceProvider),
    ref.watch(profileGeneratorProvider), // << 动态注入
  );
});

总结

通过上述设计,我们实现了:

  1. 概念清晰: 明确划分了认证、设置、偏好和画像的边界,避免了职责混乱。
  2. 结构合理: 将用户画像作为一个独立的、高内聚的功能模块,易于维护和扩展。
  3. 拥抱变化: 通过在 Domain 层定义稳定的 UserProfileRepository 接口,将复杂的画像生成逻辑(无论是基于规则还是 ML)封装在 Infrastructure 层。
  4. 无缝升级: 为端侧 ML 预留了清晰的、非侵入式的“挂载点” (ProfileGeneratorService)。未来引入 Core ML 时,只需添加一个新的实现类并修改一行 Provider 的构造代码,而无需触动任何业务逻辑(Use Cases)或 UI 代码,这正是顶级架构弹性的完美体现。

下一篇, 讲述这个用户画像系统如何与其他模块进行交互。

❌
❌