Search code examples
flutterviewcontrollertabsswipe

Flutter TabView only refreshes on Tab, but not on Swipe


I'm trying to improve my app with the following challenge : I want to populate my Tab View with initially empty TabPages(Widgets) and do the complex stuff (e.g. like database actions) in the TabViews when the Tab is clicked or the Tab is swiped on it. I finished my programming now but the view is only refreshed when I tab on the TabHeader, but not if I swipe from one page to another. In debugger I see that the widget is replaced by the correct one but it is not displayed. It remains on the old widget.

I have written a little app which shows the problem. It has 5 Tabs, initially filled with MyDefaultWidget, and replaced with MySpecialWidget if selected. Here is the code

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My Tab Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({
    super.key,
  });

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
  late TabController _tabController;

  List<Tab> myTabHeaders = [];
  List<Widget> myTabPages = [];

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 5, vsync: this);
    _tabController.addListener(myTabControllerListener);

    for (int i = 0; i < 5; i++) {
      myTabHeaders.add(Tab(text: '$i'));
      myTabPages.add(const MyDefaultWidget());
    }
  }

  void myTabControllerListener() {
    if (_tabController.indexIsChanging) {
      int newIndex = _tabController.index;
      setState(() {
        myTabPages[newIndex] = const MySpecialWidget();
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.white,
        title: TabBar(
          controller: _tabController,
          indicatorColor: Colors.orange,
          labelColor: Colors.orange,
          unselectedLabelColor: Colors.black54,
          tabs: myTabHeaders,
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: myTabPages,
      ),
    );
  }
}

class MyDefaultWidget extends StatelessWidget {
  const MyDefaultWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text('DEFAULT'),
    );
  }
}

class MySpecialWidget extends StatelessWidget {
  const MySpecialWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text('SPECIAL'),
    );
  }
}

If tapping MySpecialWidget is shown. If swiping MyDefaultWidget is shown.

Is there anything I have forgotten to configure ? I'm a little bit confused about the inconsistence. Thanks a lot in advance for some help !


Solution

  • As @Dhafin Rayhan's answer describes, the indexIsChanging property is only used for the animation itself. Removing it has the effect that your listener function can't tell if the index has changed at all. To fix this you should keep track of the _tabController.index by storing it in a variable and refresh your tab's content when it changed. The below code is tested and working.

    I also replaced the static colors and changed up the way you store and initialize your tabs. Decide for yourself if you would like to adapt it, but personally, I prefer it this way as I think it's a bit cleaner and simpler to read.

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'My Tab Demo',
          theme: ThemeData.from(
            colorScheme: ColorScheme.fromSwatch(
              brightness: Brightness.light,
              primarySwatch: Colors.orange,
              backgroundColor: Colors.white,
            ),
            useMaterial3: true,
          ),
          home: const MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      const MyHomePage({
        super.key,
      });
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
      late final TabController _tabController = TabController(length: tabs.length, vsync: this);
    
      late final Map<Tab, Widget> tabs = {
        for (int i = 0; i < 5; i++) Tab(text: '$i'): const MyDefaultWidget(),
      };
    
      int? tabIndex;
    
      @override
      void initState() {
        super.initState();
        _tabController.addListener(tabBarListener);
      }
    
      @override
      void dispose() {
        super.dispose();
        _tabController.dispose();
      }
    
      void tabBarListener() {
        if (tabIndex != _tabController.index) {
          tabIndex = _tabController.index;
          setState(() => tabs[tabs.keys.elementAt(tabIndex!)] = const MySpecialWidget());
        }
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            backgroundColor: Theme.of(context).colorScheme.background,
            title: TabBar(
              controller: _tabController,
              // I advice you to customize the TabBarTheme instead of providing these one-off values
              indicatorColor: Theme.of(context).colorScheme.primary,
              labelColor: Theme.of(context).colorScheme.primary,
              unselectedLabelColor: Colors.black54,
              tabs: tabs.keys.toList(),
            ),
          ),
          body: TabBarView(
            controller: _tabController,
            children: tabs.values.toList(),
          ),
        );
      }
    }
    
    class MyDefaultWidget extends StatelessWidget {
      const MyDefaultWidget({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const Center(
          child: Text('DEFAULT'),
        );
      }
    }
    
    class MySpecialWidget extends StatelessWidget {
      const MySpecialWidget({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const Center(
          child: Text('SPECIAL'),
        );
      }
    }
    

    WARNING: The tab is only refreshed, when the drag animation is fully completed! This could be perceived as lag. If you would like to already refresh the tabs content when a user half-swipes a tab, use the offset property of TabController and check if it passes either -0.5 or 0.5.

    Feel free to reach out if you need any more help :)