AppBar, Drawer, BottomNavigation — 앱 껍데기 만들기

앱의 전체적인 껍데기(shell)를 구성하는 위젯들입니다. AppBar는 상단 바, Drawer는 사이드 메뉴, BottomNavigationBar는 하단 탭을 담당합니다.


AppBar

DART
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)

DART
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 — 사이드 메뉴

DART
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)

DART
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 — 하단 탭

DART
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)

DART
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을 사용하면 모든 탭의 상태를 유지할 수 있습니다.

DART
Scaffold(
  body: IndexedStack(
    index: _currentIndex,
    children: const [
      HomeTab(),     // 모든 탭이 메모리에 유지됨
      SearchTab(),
      ProfileTab(),
    ],
  ),
  bottomNavigationBar: NavigationBar(...),
)

면접 포인트: IndexedStack은 모든 자식 위젯을 한 번에 빌드하고 메모리에 유지합니다. 탭 수가 적을 때는 좋지만, 탭이 많거나 무거운 위젯이 있으면 메모리 문제가 발생할 수 있습니다.


TabBar + TabBarView

DART
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로 스와이프 가능한 탭을 만들 수 있습니다
댓글 로딩 중...