[Flutter] BottomNavigation 구현하기

0

Flutter

목록 보기
1/1
post-thumbnail

제가 iOS(찍먹), Android 개발만 계속 하다가 Flutter의 세계에 발을 들였던 게 올해 1월로, 어느덧 10여 개월이 흘렀는데 말이죠.
그동안 플러터로 진행한 프로젝트도 2개나(그 중 하나는 무려 Andriod로 출시까지 했다가 Flutter로 넘어갔습니다) 되다보니 작성할 콘텐츠 거리가 꽤나 쌓였습니다. 앞으로는 플러터 포스트도 하나씩 부지런히 풀어가도록 하겠습니다😊

그동안 진행한 프로젝트 마다 바텀네비게이션으로 화면을 이동했기에 바텀네비게이션을 구현할 일이 많았는데요, 생각해보면 진행한 프로젝트에서 90%는 제가 바텀네비를 도맡아 만들었던 것 같습니다. 이 정도면 바텀네비 전문 개발자가 아닌지?
그치만 전 개인적으로 바텀네비를 구현하는 일을 좋아합니다. 한 번 만들어놓으면 바뀔 일이 잘 없긴 하지만, 어찌보면 프로젝트의 시작이 되는 일이니까요. 제가 바텀네비를 작업하고 나면 각 탭 별 화면 담당자를 나눠 작업이 착착 진행되는, 그 지점이 참 좋습니다.

✍🏻 요구사항 분석

앞서 사담이 좀 길었지만.., 이번에는 조금 특이했던 게 아이콘 밑에 label도 함께 표시해 주어야 한다는 점이었습니다. 학교 성적 조회 어플을 만들다보니 공시, 학기, 전체 성적이라는 3가지 각기 다른 성적을 아이콘만으로 구별하기는 좀 애매하더라구요 ^~^

바텀네비게이션이 늘상 그렇듯, 구현해주어야 하는 요구사항은 크게 2가지가 될 것입니다.

  1. 선택한 아이템은 바텀네비에서 흰색으로 표시해주어야 한다.
  2. 아이템이 선택되면, 위에 표시되는 페이지를 변경해주어야 한다.

💻 코드 작성

1️⃣ 아이템 이미지 추가하기

assets > images > menu를 두어 바텀네비 아이콘이 활성화/비활성화 되었을 때의 에셋을 추가해 준다.
Flutter에서는 assets 안에 패키지를 자유롭게 만들 수 있다는 게 큰 장점인 것 같다. (menu 폴더 안에 이미지를 추가할 수 있다는 것에 감명받은 Android 개발자🥹)

그렇지만 이미지 사용을 위해서는 선행되어야 하는 작업이 있다.
pubspec.yamlflutter_svg(svg 이미지를 추가했기 때문에)와 assets의 경로를 추가해 준다.

dependencies:
  flutter:
    sdk: flutter
  flutter_svg: ^2.0.9

# The following section is specific to Flutter packages.
flutter:
  assets:
    - assets/images/
    - assets/images/menu/

바텀네비 아이콘은 menu로 한 번 더 들어가서 저장했기 때문에 assets/images/menu/까지 꼭 추가해 주어야 한다.

바텀네비에 사용될 이미지들은 관리를 손쉽게 하기 위해 svg_icons에 경로를 저장해 주었다.

class SvgIcons {
  /// Menu
  static SvgPicture menuPublic =
      SvgPicture.asset("assets/images/menu/public_grade.svg");
  static SvgPicture menuPublicActive =
      SvgPicture.asset("assets/images/menu/public_grade_active.svg");
  static SvgPicture menuSemester =
      SvgPicture.asset("assets/images/menu/semester_grade.svg");
  static SvgPicture menuSemesterActive =
      SvgPicture.asset("assets/images/menu/semester_grade_active.svg");
  static SvgPicture menuTotal =
      SvgPicture.asset("assets/images/menu/total_grade.svg");
  static SvgPicture menuTotalActive =
      SvgPicture.asset("assets/images/menu/total_grade_active.svg");
  static SvgPicture menuSetting =
      SvgPicture.asset("assets/images/menu/setting.svg");
  static SvgPicture menuSettingActive =
      SvgPicture.asset("assets/images/menu/setting_active.svg");
}

2️⃣ BottomNavigation 코드 작성

BottomNavigationBar 안에서 아이템의 label를 표시해줄지, 배경색은 뭘로 할지, 선택되었을 때와 선택되지 않았을 떄의 label 폰트 크기는 뭘로 할지 등 다양한 옵션을 부여할 수 있다.

 BottomNavigationBar(
              type: BottomNavigationBarType.fixed,
              currentIndex: _tabController.index,
              showSelectedLabels: true,
              showUnselectedLabels: true,
              backgroundColor: ColorStyles.bottomNavBackground,
              selectedItemColor: Colors.white,
              unselectedItemColor: ColorStyles.itemBackground,
              selectedFontSize: 12,
              unselectedFontSize: 12,
              items: <BottomNavigationBarItem>[
                BottomNavigationBarItem(
                    icon: SvgIcons.menuPublic,
                    activeIcon: SvgIcons.menuPublicActive,
                    label: "공시 성적"),
                BottomNavigationBarItem(
                    icon: SvgIcons.menuSemester,
                    activeIcon: SvgIcons.menuSemesterActive,
                    label: "학기 성적"),
                BottomNavigationBarItem(
                    icon: SvgIcons.menuTotal,
                    activeIcon: SvgIcons.menuTotalActive,
                    label: "전체 성적"),
                BottomNavigationBarItem(
                    icon: SvgIcons.menuSetting,
                    activeIcon: SvgIcons.menuSettingActive,
                    label: "마이페이지"),
              ])

위의 BottomNavigation 코드를 화면에 표시해주기 위해 Scaffold 안에 넣게 되면

class MainScreen extends StatefulWidget {
  const MainScreen({super.key});

  static String routeName = "/";

  
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {

  
  Widget build(BuildContext context) {
    return Scaffold(
        bottomNavigationBar: BottomNavigationBar(
              // 위의 BottomNavigationBar 코드
        );
  }
}

위와 같은 형태가 된다.
main.dart 코드만 수정하고 바로 돌려보자.

void main() {
  runApp(const GachonGradeApp());
}

/// 라우팅 설정 (pushNamed를 통해 쉽게 화면 라우팅을 할 수 있다)
final route = {
  MainScreen.routeName: (context) => const MainScreen(),
};


class GachonGradeApp extends StatelessWidget {
  const GachonGradeApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'GachonGrade',
      theme: ThemeData(
      ),
      initialRoute: MainScreen.routeName,
      routes: route,
    );
  }
}

첫 화면으로 MainScreen이 나오도록 한다.

실행시켜보면 위와 같은 화면이 나온다. 아직 아이템 클릭 이벤트는 없다.
클릭했을 때 선택한 아이템을 변경하고, 화면을 전환하는 코드를 추가해야 한다.

3️⃣ Bottom Navigation 클릭 동작 정의 (선택 아이템 및 화면 변경)

a. 탭에 들어갈 화면 정의

class PublicGradeTab extends StatelessWidget {
  const PublicGradeTab({super.key});

  
  Widget build(BuildContext context) {
    return const Scaffold(
      backgroundColor: ColorStyles.defaultScreenBackground,
      body: Center(
          child: Text(
            "공시 성적 화면",
            style: TextStyle(color: Colors.white),
      )),
    );
  }
}

현재 바텀네비 아이템 개수는 총 4개이다. 바텀네비 하나당 화면 하나를 매칭해 총 4개의 화면이 필요하다.
위의 예시는 공시 성적의 PublicGradeTab이지만, 이런 식으로 화면마다 구분이 될 수 있게끔 SemesterGradeTab, TotalGradeTab, SettingTab를 각각 추가해 준다.

b. TabController, TabItem 정의

화면 전환을 관리해 줄 TabController와, 화면 안에 들어갈 TabItem을 정의한다.
TabItem에는 바로 직전 만들어준 화면들을 List<Widget> 형태로 넣어준다.

TabController에서는 애니메이션을 지원하는데, TabController 초기화 및 필수 조건인 vsync 사용을 위해서는 class 뒤에 with SingleTickerProviderStateMixin를 붙여줘야 한다. vsync는 애니메이션 최적화 및 언제 재생할지 시간을 세어주는 등의 기능을 위해 필요하다. SingleTickerProviderStateMixin는 현재 위젯 트리가 활성화된 동안만 Tick하는 단일 Ticker를 제공하는 것이다.

  class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateMixin {
  late TabController _tabController;

  final List<Widget> _tabItem = [
    const PublicGradeTab(),
    const SemesterGradeTab(),
    const TotalGradeTab(),
    const SettingTab(),
  ];

  
  void initState() {
    super.initState();
    _tabController = TabController(length: _tabItem.length, vsync: this);
  }

  // 네비게이션바 클릭 이벤트
  void _onNavigationBarTab(int index) {
    setState(() {
      _tabController.animateTo(index);
    });
  }
  
  
  Widget build(BuildContext context) {
  	// ...
  }
}

TabBarView 코드 작성

class MainView extends StatefulWidget {
  const MainView({super.key});

  
  State<MainView> createState() => _MainViewState();
}

class _MainViewState extends State<MainView> with SingleTickerProviderStateMixin {

  
  Widget build(BuildContext context) {
    return Scaffold(
        body: TabBarView(
          physics: const NeverScrollableScrollPhysics(),
          controller: _tabController,
          children: _tabItem,
        ),
        bottomNavigationBar: Theme(
          data: ThemeData(
            // 아이템 클릭 효과 제거
            splashColor: Colors.transparent,
            highlightColor: Colors.transparent,
          ),
          child: BottomNavigationBar(
              // ...
              onTap: _onNavigationBarTab),
        )
    );
  }
}

Scaffold의 body에 TabBarView를 두고, 앞서 만든 _tabController_tabItem를 넣어준다.
BottomNavigatoin 코드는 위에서 작성했던 것과 동일한데, onTap으로 마찬가지로 _onNavigationBarTab을 넣어준다. 클릭 시 다른 화면으로 이동시켜주는 역할이다.
바텀네비 아이템 클릭 효과 제거를 위해서 BottomNavigationBar를 Theme로 감싸는 코드도 추가했다.

전체 코드

  class MainScreen extends StatefulWidget {
  const MainScreen({super.key});

  static String routeName = "/";

  
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> with SingleTickerProviderStateMixin {
  late TabController _tabController;

  final List<Widget> _tabItem = [
    const PublicGradeTab(),
    const SemesterGradeTab(),
    const TotalGradeTab(),
    const SettingTab(),
  ];

  
  void initState() {
    super.initState();
    _tabController = TabController(length: _tabItem.length, vsync: this);
  }

  void _onNavigationBarTab(int index) {
    setState(() {
      _tabController.animateTo(index);
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
        body: TabBarView(
          physics: const NeverScrollableScrollPhysics(),
          controller: _tabController,
          children: _tabItem,
        ),
        bottomNavigationBar: Theme(
          data: ThemeData(
            // 아이템 클릭 효과 제거
            splashColor: Colors.transparent,
            highlightColor: Colors.transparent,
          ),
          child: BottomNavigationBar(
              type: BottomNavigationBarType.fixed,
              currentIndex: _tabController.index,
              showSelectedLabels: true,
              showUnselectedLabels: true,
              backgroundColor: ColorStyles.bottomNavBackground,
              selectedItemColor: Colors.white,
              unselectedItemColor: ColorStyles.itemBackground,
              selectedFontSize: 12,
              unselectedFontSize: 12,
              items: <BottomNavigationBarItem>[
                BottomNavigationBarItem(
                    icon: SvgIcons.menuPublic,
                    activeIcon: SvgIcons.menuPublicActive,
                    label: "공시 성적"),
                BottomNavigationBarItem(
                    icon: SvgIcons.menuSemester,
                    activeIcon: SvgIcons.menuSemesterActive,
                    label: "학기 성적"),
                BottomNavigationBarItem(
                    icon: SvgIcons.menuTotal,
                    activeIcon: SvgIcons.menuTotalActive,
                    label: "전체 성적"),
                BottomNavigationBarItem(
                    icon: SvgIcons.menuSetting,
                    activeIcon: SvgIcons.menuSettingActive,
                    label: "설정"),
              ],
              onTap: _onNavigationBarTab),
        )
    );
  }
}

📱 완성 화면

탭을 클릭했을 떄 화면 이동이 잘 이루어지는 걸 확인할 수 있다😊

📚 참고 자료

profile
안드로이드 개발자를 꿈꾸는 학생입니다

0개의 댓글