티스토리 뷰
https://medium.com/@theboringdeveloper/common-bottom-navigation-bar-flutter-e3693305d2d
해결하고 싶은 문제점
플러터에서 BottomNavigationBar를 사용하고 있는데, 내비게이션으로 화면 이동시 BottomNavigationBar가 사라진다. 그래서 화면을 이동해도 BottomNavigationBar를 계속 사용할 수 있는 방법에 대해서 구글링하였다.
이 문제를 해결하기 위해서는 플러터에서 route가 무엇인지 Navigator가 무엇인지 제대로 알고 있어야 한다.
문제가 되는 코드
먼저 화면을 이동하기 위해서 버튼을 클릭 시 아래 코드가 실행되게 했습니다.
onPressed: () {
Navigator.push(context, MaterialPageRoute(
builder: (context) => Screen2()
));
},
공식 문서에서 Navigator.push 메서드는 주어진 route를 주어진 context가 가지는 Navigator에 푸시한다고 설명되어 있다.
route는 무엇인가?
route는 플러터에서 화면(또는 페이지)라고 할 수 있다. 플러터에서 route는 그저 위젯이다. Navigator를 사용하여 새 route로 이동한다.
Navigator란 무엇인가?
Navigator는 스택 방식으로 route를 관리하는 위젯이다. Navigator는 route 객체를 스택으로 관리하면서 Navigator.push와 Navigator.pop과 같은 메서들을 사용하여 스택처럼 관리하는 기능을 제공한다. 그래서 Navigator의 최상단에 있는 route가 현재 화면이 되는 것이다.
직접 Navigator를 생성할 수도 있지만, 대부분 WidgetsApp 또는 MaterialApp 위젯에 의해 생성되는 Navigator를 사용하는 것이 일반적이다.
MaterialApp은 Navigator 관점에서 어떻게 사용되는가?
MaterialApp의 home은 Navigator 스택의 하단에 있는 route가 된다. 동시에 앱이 시작될 때 보이는 화면이 된다.
Navigator는 어떻게 사용하는가?
Navigator는 Navigator.of를 사용하여 참조할 수 있다. 하지만 보통 Navigator.of 대신 Navigator.push를 사용한다. 그러나 Navigator.push 메서드의 구현을 확인해 보면, 내부에서는 Navigator.of가 사용되는 것을 확인할 수 있다.
of 메서드는 무엇을 하는가?
주어진 context가 가지는 가장 가까운 해당 클래스의 상태를 반환한다. Navigator.of(context)는 context에 가장 가까운 Navigator를 반환하게 되는 것이다.
따라서 Navigator.push()는 context에서 가장 가까운 Navigator에 주어진 route를 푸시해서 현재 화면이 되게 하는 것이다. 만약 Navigator를 따로 사용하지 않았다면, Navigator.of()는 MaterialApp의 Navigator가 될 것이다.
공용 BottomNavigationBar 만들기
지금까지 내용을 정리하면, BottomNavigationBar를 사용하면서 Navigator.push()를 하면 MaterialApp의 Navigator가 사용되기 때문에 BottomNaivgationBar가 사라지게 되는 것을 알 수 있다. 그래서 공용 BottomNavigationBar를 만들기 위해서는 MaterialApp에서 제공하는 Navigator가 아닌 각 항목이 Navigator를 가져야 한다는 것을 알 수 있다.
BottomNavigationBar의 항목에 화면이 이동이 있다면 항목에 Navigator를 추가해주어야 한다.
그리고 WillpopScope로 Scaffold를 감싸서 시스템 백 버튼이 눌러져도 Navigator가 작동할 수 있도록 코드를 추가해야 한다. 이 작업을 하지 않으면 시스템 백 버튼이 눌러지면 앱이 종료되어 버린다.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MainPage(),
);
}
}
class Destination {
final int index;
final Widget child;
const Destination(this.index, this.child);
}
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _selectedIndex = 0;
static final List<GlobalKey<NavigatorState>?> _navigatorKeys = [
GlobalKey(),
null,
];
static final List<Destination> _rootPages = [
Destination(0, HomeRootPage(navigationKey: _navigatorKeys[0]!)),
const Destination(1, BushinessRootPage()),
];
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
final NavigatorState? navigator =
_navigatorKeys[_selectedIndex]?.currentState;
if (navigator == null) {
return true;
}
if (!navigator.canPop()) {
return true;
}
navigator.pop();
return false;
},
child: Scaffold(
body: Stack(
fit: StackFit.expand,
children: _rootPages.map((destination) {
if (destination.index == _selectedIndex) {
return Offstage(
offstage: false,
child: destination.child,
);
} else {
return Offstage(
child: destination.child,
);
}
}).toList(),
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
label: 'Business',
),
],
currentIndex: _selectedIndex,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
),
),
);
}
}
class HomeRootPage extends StatelessWidget {
const HomeRootPage({
super.key,
required this.navigationKey,
});
final Key navigationKey;
@override
Widget build(BuildContext context) {
return Navigator(
key: navigationKey,
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) {
switch (settings.name) {
case '/':
return const FirstPage();
case '/second':
return const SecondPage();
}
return const SizedBox();
});
},
);
}
}
class FirstPage extends StatelessWidget {
const FirstPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("First Page")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("HI"),
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/second');
},
child: const Text("Go to Second page"),
),
],
),
),
);
}
}
class SecondPage extends StatelessWidget {
const SecondPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Second Page")),
bottomNavigationBar: const Center(
child: Text("HI"),
),
);
}
}
class BushinessRootPage extends StatelessWidget {
const BushinessRootPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Bushiness Page")),
bottomNavigationBar: const Center(
child: Text("HI"),
),
);
}
}