Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TabView does not preserve state when swiping from one tab to another

Context: Here's page that has a TabView to navigate between tabs all of these tabs are making use of flutter_bloc (version 6.0.1).

Problem: When swiping to to any tab, the state is not being preserved and the entire widget tree is being rebuilt as shown in the gif below

PageView State not being preserved when swiping

Here is the build() method:

 @override
  Widget build(BuildContext context) {
    super.build(context);
    return DefaultTabController(
      initialIndex: 0,
      length: 3,
      child: Scaffold(
        backgroundColor: Colors.white,
        appBar: _buildAppBarWithTabs(),
        body: TabBarView(
          children: <Widget>[
            defaultViewforCategory(1), //Women
            defaultViewforCategory(3), //Men
            defaultViewforCategory(2), //Kids
          ],
        ),
      ),
    );
  }

Here is the implementation of the function defaultViewforCategory()

Widget defaultViewforCategory(int mainCategoryId) {
    return PageStorage(
      bucket: bucket,
      key: PageStorageKey(mainCategoryId),
      child: ConstrainedBox(
        constraints: BoxConstraints(maxWidth: 1200),
        child: ListView(
          scrollDirection: Axis.vertical,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(bottom: 150),
              child: Container(
                height: 800,
                child: RefreshIndicator(
                  onRefresh: () => refreshTimeline(),
                  child: CustomScrollView(
                    scrollDirection: Axis.vertical,
                    slivers: <Widget>[
                      SliverToBoxAdapter(
                        child: MasonryGrid(
                          column: getResponsiveColumnNumber(context, 1, 2, 6),
                          children: <Widget>[
                            // First Bloc
                            BlocProvider(
                              create: (context) {
                                BrandBloc(repository: _brandRepository);
                              },
                              child: Container(
                                width: 200,
                                alignment: Alignment.center,
                                height: 90,
                                child: BrandScreen(
                                  brandBloc: context.bloc(),
                                ),
                              ),
                            ),
                            CategoryScreen(
                              // Second Bloc
                              categoryBloc: CategoryBloc(
                                  mainCategoryId: mainCategoryId,
                                  repository: _categoryRepository),
                            ),

                            // -------------- Featured Items--------------------------
                            Container(
                              width: 200,
                              alignment: Alignment.center,
                              height: 350,
                              child: _buildFeaturedItemsList(mainCategoryId),
                            ),
                            Placeholder(strokeWidth: 0, color: Colors.white)
                          ],
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

Tried-solutions: 1 - I tried the AutomaticKeepAliveClientMixin but it turned out this mixin preserve the state of a page when switching to a another page using BottomNavigationBar.

2 - PageStorage didn't solve the problem.

Question: How to stop the TabView from being rebuilt each time the user swipes to another tab?

like image 248
Waleed Alrashed Avatar asked Aug 02 '20 16:08

Waleed Alrashed


1 Answers

As you stated, one of the problems is that the TabBarView is rebuild every time the tab is presented. For that issue is an open topic here. Because of that, a fresh new Bloc instance is created every time when screen changes.

NOTE Because the CategoryBloc is not passed using BlocProvider you should dispose the bloc manually.

A simple solution here is to move the BlocProvider up in the hierarchy outside of the TabBarView - by example, first component in the build method.

NOTE As performance wise this is okay because the BLOCs are lazily initialized when a bloc is requested.

Now the more delicate problem is the the way the CategoryBloc is created (because has a dynamic constructor). Here you can have two solution:

  1. Either you modify the CategoryBloc to be shareable by all categories screen - here I can not help you too much because I don't have the code of it. The idea is to send the mainCategoryId through events and emits a new state with the results. In this case you should forward the mainCategoryId into the state and, on BlocBuilder, use buildWhen parameter to build only when the mainCategoryId matches the CategoryScreen id (which can be passed when the screen is created). AND to not forget also to provide the CategoryBloc using BlocProvider outside of the TabBarView child.

  2. OR move the CategoryBloc creation outside of the TabBarView and cache them for further access. I have created an example below to emphasize this.

    // ...
    
    ///
    /// Categories blocs cache.
    ///
    Map<int, CategoryBloc> _categoriesBlocs;
    
    ///
    /// Creates UNIQUE instances of CategoryBloc by id.
    ///
    CategoryBloc getCategoryBlocById(int id) {
        // If you don't already have a bloc for that particular id, create a new one
        // and cache it (by saving it in the Map)
        this._categoriesBlocs.putIfAbsent(
            id,
            () => CategoryBloc(
            mainCategoryId: id,
                      repository: _categoryRepository,
            ));
    
        // Return the cached category bloc
        return this._categoriesBlocs[id];
      }
    
    ///
    /// This is very important. Because we manually create the BLOCs we have 
    /// to manually dispose them 
    ///
    @override
    void dispose() {
        for (var bloc in this._categoriesBlocs) {
         bloc.displose();
        }
        super.dispose();
    }
    
    @override
    Widget build(BuildContext context) {
        super.build(context);
    
        return MultiBlocProvider(
            providers: [
                BlocProvider(
                    create: (context) => BrandBloc(repository: _brandRepository),
                )
            ],
            child: DefaultTabController(
                initialIndex: 0,
                length: 3,
                child: Scaffold(
                    backgroundColor: Colors.white,
                    appBar: _buildAppBarWithTabs(),
                    body: TabBarView(
                        children: <Widget>[
                            defaultViewforCategory(1), //Women
                            defaultViewforCategory(3), //Men
                            defaultViewforCategory(2), //Kids
                        ],
                    ),
                ),
            ),
        );
    }
    
      // ...
    
      CategoryScreen(
      // Second Bloc
      // Now, here you will get the same BLOC instance every time
          categoryBloc: getCategoryBlocById(mainCategoryId),
      ),
    
      // ...
    
like image 195
jorjdaniel Avatar answered Nov 15 '22 07:11

jorjdaniel