はじめに
FlutterのBottomNavigationBarはタブバーを使ってページの切り替えを支援するWidgetです。実はこのWidget、ページを切り替えるときにいちいち初期化します。スクロールしていた位置などを記憶したいときには手を加える必要があります。PageStoreやAutomaticKeepAliveClientMixinを使う方法などがあるようですが、今回はその中で最も手軽であろうIndexedStackを使って対処する方法を共有します。IndexedStackの詳細は公式ページを参考にするのがおすすめです。
https://api.flutter.dev/flutter/widgets/IndexedStack-class.html
※追記あり
コードその1でWidgetによってはエラーが生じる場合があります。
inheritFromWidgetOfExactType() or inheritFromElement() was called before initState() completed
というような内容です。その場合はinitStateの外で_pageListにWidgetを追加する必要があります。追加するWidgetがStatelessWidgetであれば簡単に解決できます。コードその2がその方法です。
StatefulWidgetでもScoped Modelを使うとStatelessWidgetにすることができます。あとはコードその2と同じように書けば解決できます。書いたものがコードその3となります。
コードその1
3つのタブとそれぞれのページでListViewを表示するコードです。
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Named Routes Demo',
initialRoute: '/',
routes: {
'/': (context) => MainPage(),
},
theme: ThemeData(
primarySwatch: Colors.deepOrange,
accentColor: Colors.deepOrangeAccent
),
));
}
class MainPage extends StatefulWidget{
@override
State<StatefulWidget> createState() => MainState();
}
class MainState extends State<MainPage>{
int _selectedIndex=0;
List<Widget> _pageList=[];
@override
void initState(){
super.initState();
_pageList.add(
ListView.builder(
itemBuilder: (context,index){
return Text('aaa $index');
},
)
);
_pageList.add(
ListView.builder(
itemBuilder: (context,index){
return Text('bb $index');
},
),
);
_pageList.add(
ListView.builder(
itemBuilder: (context,index){
return Text('c $index');
},
)
);
}
@override
Widget build(context){
return Scaffold(
body:IndexedStack(
index: _selectedIndex,
children:_pageList,
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
title: Text('A'),
icon: Icon(Icons.home)
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
title: Text('B'),
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
title: Text('C'),
),
],
selectedItemColor: Colors.amber[300],
currentIndex: _selectedIndex,
onTap: (int index){
setState(() {
_selectedIndex=index;
});
},
),
);
}
}
コードその2
initStateでWidgetの追加をしないようにしたものです。StatelessWidgetしか追加できない難点があります。
void main() {
runApp(MaterialApp(
title: 'Named Routes Demo',
initialRoute: '/',
routes: {
'/': (context) => MainPage(),
},
theme: ThemeData(
primarySwatch: Colors.deepOrange,
accentColor: Colors.deepOrangeAccent
),
));
}
class MainPage extends StatefulWidget{
@override
State<StatefulWidget> createState() => MainState();
}
class MainState extends State<MainPage>{
int _selectedIndex=0;
List<Widget> _pageList=[PageA(),PageB(),PageC()];
@override
Widget build(context){
return Scaffold(
body:IndexedStack(
index: _selectedIndex,
children:_pageList,
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
title: Text('A'),
icon: Icon(Icons.home)
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
title: Text('B'),
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
title: Text('C'),
),
],
selectedItemColor: Colors.amber[300],
currentIndex: _selectedIndex,
onTap: (int index){
setState(() {
_selectedIndex=index;
});
},
),
);
}
}
class PageA extends StatelessWidget{
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context,index){
return Text('A $index');
},
);
}
}
class PageB extends StatelessWidget{
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context,index){
return Text('B $index');
},
);
}
}
class PageC extends StatelessWidget{
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context,index){
return Text('C $index');
},
);
}
}
コードその3
コードその2をScoped ModelにしてStatefulWidgetにも対応できるようにしたものです。この例では恩恵が分かりにくいかもしれません。StatefulWidgetも追加するときに効果を発揮してくれるはずです。個人的には3つの中で1番おすすめです。
class MainPage extends StatelessWidget{
final List<Widget> _pageList=[PageA(),PageB(),PageC()];
@override
Widget build(BuildContext context) {
return ScopedModelDescendant<IndexModel>(
builder: (context,child,model){
return Scaffold(
body:IndexedStack(
index: model.selectedIndex,
children: _pageList,
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
title: Text('A'),
icon: Icon(Icons.home)
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
title: Text('B'),
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
title: Text('C'),
),
],
selectedItemColor: Colors.amber[300],
currentIndex: model.selectedIndex,
onTap: (int index){
model.selectedIndex=index;
},
),
);
}
);
}
}
class PageA extends StatelessWidget{
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context,index){
return Text('A $index');
},
);
}
}
class PageB extends StatelessWidget{
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context,index){
return Text('B $index');
},
);
}
}
class PageC extends StatelessWidget{
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (context,index){
return Text('C $index');
},
);
}
}
class IndexModel extends Model {
int _selectedIndex = 0;
get selectedIndex=>_selectedIndex;
set selectedIndex(value){
_selectedIndex=value;
notifyListeners();
}
}