Flutter 三方库 Riverpod 的鸿蒙化适配指南

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

各位小伙伴们大家好呀!今天要和大家分享一个超级实用的Flutter状态管理方案——Riverpod在鸿蒙设备上的适配之旅

话说Flutter发展到现在啊,状态管理可以说是一个永恒的话题。从最早的Provider一路走来,社区里诞生了各种各样优秀的状态管理方案。而Riverpod作为后起之秀,凭借其独特的设计理念和强大的功能,已经成为了Flutter生态中最受欢迎的状态管理库之一。那么问题来了——在鸿蒙化的大潮中,Riverpod能否稳定运行呢?答案是肯定的!接下来就让我带大家一起探索Riverpod的鸿蒙化适配之旅吧~

一、为什么选择Riverpod?

咳咳,在开始动手之前,我们先来聊聊为什么选择Riverpod作为我们的新状态管理方案吧!

首先呢,Riverpod具有以下几个让人心动不已的优点:

第一,依赖注入超级方便! Riverpod采用了声明式的Provider定义方式,让我们可以轻松实现依赖注入和服务定位,再也不用担心循环依赖的噩梦啦~

第二,编译时安全性! 不同于Provider的运行时错误,Riverpod的很多错误都能在编译时被捕获,这意味着更少的线上bug和更安心的开发体验呢~

第三,测试友好! Riverpod的设计天然支持mock,我们可以轻松地为每个Provider编写单元测试,妈妈再也不用担心我的代码无法测试啦!

第四,异步数据流处理强大! 内置的AsyncNotifierProvider和StreamProvider让处理异步数据变得轻而易举,特别适合网络请求这种场景呢~

二、核心适配方案设计

好啦,理论部分讲完了,现在让我们开始动手实践吧!

2.1 状态类设计

首先呢,我们要聊一聊状态类的设计。Riverpod推荐使用不可变(Immutable)状态类,这可不是什么花里胡哨的概念哦——它的核心思想是每次状态更新都生成一个新的状态对象,而不是在原对象上修改。这种设计虽然看起来代码量多了一点点,但是带来的可追溯性和可测试性提升是非常值得的!

想象一下,当状态变更出现异常时,不可变设计可以让我们轻松通过打印日志定位问题。而且呀,这种设计天然支持撤销/重做功能的实现,简直是一举多得呢~

2.2 数据模型定义

class TodoItem {
  final int userId;
  final int id;
  final String title;
  final bool completed;

  TodoItem({
    required this.userId,
    required this.id,
    required this.title,
    required this.completed,
  });

  factory TodoItem.fromJson(Map<String, dynamic> json) {
    return TodoItem(
      userId: json['userId'] as int,
      id: json['id'] as int,
      title: json['title'] as String,
      completed: json['completed'] as bool,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'userId': userId,
      'id': id,
      'title': title,
      'completed': completed,
    };
  }

  TodoItem copyWith({
    int? userId,
    int? id,
    String? title,
    bool? completed,
  }) {
    return TodoItem(
      userId: userId ?? this.userId,
      id: id ?? this.id,
      title: title ?? this.title,
      completed: completed ?? this.completed,
    );
  }
}

看看上面这个TodoItem类,我们定义了完整的copyWith方法,这是实现不可变状态更新的关键所在~每次更新数据时,我们不是修改原对象,而是创建一个全新的对象,这样做的好处多多的呢!

2.3 服务层封装

接下来让我们看看网络请求服务层的设计。这里我们使用Dio作为HTTP客户端,并结合Riverpod进行状态管理。

class TodoService {
  static const String _baseUrl = 'https://jsonplaceholder.typicode.com';
  final Dio _dio;

  TodoService(this._dio) {
    _dio.options = BaseOptions(
      connectTimeout: const Duration(seconds: 30),
      receiveTimeout: const Duration(seconds: 30),
      sendTimeout: const Duration(seconds: 30),
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
    );
  }

  Future<List<TodoItem>> getTodos() async {
    try {
      final response = await _dio.get('$_baseUrl/todos');
      final List<dynamic> data = response.data;
      return data.map((json) => TodoItem.fromJson(json)).toList();
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  Future<TodoItem> getTodoById(int id) async {
    try {
      final response = await _dio.get('$_baseUrl/todos/$id');
      return TodoItem.fromJson(response.data);
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  Exception _handleError(DioException e) {
    switch (e.type) {
      case DioExceptionType.connectionTimeout:
        return Exception('连接超时,请检查网络设置~');
      case DioExceptionType.sendTimeout:
        return Exception('发送请求超时啦~');
      case DioExceptionType.receiveTimeout:
        return Exception('接收数据超时了呢~');
      case DioExceptionType.badResponse:
        return Exception('服务器开小差了: ${e.response?.statusCode}');
      case DioExceptionType.cancel:
        return Exception('请求被取消了哦~');
      case DioExceptionType.connectionError:
        return Exception('连接错误,请检查网络~');
      default:
        return Exception('发生了一些小意外: ${e.message}');
    }
  }
}

2.4 Provider定义

现在最激动人心的时刻到了——让我们看看如何使用Riverpod定义各种Provider!

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dio/dio.dart';
import '../models/todo_item.dart';
import '../services/todo_service.dart';

// Dio实例Provider
final dioProvider = Provider<Dio>((ref) {
  return Dio();
});

// TodoService Provider
final todoServiceProvider = Provider<TodoService>((ref) {
  final dio = ref.watch(dioProvider);
  return TodoService(dio);
});

// Todo列表状态
class TodoListState {
  final List<TodoItem> todos;
  final bool isLoading;
  final String? errorMessage;

  const TodoListState({
    this.todos = const [],
    this.isLoading = false,
    this.errorMessage,
  });

  TodoListState copyWith({
    List<TodoItem>? todos,
    bool? isLoading,
    String? errorMessage,
  }) {
    return TodoListState(
      todos: todos ?? this.todos,
      isLoading: isLoading ?? this.isLoading,
      errorMessage: errorMessage,
    );
  }
}

// Todo列表Notifier
class TodoListNotifier extends Notifier<TodoListState> {
  
  TodoListState build() {
    return const TodoListState();
  }

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

    try {
      final todoService = ref.read(todoServiceProvider);
      final todos = await todoService.getTodos();
      state = state.copyWith(todos: todos, isLoading: false);
    } catch (e) {
      state = state.copyWith(
        isLoading: false,
        errorMessage: e.toString(),
      );
    }
  }

  Future<void> refresh() async {
    await loadTodos();
  }
}

// 核心Provider定义
final todoListProvider = NotifierProvider<TodoListNotifier, TodoListState>(
  TodoListNotifier.new,
);

这一段代码可是整个方案的核心所在呢!让我们来仔细解读一下~

首先是Dio实例的Provider——这个Provider负责管理Dio单例,我们可以在需要的地方通过ref.watch来获取它。然后是TodoService的Provider,它依赖于Dio Provider,实现了很好的解耦。

最重要的是TodoListNotifier这个类啦!它继承自Notifier,这是Riverpod中处理同步状态的利器。我们通过build方法返回初始状态,然后通过暴露的方法来更新状态。每个方法最后都会调用state = xxx来触发UI更新,这种写法既清晰又易于追踪状态变化~

2.5 异步数据流处理

对于需要处理异步加载的场景,Riverpod还提供了AsyncNotifier这个大招!让我们一起来看看吧~

// 异步Todo详情Provider
class TodoDetailNotifier extends AsyncNotifier<TodoItem?> {
  
  Future<TodoItem?> build() async {
    return null;
  }

  Future<void> loadTodo(int id) async {
    state = const AsyncValue.loading();

    try {
      final todoService = ref.read(todoServiceProvider);
      final todo = await todoService.getTodoById(id);
      state = AsyncValue.data(todo);
    } catch (e, st) {
      state = AsyncValue.error(e, st);
    }
  }

  void clear() {
    state = const AsyncValue.data(null);
  }
}

final todoDetailProvider =
    AsyncNotifierProvider<TodoDetailNotifier, TodoItem?>(
  TodoDetailNotifier.new,
);

AsyncNotifier特别适合处理网络请求这种异步操作。它内置了AsyncValue状态,包含了loading、data和error三种状态,让我们可以优雅地处理各种加载场景。

三、渐进式迁移策略

看到这里,小伙伴们可能会问了:我们的项目已经大量使用了Provider,能不能平滑过渡到Riverpod呢?当然可以啦!这里给大家介绍几种实用的渐进式迁移策略~

策略一:平行运行法

在新功能中使用Riverpod,旧功能保持Provider不变。两套系统同时运行,互不干扰。等新功能稳定后,再逐步迁移旧代码。这种方式适合项目周期比较宽裕的情况~

策略二:按模块迁移法

将项目按照功能模块划分,每次只迁移一个模块。比如先迁移用户认证模块,再迁移商品模块,最后迁移订单模块。迁移完成后立即测试,确保功能正常后再进行下一个模块。这种方式适合中大型项目,风险可控~

策略三:核心抽离法

先把共用的数据模型和服务层抽离出来,用Riverpod重写这一部分。UI层暂时保持Provider不变。等核心层稳定后,再逐步将UI层也迁移到Riverpod。这种方式可以让迁移工作更加聚焦~

四、UI层集成实践

好啦,核心代码讲完了,现在让我们看看如何在UI层使用这些Provider吧!

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    final todoState = ref.watch(todoListProvider);

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('我的待办清单'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () {
              ref.read(todoListProvider.notifier).refresh();
            },
          ),
        ],
      ),
      body: _buildBody(context, todoState, ref),
    );
  }

  Widget _buildBody(BuildContext context, TodoListState state, WidgetRef ref) {
    if (state.isLoading) {
      return const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CircularProgressIndicator(),
            SizedBox(height: 16),
            Text('正在加载数据中...'),
          ],
        ),
      );
    }

    if (state.errorMessage != null) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.error_outline, size: 64, color: Colors.red),
            const SizedBox(height: 16),
            Text(
              '哎呀出错啦: ${state.errorMessage}',
              textAlign: TextAlign.center,
              style: const TextStyle(color: Colors.red),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                ref.read(todoListProvider.notifier).loadTodos();
              },
              child: const Text('重新加载'),
            ),
          ],
        ),
      );
    }

    return ListView.builder(
      itemCount: state.todos.length,
      itemBuilder: (context, index) {
        final todo = state.todos[index];
        return ListTile(
          leading: Icon(
            todo.completed ? Icons.check_circle : Icons.circle_outlined,
            color: todo.completed ? Colors.green : Colors.grey,
          ),
          title: Text(
            todo.title,
            style: TextStyle(
              decoration: todo.completed ? TextDecoration.lineThrough : null,
            ),
          ),
          subtitle: Text('用户ID: ${todo.userId}'),
        );
      },
    );
  }
}

注意到这里使用的是ConsumerWidget而不是普通的StatelessWidget,这让我们可以通过ref来访问状态。ref.watch用于监听状态变化并重建UI,ref.read用于读取状态而不建立监听关系。

五、鸿蒙设备运行验证

在这里插入图片描述

(上图展示了Riverpod状态管理在鸿蒙设备上的运行效果,可以看到Todo列表数据成功加载,异步状态管理运作良好~)

经过实际测试验证,Riverpod在鸿蒙设备上运行稳定,各项功能表现正常:

  • Provider的依赖注入机制工作正常
  • Notifier的状态更新可以正确触发UI重建
  • AsyncNotifier的异步数据流处理表现优异
  • 网络请求与状态管理配合默契

特别值得一提的是AsyncNotifierProvider在处理网络请求时展现了出色的稳定性。即使在网络不佳的情况下,也能优雅地展示加载状态和错误信息,不会出现崩溃或白屏问题~

六、总结与展望

好啦,今天的分享就到这里啦!让我们来总结一下今天学到的知识吧~

核心要点回顾:

第一,Riverpod的不可变状态设计虽然看似增加了代码量,但带来了可追溯性和可测试性的显著提升,这是非常值得的投资呢~

第二,通过Notifier和AsyncNotifier,我们可以优雅地处理同步和异步两种状态管理场景,代码结构清晰易懂~

第三,渐进式迁移策略让我们可以在不影响现有功能的前提下,逐步完成从Provider到Riverpod的过渡~

展望未来:

Riverpod在鸿蒙设备上的稳定表现,为Flutter鸿蒙化开发提供了可靠的状态管理保障。随着鸿蒙生态的持续发展,相信会有更多优秀的Flutter三方库完成适配,让我们的开发工作越来越便捷~

最后呢,欢迎大家加入开源鸿蒙跨平台社区,一起探讨Flutter鸿蒙化的技术奥秘!如果有任何问题,欢迎在评论区留言交流哦~

那么,我们下期再见啦!拜拜~👋

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐