欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.netOpenHarmony

Flutter 状态管理方案 Riverpod 的鸿蒙化适配实践

引言

在 Flutter 跨平台应用开发领域,状态管理始终是架构设计的核心议题。从早期的 BLoC 模式到 Redux 思想在 Flutter 中的应用,再到 Provider、GetX、Riverpod 等方案的出现,Flutter 开发者面对的状态管理选择日益丰富。然而,当 OpenHarmony 平台逐渐走入开发者的视野时,一个关键问题浮出水面:现有的状态管理方案能否平滑迁移到这一新兴平台?本文将详细介绍如何将 Flutter 生态中备受推崇的状态管理库 Riverpod 适配到 OpenHarmony 平台,重点阐述 AsyncNotifierProvider 在鸿蒙网络请求场景中的实际应用,以及与现有 Provider 代码的渐进式迁移策略。

Riverpod 作为 Provider 的进阶方案,由 Dart 社区知名开发者 Remi Rousselet 设计,其设计理念融合了函数式编程与依赖注入的最佳实践。Riverpod 的核心优势在于:编译时类型安全、声明式的 Provider 组合、与 BuildContext 完全解耦的架构设计、强大的异步状态管理能力。这些特性使得 Riverpod 特别适合构建复杂的企业级应用。对于面向 OpenHarmony 平台的 Flutter 应用而言,Riverpod 的纯 Dart 实现特性使其具备天然的跨平台兼容性优势,无需任何平台特定适配即可在鸿蒙设备上运行。

本文将按照技术选型、方案设计、实现细节、测试验证的逻辑展开,帮助读者全面理解 Riverpod 在 OpenHarmony 平台上的适配过程。读者不仅能够了解适配的技术细节,还能掌握迁移的最佳实践,为自己的项目迁移工作提供参考。

一、需求分析与技术选型

1.1 现有架构的审视与痛点识别

在本次适配工作开展之前,项目采用 Provider 作为主要的状态管理方案。Provider 在 Flutter 生态中的普及度很高,其简单易用的特点降低了状态管理的入门门槛。项目中的 Provider 使用主要分布在四个业务模块:帖子列表管理(PostProvider)、应用设置管理(SettingsProvider)、待办事项管理(TodoProvider)以及消息通知管理(MessageProvider)。

以 PostProvider 为例,其核心职责包括:帖子列表的加载与缓存、用户筛选功能的实现、加载状态与错误信息的维护。以下是原有 Provider 的实现结构:

class PostProvider with ChangeNotifier {
  final PostService _postService = PostService();

  List<Post> _posts = [];
  bool _isLoading = false;
  String? _errorMessage;
  int _selectedPostId = 0;

  List<Post> get posts => _posts;
  bool get isLoading => _isLoading;
  String? get errorMessage => _errorMessage;
  int get selectedPostId => _selectedPostId;

  Future<void> loadPosts({int limit = 20}) async {
    setLoading(true);
    _errorMessage = null;

    try {
      final posts = await _postService.getPosts(limit: limit);
      _posts = posts.map((p) => p.copyWithImage(
        Post.generateImageUrl(p.id),
        Post.generateAvatarUrl(p.userId),
      )).toList();
    } catch (e) {
      _errorMessage = e.toString();
    } finally {
      setLoading(false);
    }
  }

  void setLoading(bool value) {
    _isLoading = value;
    notifyListeners();
  }

  void clearPosts() {
    _posts = [];
    notifyListeners();
  }
}

这种实现模式存在几个值得改进的地方。首先,isLoading、errorMessage 等状态字段需要在每个 Provider 中重复定义,增加了代码冗余。其次,ChangeNotifier 的 build 方法无法执行异步操作,这意味着初始化逻辑必须放在页面的 initState 中处理,增加了页面代码的复杂度。再次,由于 Provider 依赖运行时类型检查,类型错误只能在运行阶段被发现,影响开发效率。最后,Provider 的测试需要构建完整的 Widget 树,无法进行纯粹的单元测试。

1.2 Riverpod 核心特性与技术优势

Riverpod 相比 Provider 在多个维度实现了显著提升,这些提升对于构建可维护、可测试的应用至关重要。

编译时类型安全是 Riverpod 最重要的改进之一。在 Riverpod 中,每个 Provider 都有明确的类型声明,IDE 可以在编译阶段提供准确的代码补全和错误提示。当开发者尝试将错误类型的值赋给 Provider 时,编译器会立即报错,而非等到运行时才暴露问题。这种设计大幅缩短了问题发现周期,提升了开发效率。在大型团队协作中,这种编译时检查可以有效避免因类型误用导致的线上问题。

声明式依赖注入是 Riverpod 的另一核心优势。在 Riverpod 中,一个 Provider 可以声明对其他 Provider 的依赖,这种依赖关系以声明式的方式表达,而非传统的构造函数注入。Riverpod 会自动处理依赖的初始化顺序,确保每个 Provider 在被使用时其依赖项已经准备就绪。这种设计使得服务的管理与替换变得异常灵活,特别适合需要注入 mock 服务的测试场景。

// 服务 Provider
final postServiceProvider = Provider<PostService>((ref) {
  return PostService();
});

// 使用服务的 Notifier
class PostNotifier extends Notifier<List<Post>> {
  late final PostService _service;

  
  List<Post> build() {
    _service = ref.watch(postServiceProvider);
    return [];
  }

  Future<void> loadPosts() async {
    final posts = await _service.getPosts();
    state = posts;
  }
}

与 BuildContext 完全解耦是 Riverpod 架构设计的关键决策。在 Provider 方案中,Provider 的访问需要通过 BuildContext,这导致测试代码必须构建完整的 Widget 树才能访问 Provider。Riverpod 提供了独立的 ProviderContainer,允许在完全脱离 Widget 环境的情况下测试 Provider 逻辑。这种设计使得单元测试的编写变得简单可靠。

// Riverpod 测试示例
test('PostNotifier should load posts', () async {
  final container = ProviderContainer();

  addTearDown(container.dispose);

  final notifier = container.read(postProvider.notifier);
  await notifier.loadPosts();

  final state = container.read(postProvider);
  expect(state.posts, isNotEmpty);
});

AsyncNotifierProvider 对异步操作的原生支持是本文关注的重点。移动应用中最常见的异步操作是网络请求。传统的处理方式需要在状态类中维护 isLoading、errorMessage 等字段,逻辑分散且容易出错。Riverpod 提供的 AsyncNotifierProvider 通过 AsyncValue 包装机制,实现了异步状态的标准化管理,使得加载中、成功、错误三种状态的切换变得优雅简洁。

1.3 适配 OpenHarmony 的可行性论证

Riverpod 完全基于 Dart 语言实现,不包含任何平台特定代码或原生插件依赖。这一特性决定了 Riverpod 在理论层面对所有支持 Flutter 的平台都具有兼容性,包括 OpenHarmony。Flutter 引擎负责将 Dart 代码编译为各平台的原生指令,而 Riverpod 的所有逻辑都运行在 Dart 层,与平台特定的实现完全无关。

项目的适配验证工作重点关注两个方面:核心状态管理功能的稳定性,以及 AsyncNotifierProvider 在鸿蒙网络请求场景中的表现。稳定性验证关注点包括:状态更新的实时性、内存泄漏的可能性、异常恢复能力等。异步数据流验证关注点包括:网络请求的发起与响应、加载状态的正确展示、错误处理的完整性等。

经过实际项目验证,Riverpod 在 OpenHarmony Flutter 引擎上的表现与 Android 平台基本一致。核心功能包括状态监听、异步处理、Provider 派生等均工作正常,未发现任何平台相关的兼容性问题。以下将详细阐述具体的适配实现过程与验证结果。

二、渐进式迁移方案的设计与实现

2.1 迁移策略的制定

考虑到项目已具备一定规模,状态管理方案的切换需要遵循渐进式原则,避免对现有功能造成冲击。本次迁移采用"并行共存、逐步替换"的策略,具体原则如下:

第一,新旧代码并行存在。 原有的 Provider 代码保留不动,新编写的 Riverpod 代码与之并行存在。这种设计确保了迁移过程完全可控,任何时候都可以回滚到原有的 Provider 方案。在团队协作中,这种策略也降低了沟通成本,不需要所有开发者同时完成迁移。

第二,按模块逐步迁移。 业务模块按优先级排序,逐个完成迁移。每完成一个模块的迁移都进行充分的功能验证,确保迁移质量。建议从相对简单的模块开始,如 Settings 模块,逐步过渡到复杂的模块,如 Post 模块。

第三,文件命名保持一致。 为每个业务模块创建对应的 Riverpod 实现文件时,保持文件命名与原有 Provider 一致,仅在文件名后缀或目录结构上区分。这种设计便于后续代码维护,降低了团队成员的学习成本。

第四,统一导出管理。 在 providers.dart 统一导出文件中同时包含新旧两种实现,页面代码可以根据实际情况灵活选择使用哪种方案。这种设计支持平滑过渡,避免一次性全面替换带来的风险。

具体的目录结构规划如下:

lib/providers/
├── post_provider.dart         # 原有 Provider(保留)
├── post_riverpod.dart        # Riverpod 实现(新增)
├── settings_provider.dart     # 原有 Provider(保留)
├── settings_riverpod.dart    # Riverpod 实现(新增)
├── todo_provider.dart         # 原有 Provider(保留)
├── todo_riverpod.dart        # Riverpod 实现(新增)
├── message_provider.dart      # 原有 Provider(保留)
├── message_riverpod.dart     # Riverpod 实现(新增)
└── providers.dart              # 统一导出

2.2 状态类的规范化设计

在 Riverpod 架构中,状态类负责数据的结构化存储,类似于传统架构中的 Model 层。一个设计良好的状态类应当具备以下特征:不可变属性(使用 final 修饰)、完整的拷贝方法(copyWith)、合理的默认值设定。状态类的设计借鉴了函数式编程中不可变数据结构的思想。

以下是我们设计的 PostState 状态类:

class PostState {
  final List<Post> posts;
  final bool isLoading;
  final String? errorMessage;
  final int selectedPostId;

  const PostState({
    this.posts = const [],
    this.isLoading = false,
    this.errorMessage,
    this.selectedPostId = 0,
  });

  PostState copyWith({
    List<Post>? posts,
    bool? isLoading,
    String? errorMessage,
    int? selectedPostId,
  }) {
    return PostState(
      posts: posts ?? this.posts,
      isLoading: isLoading ?? this.isLoading,
      errorMessage: errorMessage,
      selectedPostId: selectedPostId ?? this.selectedPostId,
    );
  }
}

这种设计模式的关键优势在于不可变性。每次状态变更都生成新的状态对象,而非在原对象上修改。这种设计带来的好处是多方面的:状态变更可追溯、状态历史可回放、状态比较更简单、并发场景更安全。对于需要实现撤销/重做功能的场景,不可变状态的设计可以大大简化实现复杂度。

copyWith 方法的实现采用命名参数模式,每个参数都有默认值(当前值),调用者只需指定需要变更的字段即可。这种模式既保证了类型安全,又提供了良好的使用体验。与直接修改属性相比,copyWith 方法的优势在于:调用点明确展示了哪些字段发生了变化,便于代码审查。

2.3 Notifier 类的实现规范

Notifier 是 Riverpod 中处理业务逻辑的核心类,负责状态的读取、修改与业务方法的实现。一个标准的 Notifier 需要继承自 Notifier,其中 T 是对应的状态类型。以下是 PostNotifier 的完整实现:

class PostNotifier extends Notifier<PostState> {
  
  PostState build() => const PostState();

  final PostService _postService = PostService();

  Future<void> loadPosts({int limit = 20}) async {
    state = state.copyWith(isLoading: true, errorMessage: null);

    try {
      final posts = await _postService.getPosts(limit: limit);
      final enhancedPosts = posts.map((p) => p.copyWithImage(
        Post.generateImageUrl(p.id),
        Post.generateAvatarUrl(p.userId),
      )).toList();
      state = state.copyWith(posts: enhancedPosts, isLoading: false);
    } catch (e) {
      state = state.copyWith(errorMessage: e.toString(), isLoading: false);
    }
  }

  Future<void> loadPostsByUser(int userId) async {
    state = state.copyWith(isLoading: true, errorMessage: null);

    try {
      final posts = await _postService.getPostsByUser(userId);
      final enhancedPosts = posts.map((p) => p.copyWithImage(
        Post.generateImageUrl(p.id),
        Post.generateAvatarUrl(p.userId),
      )).toList();
      state = state.copyWith(posts: enhancedPosts, isLoading: false);
    } catch (e) {
      state = state.copyWith(errorMessage: e.toString(), isLoading: false);
    }
  }

  Future<Post?> loadPostById(int id) async {
    state = state.copyWith(selectedPostId: id);
    try {
      return await _postService.getPostById(id);
    } catch (e) {
      state = state.copyWith(errorMessage: e.toString());
      return null;
    }
  }

  void clearPosts() {
    state = state.copyWith(posts: []);
  }

  void clearError() {
    state = PostState(
      posts: state.posts,
      isLoading: state.isLoading,
      errorMessage: null,
      selectedPostId: state.selectedPostId,
    );
  }
}

Notifier 类的实现有几个关键要点需要把握。第一,build 方法返回初始状态,该方法仅在 Provider 首次被访问时执行一次,用于初始化 Notifier 实例。与 ChangeNotifier 不同,build 方法可以返回需要复杂初始化的状态对象。第二,业务方法的实现中通过修改 state 属性触发状态更新,Riverpod 会自动进行必要的 UI 刷新。第三,将服务实例化放在 Notifier 内部而非 build 方法中,可以避免不必要的重复初始化。

2.4 Provider 的声明与派生

Provider 是 Riverpod 暴露给外部访问的入口点,其声明方式直接影响使用体验。以下是基础 Provider 的声明方式:

final postProvider = NotifierProvider<PostNotifier, PostState>(() {
  return PostNotifier();
});

这种声明方式的优势在于类型推导清晰。PostNotifier 明确了业务逻辑的处理类,PostState 明确了管理的数据类型,外部使用时 IDE 可以准确提供代码补全。结合 Dart 的空安全特性,类型错误在编译阶段即可被发现。

除了基础 Provider,我们还设计了多个派生 Provider 用于满足不同的业务场景:

/// 选中帖子 ID 的简单状态 Provider
final selectedPostIdProvider = StateProvider<int>((ref) => 0);

/// 派生 Provider - 基于 postProvider 派生的只读数据
final postCountProvider = Provider<int>((ref) {
  return ref.watch(postProvider).posts.length;
});

/// 派生 Provider - 筛选后的帖子列表
final filteredPostsProvider = Provider<List<Post>>((ref) {
  final postState = ref.watch(postProvider);
  if (postState.selectedPostId == 0) {
    return postState.posts;
  }
  return postState.posts
      .where((p) => p.userId == postState.selectedPostId)
      .toList();
});

/// 派生 Provider - 帖子是否为空
final postsEmptyProvider = Provider<bool>((ref) {
  return ref.watch(postProvider).posts.isEmpty;
});

派生 Provider 的设计遵循了单一职责原则,每个 Provider 只负责一个特定的计算或派生逻辑。使用方无需关心计算过程,只需直接使用计算结果。这种模式借鉴了 Redux 中 selector 的设计思想,有助于保持业务逻辑的内聚性。当计算逻辑发生变化时,只需修改对应的 Provider 定义,使用方无需任何改动。

三、AsyncNotifierProvider 异步数据流实践

3.1 异步状态管理的必要性分析

移动应用中,网络请求是最常见的异步操作来源。用户下拉刷新获取最新数据、上拉加载更多分页数据、搜索功能的实时查询,这些场景都涉及网络请求。与本地状态变更不同,网络请求存在不确定性:请求可能成功返回数据、可能因网络问题超时失败、可能因服务器异常返回错误。传统的处理方式需要在状态类中维护 isLoading、errorMessage 等字段,逻辑分散且容易出错。

考虑一个典型的网络请求场景:用户进入帖子列表页面,应用需要从服务器获取帖子数据。在请求发起时,应显示加载指示器;在请求进行中,应禁用刷新操作防止重复请求;在请求完成时,应根据结果更新界面;在请求失败时,应展示错误信息并提供重试选项。传统的实现方式需要在状态类中维护 isLoading、isRefreshing、errorMessage、lastRequestTime 等多个字段,业务逻辑与状态管理混杂在一起。

Riverpod 提供的 AsyncNotifierProvider 专门针对这种场景进行了优化。通过 AsyncValue 包装机制,可以优雅地处理加载中、成功、错误三种状态的切换。AsyncValue 是 Riverpod 封装的一个特殊类型,它可以处于以下三种状态之一:AsyncLoading(加载中)、AsyncData(成功数据)、AsyncError(错误信息)。

3.2 AsyncNotifierProvider 的实现

以下是 AsyncNotifierProvider 的完整实现示例:

class PostAsyncNotifier extends AsyncNotifier<List<Post>> {
  final PostService _postService = PostService();

  
  Future<List<Post>> build() async {
    return await _fetchPosts(limit: 20);
  }

  Future<List<Post>> _fetchPosts({int limit = 20}) async {
    final posts = await _postService.getPosts(limit: limit);
    return posts.map((p) => p.copyWithImage(
      Post.generateImageUrl(p.id),
      Post.generateAvatarUrl(p.userId),
    )).toList();
  }

  Future<void> refresh({int limit = 20}) async {
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() => _fetchPosts(limit: limit));
  }

  Future<void> loadMore({int limit = 20}) async {
    final currentPosts = state.valueOrNull ?? [];
    final newPosts = await _fetchPosts(limit: limit);
    state = AsyncValue.data([...currentPosts, ...newPosts]);
  }
}

final postAsyncProvider = AsyncNotifierProvider<PostAsyncNotifier, List<Post>>(() {
  return PostAsyncNotifier();
});

AsyncNotifier 与普通 Notifier 的区别在于状态类型。AsyncNotifier 的状态类型是 Future 而非 T,其中 T 是最终返回的数据类型。build 方法返回 Future,Riverpod 会自动处理异步状态的转换。当 build 方法返回的 Future 处于 pending 状态时,AsyncValue 处于 AsyncLoading 状态;当 Future 成功完成时,AsyncValue 转换为 AsyncData 状态并携带数据;当 Future 抛出异常时,AsyncValue 转换为 AsyncError 状态并携带错误信息。

refresh 方法展示了手动触发刷新的模式。使用 AsyncValue.guard 可以自动捕获异常并转换为 AsyncError 状态,简化了错误处理逻辑。loadMore 方法展示了列表追加数据的模式,通过 valueOrNull 获取当前数据,在其后追加新数据。

3.3 AsyncValue 在 UI 层的使用模式

AsyncValue 提供了丰富的 API 用于状态判断与数据提取。在 Widget 层使用时,需要配合特定的模式来优雅处理各种状态。以下是 PostListView 的实现示例:

class PostListView extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncPosts = ref.watch(postAsyncProvider);

    return asyncPosts.when(
      data: (posts) => _PostListContent(posts: posts),
      loading: () => const _LoadingView(),
      error: (error, stack) => _ErrorView(
        error: error,
        onRetry: () => ref.refresh(postAsyncProvider),
      ),
    );
  }
}

class _PostListContent extends StatelessWidget {
  final List<Post> posts;

  const _PostListContent({required this.posts});

  
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: posts.length,
      itemBuilder: (context, index) => PostCard(post: posts[index]),
    );
  }
}

class _LoadingView extends StatelessWidget {
  const _LoadingView();

  
  Widget build(BuildContext context) {
    return const Center(
      child: CircularProgressIndicator(),
    );
  }
}

class _ErrorView extends StatelessWidget {
  final Object error;
  final VoidCallback onRetry;

  const _ErrorView({required this.error, required this.onRetry});

  
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Icon(Icons.error_outline, size: 64, color: Colors.red),
          const SizedBox(height: 16),
          Text(
            '加载失败',
            style: Theme.of(context).textTheme.titleLarge,
          ),
          const SizedBox(height: 8),
          Text(
            error.toString(),
            style: Theme.of(context).textTheme.bodyMedium?.copyWith(
              color: Colors.grey,
            ),
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 24),
          ElevatedButton.icon(
            onPressed: onRetry,
            icon: const Icon(Icons.refresh),
            label: const Text('重试'),
          ),
        ],
      ),
    );
  }
}

when 方法是 AsyncValue 最重要的 API,它根据当前状态自动分发到对应的处理分支。这种模式的优势在于:代码结构清晰、状态覆盖完整、无需手动进行空值判断。在实际开发中,还可以使用 whenData 方法只处理成功状态,loading 和 error 状态可以委托给全局的错误处理机制。

3.4 鸿蒙网络请求场景测试验证

针对 OpenHarmony 平台的特殊性,我们设计了全面的测试用例来验证 AsyncNotifierProvider 在鸿蒙环境中的表现。测试覆盖了以下几个关键场景:

基础网络请求稳定性测试。 在连续多次发起网络请求的场景下,AsyncNotifierProvider 能够正确维护请求状态,未出现状态混乱或内存泄漏问题。每次状态变更都正确触发 UI 重建,加载指示器与内容区域的切换流畅自然。

错误状态正确性测试。 当网络请求失败时(模拟网络断开、服务器超时、返回错误码等场景),AsyncError 状态能够正确携带错误信息,通过 when 方法的 error 分支进行展示。用户可以看到清晰的错误提示,并可以点击重试按钮重新发起请求。

刷新功能完整性测试。 通过 ref.refresh 方法触发的数据重新加载,UI 能够正确显示加载状态遮罩,随后更新数据列表。刷新过程中列表内容保持不变,刷新完成后内容平滑过渡,未出现闪烁或内容跳动问题。

分页加载更多测试。 在支持分页加载的场景中,通过 loadMore 方法追加新数据时,列表能够正确追加项目,未出现列表闪烁、数据丢失或重复添加问题。加载更多完成后,UI 能够正确显示新的数据总量。

状态持久化测试。 在应用切后台再切回前台的场景下,AsyncNotifierProvider 能够正确恢复状态,无需重新加载数据。这种能力对于用户体验的提升非常重要,可以避免用户在切换应用后需要重新等待数据加载。

四、应用入口的 Riverpod 集成

4.1 ProviderScope 的配置方法

将 Riverpod 集成到 Flutter 应用中,需要在应用根节点包裹 ProviderScope 组件。ProviderScope 是 Riverpod 的上下文容器,负责 Provider 的初始化与销毁。以下是 main.dart 的完整改造示例:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'providers/providers.dart';
import 'routing/router.dart';
import 'utils/theme_utils.dart';

final _appRouter = createAppRouter();

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(
    const ProviderScope(
      child: OpenHarmonyApp(),
    ),
  );
}

class OpenHarmonyApp extends ConsumerWidget {
  const OpenHarmonyApp({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final settings = ref.watch(settingsProvider);
    final themeMode = settings.flutterThemeMode;

    return MaterialApp.router(
      title: 'OpenHarmony App',
      debugShowCheckedModeBanner: false,
      theme: AppTheme.lightTheme,
      darkTheme: AppTheme.darkTheme,
      themeMode: themeMode,
      routerConfig: _appRouter,
    );
  }
}

ProviderScope 的使用非常简单,只需将整个应用包裹在其中即可。无需传递任何参数,Riverpod 会使用默认配置初始化所有 Provider。如果需要自定义 Provider 的作用域(如在测试环境中使用 mock Provider),可以在 ProviderScope 中通过 providers 参数传入自定义的 Provider 覆盖。

我们将 settingsProvider 的初始化逻辑放在了 SettingsNotifier 的 build 方法中,通过 FutureProvider 控制初始化时机。这种设计确保了设置数据的加载不会阻塞应用启动,提升了用户体验。以下是相关实现:

class SettingsNotifier extends Notifier<SettingsState> {
  
  SettingsState build() {
    Future.microtask(() => loadSettings());
    return const SettingsState();
  }

  Future<void> loadSettings() async {
    state = state.copyWith(isLoading: true);
    await _storageService.init();
    state = SettingsState(
      themeMode: _storageService.getThemeMode(),
      userName: _storageService.getUserName(),
      userSignature: _storageService.getUserSignature(),
      isDarkMode: _storageService.getThemeMode() == 'dark',
      isLoading: false,
    );
  }
}

使用 Future.microtask 的目的是将初始化延迟到当前事件循环结束之后执行,确保 UI 能够先完成首次渲染。这种模式在需要异步初始化的场景中非常有用,既保证了初始状态的返回,又不会阻塞 UI 渲染。

4.2 ConsumerWidget 的改造模式

在页面层使用 Riverpod,需要将原有的 StatelessWidget 或 StatefulWidget 改造为 ConsumerWidget。以下是 DiscoverPage 的完整改造示例,展示了从 Provider 迁移到 Riverpod 的具体步骤:

class DiscoverPage extends ConsumerStatefulWidget {
  const DiscoverPage({super.key});

  
  ConsumerState<DiscoverPage> createState() => _DiscoverPageState();
}

class _DiscoverPageState extends ConsumerState<DiscoverPage> {
  final RefreshController _refreshController = RefreshController(
    initialRefresh: false,
  );
  late TabController _tabController;
  late AnimationController _bannerController;

  
  void initState() {
    super.initState();
    _tabController = TabController(length: _tabs.length, vsync: this);
    _bannerController = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 3),
    )..repeat();

    WidgetsBinding.instance.addPostFrameCallback((_) {
      ref.read(postProvider.notifier).loadPosts(limit: 30);
    });
  }

  
  void dispose() {
    _tabController.dispose();
    _bannerController.dispose();
    _refreshController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    final postState = ref.watch(postProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('发现'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () => ref.read(postProvider.notifier).loadPosts(limit: 30),
          ),
        ],
      ),
      body: Column(
        children: [
          _buildFeaturedBanner(postState),
          Expanded(child: _buildPostList(postState)),
        ],
      ),
    );
  }

  Widget _buildFeaturedBanner(PostState postState) {
    if (postState.posts.isEmpty) {
      return const SizedBox.shrink();
    }

    return Container(
      height: 200,
      margin: const EdgeInsets.all(16),
      child: PageView.builder(
        itemCount: postState.posts.take(5).length,
        itemBuilder: (context, index) {
          return _buildFeaturedCard(postState.posts[index]);
        },
      ),
    );
  }

  Widget _buildPostList(PostState postState) {
    if (postState.isLoading) {
      return const Center(child: CircularProgressIndicator());
    }

    if (postState.errorMessage != null) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('错误: ${postState.errorMessage}'),
            ElevatedButton(
              onPressed: () => ref.read(postProvider.notifier).loadPosts(limit: 30),
              child: const Text('重试'),
            ),
          ],
        ),
      );
    }

    return ListView.builder(
      itemCount: postState.posts.length,
      itemBuilder: (context, index) => PostCard(post: postState.posts[index]),
    );
  }
}

改造过程中的关键变化需要注意。第一,Widget 类型从 StatefulWidget 改为 ConsumerStatefulWidget,State 类型从 State 改为 ConsumerState。第二,build 方法增加 WidgetRef 参数,通过 ref.watch 监听状态变化,通过 ref.read 调用业务方法。第三,状态对象从 Provider 类变更为独立的 State 类,属性访问方式保持不变(如 postState.posts)。

五、Settings、Todo、Message 模块的迁移实现

5.1 Settings 模块的迁移

Settings 模块负责管理应用的主题模式、用户信息等配置数据。这是一个相对简单的模块,状态字段较少,业务逻辑主要是数据的读取与持久化。以下是 SettingsNotifier 的实现:

class SettingsState {
  final String themeMode;
  final String userName;
  final String userSignature;
  final bool isDarkMode;
  final bool isLoading;

  const SettingsState({
    this.themeMode = 'system',
    this.userName = 'OpenHarmony 用户',
    this.userSignature = '这个人很懒,什么都没写~',
    this.isDarkMode = false,
    this.isLoading = false,
  });

  ThemeMode get flutterThemeMode {
    switch (themeMode) {
      case 'light':
        return ThemeMode.light;
      case 'dark':
        return ThemeMode.dark;
      default:
        return ThemeMode.system;
    }
  }

  SettingsState copyWith({
    String? themeMode,
    String? userName,
    String? userSignature,
    bool? isDarkMode,
    bool? isLoading,
  }) {
    return SettingsState(
      themeMode: themeMode ?? this.themeMode,
      userName: userName ?? this.userName,
      userSignature: userSignature ?? this.userSignature,
      isDarkMode: isDarkMode ?? this.isDarkMode,
      isLoading: isLoading ?? this.isLoading,
    );
  }
}

class SettingsNotifier extends Notifier<SettingsState> {
  
  SettingsState build() {
    Future.microtask(() => loadSettings());
    return const SettingsState();
  }

  final StorageService _storageService = StorageService();

  Future<void> loadSettings() async {
    state = state.copyWith(isLoading: true);
    await _storageService.init();
    state = SettingsState(
      themeMode: _storageService.getThemeMode(),
      userName: _storageService.getUserName(),
      userSignature: _storageService.getUserSignature(),
      isDarkMode: _storageService.getThemeMode() == 'dark',
      isLoading: false,
    );
  }

  Future<void> setThemeMode(String mode) async {
    state = state.copyWith(
      themeMode: mode,
      isDarkMode: mode == 'dark',
    );
    await _storageService.setThemeMode(mode);
  }

  Future<void> setUserName(String name) async {
    state = state.copyWith(userName: name);
    await _storageService.setUserName(name);
  }

  Future<void> setUserSignature(String signature) async {
    state = state.copyWith(userSignature: signature);
    await _storageService.setUserSignature(signature);
  }
}

final settingsProvider = NotifierProvider<SettingsNotifier, SettingsState>(() {
  return SettingsNotifier();
});

Settings 模块的迁移相对直接,主要的变化是将 ChangeNotifier 模式替换为 Notifier 模式。值得注意的是,我们将主题模式的转换逻辑放在了 State 类中作为 getter 方法,这种设计使得 UI 层可以直接使用 flutterThemeMode 属性,无需关心内部的字符串表示与 Flutter ThemeMode 枚举之间的转换。

5.2 Todo 模块的迁移

Todo 模块负责管理待办事项列表,是四个模块中业务逻辑最复杂的。它涉及数据筛选、用户分组、统计计算等功能。以下是 TodoNotifier 的核心实现:

class TodoState {
  final List<TodoItem> todos;
  final List<TodoItem> filteredTodos;
  final List<TodoItem> allTodos;
  final bool isLoading;
  final String? errorMessage;
  final String currentFilter;
  final int selectedUserId;

  const TodoState({
    this.todos = const [],
    this.filteredTodos = const [],
    this.allTodos = const [],
    this.isLoading = false,
    this.errorMessage,
    this.currentFilter = 'all',
    this.selectedUserId = 0,
  });

  int get completedCount => allTodos.where((t) => t.completed).length;
  int get pendingCount => allTodos.where((t) => !t.completed).length;
  int get totalCount => allTodos.length;

  Map<String, int> get statistics => {
    'total': totalCount,
    'completed': completedCount,
    'pending': pendingCount,
  };

  TodoState copyWith({
    List<TodoItem>? todos,
    List<TodoItem>? filteredTodos,
    List<TodoItem>? allTodos,
    bool? isLoading,
    String? errorMessage,
    String? currentFilter,
    int? selectedUserId,
  }) {
    return TodoState(
      todos: filteredTodos ?? this.filteredTodos,
      filteredTodos: filteredTodos ?? this.filteredTodos,
      allTodos: allTodos ?? this.allTodos,
      isLoading: isLoading ?? this.isLoading,
      errorMessage: errorMessage,
      currentFilter: currentFilter ?? this.currentFilter,
      selectedUserId: selectedUserId ?? this.selectedUserId,
    );
  }
}

class TodoNotifier extends Notifier<TodoState> {
  
  TodoState build() => const TodoState();

  final TodoService _todoService = TodoService();

  Future<void> loadTodos({int? userId}) async {
    state = state.copyWith(isLoading: true, errorMessage: null);

    try {
      List<TodoItem> todos;
      int selectedId;
      if (userId != null && userId > 0) {
        todos = await _todoService.getTodosByUser(userId);
        selectedId = userId;
      } else {
        todos = await _todoService.getTodos();
        selectedId = 0;
      }

      state = state.copyWith(
        allTodos: todos,
        todos: _applyFilterInternal(todos, state.currentFilter),
        isLoading: false,
        selectedUserId: selectedId,
      );
    } catch (e) {
      state = state.copyWith(errorMessage: e.toString(), isLoading: false);
    }
  }

  void setFilter(String filter) {
    state = state.copyWith(
      currentFilter: filter,
      todos: _applyFilterInternal(state.allTodos, filter),
    );
  }

  List<TodoItem> _applyFilterInternal(List<TodoItem> todos, String filter) {
    switch (filter) {
      case 'completed':
        return todos.where((t) => t.completed).toList();
      case 'pending':
        return todos.where((t) => !t.completed).toList();
      default:
        return List.from(todos);
    }
  }
}

final todoProvider = NotifierProvider<TodoNotifier, TodoState>(() {
  return TodoNotifier();
});

Todo 模块的迁移展示了派生状态的处理模式。filteredTodos 是根据 allTodos 和 currentFilter 计算得出的派生状态,我们将其与原始数据分开存储,简化了 UI 层的访问逻辑。同时,completedCount、pendingCount、statistics 等统计信息也作为派生属性提供,调用方无需关心计算过程。

5.3 Message 模块的迁移

Message 模块负责管理消息通知列表,涉及消息的读取状态、分类筛选、批量操作等功能。以下是 MessageNotifier 的实现:

class MessageState {
  final List<Message> messages;
  final List<Message> unreadMessages;
  final bool isLoading;
  final MessageType? currentFilter;

  const MessageState({
    this.messages = const [],
    this.unreadMessages = const [],
    this.isLoading = false,
    this.currentFilter,
  });

  int get unreadCount => unreadMessages.length;

  MessageState copyWith({
    List<Message>? messages,
    List<Message>? unreadMessages,
    bool? isLoading,
    MessageType? currentFilter,
    bool clearFilter = false,
  }) {
    return MessageState(
      messages: messages ?? this.messages,
      unreadMessages: unreadMessages ?? this.unreadMessages,
      isLoading: isLoading ?? this.isLoading,
      currentFilter: clearFilter ? null : (currentFilter ?? this.currentFilter),
    );
  }
}

class MessageNotifier extends Notifier<MessageState> {
  
  MessageState build() => const MessageState();

  final MessageService _messageService = MessageService();

  void loadMessages() {
    state = state.copyWith(isLoading: true);

    final allMessages = _messageService.getAllMessages();
    final unreadMessages = _messageService.getUnreadMessages();
    List<Message> filteredMessages;

    if (state.currentFilter != null) {
      filteredMessages = _messageService.getMessagesByType(state.currentFilter!);
    } else {
      filteredMessages = allMessages;
    }

    state = MessageState(
      messages: filteredMessages,
      unreadMessages: unreadMessages,
      isLoading: false,
      currentFilter: state.currentFilter,
    );
  }

  void setFilter(MessageType? type) {
    List<Message> messages;
    if (type != null) {
      messages = _messageService.getMessagesByType(type);
    } else {
      messages = _messageService.getAllMessages();
    }

    state = state.copyWith(
      messages: messages,
      currentFilter: type,
      clearFilter: type == null,
    );
  }

  Future<void> markAsRead(String messageId) async {
    await _messageService.markAsRead(messageId);
    final unreadMessages = _messageService.getUnreadMessages();
    state = state.copyWith(unreadMessages: unreadMessages);
  }

  Future<void> markAllAsRead() async {
    await _messageService.markAllAsRead();
    final unreadMessages = _messageService.getUnreadMessages();
    loadMessages();
    state = state.copyWith(unreadMessages: unreadMessages);
  }

  Future<void> deleteMessage(String messageId) async {
    await _messageService.deleteMessage(messageId);
    loadMessages();
  }
}

final messageProvider = NotifierProvider<MessageNotifier, MessageState>(() {
  return MessageNotifier();
});

Message 模块的迁移展示了 nullable 参数的处理技巧。在 copyWith 方法中,我们添加了 clearFilter 布尔参数来区分"将 filter 设为 null"和"不修改 filter"两种场景,避免了空值传递的歧义。

六、经验总结与最佳实践

6.1 迁移过程的注意事项

在本次 Riverpod 迁移实践中,我们总结出以下关键经验:

第一,状态类的设计要充分考虑不可变性。 每次状态更新都应生成新的状态对象,而非在原对象上修改。这种设计虽然增加了些许代码量,但带来的可追溯性与可测试性提升是值得的。当状态变更出现异常时,不可变设计可以轻松通过打印日志定位问题。此外,不可变状态也天然支持撤销/重做功能的实现。

第二,Notifier 内部的逻辑要保持精简。 将复杂的计算逻辑分散到派生 Provider 中,可以保持 Notifier 的清晰结构。Notifier 的核心职责应当是状态管理与业务方法调用,而非状态计算。派生 Provider 的计算结果会被缓存,只有依赖的原始状态变更时才会重新计算,这种机制确保了性能不会因为派生计算而下降。

第三,异步操作优先使用 AsyncNotifierProvider。 对于涉及网络请求的业务模块,使用 AsyncValue 处理异步状态可以大幅简化代码逻辑,避免手动维护 isLoading、errorMessage 等字段。AsyncValue.when 方法提供了优雅的状态分发机制,代码结构清晰且不易出错。AsyncValue 还支持 andWhen、or 等组合操作,可以应对更复杂的异步场景。

第四,渐进式迁移要控制节奏。 不要试图一次性完成所有模块的迁移,这样风险太大且难以调试。按照业务优先级逐个模块迁移,每完成一个模块都进行充分测试,可以有效控制风险。迁移过程中保持新旧代码并行,即使出现问题也可以快速回滚。建议每个模块迁移完成后进行一次完整的回归测试。

第五,合理使用派生 Provider。 派生 Provider 可以将复杂的计算逻辑封装起来,使用方无需关心计算过程。需要注意,派生 Provider 会自动缓存计算结果,直到其依赖的状态变更才会重新计算。对于计算量较大的派生逻辑,应评估是否需要手动添加缓存策略。

6.2 OpenHarmony 平台兼容性验证结论

经过实际项目验证,Riverpod 在 OpenHarmony Flutter 引擎上的表现与 Android 平台基本一致。核心功能包括状态监听、异步处理、Provider 派生等均工作正常,未发现任何平台相关的兼容性问题。AsyncNotifierProvider 在鸿蒙网络请求场景中的表现尤为稳定,能够正确处理请求的发起、响应、错误等各个阶段。

本次适配工作验证了 Riverpod 作为 Flutter 跨平台状态管理方案的可行性。开发者可以放心地在 OpenHarmony 项目中使用 Riverpod,无需担心平台兼容性问题。同时,Riverpod 的纯 Dart 实现特性意味着它可以随着 Flutter 对 OpenHarmony 支持的完善而自动受益。

6.3 代码托管与社区协作

本次适配工作涉及的完整代码已托管至 AtomGit 平台,仓库地址为 https://atomgit.com/openharmony-flutter/riverpod-adaptation 。代码按照业务模块组织,每个 Provider 的新旧实现并行存放,便于开发者对比学习。仓库同时包含了完整的示例项目,可以直接导入开发环境运行验证。

我们鼓励开发者在使用过程中提出问题与建议,通过社区协作不断完善这一适配方案。OpenHarmony 的生态建设需要每一位开发者的参与和贡献,状态管理作为应用架构的核心组成部分,值得投入更多精力去探索与优化。
这是我的运行截图:在这里插入图片描述

结语

Riverpod 作为 Flutter 生态中功能最为强大的状态管理方案之一,其在 OpenHarmony 平台上的稳定表现为跨平台应用开发提供了更多可能性。通过本次适配实践,我们验证了 Riverpod 核心功能的鸿蒙兼容性,探索了渐进式迁移的可行路径,并积累了 AsyncNotifierProvider 在真实业务场景中的使用经验。

状态管理的演进永无止境,Riverpod 也不是终点。随着应用复杂度的提升,开发者需要持续关注状态管理的最佳实践,根据项目实际情况选择最适合的方案。OpenHarmony 作为一个充满活力的新生平台,其生态建设需要每一位开发者的参与和贡献。

欢迎加入开源鸿蒙跨平台社区,与志同道合的开发者共同探索跨平台技术的无限可能。在社区中,你可以分享自己的适配经验,也可以从他人的实践中学习。跨平台技术的未来,需要我们共同努力书写。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐