Animated nested lists fetching real time data from Firebase with StreamBuilder - Flutter

140 Views Asked by At

I'm trying to create a program where teachers can create classes and other users can request an invitation for those classes. This is the document structure in Firebase:

  • classes_data (collection)
    • {class_id} (document)
      • invitations (collection)
        • {uid} (document)
  • users_data (collection)
    • {uid} (document)
      • classes_hosting (collection)
        • {class_id} (document)

I want a screen with a list that displays all of the classes the user is hosting, and inside each class I want a nested list to display all the pending invitations for that class. However, I want the list of invitations to be animated so that the list changes in real-time with an animation every time someone requests an invitation for that class. People can request invitations and cancel requests, so the list has to be able to have the elements added and removed.

The code that I wrote works fine with nested ListViews. However, as soon as I change the inner ListView to an AnimatedList, it stops working properly.

import 'package:classes_app/models/classHosting.dart';
import 'package:classes_app/models/pending_invitations.dart';
import 'package:classes_app/models/user.dart';
import 'package:classes_app/services/database.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

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

  @override
  State<Hosting5> createState() => _Hosting5State();
}

class _Hosting5State extends State<Hosting5> with AutomaticKeepAliveClientMixin {

  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);

    // Stream with all classes that user is hosting and will happen in the future (after today):
    var stream1 = DatabaseService(uid: hiphenUserSnapshot!.uid!).futureClassesHosting;

    return SingleChildScrollView(
      child: StreamBuilder<List<ClassHosting>>(
        stream: stream1,
        builder: (context, snapshot) {

          if (!snapshot.hasData) {
            return const Text("Loading");
          }

          if (snapshot.data!.isNotEmpty) {
            return ListView.builder(
                shrinkWrap: true,
                physics: const NeverScrollableScrollPhysics(),
                padding: const EdgeInsets.only(top: 15.0),
                itemCount: snapshot.data!.length,
                itemBuilder: (context, index) {

                  // Stream with all pending invitations for that class:
                  var stream2 = DatabaseService(uid: hiphenUserSnapshot!.uid!).getPendingInvitationRequests(snapshot.data![index].classId!);

                  return Padding(
                    padding: const EdgeInsets.all(20.0),
                    child: Column(
                      children: [
                        Text(
                          "Class: ${snapshot.data![index].title!}",
                          style: const TextStyle(
                            fontSize: 16,
                            color: Color(0xff101010),
                            fontWeight: FontWeight.w500,
                            height: 1.0,
                          ),
                        ),
                        StreamBuilder<List<PendingInvitation>>(
                          stream: stream2,
                          builder: (context, snapshot) {

                            if (!snapshot.hasData) return const Text("Loading...");

                            if (snapshot.data!.isNotEmpty) {

                              return AnimatedList( // WORKS FINE WHEN THIS IS A LISTVIEW.BUILDER
                                shrinkWrap: true,
                                physics: const NeverScrollableScrollPhysics(),
                                padding: EdgeInsets.zero,
                                initialItemCount: snapshot.data!.length,
                                itemBuilder: (context, index, animation) {

                                  return Row(
                                    children: [
                                      Column(
                                        crossAxisAlignment: CrossAxisAlignment.center,
                                        children: [
                                            Text(
                                              "Pending invitation name: ${snapshot.data![index].name}",
                                              textAlign: TextAlign.center,
                                              style: const TextStyle(
                                                fontWeight: FontWeight.w700,
                                              ),
                                            ),
                                        ],
                                      ),
                                    ],
                                  );
                                },
                              );
                            } else {
                              return const SizedBox();
                            }
                          },
                        ),
                      ],
                    ),
                  );
                }
            );
          } else {
            return const SizedBox();
          }
        },
      ),
    );
  }
}

I get a RangeError (index) most of the time when the list changes.

I know that I still have to provide an animation, but I'm trying to get the rest to work first. I believe this error happens because the AnimatedList has to know which elements to remove and which ones to insert through a listKey. I tried to use stream.listen((){}) and then compare the old list with the new list to remove/add the necessary items like explained in this question. The issue is that I can't figure out how to do this with each event. Basically, since the AnimatedList is nested inside a ListView, I don't think the approach explained in the question work for this case, or at least not without some modifications that I can't figure out.

I would really appreciate if someone could help me make this work.

0

There are 0 best solutions below