Closed1

[調査][Flutter] AutomaticKeepAliveClientMixin で状態が保持されるのはなぜなのか

ykrodsykrods

導入

TabBar + TabBarView でタブを切り替える場合、何もしないとタブ内ページの状態がリセットされる。状態を保持する方法の一つとして AutomaticKeepAliveClientMixin がある。

BottomNavigationBar のタブ切り替えでも同じように使えるものと思ったがコードを書いてみると状態が保持されない、ということで理由を調べる。

(参考)

調査1

  • AutomaticKeepAliveClientMixin について、公式ドキュメント によると以下のことが書いてある

    • AutomaticKeepAliveClientMixinKeepAliveNotification を送信する
    • KeepAliveNotificationAutomaticKeepAlive が受け取る
    • ListView , GridView, SliverList, SliverGrid の子要素は自動的に AutomaticKeepAlive ウィジェットが追加される
    • ( AutomaticKeepAliveClientMixin, KeepAliveNotification , AutomaticKeepAlive より
  • では TabBarView でも AutomaticKeepAlive を子要素に追加しているかというと、ドキュメントには特に記載がない

  • 仕方ないのでコードを読むと、TabBarViewStatefulWidget で、 _TabBarViewState.build の内部で PageView を返している

  • PageView のドキュメントにも KeepAlive 関連のことは書いてない

  • PageView のコードをさらに読んだ結果、その内部実装で SliverChildListDelegate というクラスを利用しており、それが AutomaticKeepAlive を子要素に追加していた

検証1

では AutomaticKeepAlive を自分でタブ内ページのウィジェットに仕込めば TabBarView などを使わなくても状態が保持されるのか、ということで実装してみると、これはランタイムエラーになる

// countup.dart
import 'package:flutter/material.dart';


class CountUp extends StatefulWidget {
  const CountUp({ super.key });

  
  State<CountUp> createState() => _CountUpState();
}


class _CountUpState extends State<CountUp> with AutomaticKeepAliveClientMixin {
  int count = 0;

  
  bool get wantKeepAlive => true;

  
  Widget build(BuildContext context) {
    super.build(context);

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text("count: ${count}"),
          TextButton(
            onPressed: () => setState(() { count++; }),
            child: Text("countUp")
          )
        ]
      )
    );
  }
}
// my_home.dart
//
// 以下のサンプルコードを改変しています
// - https://api.flutter.dev/flutter/material/BottomNavigationBar-class.html 
import 'package:flutter/material.dart';
import 'countup.dart';

class MyHome extends StatefulWidget {
  const MyHome({ super.key });

  
  State<MyHome> createState() => _MyHomeState();
}


class _MyHomeState extends State<MyHome> {
  int _selectedIndex = 0;

  static const TextStyle optionStyle =
    TextStyle(fontSize: 30, fontWeight: FontWeight.bold);

  final List<Widget> _widgets = [
    AutomaticKeepAlive(child: CountUp()),
    Text("Index 1: Business", style: optionStyle),
    Text("Index 2: School", style: optionStyle),
  ];

  
  Widget build(BuildContext context) => Scaffold(
    body: _widgets.elementAt(_selectedIndex),
    bottomNavigationBar: BottomNavigationBar(
      items: [
        BottomNavigationBarItem(
          icon: Icon(Icons.home),
          label: "Home",
        ),
        BottomNavigationBarItem(
          icon: Icon(Icons.business),
          label: "Business",
        ),
        BottomNavigationBarItem(
          icon: Icon(Icons.school),
          label: "School",
        ),
      ],
      currentIndex: _selectedIndex,
      selectedItemColor: Colors.amber[800],
      onTap: (int index) => setState(() {
          _selectedIndex = index;
      })
    )
  );
}

エラー内容

The following assertion was thrown while looking for parent data.:
Incorrect use of ParentDataWidget.
The following ParentDataWidgets are providing parent data to the same RenderObject:
- KeepAlive(keepAlive: false) (typically placed directly inside a SliverWithKeepAliveWidget widget)
- LayoutId-[<_ScaffoldSlot.body>](id: _ScaffoldSlot.body) (typically placed directly inside a
CustomMultiChildLayout widget)

どうやら KeepAlive は SliverWithKeepAliveWidget の中でしか使えないらしい

調査2

  • SliverWithKeepAliveWidget は公式ドキュメントによると

    A base class for sliver that have KeepAlive children.

    とあり、いまいち具体的にははっきりしない

  • コードを読む限り、どうやらスクロール系のウィジェットでつかうものらしい

    • SliverMultiBoxAdaptorWidget が SliverWithKeepAliveWidget を継承
    • SliverList, SliverGrid が SliverMultiBoxAdaptorWidget を継承
    • PageView (_PageViewState) の内部でつかっている SliverFillViewport の内部で使っている _SliverFillViewportRenderObjectWidget が SliverMultiBoxAdaptorWidget を継承

つまるところ...?

AutomaticKeepAliveClientMixin はスクローラブルなウィジェットの子要素に対して非表示になっても状態を保持するための仕組みと思われる。

TabBarView が「横スワイプでタブ切り替え可能な UI 」なので、実装上スクローラブルで、そのため AutomaticKeepAliveClientMixin が利用できる、ということだと思われる。なので TabBarView じゃなくてもいいよ(スワイプでタブを切り替えしなくていいよ)という場合には別の方法を使う必要がある。

(一般的に?は BottomNavigationBar は IndexedStack と合わせて使うのがいいらしい

所感

仮に TabBarView を使う場合でも AutomaticKeepAlive は「たまたまできるだけ」という雰囲気があり、アンドキュメンテッドな方法より別の方法を取った方がいいんではというお気持ちはあるが、実際に他の別の方法があるかは不明。

このスクラップは2023/11/10にクローズされました