フラッタ底部ナビゲーションバーのための状態持続性技術


下のナビゲーションバーを構築するフラッタで与えられたBottomNavigationBar Widget . しかし、我々は標準のウィジェットに機能を追加したい.

概要


  • 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
  • 我々は底のナビゲーションと基本的なフラッターアプリを設定し、我々は直面している問題を見ていきます.タブを切り替えるか、下のタブページの状態を保存するたびに、一番下のナビゲーションバーページを初期化しないように.
    それから、我々は彼らを解決するために2、3のアプローチをためします.我々は結果を比較し、我々はどちらを進めるために決めることができます.

    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 with BottomNavigationBar 2を含む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 タイマーを実行し、我々のエスケープ時間ごとに2秒を更新します.

    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.

    スタックとオフステージ

    One way to persist the bottom navigation bar page is to use Stack ウィジェット.
    我々は、すべてのページを子供たちとして加えますStack 一番下のタブの順序で、現在選択されている一番下のタブについて一度に1つの子を表示します.

    実装

    We'll wrap the Tabbar widgets with OffStage そして、Stack .
    offstageパラメータはブール値をとります.それが本当ならば、子供はそうですhidden or 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 .自動化

    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:アイテムのスクロール可能なリスト


    私たちはStatefulWidget こちらからAutomaticKeepAliveClientMixin だけで動作するState クラスで定義されるクラスimplementation .

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


    この後2つだけ追加する必要があります.
    まず、呼び出し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.