You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter基于三状态的路由架构设计最佳实践(GoRouter、Riverpod)

Flutter三状态(Error/Loading/Data)架构最佳实践

两种方案核心分析

  • 单页面组件切换:社区主流常规方案,通过Consumer/ConsumerWidget监听状态,在build方法中分支渲染对应状态组件(如if (state is Loading) LoadingWidget() else if (state is Error) ErrorWidget() else DataWidget())。优点是状态与页面强绑定、路由层级简单无额外开销;缺点是复杂场景下build易臃肿,可通过拆分独立状态组件解决。
  • 三页面路由切换:追求解耦的方案,但存在路由决策与状态同步的天然痛点,以下是具体实现、问题及替代技巧。

三页面方案的实现方式

若坚持使用该方案,结合GoRouter与Riverpod可按以下步骤实现:

  1. 全局状态托管:用AsyncNotifier或StateNotifier托管页面加载状态(如AsyncValue<Data>),通过Provider将状态挂载到路由树上层:
final pageStateProvider = AsyncNotifierProvider<PageStateNotifier, Data>(() => PageStateNotifier());

class PageStateNotifier extends AsyncNotifier<Data> {
  @override
  Future<Data> build() async {
    // 发起初始数据请求
    return await fetchData();
  }

  Future<void> retry() => ref.invalidateSelf();
}
  1. GoRouter路由守卫:利用redirect回调根据状态动态跳转,避免在build中直接导航的错误:
final router = GoRouter(
  routes: [
    GoRoute(path: '/', builder: (context, state) => const DataPage()),
    GoRoute(path: '/loading', builder: (context, state) => const LoadingPage()),
    GoRoute(path: '/error', builder: (context, state) => const ErrorPage()),
  ],
  redirect: (context, state) {
    final ref = ProviderScope.containerOf(context);
    final pageState = ref.watch(pageStateProvider);
    
    // 避免无限重定向,需判断当前路由是否已匹配目标状态
    if (pageState.isLoading && state.location != '/loading') {
      return '/loading';
    } else if (pageState.hasError && state.location != '/error') {
      return '/error';
    } else if (pageState.hasValue && state.location != '/') {
      return '/';
    }
    return null;
  },
);
  1. 状态触发更新:当PageStateNotifier中状态变化时,GoRouter会自动重新计算redirect规则,完成页面跳转。

不推荐三页面方案的原因

  1. 路由语义混淆:路由应对应用户可感知的独立页面(如"商品详情页"),而Loading/Error是页面临时状态,将其设为独立路由违背路由设计初衷,还会增加埋点、页面统计的复杂度。
  2. 路由栈异常:用户按返回键时,会从Data页回到Loading/Error页,不符合正常交互预期;虽可用replace调整路由栈,但会增加额外逻辑成本。
  3. 状态耦合度提升:Error页的错误信息、重试逻辑,Loading页的进度数据等,都需通过全局Provider传递,反而不如单页面组件直接接收参数灵活。

更优雅的替代方案:单页面+通用状态组件

若想复用状态组件同时保持解耦,推荐封装通用状态Wrapper:

class StatefulPageWrapper<T> extends StatelessWidget {
  final AsyncValue<T> state;
  final Widget Function(T data) builder;
  final VoidCallback? onRetry;

  const StatefulPageWrapper({
    super.key,
    required this.state,
    required this.builder,
    this.onRetry,
  });

  @override
  Widget build(BuildContext context) {
    return state.when(
      loading: () => const Center(child: CircularProgressIndicator()),
      error: (error, stack) => ErrorPage(error: error, onRetry: onRetry),
      data: builder,
    );
  }
}

在具体页面中使用该Wrapper:

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final dataState = ref.watch(dataProvider);
    return StatefulPageWrapper(
      state: dataState,
      onRetry: () => ref.invalidate(dataProvider),
      builder: (data) => ListView.builder(
        itemCount: data.length,
        itemBuilder: (context, index) => ListTile(title: Text(data[index])),
      ),
    );
  }
}

该方案既实现了状态组件复用,又保持了状态与页面的绑定关系,路由逻辑简洁,符合Flutter组件化思想。

最佳实践总结

  • 优先选择单页面组件切换方案,通过通用状态Wrapper解决代码冗余问题。
  • 仅在有特殊需求(如Loading页需独立路由栈、特定埋点要求)时,才考虑三页面路由方案。
  • 状态管理上,优先使用Riverpod自带的AsyncValue统一管理Loading/Error/Data状态,减少自定义状态类的冗余。

内容的提问来源于stack exchange,提问作者superfuzzy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 16:35:27