목차
- persistent_bottom_nav_bar 라이브러리
- 직접 구현하기
- 기존 코드
- Navigator 추가하기
- WillPopScope 추가하기
- IndexedStack 추가하기
- AppBar 구현하기
ui를 구성하다보면 BottomNavigationBar를 유지한 채로 다른 페이지로 라우팅 하고 싶은 경우가 있다.
'플라토'탭에서 강좌를 이름을 눌러 강좌 페이지로 이동하고 싶은데 Navigator.push를 이용하여 페이지를 불러올 경우 위 그림과 같이 BottmNavigationBar가 사라지게 된다.
임시방편으로 모든 페이지에 BottomNavigationBar를 추가하게 될 경우, 에니메이션을 제거한다고 하더라도 정상적으로 네비게이션바가 동작하지 않게 된다.
해결방법으로는 직접 구현하는 방법과 라이브러리를 쓰는 방법이 있다.
1. persistent_bottom_nav_bar 라이브러리
내부적으로 CupertinoTabScffold와 CupertinoTabView을 이용하여 구현된 라이브러리다.
import 'package:persistent_bottom_nav_bar/persistent-tab-view.dart';
를 통해 import하고,
final _pages = [const PlatoPage(), const CalendarPage(), const ChatPage()];
return PersistentTabView(
context,
controller: _controller,
screens: _pages ,
items: [
PersistentBottomNavBarItem(
icon: const Icon(Icons.home),
activeColorPrimary: Get.theme.primaryColor,
inactiveColorPrimary: Get.theme.disabledColor,
title: '플라토',
),
PersistentBottomNavBarItem(
icon: const Icon(Icons.calendar_today_outlined),
activeColorPrimary: Get.theme.primaryColor,
inactiveColorPrimary: Get.theme.disabledColor,
title: '캘린더',
),
PersistentBottomNavBarItem(
icon: const Icon(Icons.mail),
activeColorPrimary: Get.theme.primaryColor,
inactiveColorPrimary: Get.theme.disabledColor,
title: '쪽지',
),
],
navBarStyle: NavBarStyle.style3,
);
를 return 해주면 된다. 기본적으로 Scaffold가 내장되어 있으므로, 기존의 Scaffold를 삭제하고, 해당 widget을 그 위치로 넣어주면 된다.
한 가지 문제점은 appbar와 drawer를 사용할 수 없다. screens에 연결되어 있는 page들이 scaffold를 갖게 하여 appbar와 drawer를 갖게 구현해야한다.
List<Widget> _drawPage() {
return List.generate(_pages.length, (index) {
return Scaffold(
appBar: AppBar(
elevation: 0.0,
),
drawer: const MainDrawer(),
body: FutureBuilder(
future: _loginFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return _pages[index];
}
return const LoadingPage(msg: '로그인 중...');
}),
);
});
}
따라서, 위와 같은 메소드를 하나 선언하고,
Widget build(BuildContext context) {
return PersistentTabView(
context,
controller: _controller,
screens: _drawPage(),
items: [ ...
처럼 screens에 _drawPage()를 넘겨주어 appbar와 drawer를 구현가능하다.
2. 직접 구현
2-1. 기존 코드
라이브러리를 쓰거나, CupertinoScaffold를 사용하여 구현할 경우 구현이 간단하지만, 화면 전환 애니메이션이 Cupertino Style로 뜨기 때문에 뭔가... 어색하다.
그래서 기존의 Scaffold 위젯과 Navigator위젯을 활용하여 구현하는 방법을 찾아보았고, 개발하는남자님의 유튜브를 통해 구현해보았다.
먼저, 치명적인 단점이 있는데, GetX를 이용한 Get.to()나 Navigator.pushNamed() 사용이 불가능하다. Navigator.push((context) => Material....)를 사용해야한다.
class Root extends StatefulWidget {
const Root({Key? key}) : super(key: key);
@override
State<Root> createState() => _RootState();
}
class _RootState extends State<Root> {
int _currentIndex = 0;
final _pages = const [Home(), Explore(), Setting()];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text('Nested Route Sample'),
),
body: _pages[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: const [
BottomNavigationBarItem(
icon: Icon(
Icons.home,
),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(
Icons.explore,
),
label: 'Explore',
),
BottomNavigationBarItem(
icon: Icon(
Icons.settings,
),
label: 'Search',
),
],
),
);
}
}
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: TextButton(
child: const Text('Home'),
onPressed: () => Get.to(const SecondPage()),
)
);
}
}
class Explore extends StatelessWidget {
const Explore({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: TextButton(
child: const Text('Explore'),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SecondPage())),
)
);
}
}
class Setting extends StatelessWidget {
const Setting({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: TextButton(
child: const Text('Setting'),
onPressed: () => Navigator.pushNamed(context, '/second'),
)
);
}
}
먼저, 기존의 일반적인 BottomNavigationBar와 Page전환 코드이다. 실행을 해보면,
이렇게, BottomNavigationBar와 AppBar가 모두 덮어져버린다.
2-2. Navigator 추가하기
body: Navigator(
onGenerateRoute: (routeSettings) {
return MaterialPageRoute(
builder: (context) => _pages[_currentIndex],
);
},
),
여기서 최상한 Scaffold의 body를 Navigator 위젯으로 감싸주기만 해주면 원하는대로 동작이 된다.
위 동영상처럼 Get.to를 사용할 경우 원래의 라우팅대로 구현되며, Navigator.push를 사용할 경우 원하는 동작이 구현 된다. 마지막으로 Navigator.pushNamed는 왜 인지 모르겠으나 똑같은 화면인 Setting이 계속 위에 쌓이게 된다.
2-3. WillPopScope 추가하기
그리고 여기서 한가지 문제가 발생하는데,
이렇게, 다음 페이지가 열렸음에도 불구하고 뒤로가기 키를 누를 경우 앱이 바로 종료되게 된다. 이를 방지하기 위해서는 WillPopScope 위젯을 Scaffold에 감싸주어 Scaffold가 pop되려할 때 이벤트를 잡아낼 수 있다.
late List<GlobalKey<NavigatorState>> _navigatorKeyList;
@override
void initState() {
// TODO: implement initState
super.initState();
_navigatorKeyList = List.generate(_pages.length, (index) => GlobalKey<NavigatorState>());
}
return WillPopScope(
onWillPop: () async {
return !(await _navigatorKeyList[_currentIndex].currentState!.maybePop());
},
child: Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text('Nested Route Sample'),
),
body: Navigator(
key: _navigatorKeyList[_currentIndex],
onGenerateRoute: (_) {
return MaterialPageRoute(builder: (context) => _pages[_currentIndex]);
},
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
이렇게 Scaffold를 WillPopScope 위젯으로 감싸주고, onWillPop 함수를 통해 Scaffold가 제거되려할 때 이벤트를 정의내릴 수 있다.
GlobalKey를 이용해 Naviagtor의 state를 가져오고, mayPop메소드를 통해 Scaffold를 pop할 지 결정해야한다. onWillPop의 경우 true를 리턴하게 되면 child를 pop하게 된다.
page가 많아질 경우에 매번 Navigator를 씌워주고 GlobalKey를 생성하는 과정이 번거로워질 수가 있기 때문에 keyList를 자동으로 생성해주도록 만들어주었다. 근데, 코드 가독성은 더 나빠진 것 같기도 하고...
2-4. IndexedStack 추가하기
여기까지만 해주어도 원하는 동작을 구현할 수는 있지만, 탭을 이동해도 page가 남아있게 구현하려면 IndexedStackd을 사용하면 된다.
body: IndexedStack(
index: _currentIndex,
children: _pages.map((page) {
int index = _pages.indexOf(page);
return Navigator(
key: _navigatorKeyList[index],
onGenerateRoute: (_) {
return MaterialPageRoute(builder: (context) => page);
},
);
}).toList(),
),
최종 코드 및 동작
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() => runApp(const TestApp());
class TestApp extends StatelessWidget {
const TestApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Flutter Demo',
home: const Root(),
routes: {
'/second': (context) => const SecondPage(),
}
);
}
}
class Root extends StatefulWidget {
const Root({Key? key}) : super(key: key);
@override
State<Root> createState() => _RootState();
}
class _RootState extends State<Root> {
int _currentIndex = 0;
final _pages = const [Home(), Explore(), Setting()];
late List<GlobalKey<NavigatorState>> _navigatorKeyList;
@override
void initState() {
// TODO: implement initState
super.initState();
_navigatorKeyList = List.generate(_pages.length, (index) => GlobalKey<NavigatorState>());
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
return !(await _navigatorKeyList[_currentIndex].currentState!.maybePop());
},
child: Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text('Nested Route Sample'),
),
body: IndexedStack(
index: _currentIndex,
children: _pages.map((page) {
int index = _pages.indexOf(page);
return Navigator(
key: _navigatorKeyList[index],
onGenerateRoute: (_) {
return MaterialPageRoute(builder: (context) => page);
},
);
}).toList(),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: const [
BottomNavigationBarItem(
icon: Icon(
Icons.home,
),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(
Icons.explore,
),
label: 'Explore',
),
BottomNavigationBarItem(
icon: Icon(
Icons.settings,
),
label: 'Search',
),
],
),
),
);
}
}
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: TextButton(
child: const Text('Home'),
onPressed: () => Get.to(const SecondPage()),
)
);
}
}
class Explore extends StatelessWidget {
const Explore({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: TextButton(
child: const Text('Explore'),
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => SecondPage())),
)
);
}
}
class Setting extends StatelessWidget {
const Setting({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: TextButton(
child: const Text('Setting'),
onPressed: () => Navigator.pushNamed(context, '/second'),
)
);
}
}
class SecondPage extends StatelessWidget {
const SecondPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Center(child: Text('secondPage'));
}
}