MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Flutter State管理框架Riverpod入门与进阶

2021-01-303.8k 阅读

Riverpod 基础概念

什么是 Riverpod

Riverpod 是 Flutter 生态中一个强大的状态管理框架,它构建在 Provider 之上,提供了更简洁、更灵活且更具可维护性的状态管理解决方案。与传统的状态管理方式相比,Riverpod 能够有效地处理复杂应用中的状态共享、依赖注入等问题。它的设计理念强调了代码的可读性、可测试性以及可扩展性,使得开发者在构建大型 Flutter 应用时能够更高效地管理状态。

Riverpod 的核心概念

  1. Provider:在 Riverpod 中,Provider 是一个核心概念,它用于提供数据或状态。可以将 Provider 看作是一个工厂,负责创建和管理数据的实例。例如,一个简单的 Provider 可以用来提供一个计数器的值:
final counterProvider = Provider<int>((ref) => 0);

这里的 counterProvider 是一个 Provider,它返回一个 int 类型的值,初始值为 0。在 Flutter 组件中,可以通过 ref.watch(counterProvider) 来获取这个值。

  1. RefRef 是一个上下文对象,它提供了访问其他 Provider 以及执行一些操作(如监听状态变化、取消订阅等)的能力。在 Provider 的创建函数中,会传入一个 Ref 对象,通过它可以获取依赖的其他 Provider 的值。例如:
final anotherProvider = Provider<int>((ref) {
  final counter = ref.watch(counterProvider);
  return counter * 2;
});

anotherProvider 的创建函数中,通过 ref.watch(counterProvider) 获取了 counterProvider 的值,并在此基础上进行了一些计算。

  1. StateProviderStateProvider 用于管理可变状态。它结合了 ProviderState 的概念,使得状态的管理更加方便。例如,我们可以创建一个用于管理用户登录状态的 StateProvider
final isLoggedInProvider = StateProvider<bool>((ref) => false);

可以通过 ref.read(isLoggedInProvider.notifier).state = true 来更新状态。

Riverpod 的入门使用

安装与配置

首先,在 pubspec.yaml 文件中添加 riverpod 依赖:

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.0.0

然后运行 flutter pub get 来安装依赖。

简单的计数器示例

  1. 创建计数器的 Provider
import 'package:flutter_riverpod/flutter_riverpod.dart';

final counterProvider = StateProvider<int>((ref) => 0);
  1. 在 Flutter 组件中使用这个 Provider
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(counterProvider);
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Riverpod Counter')),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('Counter: $counter'),
              ElevatedButton(
                onPressed: () => ref.read(counterProvider.notifier).state++,
                child: const Text('Increment'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

在这个示例中,ProviderScope 是 Riverpod 应用的根,它提供了一个上下文环境,使得组件能够访问到各种 Provider。ConsumerWidget 是 Riverpod 提供的一个特殊的 Flutter 组件,它提供了一个 WidgetRef 对象,通过这个对象可以方便地访问 Provider。ref.watch(counterProvider) 用于监听 counterProvider 的变化,并在变化时重建组件。ref.read(counterProvider.notifier).state++ 则用于更新计数器的值。

依赖注入

假设我们有一个数据服务类 UserDataService,它用于获取用户数据:

class UserDataService {
  Future<String> fetchUserData() async {
    // 模拟网络请求
    await Future.delayed(const Duration(seconds: 1));
    return 'User Data';
  }
}

我们可以通过 Provider 来实现依赖注入:

final userDataServiceProvider = Provider<UserDataService>((ref) => UserDataService());

final userDataProvider = FutureProvider<String>((ref) {
  final service = ref.watch(userDataServiceProvider);
  return service.fetchUserData();
});

在组件中使用:

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userDataAsync = ref.watch(userDataProvider);
    return Scaffold(
      appBar: AppBar(title: const Text('User Data')),
      body: userDataAsync.when(
        data: (data) => Center(child: Text(data)),
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (error, stackTrace) => Center(child: Text('Error: $error')),
      ),
    );
  }
}

这里通过 userDataServiceProvider 提供了 UserDataService 的实例,userDataProvider 依赖于 userDataServiceProvider 来获取用户数据。在 UserDataScreen 组件中,通过 ref.watch(userDataProvider) 获取用户数据,并根据数据的加载状态进行相应的显示。

Riverpod 的进阶应用

自动刷新与缓存

  1. 自动刷新:Riverpod 能够自动处理状态变化并刷新依赖的组件。例如,当 counterProvider 的值发生变化时,所有依赖于它的组件(通过 ref.watch)都会自动重建。这种自动刷新机制使得状态管理变得非常直观,开发者无需手动处理状态变化的通知。

  2. 缓存:Riverpod 会自动缓存 Provider 的值,除非明确指定不缓存。例如,对于一个 FutureProvider,如果多次调用 ref.watch,只要 Future 没有完成,Riverpod 不会重复执行异步操作,而是返回缓存中的值。这在处理重复的网络请求或其他昂贵的操作时非常有用。

final expensiveDataProvider = FutureProvider<String>((ref) async {
  // 模拟昂贵的操作
  await Future.delayed(const Duration(seconds: 3));
  return 'Expensive Data';
});

在组件中多次调用 ref.watch(expensiveDataProvider),只有第一次会触发异步操作,后续调用会直接从缓存中获取值。

复杂状态管理

  1. 多层级状态管理:在大型应用中,状态可能存在多层级的依赖关系。Riverpod 能够很好地处理这种情况。例如,我们有一个电商应用,有用户信息、购物车等状态,购物车状态可能依赖于用户登录状态。
final isLoggedInProvider = StateProvider<bool>((ref) => false);

class CartService {
  List<String> items = [];
  Future<void> addItem(String item) async {
    // 模拟添加商品到购物车的操作
    await Future.delayed(const Duration(seconds: 1));
    items.add(item);
  }
}

final cartServiceProvider = Provider<CartService>((ref) => CartService());

final cartItemsProvider = FutureProvider<List<String>>((ref) {
  final isLoggedIn = ref.watch(isLoggedInProvider).state;
  if (!isLoggedIn) {
    throw Exception('User is not logged in');
  }
  final cartService = ref.watch(cartServiceProvider);
  return cartService.addItem('Sample Item').then((_) => cartService.items);
});

在这个示例中,cartItemsProvider 依赖于 isLoggedInProvidercartServiceProvider。只有当用户登录时,才能获取购物车的商品列表。

  1. 共享状态与局部状态:Riverpod 可以清晰地区分共享状态和局部状态。共享状态可以通过全局的 Provider 来管理,而局部状态可以在组件内部使用 StateProvider 来管理。例如,在一个聊天界面中,聊天列表的状态可能是共享状态,而某个聊天消息的展开/收起状态可能是局部状态。
class ChatMessage {
  String content;
  bool isExpanded = false;
  ChatMessage(this.content);
}

final chatMessagesProvider = StateProvider<List<ChatMessage>>((ref) => [
      ChatMessage('Hello'),
      ChatMessage('How are you?'),
    ]);

class ChatMessageWidget extends ConsumerWidget {
  final ChatMessage message;
  const ChatMessageWidget({super.key, required this.message});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Column(
      children: [
        InkWell(
          onTap: () {
            message.isExpanded =!message.isExpanded;
            ref.invalidate(chatMessagesProvider);
          },
          child: Text(message.content),
        ),
        if (message.isExpanded) const Text('Expanded content'),
      ],
    );
  }
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final chatMessages = ref.watch(chatMessagesProvider);
    return Scaffold(
      appBar: AppBar(title: const Text('Chat')),
      body: ListView.builder(
        itemCount: chatMessages.length,
        itemBuilder: (context, index) {
          return ChatMessageWidget(message: chatMessages[index]);
        },
      ),
    );
  }
}

在这个示例中,chatMessagesProvider 管理共享的聊天消息列表状态,而每个 ChatMessage 中的 isExpanded 是局部状态。当某个消息的展开状态改变时,通过 ref.invalidate(chatMessagesProvider) 来通知相关组件重建。

测试

  1. 单元测试 Provider:Riverpod 提供了方便的测试工具来测试 Provider。例如,对于前面的 counterProvider,我们可以编写如下单元测试:
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  test('Counter provider initial value', () {
    final container = ProviderContainer();
    final counter = container.read(counterProvider);
    expect(counter, 0);
  });

  test('Increment counter', () {
    final container = ProviderContainer();
    container.read(counterProvider.notifier).state++;
    final counter = container.read(counterProvider);
    expect(counter, 1);
  });
}

这里使用 ProviderContainer 来创建一个测试环境,通过 container.read 来读取 Provider 的值,并进行断言测试。

  1. 测试依赖关系:对于依赖其他 Provider 的 Provider,也可以进行测试。例如,对于 anotherProvider(依赖 counterProvider):
test('Another provider value', () {
  final container = ProviderContainer();
  container.read(counterProvider.notifier).state = 5;
  final anotherValue = container.read(anotherProvider);
  expect(anotherValue, 10);
});

通过在测试环境中设置依赖 Provider 的值,来测试依赖它的 Provider 的输出是否正确。

与其他 Flutter 特性结合

  1. 与路由结合:在 Flutter 应用中,路由是常用的功能。Riverpod 可以与路由很好地结合。例如,我们可以在路由之间共享状态。假设我们有一个用户设置页面和一个主页面,用户设置页面的某些设置会影响主页面的显示。
final userSettingsProvider = StateProvider<Map<String, dynamic>>((ref) => {
      'theme': 'light',
      'language': 'en',
    });

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final settings = ref.watch(userSettingsProvider);
    return Scaffold(
      appBar: AppBar(title: const Text('Settings')),
      body: Column(
        children: [
          // 设置主题的开关等控件
          ElevatedButton(
            onPressed: () {
              ref.read(userSettingsProvider.notifier).state['theme'] =
                  settings['theme'] == 'light'? 'dark' : 'light';
            },
            child: Text('Toggle Theme'),
          ),
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final settings = ref.watch(userSettingsProvider);
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: Center(
        child: Text('Theme: ${settings['theme']}'),
      ),
    );
  }
}

class MyRouter {
  static Route<dynamic> generateRoute(RouteSettings settings) {
    switch (settings.name) {
      case '/settings':
        return MaterialPageRoute(builder: (context) => const SettingsScreen());
      case '/home':
        return MaterialPageRoute(builder: (context) => const HomeScreen());
      default:
        return MaterialPageRoute(
            builder: (context) => Scaffold(body: const Text('Not Found')));
    }
  }
}

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateRoute: MyRouter.generateRoute,
      initialRoute: '/home',
    );
  }
}

在这个示例中,userSettingsProviderSettingsScreenHomeScreen 之间共享,当在 SettingsScreen 中更新设置时,HomeScreen 会自动更新显示。

  1. 与动画结合:Riverpod 可以与 Flutter 的动画特性相结合。例如,我们可以通过 StateProvider 来控制动画的状态。
final animationControllerProvider = StateProvider<AnimationController>((ref) {
  final controller = AnimationController(
    vsync: ref.owner,
    duration: const Duration(seconds: 2),
  );
  controller.forward();
  return controller;
});

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final controller = ref.watch(animationControllerProvider);
    return Scaffold(
      appBar: AppBar(title: const Text('Animated Widget')),
      body: Center(
        child: AnimatedBuilder(
          animation: controller,
          builder: (context, child) {
            return Transform.scale(
              scale: controller.value,
              child: child,
            );
          },
          child: const FlutterLogo(size: 200),
        ),
      ),
    );
  }
}

在这个示例中,animationControllerProvider 提供了一个动画控制器,通过 ref.watch 监听控制器的变化,并在 AnimatedBuilder 中根据控制器的值来更新动画效果。

通过以上从基础概念到进阶应用的介绍,相信你对 Flutter 状态管理框架 Riverpod 有了更深入的理解和掌握,可以在实际项目中灵活运用它来构建高效、可维护的应用程序。