1

I have screens in a TabBarView that share data so I decided to wrap the TabBarView in a FutureBuilder and pass the data from that into both screens. The Future is called fetchUsersAndPosts() and it is meant to return [[*users], [*posts*]] but the snapshot of the FutureBuilder looks like this [[], []]. In the Future I print both lists before returning them so I know they aren't empty. Why are they returning as empty? Additionally, the print statements are continuous which tells me the methods are repeating (I think the FutureBuilder is calling the Future endlessly) and I went to check Firebase and it shows 700 reads today because of the repeated functions. How do I fix that?

Future:

Future<List<QueryDocumentSnapshot>> getUserDocs() async {

    List<QueryDocumentSnapshot<Object?>> userDocs = [];

    try {
      final userRef = FirebaseFirestore.instance.collection('users');
      final QuerySnapshot result = await userRef.get();
      userDocs = result.docs;
    } on FirebaseException catch (e) {
      print(e.message);
    }

    print('length: ${userDocs.length}');

    return Future.value(userDocs);
  }

  Future<List<QueryDocumentSnapshot>> getPostDocs(List<QueryDocumentSnapshot> userDocs) async {

    List<QueryDocumentSnapshot> postsDocs = [];

    for (final resultValue in userDocs) {
      try {
        final postsRef = FirebaseFirestore.instance
            .collection('users')
            .doc(resultValue.id)
            .collection('posts');
        final QuerySnapshot postsResult = await postsRef.get();
        final postDocs = postsResult.docs;

        for (var post in postDocs) {
          postsDocs.add(post);
        }
      } on FirebaseException catch (e) {
        print(e.message);
      }
    }
    return Future.value(postsDocs);
  }

  Future<List> fetchUsersAndPosts() async {

    List<QueryDocumentSnapshot> userDocs = await getUserDocs();
    List<QueryDocumentSnapshot> postsDocs = await getPostDocs(userDocs);

    print('userDocs: $userDocs');
    print('postsDocs: $postsDocs');

    List<UserSearchResult> usersList = [];
    List<Post> postsList = [];

    for (var postDoc in postsDocs) {
      final post = Post.fromJson(postDoc.data() as Map<String, dynamic>);
      if (!postsList.contains(post)) {
        postsList.add(post);
      }
    }

    for (final userDoc in userDocs) {
      final profileInfo =
      ProfileInfoObject.fromJson(userDoc.data() as Map<String, dynamic>);

      Profile profile = Profile(
          profileInfo, postsList.where((p) => p.uid == userDoc.id).toList());

      UserSearchResult user = (UserSearchResult(profile, userDoc.id));

      if (usersList.where((u) => u.uid == user.uid).toList().isEmpty) {
        usersList.add(user);
        print('usersList: $usersList');
      }
    }

    print('usersList: $usersList');
    print('postsList: $postsList');

    postsList.sort((a, b) {
      return b.date.compareTo(a.date);
    });

    return [usersList, postsList];
  }

FutureBuilder:

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

  @override
  State<FloatingTabBarView> createState() => _FloatingTabBarViewState();
}

class _FloatingTabBarViewState extends State<FloatingTabBarView>
    with TickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation _animation;

  List<Post> currentUserPosts = [];

  Profile currentProfile = Profile(
    ProfileInfoObject(
      FirebaseAuth.instance.currentUser!.photoURL!,
      FirebaseAuth.instance.currentUser!.displayName!,
    ),
    [],
  );

  void fetchCurrentUserPosts() async {
    final postsRef = FirebaseFirestore.instance
        .collection('users')
        .doc(FirebaseAuth.instance.currentUser!.uid)
        .collection('posts');
    final postsResult = await postsRef.get();
    final postDocs = postsResult.docs.asMap();

    List<Post> posts = [];

    postDocs.forEach((index, value) {
      final post = Post.fromJson(value.data());

      posts.add(post);

      setState(() {
        currentUserPosts.add(post);
        currentProfile = Profile(
          ProfileInfoObject(
            FirebaseAuth.instance.currentUser!.photoURL!,
            FirebaseAuth.instance.currentUser!.displayName!,
          ),
          currentUserPosts,
        );
      });
    });
  }

  final DataProvider dataProvider = DataProvider();

  late Future<List> _futureLists;

  @override
  void initState() {
    fetchCurrentUserPosts();

    _animationController =
        AnimationController(vsync: this, duration: const Duration(seconds: 2));
    _animationController.repeat(reverse: true);
    _animation = Tween(begin: 2.0, end: 15.0).animate(_animationController)
      ..addListener(() {
        setState(() {});
      });

    _futureLists = dataProvider.fetchUsersAndPosts();

    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
  }

  Widget floatingTabBarPageView() {
    List<TabItem> tabList(List<UserSearchResult> users, List<Post> posts) {
      List<TabItem> list = [
        TabItem(
          icon: const Icon(Icons.home_outlined, size: 35),
          selectedIcon: const Icon(Icons.home_rounded, size: 35),
          label: "Home",
          tabWidget: HomeScreen(usersMap: users, postsList: posts),
        ),
        TabItem(
          icon: const Icon(Icons.travel_explore_outlined, size: 35),
          selectedIcon: const Icon(Icons.travel_explore, size: 35),
          label: "Explore",
          tabWidget: ExploreScreen(usersMap: users, postsList: posts),
        ),
        if (!kIsWeb)
          if (Platform.isIOS || Platform.isAndroid)
            const TabItem(
              icon: Icon(Icons.post_add_outlined, size: 35),
              selectedIcon: Icon(Icons.post_add, size: 35),
              label: "Post",
              tabWidget: CreatePostScreen(),
            ),
        TabItem(
          icon: const Icon(Icons.account_circle_outlined, size: 35),
          selectedIcon: const Icon(Icons.account_circle_rounded, size: 35),
          label: "Account",
          tabWidget: ProfileScreen(
            uid: FirebaseAuth.instance.currentUser!.uid,
            profile: currentProfile,
          ),
        ),
      ];
      return list;
    }

    return FutureBuilder(
        future: _futureLists,
        builder: (context, AsyncSnapshot<List<dynamic>> snapshot) {
          List<UserSearchResult> users = [];
          if (snapshot.data?[0] != null) {
            users = snapshot.data![0];
          }
          List<Post> posts = [];
          if (snapshot.data?[1] != null) {
            posts = snapshot.data![1];
          }
          print('Snapshots: $users, $posts');
          if (!snapshot.hasData) {
            return Scaffold(
              body: Center(
                child: Container(
                  width: 100,
                  height: 100,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: const Color.fromARGB(255, 27, 28, 30),
                    boxShadow: [
                      BoxShadow(
                        color: const Color.fromARGB(255, 178, 215, 223),
                        blurRadius: _animation.value,
                        spreadRadius: _animation.value,
                      ),
                    ],
                  ),
                  child: Image.asset('assets/logo.png'),
                ),
              ),
            );
          } else {
            return FloatingTabBar(
              activeColor: const Color(0xFF7157A0),
              inactiveColor: const Color(0xFFadc0ff),
              tabItemList: tabList(users, posts),
              isFloating: true,
              titleTapNavigationRouteWidget: HomeScreen(
                usersMap: users,
                postsList: posts,
              ),
              title: 'GLOBE',
              showTabLabelsForFloating: true,
            );
          }
        });
  }

  @override
  Widget build(BuildContext context) {
    return floatingTabBarPageView();
  }
}
Globe
  • 514
  • 3
  • 17
  • You should not call the `fetchUsersAndPosts ` function in the build method. The build method can be called many times. So you must call the function in the `initState` and assign the resultant future to a state variable of type Future. For this you must use a Stateful widget. The resultant future can then be passed to future parameter of the FutureBuilder – Calvin Gonsalves Sep 24 '22 at 05:54
  • i don't see anything wrong with current code, can you please post the full code of widget that has FutureBuilder – Sanketh B. K Sep 24 '22 at 05:54
  • @CalvinGonsalves i don't see any problem there, it is called inside FutureBuilder's future which expects future – Sanketh B. K Sep 24 '22 at 05:59
  • @SankethB.K I just uploaded the full widget (including the list of the tabs which use the data from the `FutureBuilder`). – Globe Sep 24 '22 at 06:21
  • @Globe can you also post the build method of the main widget, need that to know what is triggering the rebuilds – Sanketh B. K Sep 24 '22 at 06:27
  • @SankethB.K I uploaded the whole class now so you can see that. – Globe Sep 24 '22 at 06:31

1 Answers1

2

It's the animation! AnimationController triggers the build method, which involves FutureBuilder as well, so everytime a new FutureBuilder is created it invokes a new call to Firebase.

You have to separate the animation logic and data fetching logic into different widget trees

My recommendation is to refactor this into its own widget and place the animation controller and all inside it, as this is the only widget using the _animtion.value

            return Scaffold(
              body: Center(
                child: Container(
                  width: 100,
                  height: 100,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: const Color.fromARGB(255, 27, 28, 30),
                    boxShadow: [
                      BoxShadow(
                        color: const Color.fromARGB(255, 178, 215, 223),
                        blurRadius: _animation.value,
                        spreadRadius: _animation.value,
                      ),
                    ],
                  ),
                  child: Image.asset('assets/logo.png'),
                ),
              ),
            );
Sanketh B. K
  • 759
  • 8
  • 22
  • How could I do that? Should I create a separate class for the animated widget? – Globe Sep 24 '22 at 06:42
  • Yes just create a separate widget and return the above scaffold from your build method, and you can move all the code related to animated controller inside this new widget – Sanketh B. K Sep 24 '22 at 16:06