Flutter Bottom 내비게이션 바의 상태 지속성 기법

BottomNavigationBar Widget 이 주어진 Flutter에서 하단 내비게이션 바를 만드는 것은 간단합니다. 그러나 우리는 표준 위젯에 기능을 추가하고 싶습니다.

개요



  • Setup
  • What do we want to achieve?
  • Implementation
  • Demo
  • Result


  • 1. Stack and OffStage
  • Implementation
  • Demo
  • Result
  • Alternative - Indexed Stack


  • 2. AutomaticKeepAliveClientMixin
  • Implementation
  • Demo
  • Result

  • Conclusion
  • Final Note

  • 하단 탐색 기능이 있는 기본 Flutter 앱을 설정하고 직면한 문제를 살펴보겠습니다. 탭을 전환할 때마다 하단 탐색 모음 페이지를 초기화하지 않거나 하단 탭 페이지의 상태를 유지하는 것과 같습니다.
    그런 다음 이를 해결하기 위해 몇 가지 접근 방식을 시도합니다. 우리는 결과를 비교하고 앞으로 진행할 것을 결정할 수 있습니다.

    Here's the GitHub repo with all the code.


    설정

    We're going to start with the basic app containing bottom navigation with two tabs.

    • Tab 1: Scrollable list of items.
    • Tab 2: Displaying the escaped seconds of a Timer.

    우리는 무엇을 달성하고 싶습니까?

    1. Create the navigation bar page only when we open the page.
    2. Preserve scroll position of navigation bar page in Tab 1.
    3. Preserve the escaped time of Timer in Tab 2.

    구현

    Let's start with a new Flutter project.

    상위 위젯: 하단 탐색 모음



    우리는 두 개의 Scaffold를 포함하는 BottomNavigationBar가 있는 간단한 Tabs를 가지고 있습니다.

    class BasicBottomNavigation extends StatefulWidget {
      const BasicBottomNavigation({Key? key}) : super(key: key);
    
      @override
      State<BasicBottomNavigation> createState() => _BasicBottomNavigationState();
    }
    
    class _BasicBottomNavigationState extends State<BasicBottomNavigation> {
      int currentIndex = 0;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: [         /// List of tab page widgets
            const _Tabbar1(),     
            const _Tabbar2(),
          ][currentIndex],
          bottomNavigationBar: BottomNavigationBar(
            currentIndex: currentIndex,
            onTap: (index) {
              setState(() {
                currentIndex = index;     /// Switching tabs
              });
            },
            items: const [
              BottomNavigationBarItem(icon: Text("1"), label: "Tab"),
              BottomNavigationBarItem(icon: Text("2"), label: "Tab"),
            ],
          ),
        );
      }
    }
    


    탭 1: 스크롤 가능한 항목 목록


    ListView 안에 색인을 표시하는 ListTile가 있습니다.

    class _Tabbar1 extends StatelessWidget {
      const _Tabbar1({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        print("Tabbar 1 build");
    
        return Scaffold(
          appBar: AppBar(title: const Text("Tab bar 1")),
          body: ListView.builder(
            itemBuilder: (context, index) {
              return ListTile(
                title: Text("${index + 1}"),
              );
            },
            itemCount: 50,
          ),
        );
      }
    }
    


    탭 2: 타이머의 이스케이프된 초 표시



    Ticker을 사용하여 타이머를 실행하고 이스케이프된 기간을 매초 업데이트합니다.

    Fun Fact: Ticker is used in Flutter for callbacks during Animation frames.



    
    class _Tabbar2 extends StatefulWidget {
      const _Tabbar2({Key? key}) : super(key: key);
    
      @override
      State<_Tabbar2> createState() => _Tabbar2State();
    }
    
    class _Tabbar2State extends State<_Tabbar2>
        with SingleTickerProviderStateMixin {
      late final Ticker _ticker;
      Duration _escapedDuration = Duration.zero;
    
      get escapedSeconds => _escapedDuration.inSeconds.toString();
    
      @override
      void initState() {
        super.initState();
        print("Tabbar 2 initState");
    
        _ticker = createTicker((elapsed) {
          if (elapsed.inSeconds - _escapedDuration.inSeconds == 1) {
            setState(() {
              _escapedDuration = elapsed;
            });
          }
        });
    
        _ticker.start();
      }
    
      @override
      void dispose() {
        _ticker.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text("Tab bar 2")),
          body: Center(
            child: Text(escapedSeconds),
          ),
        );
      }
    }
    


    데모



    결과

    1. Tabs are initialized only when we click on them.
    2. The scroll position is not preserved.
    3. Escaped time of the Timer is not preserved.

    Nothing was preserved. We create new tab pages every time we click on them. The scroll position is lost we switch back to Tab 1 . The Timer starts from 0 whenever we open Tab 2 .

    There is no problem with this approach as long as we don't need to preserve any state.

    But since we do, let's look at how we can achieve it.

    1. 스택과 오프스테이지

    One way to persist the bottom navigation bar page is to use Stack 위젯.
    모든 페이지를 아래쪽 탭의 순서로 Stack의 자식으로 추가하고 현재 선택한 아래쪽 탭과 관련하여 한 번에 하나의 자식을 표시합니다.

    구현

    We'll wrap the Tabbar widgets with OffStageStack 가 포함된 하위 목록입니다.
    오프스테이지 매개변수는 부울 값을 사용합니다. 참이면 자식은 hidden 또는 offstage 이고, 그렇지 않으면 자식이 표시됩니다.
    Tabbar 클래스에는 변경 사항이 없습니다.

    상위 위젯: 하단 탐색 모음



     return Scaffold(
          body: Stack(       ///  Added Stack Widget
            children: [
              Offstage(          /// Wrap Tab with OffStage 
                offstage: currentIndex != 0,
                child: const _Tabbar1(),
              ),
              Offstage(
                offstage: currentIndex != 1,
                child: const _Tabbar2(),
              ),
            ],
          ),
    

    데모


    사용할 수 있는

    결과

    1. Tabs are not initialized only when we click on them.
    2. The scroll position is preserved.
    3. Escaped time of the Timer is preserved.

    All the tabs are initialized with the parent Widget. Hence the timer in Tabbar 2 started before we even opened that Tab. The good thing is that it preserves the scroll position and escaped time.

    If creating all the tabs at once does not affect the performance and is what we want, then we use this technique.

    1. 대안 - 인덱스 스택

    Turns out there's a Widget (as always with Flutter 😇) called IndexedStack. 동일한 결과를 가진 적은 코드입니다.

    상위 위젯: 하단 탐색 모음



    return Scaffold(
          body: IndexedStack(      /// Replaced with IndexedStack
            index: currentIndex,
            children: const [
              _Tabbar1(),
              _Tabbar2(),
            ],
          ),
    

    2. AutomaticKeepAliveClientMixin

    As the name suggests, this mixin makes the client (Tabbar child widgets) keep themselves alive (not disposed of) after we switch the tabs. It also creates the Tab only when it is first clicked and not with the Parent Widget like the above methods.

    구현

    AutomaticKeepAliveClientMixin needs a PageView 상위 위젯. 따라서 본문을 PageView로 래핑하고 탭 목록을 자식으로 전달합니다.

    Further Reading: Other than PageView, there's a TabBarView (for top app bar tabs), which also makes AutomaticKeepAliveClientMixin work for tabs (child widgets) because it uses PageView internally.


    상위 위젯: 하단 탐색 모음



    class AliveMixinDemo extends StatefulWidget {
      const AliveMixinDemo({Key? key}) : super(key: key);
    
      @override
      State<AliveMixinDemo> createState() => _AliveMixinDemoState();
    }
    
    class _AliveMixinDemoState extends State<AliveMixinDemo> {
      final PageController controller = PageController();  /// initializing controller for PageView
    
      int currentIndex = 0;
      final tabPages = [
        const _Tabbar1(),
        const _Tabbar2(),
      ];
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: PageView(        /// Wrapping the tabs with PageView
            controller: controller,
            children: tabPages,
            onPageChanged: (index) {
              setState(() {
                currentIndex = index;     /// Switching bottom tabs
              });
            },
          ),
          bottomNavigationBar: BottomNavigationBar(
            currentIndex: currentIndex,
            onTap: (index) {
              controller.jumpToPage(index);    /// Switching the PageView tabs
              setState(() {
                currentIndex = index;
              });
            },
            items: const [
              BottomNavigationBarItem(icon: Text("1"), label: "Tab"),
              BottomNavigationBarItem(icon: Text("2"), label: "Tab"),
            ],
          ),
        );
      }
    }
    

    탭 1: 스크롤 가능한 항목 목록


    StatefulWidgetimplementation에 정의된 AutomaticKeepAliveClientMixin 클래스에서만 작동하기 때문에 여기서는 State로 대체합니다.

    "A mixin with convenience methods for clients of AutomaticKeepAlive. Used with State subclasses."



    이 후에 두 가지만 추가하면 됩니다.
    먼저 빌드 메서드 내에서 super.build()를 호출합니다. 둘째, wantKeepAlive를 재정의하고 true를 반환합니다.

    class _Tabbar1 extends StatefulWidget {
      const _Tabbar1({Key? key}) : super(key: key);
    
      @override
      State<_Tabbar1> createState() => _Tabbar1State();
    }
    
    class _Tabbar1State extends State<_Tabbar1> 
        with AutomaticKeepAliveClientMixin {     /// Using the mixin
      @override
      Widget build(BuildContext context) {
        super.build(context);    /// Calling build method of mixin
        print("Tabbar 1 build");
        return Scaffold(
          appBar: AppBar(title: const Text("Tab bar 1")),
          body: ListView.builder(
            itemBuilder: (context, index) {
              return ListTile(
                title: Text("${index + 1}"),
              );
            },
            itemCount: 50,
          ),
        );
      }
    
      @override
      bool get wantKeepAlive => true;    /// Overriding the value to preserve the state
    }
    


    탭 2: 타이머의 이스케이프된 초 표시



    변경 사항은 위의 Tabbar 1 클래스와 동일합니다.

    class _Tabbar2State extends State<_Tabbar2>
        with SingleTickerProviderStateMixin, 
        AutomaticKeepAliveClientMixin {    /// Using the mixin
      late final Ticker _ticker;
    



      @override
      Widget build(BuildContext context) {
        super.build(context);     /// Calling build method of mixin
        return Scaffold(
          appBar: AppBar(title: const Text("Tab bar 2")),
          body: Center(
            child: Text(escapedSeconds),
          ),
        );
      }
    
      @override
      bool get wantKeepAlive => true;   /// Overriding the value to preserve the state
    }
    


    데모



    결과

    1. Tabs are initialized only when we click on them.
    2. The scroll position is preserved.
    3. Escaped time of the Timer is preserved.

    The Tabbar 2 is initialized only the first time when we click on it. The Timer preserves its state and so does the scrolling position in Tabbar 1 .

    If we want to programmatically change the keepAlive condition, then we can use the updateKeepAlive() method of AutomaticKeepAliveClientMixin. For further reading, refer to this StackOverflow answer.




    결론

    We can choose any one approach from the above options according to our requirements.

    • Don't want to preserve any state -> standard BottomBarNavigation .
    • Want to preserve state but fine with creating all the tabs at once -> IndexedStack or Stack and OffStage .
    • Want to preserve state and build tabs only once when clicked on them -> AutomaticKeepAliveClientMixin .

    IndexedStack is the simplest approach while AutomaticKeepAliveClientMixin covers our need. Since we usually have API calls in tabs and don't want to call them every time we switch to that tab.

    최종 참고 사항

    Thank you for reading this article. If you enjoyed it, consider sharing it with other people.

    If you find any mistakes, please let me know.

    Feel free to share your opinions below.

    좋은 웹페이지 즐겨찾기