Flutter BottomBar寫法

前言

BottomBar就是底部導航,使用的是BottomNavigationBar這個元件

有兩種寫法(名字都是我為了方便解釋自己取名的) :1. 頁面抽換法 2. 頁面導航法

頁面抽換法

直接在 BottomNavigationBaronTap 方法中處理導航

完整程式碼先附上

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter BottomNavigationBar Example',
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int _selectedIndex = 0; // 當前選中的索引

  // 頁面列表,每個選項對應一個頁面
  final List<Widget> _pages = [
    HomePage(),
    SearchPage(),
    ProfilePage(),
  ];

  // 切換底部導航項目的回調函數
  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("BottomNavigationBar Example"),
      ),
      body: _pages[_selectedIndex], // 顯示當前選中的頁面
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _selectedIndex, // 設定當前選中的索引
        onTap: _onItemTapped, // 處理點擊事件
        selectedItemColor: Colors.blue, // 選中顏色
        unselectedItemColor: Colors.grey, // 未選中顏色
        backgroundColor: Colors.white,
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.search),
            label: 'Search',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: 'Profile',
          ),
        ],
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(child: Text("Home Page"));
  }
}

class SearchPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(child: Text("Search Page"));
  }
}

class ProfilePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(child: Text("Profile Page"));
  }
}

以下解釋

  • _selectedIndex:當前選中的底部導航項目索引,用來確定顯示哪一頁。
  • _pages:頁面列表,包含不同的頁面 widget(如 HomePage、SearchPage、ProfilePage)。每個頁面與 BottomNavigationBarItem 一一對應。
  • _onItemTapped:當點擊 BottomNavigationBarItem 時觸發,會更新 _selectedIndex 並觸發重繪,從而切換頁面。
  • BottomNavigationBar:
  • currentIndex:當前選中的項目索引,通過 _selectedIndex 設置。
  • onTap:回調函數,用於處理點擊事件。
  • items:定義每個 BottomNavigationBarItem,包括圖標和標籤。

頁面導航法

使用回調 onChanged 來處理導航,且每次導航都是重新建構頁面

DhiWise是採用此種方式去寫BottomBar,有高擴充性和UI抽離的優點,但是還是有效能缺點和狀態不保留的缺點

先附上程式碼

以下是DhiWise原碼,我只有刪掉一些不相干的,還是會有點複雜,請見諒

以下程式碼中的 BottomNavigationBar 導航機制主要使用 CustomBottomBar 和 _buildBottomNavigation 函式。當用戶點擊 BottomNavigationBar 上的按鈕時,會透過 onChanged 回調觸發 getCurrentRoute 方法,進行頁面切換。

class WorkModeScreen extends StatefulWidget {
  const WorkModeScreen({Key? key})
      : super(
          key: key,
        );

  @override
  WorkModeScreenState createState() => WorkModeScreenState();
  static Widget builder(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => WorkModeProvider(),
      child: WorkModeScreen(),
    );
  }
}

// ignore_for_file: must_be_immutable
class WorkModeScreenState extends State<WorkModeScreen> {
  final TaskService _taskService = TaskService();
  List<TaskModel> _tasks = [];
  GlobalKey<NavigatorState> navigatorKey = GlobalKey();
  int _selectedIndex = 0;

  @override
  void initState() {
    super.initState();
  }


  Future<void> _loadTasks() async {
    final tasks = await _taskService.getTasks();
    setState(() {
      _tasks = tasks;
    });
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        key: navigatorKey,
        drawer: WorkModeMenuDrawerDraweritem(),
        appBar: _buildAppBar(context),
        body: SizedBox(
          width: double.maxFinite,
          child: SingleChildScrollView(
            child: Container(
              width: double.maxFinite,
              padding: EdgeInsets.only(
                left: 12.h,
                top: 12.h,
                right: 12.h,
              ),
              child: Column(
                children: [
                  _buildHighlightSection(context),
                  SizedBox(height: 12.h),
                  Container(
                    width: double.maxFinite,
                    padding: EdgeInsets.all(12.h),
                    decoration: BoxDecoration(
                      color: appTheme.teal50,
                      borderRadius: BorderRadiusStyle.roundedBorder10,
                    ),
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        SizedBox(height: 4.h),
                        SizedBox(
                          width: double.maxFinite,
                          child: _buildRowcheckOne(
                            context,
                            checkOne: ImageConstant.imgCheck,
                            one: "lbl32".tr,
                          ),
                        ),
                        SizedBox(height: 12.h),
                        _buildTaskCard(context),
                        SizedBox(height: 12.h),
                        _buildTaskCard1(context),
                        SizedBox(height: 12.h),
                        _buildTaskCard2(context)
                      ],
                    ),
                  ),
                  SizedBox(height: 28.h)
                ],
              ),
            ),
          ),
        ),
        bottomNavigationBar: SizedBox(
          width: double.maxFinite,
          child: _buildBottomNavigation(context),
        ),
      ),
    );
  }

  /// Section Widget
  PreferredSizeWidget _buildAppBar(BuildContext context) {
    return CustomAppBar(
      leadingWidth: 48.h,
      leading: Builder(
        builder: (context) => AppbarLeadingImage(
          imagePath: ImageConstant.imgMenu,
          margin: EdgeInsets.only(
            left: 24.h,
          ),
          onTap: () {
            Scaffold.of(context).openDrawer();
          },
        ),
      ),
      title: SizedBox(
        width: double.maxFinite,
        child: AppbarTitleButton(
          margin: EdgeInsets.only(
            left: 12.h,
          ),
          onTap: () {
            onTaptf(context);
          },
        ),
      ),
      actions: [
        AppbarTrailingImage(
          imagePath: ImageConstant.imgClockBlack900,
        ),
        AppbarSubtitleFour(
          text: "lbl_0_13".tr,
          margin: EdgeInsets.only(
            left: 6.h,
            right: 12.h,
          ),
        )
      ],
      // styleType: Style.bgOutlineGray400,
    );
  }

  /// Section Widget
  Widget _buildBottomNavigation(BuildContext context) {
    return SizedBox(
      width: double.maxFinite,
      child: CustomBottomBar(
        onChanged: (BottomBarEnum type) {
          Navigator.pushNamed(
              navigatorKey.currentContext!, getCurrentRoute(type));
        },
      ),
    );
  }

  ///Handling route based on bottom click actions
  String getCurrentRoute(BottomBarEnum type) {
    switch (type) {
      case BottomBarEnum.Home:
        return AppRoutes.workModeScreen;
      case BottomBarEnum.Calendar:
        return AppRoutes.meModeScreen;
      case BottomBarEnum.Search:
        return "/";
      case BottomBarEnum.Plus:
        return AppRoutes.workModeAddScreen;
      default:
        return "/";
    }
  }

  /// Navigates to the meModeScreen when the action is triggered.
  onTaptf(BuildContext context) {
    NavigatorService.pushNamed(
      AppRoutes.meModeScreen,
    );
  }
}

BottomBar導航機制解說

  1. BottomNavigationBar 的生成
    1. CustomBottomBar 是一個客製化的 BottomNavigationBar,用於顯示底部導航按鈕。每個按鈕點擊時,都會觸發 onChanged 回調,並將當前的頁面類型 (BottomBarEnum) 傳遞給 _buildBottomNavigation。
  2. 底部導航欄位建構函式 _buildBottomNavigation:
    1. _buildBottomNavigation 函式負責生成底部導航欄。這裡通過 CustomBottomBar 的 onChanged 屬性,將 BottomBarEnum 類型的值傳遞給 getCurrentRoute 方法。
  3. 根據按鈕類型決定導航路徑 getCurrentRoute:
    1. getCurrentRoute 根據 BottomBarEnum 中的頁面類型決定導航的路由地址。
    2. 它接受 BottomBarEnum 作為輸入,並根據其值返回對應的路由名稱。
    3. BottomBarEnum.Home:返回 AppRoutes.workModeScreen 路由。
    4. BottomBarEnum.Calendar:返回 AppRoutes.meModeScreen 路由。
    5. BottomBarEnum.Search:返回 /(主頁)。
    6. BottomBarEnum.Plus:返回 AppRoutes.workModeAddScreen。
  4. 導航至指定路由 Navigator.pushNamed:
    1. Navigator.pushNamed 使用 navigatorKey 進行頁面導航。
    2. navigatorKey 是一個全域 GlobalKey,用來在當前應用的 Navigator 中進行路由導航。Navigator.pushNamed 接受 context 和頁面路徑,來導航到 getCurrentRoute 返回的頁面。

優缺點比較

寫法類型優點缺點適用場景
直接在 onTap 處理導航簡單清晰,代碼量少 一目了然,導航邏輯集中在一處效能較高,沒有多餘的回調和組件層級 – 適合不頻繁改動的頁面切換,減少不必要的重繪高耦合,導航和 UI 顯示綁定在一起,不利於測試和擴展 不易重用,增加代碼重複度對小型應用來說,響應快速、體驗流暢 – 簡單清晰的結構降低導航延遲,但缺乏自定義彈性適合小型應用或頁面邏輯簡單的應用
使用自定義 BottomNavigationBar低耦合,導航和 UI 顯示分離 擴展性強,便於樣式和功能的變化 更適合大型項目和重用性使用者體驗靈活,便於在導航間加入動畫效果、過渡樣式等 支持更動態的頁面內容,有助於提升頁面交互體驗代碼量增加,需要自定義組件 學習成本較高,初學者理解回調和封裝可能有些難度稍增加效能開銷,取決於頁面層級和回調次數 若結構設計不佳,可能導致不必要的重繪適合大型應用,頁面結構多樣或需要重用