avatar nyne
Open App

flutter PageStorage

#flutter #开发

在给一个widget加key的时候, copilot给我补了一个PageStorageKey. 这东西我还是第一次见, 搜了一下, 发现它用于切换页面时保存页面的状态.

好东西, 之前想要保存组件状态, 我通常是将其存在父组件或是一个全局的StateController, 因为太麻烦了, 很多时候懒得写, 用户体验低点就低点吧.

这么好用的东西当然要立即添加到我的项目里

  final _pages = [
    const HomePage(
      key: PageStorageKey('home'),
    ),
    const FavoritesPage(
      key: PageStorageKey('favorites'),
    ),
    const ExplorePage(
      key: PageStorageKey('explore'),
    ),
    const CategoriesPage(
      key: PageStorageKey('categories'),
    ),
  ];

然而没有这么简单, HomePageFavoritesPage工作正常, 成功保存了滚动状态. 而ExplorePageCategoriesPage是非常复杂的页面, 它们都是TabBarView, 并且每个Tab都是一个滚动页面. 当切换到这个页面时, TabBar被移动到了超出最大值的位置, 然后滚动到最后一个Tab. 猜测是当前的Tab页面的滚动状态被记录, 并且当页面重建时应用在了TabBar上. 查看文档, 发现PageStorageBucket.readState里注释有这样的话using the specified identifier or an identifier computed from the given context. 也就是说滚动页面是调用此方法读取先前保存的数据, 标识符的计算方法应该是从当前组件到PageStorage组件路径上所有的PageStorageKey合并结果. 那么给每个Tab页面都加一个PageStorageKey应该就没问题了. 这样做之后, 我的TabBar的状态却没有被保留, 看来还是要自己写一些代码.

给TabBar加一个状态保留

首先在state内加上这一行

PageStorageBucket get bucket => PageStorage.of(context);

当index改变时记录状态

  void onTabChanged() {
    final int i = _controller.index;
    if (i == previousIndex) {
      return;
    }
    updateScrollOffset(i);
    previousIndex = i;
    bucket.writeState(context, i);  // 记录状态
  }

组件重建时恢复状态

  @override
  void didChangeDependencies() {
    _controller = widget.controller ?? DefaultTabController.of(context);
    _controller.animation!.addListener(onTabChanged);
    initPainter();
    super.didChangeDependencies();
    var prevIndex = bucket.readState(context) as int?;
    if (prevIndex != null && prevIndex != _controller.index) {
      _controller.index = prevIndex;
    }
  }

ScrollController的恢复状态行为需要被禁用

  var scrollController = ScrollController(
    keepScrollOffset: false
  );

这样就完成了TabBar的状态保存

保存更多状态

在一个需要网络请求加载数据的组件里, 这样写


  Map<String, dynamic> get state => {
    'maxPage': _maxPage,
    'data': _data,
    'page': _page,
    'error': _error,
    'loading': _loading,
    'nextUrl': _nextUrl,
  };

  void restoreState(Map<String, dynamic>? state) {
    if (state == null) {
      return;
    }
    _maxPage = state['maxPage'];
    _data.clear();
    _data.addAll(state['data']);
    _page = state['page'];
    _error = state['error'];
    _loading.clear();
    _loading.addAll(state['loading']);
    _nextUrl = state['nextUrl'];
  }

  void storeState() {
    PageStorage.of(context).writeState(context, state);
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    restoreState(PageStorage.of(context).readState(context));
  }

当网络请求完成时, 调用一下storeState即可

需要注意的是, 如过该组件构建了一个可滚动组件, 要给他加一个PageStorageKey, 否则滚动状态的储存和数据状态的储存会共用一个identifier

return SmoothCustomScrollView(
      key: const PageStorageKey('scroll'),
      controller: controller,
      slivers: [],
    );