Globe
Globe

Reputation: 564

FutureBuilder snapshot is empty but Future has correct data AND FutureBuilder constantly refreshing - Flutter

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();
  }
}

Upvotes: 1

Views: 274

Answers (1)

Sanketh B. K
Sanketh B. K

Reputation: 837

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'),
                ),
              ),
            );

Upvotes: 2

Related Questions