[調査][Flutter] AutomaticKeepAliveClientMixin で状態が保持されるのはなぜなのか
導入
TabBar
+ TabBarView
でタブを切り替える場合、何もしないとタブ内ページの状態がリセットされる。状態を保持する方法の一つとして AutomaticKeepAliveClientMixin
がある。
BottomNavigationBar
のタブ切り替えでも同じように使えるものと思ったがコードを書いてみると状態が保持されない、ということで理由を調べる。
(参考)
調査1
-
AutomaticKeepAliveClientMixin について、公式ドキュメント によると以下のことが書いてある
-
AutomaticKeepAliveClientMixin
がKeepAliveNotification
を送信する -
KeepAliveNotification
はAutomaticKeepAlive
が受け取る -
ListView
,GridView
,SliverList
,SliverGrid
の子要素は自動的にAutomaticKeepAlive
ウィジェットが追加される - ( AutomaticKeepAliveClientMixin, KeepAliveNotification , AutomaticKeepAlive より
-
-
では
TabBarView
でもAutomaticKeepAlive
を子要素に追加しているかというと、ドキュメントには特に記載がない -
仕方ないのでコードを読むと、
TabBarView
はStatefulWidget
で、 _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
は「たまたまできるだけ」という雰囲気があり、アンドキュメンテッドな方法より別の方法を取った方がいいんではというお気持ちはあるが、実際に他の別の方法があるかは不明。