タブを利用は、マテリアルデザインガイドラインに従ったアプリの一般的なパターンです。Flutterにはマテリアルライブラリの一部として、タブレイアウトを生成する便利な方法が用意されています。
Cupertino appでタブを利用するには、Building a Cupertino app with Flutter コードラボを見てみましょう。
ここでは以下の手順でタブを使ったサンプルを見ていきます。
- TabControllerを生成する。
- タブを生成する。
- それぞれのタブに対応するコンテンツを生成する。
Contents
1.TabControllerを生成する。
タブを利用するためには、選択されているタブと、そのタブに対応するコンテンツを同期的に保持する必要があります。その仕事をするのがTabControllerクラスです。
TabControllerインスタンスを自分で生成する方法と、DefaultTabControllerウィジェットを利用して自動的に生成する方法があります。
DefaultTabControllerウィジェットはTabControllerを生成し、自身(DefaultTabControllerウィジェット)の子孫ウィジェットがそのTabControllerを使用できるようにするので、DefaultTabControllerウィジェットを利用するのはシンプルな選択肢です。
DefaultTabController( // lengthパラメータに、表示するタブの個数を指定する。 length: 3, child: // 次のセクションで実装する。 );
2.タブを生成する。
特定のタブが選択されると、そのタブに対応するコンテンツを表示する必要があります。タブを生成するにはTabBarウィジェットを使います。このサンプルでは、三つのTabウィジェットを配置したTabBarウィジェットを生成し、それをAppBarウィジェット内に配置します。
DefaultTabController( length: 3, child: Scaffold( appBar: AppBar( bottom: TabBar( tabs: [ Tab(icon: Icon(Icons.directions_car)), Tab(icon: Icon(Icons.directions_transit)), Tab(icon: Icon(Icons.directions_bike)), ], ), ), ), );
デフォルトでは、TabBarウィジェットはウィジェットツリーを見上げて、直近のDefaulTabControllerを参照します。
自分でTabControllerを生成する場合は、TabBarに渡す必要があります。
3.それぞれのタブに対応するコンテンツを生成する。
タブができたので、タブが選択された時に、対応するコンテンツを表示するようにします。そのためにTabBarViewウィジェットを使います。
Note
タブの順番と、TabBarViewで指定するコンテンツの順番が対応するように配置する必要があります。
import 'package:flutter/material.dart'; void main() { runApp(TabBarDemo()); } class TabBarDemo extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: DefaultTabController( length: 3, child: Scaffold( appBar: AppBar( bottom: TabBar( tabs: [ Tab(icon: Icon(Icons.directions_car)), Tab(icon: Icon(Icons.directions_transit)), Tab(icon: Icon(Icons.directions_bike)), ], ), title: Text('Tabs Demo'), ), body: TabBarView( children: [ Icon(Icons.directions_car), Icon(Icons.directions_transit), Icon(Icons.directions_bike), ], ), ), ), ); } }
上記のような使い方に従えば簡単にタブが使える、ということですね。
自分でTabControllerを作って問題なく動くようにするとなると、一気に必要となる知識が増える、という感じです(笑)
Flutterの根幹に近い技術だと思うので、知っといた方が良いのでしょうけど。
それはそれとして、ただタブを設置して利用したいだけならDefaultTabControllerを使っとくのがいいような気がします。
参照
https://flutter.dev/docs/cookbook/design/tabs
基本的に上記の解説とほぼ同じ内容。
//bottomNavigationBarフィールドにBottomAppBarを置き、 //そのchildにTabBarを設置したパターン。 //構図としてあまり見ない気がする。 import 'package:flutter/material.dart'; void main() { runApp(TabBarDemo()); } class TabBarDemo extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: DefaultTabController( length: 3, child: Scaffold( appBar: AppBar( /* bottom: TabBar( indicatorColor: Colors.deepOrange, labelColor:Colors.deepOrange, tabs: [ Tab(icon: Icon(Icons.directions_car)), Tab(icon: Icon(Icons.directions_transit)), Tab(icon: Icon(Icons.directions_bike)), ], ), */ title: Text('Tabs Demo'), ), body: TabBarView( children: [ Icon(Icons.directions_car), Icon(Icons.directions_transit), Icon(Icons.directions_bike), ], ), bottomNavigationBar: BottomAppBar( color: Theme .of(context) .primaryColor, notchMargin: 6.0, shape: AutomaticNotchedShape( RoundedRectangleBorder(), StadiumBorder( side: BorderSide(), ), ), child: SafeArea( child: TabBar( isScrollable: false, indicatorColor: Colors.deepOrange, labelColor: Colors.deepOrange, tabs: [ Tab(icon: Icon(Icons.directions_car),text:"car",), Tab(icon: Icon(Icons.directions_transit,),text:"train",), Tab(icon: Icon(Icons.directions_bike),text:"bike",), ], ), ), ), floatingActionButton: FloatingActionButton( onPressed:(){}, ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,//←(1) ),) , ); } }
↑で説明されていたが、bottomNavigationBarとFABがある場合、
ScaffoldウィジェットのfloatingActionButtonLocationフィールドを設定すれば、配置がいい感じにできますね、という話もある(1)。
↓関連項目としてBottomNavigationBarウィジェットのサンプル。
//https://dev.classmethod.jp/articles/basic_bottom_navigation_flutter/ //↑のサンプルを少し改造。stateを用意して、stateに応じて表示を切り替え。 //改造していて気付いたが、この場合、ページ遷移ではないので、 //状態更新用メソッド(↓の例ではsetN())をバケツリレーしなくて済む。 //状態nはバケツリレーする必要がある。 import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { static const String _title = 'Flutter Code Sample'; @override Widget build(BuildContext context) { return MaterialApp( title: _title, home: HomePage(), ); } } class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int _selectedIndex = 0; int n=0; List<Widget> _pageList()=> [ CustomPage(pannelColor: Colors.cyan, title: 'Home',n:n,), CustomPage(pannelColor: Colors.green, title: 'Settings',n:n,), CustomPage(pannelColor: Colors.pink, title: 'Search',n:n,), ]; void _onItemTapped(int index) { setState(() { _selectedIndex = index; }); } void setN(){ setState((){ n++; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('サンプル1'), ), body: _pageList()[_selectedIndex], bottomNavigationBar: BottomNavigationBar( items: const <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: Icon(Icons.home), title: Text('Home'), ), BottomNavigationBarItem( icon: Icon(Icons.settings), title: Text('Setting'), ), BottomNavigationBarItem( icon: Icon(Icons.search), title: Text('Search'), ), ], currentIndex: _selectedIndex, onTap: _onItemTapped, ), floatingActionButton: FloatingActionButton( onPressed:(){setN();}, ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, ); } } class CustomPage extends StatelessWidget { final Color pannelColor; final String title; final int n; CustomPage({@required this.pannelColor, @required this.title, this.n}); @override Widget build(BuildContext context) { final titleTextStyle = Theme.of(context).textTheme.title; return Container( child: Center( child: Container( width: 200, height: 200, decoration: BoxDecoration( color: pannelColor, borderRadius: BorderRadius.all(Radius.circular(20.0))), child: Center( child: Text( "$title : $n", style: TextStyle( fontSize: titleTextStyle.fontSize, color: titleTextStyle.color, ), ), ), ), ), ); } }
参考
https://dev.classmethod.jp/articles/basic_bottom_navigation_flutter/
クラスメソッド様のサンプル(を少し改造)。
状態(_selectedIndex)を用意して、それが変わったら表示するウィジェットも切り替わる。
というかPageViewはただスワイプできるだけかと思っていたが、クラスメソッドさんの記事に「タブ付きPageView」のサンプルもある。
指摘されている通り、表示の機能だけなら「PageView」、「DefaultTabController+TabBar+TabBarView」とほとんど変わらないので、どれでも良いと思う。
それはそれとして、状態を共有したい場合、_selectedIndexで画面を切り替える場合、ページ遷移ではないので、状態更新用メソッド(上記の例ではsetN())をバケツリレーしなくて済む。
共有する状態nはバケツリレーする必要がある(と思われる)。
もう少し複雑になると、結局「Providerなどで状態管理した方が楽なのでそうすべき」という結論になると思われる。