【Flutter】都Riverpod了还需要VM吗?集中式与分散式的两种思路

356 阅读8分钟

Riverpod集中式与分散式的两种思路

前言

要说起 Riverpod 大家肯定不陌生,毕竟也是目前官方推荐的,它的优势或者说特点是什么呢?

不同的人有不同的说法,强大的状态管理,声明式的编程方法,复杂UI的简化,增强工具的支持等等。

但是抛开表看看本质,我们为什么要用Riverpod,是因为它的灵活性,也就是可重用可组合性。

易于可重复使用,高度可组合,易维护。

所以天然上来说我们既然用了 Riverpod 就得是分散式组合式的使用方式,但是实际开发上真的这样就万事大吉了吗?

你这么说的我都不自信了,那么一个页面的功能最佳实践是怎样的呢? 还是举例吧。比如一个 UserProfilePage 页面,我需要展示用户的信息,全部行业列表,分类列表这些信息。

方案一,也就是分散的做法,通过三种不同的ProfileState IndustryState CategoryState 分别对应三种不同的 Provider ,不需要一个 ViewModel/Controller(叫法不同后面统一用ViewModel替代)去统一管理,异步可以选择 AsyncProvider 同步的选择 Provider,直接在页面中通过 watch 去观察三种状态去更新页面。

方案二,集中式管理,我创建一个 UserPorfileState 保存页面的全部状态,再加上一个 UserProfileViewModel 继承自 Notifier,通过 Provider 提供给 UserProfilePage 统一使用。三种数据的获取都写在 UserProfileViewModel 中,通过 UserPorfileState 内部统一定义三种数据,然后在页面中通过 watch 去观察并更新页面。

其实也不是什么稀奇的事情,一种前端开发者习惯写法,一种是移动端开发者习惯写法。都能完成对应的功能。两种方案都有其优点和适用场景。具体选择哪种方案取决于你的具体需求、项目规模和代码维护性。以下是对这两种方案的详细分析。

一、集中式处理

优点:

  1. 统一管理: 所有数据和业务逻辑集中在一个 ViewModel 中,便于管理。
  2. 状态一致性: 一个统一的状态类可以保证页面各部分数据的一致性。
  3. 简单的状态管理: 页面只需关注一个 Provider,简化了状态的订阅和管理。

缺点:

  1. 复杂性: 如果页面的数据和逻辑非常复杂,ViewModel 可能会变得非常庞大和难以维护。
  2. 耦合性高: 页面和 ViewModel 的耦合度较高,影响部分功能的独立性和可测试性。

我们以上面的场景为例:

final userProfileProvider = StateNotifierProvider<UserProfileViewModel, UserProfileState>((ref) {
  return UserProfileViewModel();
});

class UserProfileState {
  final User? user;
  final List<Industry>? industries;
  final List<Category>? categories;

  UserProfileState({this.user, this.industries, this.categories});
}

class UserProfileViewModel extends StateNotifier<UserProfileState> {
  UserProfileViewModel() : super(UserProfileState());

  Future<void> fetchUserProfile() async {
    // 获取用户信息
    final user = await fetchUser();
    // 获取行业列表
    final industries = await fetchIndustries();
    // 获取分类列表
    final categories = await fetchCategories();

    state = UserProfileState(
      user: user,
      industries: industries,
      categories: categories,
    );
  }
}

class UserProfilePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final state = watch(userProfileProvider);

    if (state.user == null || state.industries == null || state.categories == null) {
      return CircularProgressIndicator();
    }

    return Scaffold(
      // 页面布局逻辑
    );
  }
}

一个页面一个控制器,在内部统一的处理数据加载与 UI Loading 的逻辑处理,很符合移动端开发的习惯,如果是移动端的开发者很容易就上手。

但是当一些重复的场景每次都要写重复的逻辑,比如支付的发起与校验,类似这种功能我们其实是更方便使用(Service/UserCase叫法不同)单独的服务或用例来承载。

二、分散式处理

优点:

  1. 解耦: 不同的数据源通过不同的 Provider 管理,降低了耦合度。
  2. 独立性: 各个 Provider 独立存在,便于单独测试和复用。
  3. 灵活性: 可以根据需要灵活组合各个 Provider。

缺点:

  1. 复杂的状态管理: 页面需要同时关注多个 Provider,增加了状态管理的复杂性。
  2. 潜在的性能问题: 如果多个 Provider 状态变化频繁,可能会导致页面频繁重建。
final userProvider = FutureProvider<User>((ref) async {
  return fetchUser();
});

final industriesProvider = FutureProvider<List<Industry>>((ref) async {
  return fetchIndustries();
});

final categoriesProvider = FutureProvider<List<Category>>((ref) async {
  return fetchCategories();
});

class UserProfilePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final userAsyncValue = watch(userProvider);
    final industriesAsyncValue = watch(industriesProvider);
    final categoriesAsyncValue = watch(categoriesProvider);

    return userAsyncValue.when(
      data: (user) => industriesAsyncValue.when(
        data: (industries) => categoriesAsyncValue.when(
          data: (categories) {
            return Scaffold(
              // 页面布局逻辑
            );
          },
          loading: () => CircularProgressIndicator(),
          error: (err, stack) => Text('Error: $err'),
        ),
        loading: () => CircularProgressIndicator(),
        error: (err, stack) => Text('Error: $err'),
      ),
      loading: () => CircularProgressIndicator(),
      error: (err, stack) => Text('Error: $err'),
    );
  }
}

这里的状态加 when 的用法是 future 的加载状态处理,也可以用更推荐的 maybeWhen:

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: context.isSmallScreen ? null : Colors.transparent,
        centerTitle: true,
        title: const Text('Favorites'),
      ),
      body: ref.watch(favoritesNotifierProvider).maybeWhen(
         loading: () => const CircularProgressIndicator(),
            orElse: () => const SizedBox.shrink(),
            data: (favorites) {
              if (favorites.isEmpty) const EmptyView();
              return Wrap(
                children: [
                  ...favorites.map((entry) {
                    return Padding(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 5.0,
                        vertical: 5.0,
                      ),
                      child: BookItem(
                        img: entry.link![1].href!,
                        title: entry.title!.t!,
                        entry: entry,
                      ),
                    );
                  }),
                ],
              );
            },
          ),
    );
  }

如果全部使用分散式的我们的加载状态无法统一管理,无法处理并发的任务和顺序和 Loading 加载的状态。除非我们再用一个控制器去管理这三个 Provider,那么本质上就和集中式的处理没什么区别了。

三、综合使用

方案一 适合数据和逻辑相对简单的场景,便于集中管理和状态一致性。

方案二 适合想要降低耦合度、提高模块化和复用性的场景,但需要更复杂的状态管理。

综合使用这两种方案能够结合它们的优点,既保持灵活性,又能集中管理。你可以在一个 ViewModel 中调用多个 Provider,并将它们的结果综合到一个统一的状态类中。这样,你可以在一个地方管理页面的整体状态,同时保持各个数据源的独立。

  1. 定义多个独立的 Provider 分别管理不同的数据源。
  2. 定义一个 ViewModel 类,集中管理页面的状态。
  3. 在 ViewModel 中调用这些独立的 Provider,将结果赋值给一个统一的状态类。
  4. 使用 StateNotifierProvider 将 ViewModel 提供给页面。
  5. 在页面中观察和使用统一的状态类。

定义数据源 Provider

final userProvider = FutureProvider<User>((ref) async {
  return fetchUser();
});

final industriesProvider = FutureProvider<List<Industry>>((ref) async {
  return fetchIndustries();
});

final categoriesProvider = FutureProvider<List<Category>>((ref) async {
  return fetchCategories();
});

定义统一的状态类

class UserProfileState {
  final User? user;
  final List<Industry>? industries;
  final List<Category>? categories;

  UserProfileState({this.user, this.industries, this.categories});

  UserProfileState copyWith({
    User? user,
    List<Industry>? industries,
    List<Category>? categories,
  }) {
    return UserProfileState(
      user: user ?? this.user,
      industries: industries ?? this.industries,
      categories: categories ?? this.categories,
    );
  }
}

定义 ViewModel 控制类

class UserProfileViewModel extends StateNotifier<UserProfileState> {
  UserProfileViewModel(ProviderReference ref)
      : super(UserProfileState()) {
    _initialize(ref);
  }

  void _initialize(ProviderReference ref) {
    ref.listen(userProvider, (userAsyncValue) {
      userAsyncValue.when(
        data: (user) {
          state = state.copyWith(user: user);
        },
        loading: () {},
        error: (err, stack) {
          // 处理错误
        },
      );
    });

    ref.listen(industriesProvider, (industriesAsyncValue) {
      industriesAsyncValue.when(
        data: (industries) {
          state = state.copyWith(industries: industries);
        },
        loading: () {},
        error: (err, stack) {
          // 处理错误
        },
      );
    });

    ref.listen(categoriesProvider, (categoriesAsyncValue) {
      categoriesAsyncValue.when(
        data: (categories) {
          state = state.copyWith(categories: categories);
        },
        loading: () {},
        error: (err, stack) {
          // 处理错误
        },
      );
    });
  }
}

提供 ViewModel

final userProfileViewModelProvider = StateNotifierProvider<UserProfileViewModel, UserProfileState>((ref) {
  return UserProfileViewModel(ref);
});

页面中使用 ViewModel 和统一状态类

class UserProfilePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final state = watch(userProfileViewModelProvider);

    if (state.user == null || state.industries == null || state.categories == null) {
      return CircularProgressIndicator();
    }

    return Scaffold(
      // 页面布局逻辑
    );
  }
}

这种综合方案结合了集中管理和灵活性的优点,通过在 ViewModel 中调用多个 Provider,并将它们的结果统一到一个状态类中进行管理。这样,你可以在页面中通过一个统一的状态类来观察和更新页面,同时保留各个数据源的独立性和模块化。

当然对于一些通用单独的逻辑我们还是应该使用单独的 Provider 来替代,比如上面我们提到的支付与校验的逻辑,我们就能使用一个 PaymentUserCase / PaymentService 来单独处理。

总的来说思路就是保证每个 Provider 的单一职责,而 ViewModel 作为控制器则整合各个 Provider 对页面状态负责。

这样对于 缓存机制、懒加载、并行请求、错误处理、重用机制都有很好的帮助。

总结

在这篇文章中,我们回顾到 Provider 的移动端集中式的用法和前端分散式的用法,以及他们的优缺点。以及单独 Provider 的状态Loading管理用法。

同时我们探讨了如何结合使用多个 Provider 和 ViewModel 来管理复杂应用中的状态。这种综合方案能够有效结合集中管理与模块化的优点,在保持灵活性的同时,确保数据和逻辑的独立性。

通过定义独立的数据源 Provider 并在 ViewModel 中进行集中管理,我们可以在一个地方维护页面的整体状态。这种设计不仅提高了代码的可维护性,还增强了各个模块的重用性和可测试性。

在优化部分,我们讨论了如何通过缓存机制、懒加载、并行请求等方法来提升应用性能。此外,采用单一责任原则和封装业务逻辑有助于减少代码耦合,提高模块化程度。

在实际应用中,我们可以灵活选择使用各种 Provider /StreamProvider / FutureProvider 来使用,更灵活的实现框架搭建。

OK,那么今天的分享就到这里啦,当然如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。

如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我真的很重要。

这一期就此完结了。