こんにちは、株式会社Pentagonでアプリ開発をしている石渡港です。
https://pentagon.tokyo
今回は、FlutterでGetXを利用してBottomNavigatonのタブごとにナビゲーションを切り分ける方法について調査しました。
その結果、Get.nestedKey
とGet.toNamed
にIdを与えると、実現できることがわかりました。
また、Get.argment
の取得の際にGetPageRoute
のsettings
にNavigator
のonGenerateRoute
で取得するsettings
を設定する必要があります。今回の記事のソースコードには実装していませんが、同一ナビゲーション内で戻る場合に、WillPopScope
を利用してGet.back
する際はidを渡す必要があるようです。
【この記事を読むメリット】
- FlutterでGetXを利用して、下記の画像のようなナビゲーションを切り分ける方法がわかります。
【こんな方に参考にしていただきたい】
- FlutterでGetXを利用している人。
【調査の動機】
Flutterで普段利用しているGetXを利用して、タブごとのナビゲーションを切り分ける方法を知りたかったため。
【調査結果】
- Get.nestedKeyを
Navigator
に設定する必要があります。 Get.argments
を取得するためには、GetPageRoute
のsettings
にNavigator
のonGenerateRoute
で取得するsettings
の設定が必須です。Get.toNamed
などで遷移する際にidを渡さなければなりません。また、id
を指定しない場合、BottomNavigaton
が存在しているナビゲーション上で遷移することがわかりました。- 今回の記事のソースコードには実装していませんが、同一ナビゲーション内で戻る場合に、
WillPopScopeを
利用してGet.back
する際はidを渡す必要があるようです。
【結論】
GetXを利用してBottomNavigatonのタブごとにナビゲーションを切り分ける方法
gifで示すようにナビゲーションを切り分けられます。
以下のソースコードを書くことで実装できます。
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'タブごとのナビゲーション切り替え',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const BottomNavigationScreen(),
);
}
}
class BottomNavigationScreen extends StatefulWidget {
const BottomNavigationScreen({Key? key}) : super(key: key);
@override
_BottomNavigationScreenState createState() => _BottomNavigationScreenState();
}
class _BottomNavigationScreenState extends State<BottomNavigationScreen> {
int _selectedIndex = 0;
static final List<Widget> _pageList = [
const HomeNavigator(
key: PageStorageKey<String>("home"),
),
const SettingsNavigator(
key: PageStorageKey<String>("setting"),
),
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: _pageList.map((e) {
final idx = _pageList.indexOf(e);
// タブ切り替え時に毎回再読み込みが発生する為に、非表示で保持する
return Visibility(
visible: _selectedIndex == idx,
maintainState: true,
child: e,
);
}).toList(),
),
bottomNavigationBar: BottomNavigationBar(
items: [
const BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'ホーム',
),
const BottomNavigationBarItem(
icon: Icon(
Icons.menu_outlined,
),
label: '設定',
),
],
currentIndex: _selectedIndex,
onTap: _onItemTapped,
),
);
}
}
final homeId = 1;
class HomeNavigator extends StatelessWidget {
const HomeNavigator({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Navigator(
key: Get.nestedKey(homeId),
initialRoute: "/",
onGenerateRoute: (settings) {
switch (settings.name) {
case "/":
return GetPageRoute<dynamic>(
settings: settings,
routeName: "/",
page: () => const HomeScreen(),
);
case "/a_screen":
Get.routing.args = settings.arguments;
return GetPageRoute<dynamic>(
settings: settings,
routeName: "a_screen",
page: () => const CommonScreen(),
);
default:
return GetPageRoute<dynamic>(
settings: settings,
routeName: "/progress",
page: () => CircularProgressIndicator(),
);
}
},
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ホーム'),
),
body: SafeArea(
child: Center(
child: MaterialButton(
child: const Text('Aスクリーンへ'),
onPressed: () {
Get.toNamed(
'/a_screen',
id: homeId,
arguments: 'Aスクリーン',
);
},
),
),
),
);
}
}
class CommonScreen extends StatelessWidget {
const CommonScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final title = Get.arguments as String;
return Scaffold(
appBar: AppBar(
title: Text(title),
),
);
}
}
final settingsId = 2;
class SettingsNavigator extends StatelessWidget {
const SettingsNavigator({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Navigator(
key: Get.nestedKey(settingsId),
initialRoute: "/",
onGenerateRoute: (settings) {
switch (settings.name) {
case "/":
return GetPageRoute<dynamic>(
settings: settings,
routeName: "/",
page: () => const SettingsScreen(),
);
case "/b_screen":
Get.routing.args = settings.arguments;
return GetPageRoute<dynamic>(
settings: settings,
routeName: "b_screen",
page: () => const CommonScreen(),
);
default:
return GetPageRoute<dynamic>(
settings: settings,
routeName: "/progress",
page: () => CircularProgressIndicator(),
);
}
},
);
}
}
class SettingsScreen extends StatelessWidget {
const SettingsScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('設定'),
),
body: SafeArea(
child: Center(
child: MaterialButton(
child: const Text('Bスクリーンへ'),
onPressed: () {
Get.toNamed(
'/b_screen',
id: settingsId,
arguments: 'Bスクリーン',
);
},
),
),
),
);
}
}
Navigatorの設定方法
key: Get.nestedKey(homeId),
でNavigator
のid
とkey
を設定します。
また、Get.argments
で変数を受け渡したい場合は下記のような設定が必要になります。
...抜粋
return GetPageRoute<dynamic>(
settings: settings,
...抜粋
...省略
final homeId = 1;
class HomeNavigator extends StatelessWidget {
const HomeNavigator({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Navigator(
key: Get.nestedKey(homeId),
initialRoute: "/",
onGenerateRoute: (settings) {
switch (settings.name) {
case "/":
return GetPageRoute<dynamic>(
settings: settings,
routeName: "/",
page: () => const HomeScreen(),
);
case "/a_screen":
Get.routing.args = settings.arguments;
return GetPageRoute<dynamic>(
settings: settings,
routeName: "a_screen",
page: () => const CommonScreen(),
);
default:
return GetPageRoute<dynamic>(
settings: settings,
routeName: "/progress",
page: () => CircularProgressIndicator(),
);
}
},
);
}
}
遷移の方法
下記のようにGet.toNamed
のid
を指定することで、Navigator
を指定したタブ内で遷移できます。
...抜粋
Get.toNamed(
'/a_screen',
id: homeId,
...抜粋
...省略
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ホーム'),
),
body: SafeArea(
child: Center(
child: MaterialButton(
child: const Text('Aスクリーンへ'),
onPressed: () {
Get.toNamed(
'/a_screen',
id: homeId,
arguments: 'Aスクリーン',
);
},
),
),
),
);
}
}
...省略
また、ナビゲーションを指定してGet.back
する際に、WillPopScope
を利用している場合は下記のように指定してください。
...省略
WillPopScope(
onWillPop: () async {
Get.back<void>(id: homeId);
return false;
},
...省略
【まとめ】
Flutterで普段利用しているGetXを利用して、タブごとのナビゲーションを切り分ける方法がわかりました。
nestedKeyやID、settingを利用することで容易に実装できました。
Getを利用するパターンの方法が、日本語のリファレンスだと少なかったので参考になれば嬉しいです。
この方法で、UXがよくなると思うので、うまく利用していきたいです。