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

350 阅读6分钟

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

用户画像模块不是一个孤岛,它是一个数据枢纽,与其他模块进行着双向的数据交换。其交互的核心原则是:"单向依赖,面向接口"。即,具体功能模块(如 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, 第三方平台 tokenflutter_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, languageshared_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 开发中常见模块划分范例。