Streambuilder is causing my widgets to rebuild twice inside tabs

63 Views Asked by At

everyone, My project has 2 tabs, captain and first officer, in the main screen i have the tabs wrapped around with streamBuilder, so that it will wait until it gets data from firebase and then update it using provider, I haven't fully implemented provider yet as I am confused with riverPod usage in future. Anyways, the problem is it will load fine once, but when i go to the firstOfficer tab and back to captain tab, the init state of captain is called twice, and the streambuilder will download snapshot twice, which i can't figure out why , Is it the layout builder or sth..?

Here is the code

MainScreen calls to check if firebase data is downloaded in provider before updating the tabs

class MainScreen extends StatefulWidget {
  static const String idScreen = "mainScreen";
  const MainScreen({super.key});
  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  
  @override
  void initState() {
    print('mainScreen initstate called');
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: LayoutBuilder(
        builder: (context, constraints) {
          final isSmallScreen = constraints.maxWidth < smallScreenSize;
          final tabTextStyle =
              Theme.of(context).textTheme.titleMedium!.copyWith(
                    fontSize: isSmallScreen ? 8 : 12,
                  );
          final tabs = [
            Tab(
              text: isSmallScreen ? null : 'CAPTAIN',
              icon: Icon(Bootstrap.person,
                  size: isSmallScreen ? smallIconSize : largeIconSize),
            ),
            Tab(
              text: isSmallScreen ? null : 'FIRST OFFICER',
              icon: Icon(Icons.person_outline,
                  size: isSmallScreen ? smallIconSize : largeIconSize),
            ),
          ];
          return DefaultTabController(
            length: tabs.length,
            child: Scaffold(
              appBar: AppBar(
                toolbarHeight: isSmallScreen ? 10 : 20,
                bottom: TabBar(
                  tabs: tabs,
                  onTap: (index) {
                    if (index == 12) {
                      FirebaseAuth.instance.signOut().then((value) {
                        Navigator.pushNamedAndRemoveUntil(
                            context, LoginScreen.idScreen, (route) => false);
                      });
                    } else {
                      setState(() {
                        _currentIndex = index;
                      });
                    }
                  },
                  labelStyle: tabTextStyle,
                  labelPadding:
                      EdgeInsets.symmetric(vertical: isSmallScreen ? 2 : 4),
                  indicatorPadding:
                      EdgeInsets.symmetric(horizontal: isSmallScreen ? 2 : 8),
                  isScrollable: false,
                ),
              ),
              body: TabBarView(
                physics: NeverScrollableScrollPhysics(),
                children: [
                  StreamBuilder<List<Map<String, dynamic>>>(
                    stream:
                        Provider.of<FirebaseProvider>(context, listen: false)
                            .fetchCaptainData(),
                    builder: (context, snapshot) {
                      if (snapshot.connectionState == ConnectionState.waiting) {
                        return const Center(child: CircularProgressIndicator());
                      } else if (snapshot.hasError) {
                        return Center(child: Text('Error: ${snapshot.error}'));
                      } else {
                        return Captain();
                      }
                    },
                  ),
                  FirstOfficer(),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

this is the provider for firebase snapshot i added a print statemtn in fetchCaptainData

class FirebaseProvider extends ChangeNotifier {
  List<Map<String, dynamic>> captainSnapShot = [];
  List<Map<String, dynamic>> firstOfficerSnapShot = [];
  List<Map<String, dynamic>> cabinCrewSnapShot = [];
  static final CollectionReference captainCollection =
      FirebaseFirestore.instance.collection('CAPTAIN_DATA');

  Stream<List<Map<String, dynamic>>> fetchCaptainData() async* {
    captainSnapShot = await getCaptainData();
    print('captainProvider called');
    //notifyListeners();
    yield captainSnapShot;
  }

  static Future<List<Map<String, dynamic>>> getCaptainData() async {
    final snapshot = await captainCollection.get();
    final captainData =
        snapshot.docs.map((doc) => doc.data() as Map<String, dynamic>).toList();
    // print('Captain data fetched: $captainData');
    return captainData;
  }

  Future<void> fetchFirstOfficerData() async {
    firstOfficerSnapShot = await FirebaseServices
        .getFirstOfficerData(); // Retrieves the captain data from Firebase.
    notifyListeners();
  }

  Future<void> fetchCabinCrewData() async {
    cabinCrewSnapShot = await FirebaseServices
        .getCabinCrewData(); // Retrieves the captain data from Firebase.
    notifyListeners();
  }
}

Now this is the captain class

class Captain extends StatefulWidget {
  const Captain({
    Key? key,
  }) : super(key: key);
  @override
  _CaptainState createState() => _CaptainState();
}

class _CaptainState extends State<Captain> {
 
  @override
  void initState() {
    print('captain init state called');
    super.initState();
    i = 0;
    // _updateGrid();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (captainData == null) {
      _updateGrid();
    }
  }

  @override
  Widget build(BuildContext context) {
    final CollectionReference captainCollection =
        FirebaseFirestore.instance.collection('CAPTAIN_DATA');
    firstOfficerData = Provider.of<FirebaseProvider>(context, listen: false)
        .firstOfficerSnapShot;
    return Scaffold(
       ////some code
        ));
  }

the problem again, is this print statemtn when i go from the first officer tab to captain tab, the provider and initstate is called twice because of the streambuilder, i tried futurebuilder too, same thing, how can I fix it?

captainProvider called
captain init state called
captainProvider called
captain init state called
1

There are 1 best solutions below

2
Yes Dev On

TabBarView calls the build method on the new child each time the tab changes. To prevent this, you can turn each tab child (Captain and First Officer) into a StatefulWidget that uses the AutomaticKeepAliveMixin. You just need to override wantKeepAlive and set it to true.

class TabOne extends StatefulWidget {
  const TabOne({Key? key}) : super(key: key);

  @override
  State<TabOne> createState() => _TabOneState();
}

class _TabOneState extends State<TabOne> with AutomaticKeepAliveClientMixin{
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Builder(
        builder: (context) {

          debugPrint('Builder tab 1');
          return Center(child: Text('Tab 1'));
        }
    );
  }

  @override
  bool get wantKeepAlive => true;
}

This should keep each child from getting rebuilt.


I also created a simple state management package as an alternative to Riverpod called code_on_the_rocks. It's fairly easy to run a future using that package and I have an example in the repo: https://github.com/CodeOTR/code_on_the_rocks/tree/main/samples/flutter_future_data