AppBar, Drawer, BottomNavigation — 앱 껍데기 만들기
AppBar, Drawer, BottomNavigation — 앱 껍데기 만들기
앱의 전체적인 껍데기(shell)를 구성하는 위젯들입니다. AppBar는 상단 바, Drawer는 사이드 메뉴, BottomNavigationBar는 하단 탭을 담당합니다.
AppBar
Scaffold(
appBar: AppBar(
// 좌측 아이콘 (기본: 뒤로가기 또는 메뉴)
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: () {},
),
// 제목
title: const Text('앱 제목'),
centerTitle: true, // 제목 중앙 정렬
// 우측 액션 버튼들
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.notifications),
onPressed: () {},
),
PopupMenuButton<String>(
onSelected: (value) => print(value),
itemBuilder: (context) => [
const PopupMenuItem(value: 'settings', child: Text('설정')),
const PopupMenuItem(value: 'logout', child: Text('로그아웃')),
],
),
],
// 스크롤 시 그림자
elevation: 0,
scrolledUnderElevation: 2,
// 하단에 TabBar 추가 가능
bottom: const TabBar(
tabs: [
Tab(text: '탭1'),
Tab(text: '탭2'),
Tab(text: '탭3'),
],
),
),
)
SliverAppBar (스크롤에 반응하는 AppBar)
CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
floating: false,
pinned: true, // 스크롤해도 AppBar가 고정됨
flexibleSpace: FlexibleSpaceBar(
title: const Text('유연한 AppBar'),
background: Image.network(
'https://example.com/header.jpg',
fit: BoxFit.cover,
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('아이템 $index')),
childCount: 50,
),
),
],
)
Drawer — 사이드 메뉴
Scaffold(
appBar: AppBar(title: const Text('Drawer 예제')),
// Drawer가 있으면 AppBar 좌측에 자동으로 메뉴 아이콘 표시
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
// 상단 프로필 영역
const DrawerHeader(
decoration: BoxDecoration(color: Colors.blue),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
children: [
CircleAvatar(
radius: 30,
child: Icon(Icons.person, size: 30),
),
SizedBox(height: 8),
Text(
'심정훈',
style: TextStyle(color: Colors.white, fontSize: 18),
),
Text(
'simjunghun@email.com',
style: TextStyle(color: Colors.white70),
),
],
),
),
// 메뉴 항목들
ListTile(
leading: const Icon(Icons.home),
title: const Text('홈'),
onTap: () {
Navigator.pop(context); // Drawer 닫기
},
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('설정'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, '/settings');
},
),
const Divider(),
ListTile(
leading: const Icon(Icons.logout),
title: const Text('로그아웃'),
onTap: () {},
),
],
),
),
)
NavigationDrawer (Material 3)
NavigationDrawer(
selectedIndex: 0,
onDestinationSelected: (index) {},
children: const [
Padding(
padding: EdgeInsets.fromLTRB(28, 16, 16, 10),
child: Text('메뉴', style: TextStyle(fontWeight: FontWeight.bold)),
),
NavigationDrawerDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: Text('홈'),
),
NavigationDrawerDestination(
icon: Icon(Icons.bookmark_outline),
selectedIcon: Icon(Icons.bookmark),
label: Text('북마크'),
),
],
)
BottomNavigationBar — 하단 탭
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _currentIndex = 0;
// 각 탭의 화면
final List<Widget> _screens = const [
HomeTab(),
SearchTab(),
ProfileTab(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _screens[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
type: BottomNavigationBarType.fixed, // 3개 이하일 때
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
activeIcon: Icon(Icons.home_filled),
label: '홈',
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: '검색',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
activeIcon: Icon(Icons.person),
label: '프로필',
),
],
),
);
}
}
NavigationBar (Material 3)
NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) {
setState(() => _currentIndex = index);
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: '홈',
),
NavigationDestination(
icon: Icon(Icons.search),
label: '검색',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: '프로필',
),
],
)
IndexedStack으로 탭 상태 유지
기본적으로 탭을 전환하면 이전 탭의 상태가 초기화됩니다. IndexedStack을 사용하면 모든 탭의 상태를 유지할 수 있습니다.
Scaffold(
body: IndexedStack(
index: _currentIndex,
children: const [
HomeTab(), // 모든 탭이 메모리에 유지됨
SearchTab(),
ProfileTab(),
],
),
bottomNavigationBar: NavigationBar(...),
)
면접 포인트: IndexedStack은 모든 자식 위젯을 한 번에 빌드하고 메모리에 유지합니다. 탭 수가 적을 때는 좋지만, 탭이 많거나 무거운 위젯이 있으면 메모리 문제가 발생할 수 있습니다.
TabBar + TabBarView
class TabExample extends StatelessWidget {
const TabExample({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('탭 예제'),
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car), text: '자동차'),
Tab(icon: Icon(Icons.directions_transit), text: '대중교통'),
Tab(icon: Icon(Icons.directions_bike), text: '자전거'),
],
),
),
body: const TabBarView(
children: [
Center(child: Text('자동차 탭')),
Center(child: Text('대중교통 탭')),
Center(child: Text('자전거 탭')),
],
),
),
);
}
}
정리
AppBar는 제목, 액션 버튼, 탭 바를 포함하는 상단 바입니다SliverAppBar는 스크롤에 반응하는 확장/축소 가능한 AppBar입니다Drawer는 왼쪽에서 밀어서 나오는 사이드 메뉴입니다BottomNavigationBar(또는 Material 3의NavigationBar)로 하단 탭을 구성합니다IndexedStack으로 탭 전환 시 상태를 유지할 수 있지만, 메모리 사용량에 주의하세요DefaultTabController+TabBar+TabBarView로 스와이프 가능한 탭을 만들 수 있습니다
댓글 로딩 중...