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

Flutter中TabController结合Riverpod与SharedPreferences出现错误和警告的解决方法

Flutter中TabController结合Riverpod与SharedPreferences出现错误和警告的解决方法

嘿,我来帮你搞定这两个困扰你的问题!先逐个分析:

第一个问题:TabController长度与TabBarView子项数不匹配的错误

问题原因

你遇到的Controller's length property (0) does not match the number of children (3)...错误,本质是TabController初始化时机和Riverpod异步加载数据的时机不匹配:

  • initState执行时,ItemListNotifier的initialized方法还在异步读取SharedPreferences,此时state还是初始的空列表,你初始化了length为0的TabController;
  • 当异步加载完成后,state更新为有数据的列表,但TabController的length没同步更新,就导致了长度不匹配;
  • 点击添加按钮时,你重新创建了TabController,但这种方式不够优雅,而且容易遗漏其他state变化的场景(比如从其他页面修改了列表)。

解决方案:监听Riverpod状态变化,同步更新TabController

我们可以用ref.listen来监听itemListProvider的状态变化,每当列表长度改变时,就重新创建TabController(因为TabController的length不可变),同时处理好旧控制器的销毁:

修改_MyHomePage的代码:

class _MyHomePage extends ConsumerState<MyHomePage> with TickerProviderStateMixin {
  late TabController _tabController;

  @override
  void initState() {
    super.initState();
    // 初始先创建一个临时控制器,后续会被替换
    _tabController = TabController(vsync: this, length: 1);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // 监听itemList的变化,同步更新TabController
    ref.listen<List<TabItem>>(itemListProvider, (previous, next) {
      if (previous?.length != next.length) {
        // 销毁旧的控制器
        _tabController.dispose();
        // 创建新的控制器,长度匹配最新的列表
        _tabController = TabController(vsync: this, length: next.length);
        // 如果是添加新项,自动跳转到最后一个标签页
        if (previous != null && next.length > previous.length) {
          _tabController.animateTo(next.length - 1);
        }
      }
    });

    final state = ref.watch(itemListProvider);

    return Scaffold(
      backgroundColor: Theme.of(context).colorScheme.background,
      appBar: AppBar(
        leading: IconButton(
          icon: const Icon(Icons.add),
          onPressed: addButtonTapped,
        ),
        title: const Text('My App'),
        bottom: TabBar(
          indicatorColor: Theme.of(context).colorScheme.onPrimary,
          controller: _tabController,
          isScrollable: true,
          tabs: state.map((tab) => Tab(
            child: Text(tab.title, style: Theme.of(context).textTheme.titleMedium),
          )).toList(),
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        // 避免空列表导致的错误,至少返回一个占位组件
        children: state.isEmpty 
            ? [const Center(child: Text('暂无标签'))] 
            : state.map((item) => TabPage(tab: item)).toList(),
      ),
    );
  }

  void addButtonTapped() async {
    final notifier = ref.read(itemListProvider.notifier);
    final currentLength = ref.read(itemListProvider).length;
    await notifier.add(title: (currentLength + 1).toString());
    // 这里不需要再手动创建TabController了,ref.listen会自动处理
  }
}

第二个问题:访问notifier.state的警告

问题原因

警告The member 'state' can only be used within instance members of subclasses of 'package:state_notifier/state_notifier.dart',是因为StateNotifier的state属性设计为仅在自身子类内部访问,外部直接访问会破坏状态管理的封装性。

解决方案:通过Riverpod获取最新状态,而非直接访问notifier.state

你不需要直接访问notifier.state,而是通过ref.read(itemListProvider)来获取当前的状态:

比如之前的添加按钮代码,改成:

void addButtonTapped() async {
  final notifier = ref.read(itemListProvider.notifier);
  // 通过ref.read获取当前状态的长度,而非notifier.state.length
  final currentLength = ref.read(itemListProvider).length;
  await notifier.add(title: (currentLength + 1).toString());
}

另外,在之前的ref.listen逻辑里,我们已经通过监听的next参数拿到了最新状态,完全不需要再访问notifier的state属性。

额外优化点:处理异步初始化的加载状态

因为ItemListNotifier的initialized是异步的,页面初始渲染时可能会有短暂的空列表状态,建议添加一个加载指示器:

修改build方法里的状态获取部分:

final itemListAsync = ref.watch(itemListProvider.future);

return Scaffold(
  // ...其他部分不变
  body: FutureBuilder<List<TabItem>>(
    future: itemListAsync,
    builder: (context, snapshot) {
      if (snapshot.connectionState == ConnectionState.waiting) {
        return const Center(child: CircularProgressIndicator());
      }
      final state = snapshot.data ?? [];
      return TabBarView(
        controller: _tabController,
        children: state.isEmpty 
            ? [const Center(child: Text('暂无标签'))] 
            : state.map((item) => TabPage(tab: item)).toList(),
      );
    },
  ),
);

这样用户在启动App时不会看到空页面,而是加载指示器,体验更好。

备注:内容来源于stack exchange,提问作者raimtoon

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.22 16:03:15