iT邦幫忙

2021 iThome 鐵人賽

DAY 5
1
Mobile Development

Flutter - 複製貼上到開發套件之旅系列 第 5

【第五天 - Fluter BottomNavigationBar(下)行為分析】

前言

今日的程式碼 => GITHUB


我們在開發的時候常常會遇到幾種情況

  • 需要 BottomNavigationBarindex 改變後,再改回來時,頁面要記住最後離開時的畫面
  • 每次進入相同的 BottomNavigationBarindex 時,畫面都是同一個。
  • 有時候頁面需要 BottomNavigationBar 顯示在下方,有時候不需要。

先介紹這篇會有哪一些畫面

這邊我來給大家看今天會用到的畫面,
如果看不太懂這個在寫什麼的話,可以參考 Day-3 Route設定 的教學文章

class RouteName {
 /// 控制 BottomBar 的畫面
 static const String bottomBar = 'bottomBar';
 /// index=2 會用到的畫面,用來展示切換後畫面會被記住
 static const String home_page_1 = 'home_page_1';
 static const String home_page_2 = 'home_page_2';
 /// index=2 會用到的畫面,用來展示,只要按到 bottomBar 的按鈕,把上就會變成沒有 bottomBar 的畫面
 static const String second_page = 'second_page';
 /// index=2 會用到的畫面,用來展示切換後畫面不會被記住
 static const String third_page_1 = 'third_page_1';
 static const String third_page_2 = 'third_page_2';
 static const String third_page_3 = 'third_page_3';
 /// index == 3 aaa 單存一個畫面,用來當作展示的跳板
 /// 按到 bottomBar 的按鈕時,會先有 bottomBar,跳頁後變成,沒有 B bottomBar 的畫面
 static const String aaa = 'aaa';
 static const String bbb = 'bbb';
}

架構

這是由上而下的樹狀圖

 |App
 |---|SecondPage|----------------------------------------------------
 |---|BottomBarRoutePage|                                           |
 |---|------------------|NavigatorHomePage                          |
 |---|------------------|-----------------|HomePage1                |
 |---|------------------|-----------------|HomePage2                |
 |---|------------------|SizedBox(BottomBarRoutePage 會直接把他跳到 SecondPage)
 |---|------------------|NavigatorThirdPage
 |---|------------------|------------------|ThirdPage1
 |---|------------------|------------------|ThirdPage2
 |---|------------------|------------------|ThirdPage3
 |---|------------------|AAA
 |---|------------------|---|BBB

頁面 - BottomBarRoutePage

這邊是用來控制 BottomBar 的,和上一篇 Flutter - BottomNavigationBar(上)Animation 有一點類似,

用到的第一個技術:GlobalKey

官方文件 KeyWidgetElement 的識別符號。 只有當新的 WidgetKey 與當前 ElementWidgetKey 相同時,它才會被用來更新現有的 ElementKey 在具有相同父級的 Element 之間必須是唯一的。

  • 允許 widgetapp 中的任何位置更改其 parent 而不丟失狀態。例:不同的螢幕,顯示相同的 widget
  • globalkey 唯一定義了某個 element,它使你能夠訪問 element 相關的對象。例:不同的 widget 訪問狀態

這邊之所以需要 globalkey 是因為我需要將它傳給下一個元件,使這個狀態會被記住,這樣下次回來的時候,就會是離開的時候的狀態。

用到的第二個技術 WillPopScope

WillPopScope 是 Android 的後退鍵,可以用來控制他,這邊 WillPopScope 的邏輯是,當我按下 WillPopScope,會去用 mabyepop 的方式,如果可以 pop 的話就 pop,不行的話就不做動作。

class BottomBarRoutePage extends StatefulWidget {
 const BottomBarRoutePage({required this.pageIndex});
 /// initialize page index
 final int pageIndex;

 @override
 _BottomBarRoutePageState createState() => _BottomBarRoutePageState();
}

class _BottomBarRoutePageState extends State<BottomBarRoutePage> {
 /// page index
 late int _pageIndex;
 /// GlobalKey of navigator
 Map<int, GlobalKey> navigatorKeys = {
   0: GlobalKey(),
   1: GlobalKey(),
   2: GlobalKey(),
   3: GlobalKey(),
 };

 @override
 void initState() {
   _pageIndex = widget.pageIndex;
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: const Text('BottomNavigationBar Sample'),
     ),
     body: WillPopScope(
       onWillPop: () async {
         /// maybePop the current index context
         return !await Navigator.maybePop(
             navigatorKeys[_pageIndex]!.currentState!.context);
       },
       child: IndexedStack(
         index: _pageIndex,
         children: <Widget>[
           NavigatorHomePage(navigatorKey: navigatorKeys[0]!),
           SizedBox(), // 整個換頁。不要底部的 bottomBar
           NavigatorThirdPage(
               // 這邊是因為跳下一夜的時候不希望底下還有 bottomBar
               navigatorKey: navigatorKeys[2]!),
           AAA()
         ],
       ),
     ),
     bottomNavigationBar: BottomNavigationBar(
       type: BottomNavigationBarType.fixed,
       showSelectedLabels: false,
       showUnselectedLabels: false,
       selectedItemColor: Colors.red,
       unselectedItemColor: Colors.black,
       items: <BottomNavigationBarItem>[
         /// when index = 0, after the page A is changed to page B, then changed the index to 1, and changed the index back to 0, the screen page will be B page,
         /// And there will be bottomNavigationBar in the process
         /// 在 index  = 0 時,把A 畫面跳到 B 畫面後,把 index 改成 1,再把 index 改回 1,此時畫面一會在 B 畫面,並且在過程中都會有 bottomNavigationBar 存在
         BottomNavigationBarItem(
           icon: Icon(Icons.people_outline),
           title: Text('c'),
         ),

         /// When index = 1, after the page A is changed to screen B, there'll be no bottomNavigationBar under the B page,
         /// and then back to the A page, there will be a bottomNavigationBar at the bottom of the A page
         /// 在 index  = 1 時,把整夜 bottomBar 切換成 A 畫面跳到 B 畫面後,B 畫面的下方不會有 bottomNavigationBar,此時再跳回道 A 畫面,A 畫面下方會有 BottomBar
         BottomNavigationBarItem(
           icon: Icon(Icons.add_to_photos_rounded),
           title: Text('b'),
         ),

         /// after the page changed, index changed, will not remember the page when you left, and the page will be the same every time when you re-enter index = 2.
         /// 跳頁後,更換 index,並不會記住離開時的畫面,每次重新進入 index = 2 時,畫面都會一樣。
         BottomNavigationBarItem(
           icon: Icon(Icons.access_alarm),
           title: Text('b'),
         ),
         /// normal behavior
         /// 一般行為
         BottomNavigationBarItem(
           icon: Icon(Icons.settings),
           title: Text('c'),
         ),
       ],
       currentIndex: _pageIndex,
       onTap: (index) {
         setState(() {
           // index  0  1  2  3
           /// when index == 1 then change to another Widget.
           /// index == 1 時,代表他從 bottom_bar_route_page 跳到 新的畫面
           if (index == 1) {
             Navigator.pushNamedAndRemoveUntil(
                 context, RouteName.second_page, (route) => false);
           } else if (index == 2) {
             /// create a new GlobalKey, and the page will be the same every time when you re-enter
             navigatorKeys[2] = GlobalKey();
             _pageIndex = index;
           } else {
             _pageIndex = index;
           }
         });
       },
     ),
   );
 }
}

頁面 - NavigationHome + NavigatorThirdPage

這邊他會包成一個 Navigator。

什麼事 Navigator 呢?
官網介紹,我的理解,他也是一個路由器。

這邊我拿到 BottomBarRoutePage 的 Key,是為了讓畫面記住狀態。

onGenerateRoute = 告知我的路由器是哪個

initialRoute = 告知 App 開啟後第一個畫面是什麼

class NavigatorHomePage extends StatelessWidget {
 NavigatorHomePage({required this.navigatorKey});

 final GlobalKey navigatorKey;

 @override
 Widget build(BuildContext context) {
   return Navigator(
     key: navigatorKey,
     onGenerateRoute: (RouteSettings settings) =>
         MyRouter.generateRoute(settings),
     initialRoute: RouteName.home_page_1,
     //initialRoute: RouteName.third_page_1,  <- NavigatorThirdPage 就要把上面註解,這邊不要註解
   );
 }
}

他的概念很像是我的 App 底下有一個『根(root)』路由器,然後 BottomNavigation 算是 App 底下的一個畫面,在這個 BottomNavigation 底下還有兩個路由器。

app
 | - app(root) 的路由器
 | bottomNavigation
 |  - - - - - - - -| NavigatorHomePage 的路由器
 |  - - - - - - - -| NavigatorThirdPage 的路由器

只要是在 bottomNavigationindex == NavigatorHomePage 的 index 畫面的話(也就是現在 bottomNavigation 是顯示 NavigatorHomePage 這一頁),之後再 pushNamed 的時候會有一些特性

  • BottomNavigationindex 並不會變動
  • 底下都會有 BottomNavigation
  • BottomNavigation 的 index 變動後,再回來時,會記住最後離開時的頁面是哪一個。

如果想要跳頁後,不會顯示 BottomNavigationBar 的話,就
將 rootNavigator 設定為 true,代表他跳到 app 底下的 root route 的畫面。

Navigator.of(context,rootNavigator: true).pushNamed(RouteName.third_page_3),

如果想要按下 bottomBar 的按鈕時,就直接轉換成沒有 bottomBarItem 的畫面的話,可以直接在 BottomNavigationBar 的 onTap : (index){} 裡面做手腳,類似範例的這幾行。

        onTap: (index) {
          setState(() {
            if (index == 1) {
              Navigator.pushNamedAndRemoveUntil(
                  context, RouteName.second_page, (route) => false);
            }
          });
        },

上一篇
【第四天 - Flutter BottomNavigationBar(上)Animation】
下一篇
【第六天 - Flutter 多國語系】
系列文
Flutter - 複製貼上到開發套件之旅30

1 則留言

0
SKYDOG
iT邦新手 5 級 ‧ 2021-09-19 15:29:43

/images/emoticon/emoticon07.gif

WenYeh iT邦新手 4 級 ‧ 2021-09-19 21:45:32 檢舉

/images/emoticon/emoticon08.gif

我要留言

立即登入留言