GotJimmy / accordion

Other
46 stars 43 forks source link

StreamBuilder redrawing accordion and closing open sections on update #48

Closed lucaseduoli closed 1 year ago

lucaseduoli commented 1 year ago

Hello, I have a code that generates the Accordion list with a StreamBuilder, but when it updates something, the entire accordion gets redrawed and, because of that, the open sections close. I have tried to make a external state, setting isOpen on the sections, but when the component gets redrawed, the opening animation is triggered. How can I make this work?

  @override
  Widget build(BuildContext context) {
    return StreamBuilder(
      stream: FirebaseFirestore.instance
          .collection('users')
          .doc(widget.uid)
          .collection(
              widget.isGone ? 'destinationsGone' : 'destinationsInterest')
          .snapshots(),
      builder: (context,
          AsyncSnapshot<QuerySnapshot<Map<String, dynamic>>> snapshot) {
        if (snapshot.data == null &&
            snapshot.connectionState == ConnectionState.waiting) {
          return const Center(child: CircularProgressIndicator());
        }
        return Column(
          children: [
            Accordion(
              disableScrolling: true,
              paddingListHorizontal: 0,
              paddingListBottom: 0,
              headerBackgroundColor: Theme.of(context).highlightColor,
              rightIcon: RotatedBox(
                quarterTurns: 1,
                child: Padding(
                  padding: const EdgeInsets.all(5.0),
                  child: const Icon(Icons.chevron_right),
                ),
              ),
              children: [
                for (var index = 0; index < snapshot.data!.docs.length; index++)
                  AccordionSection(
                    header: Row(
                      children: [
                        Expanded(
                          child: Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text.rich(TextSpan(children: [
                                  TextSpan(
                                      text: snapshot.data!.docs[index]['title'],
                                      style: ThereigoTheme.boldStyle),
                                  TextSpan(text: " • "),
                                  TextSpan(children: [
                                    TextSpan(
                                        text: snapshot.data!.docs[index]['date']
                                            .toDate()
                                            .month
                                            .toString()),
                                    TextSpan(text: '/'),
                                    TextSpan(
                                        text: snapshot.data!.docs[index]['date']
                                            .toDate()
                                            .year
                                            .toString())
                                  ])
                                ])),
                                Text.rich(TextSpan(
                                  children: (snapshot
                                          .data!.docs[index]['locations']
                                          .expand((e) => [
                                                TextSpan(text: e),
                                                TextSpan(text: ', '),
                                              ])
                                          .toList()
                                        ..removeLast()) // Remove the last comma
                                      .cast<InlineSpan>()
                                      .toList(),
                                )),
                              ]),
                        ),
                        IconButton(
                          icon: Icon(Icons.edit),
                          onPressed: () => Navigator.of(context).push(
                            MaterialPageRoute(
                              builder: (context) => (ModifyDestinationScreen(
                                  isGone: widget.isGone,
                                  destinationId: snapshot.data!.docs[index]
                                      ['destinationId'],
                                  locations: (snapshot.data!.docs[index]
                                          ['locations'] as List)
                                      .map((item) => item as String)
                                      .toList(),
                                  title: snapshot.data!.docs[index]['title'])),
                            ),
                          ),
                        ),
                        IconButton(
                            icon: Icon(Icons.delete),
                            onPressed: () {
                              Widget cancelButton = TextButton(
                                child: Text("Cancel"),
                                onPressed: () {
                                  Navigator.of(context).pop();
                                },
                              );
                              Widget continueButton = TextButton(
                                child: Text("Delete"),
                                onPressed: () {
                                  FirestoreMethods().deleteDestination(
                                      widget.uid,
                                      snapshot.data!.docs[index]
                                          ['destinationId'],
                                      widget.isGone);
                                },
                              );
                              // set up the AlertDialog
                              AlertDialog alert = AlertDialog(
                                title: Text("Delete destination"),
                                content: Text(
                                    "Are you sure you want to delete this destination?"),
                                actions: [
                                  cancelButton,
                                  continueButton,
                                ],
                              );
                              // show the dialog
                              showDialog(
                                context: context,
                                builder: (BuildContext context) {
                                  return alert;
                                },
                              );
                            }),
                      ],
                    ),
                    content: GridView.builder(
                      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                          crossAxisCount: 4,
                          mainAxisSpacing: 10,
                          crossAxisSpacing: 10,
                          childAspectRatio: 1),
                      physics: NeverScrollableScrollPhysics(),
                      shrinkWrap: true,
                      itemCount: snapshot.data!.docs[index]['urls'].length + 1,
                      itemBuilder: (context, i) => i <
                              snapshot.data!.docs[index]['urls'].length
                          ? Column(
                              children: [
                                Text(snapshot.data!.docs[index]['names'][i]),
                                SizedBox(height: 3),
                                Expanded(
                                  child: InkWell(
                                    onTap: () => Navigator.of(context)
                                        .push(MaterialPageRoute(
                                            builder: (context) => widget.isGone
                                                ? AlbumScreen(snap: {
                                                    'albumId': snapshot.data!
                                                        .docs[index]['ids'][i],
                                                    'albumName': snapshot.data!
                                                        .docs[index]['names'][i]
                                                  })
                                                : FavoriteScreen(
                                                    uid: widget.uid,
                                                    favoriteId: snapshot.data!
                                                        .docs[index]['ids'][i],
                                                  ))),
                                    child: ClipRRect(
                                      borderRadius: BorderRadius.circular(10),
                                      child: Container(
                                        width: double.infinity,
                                        child: CachedNetworkImage(
                                            imageUrl: snapshot.data!.docs[index]
                                                ['urls'][i],
                                            fit: BoxFit.cover),
                                      ),
                                    ),
                                  ),
                                ),
                              ],
                            )
                          : Column(
                              children: [
                                Text("Attach"),
                                SizedBox(height: 3),
                                Expanded(
                                  child: InkWell(
                                    onTap: () => Navigator.of(context).push(
                                      MaterialPageRoute(
                                        builder: (context) => widget.isGone
                                            ? ChooseAlbumScreen(
                                                onClick: (snap) {
                                                  FirestoreMethods()
                                                      .addAlbumOrFavoriteToDestination(
                                                          widget.uid,
                                                          snapshot.data!
                                                                  .docs[index]
                                                              ['destinationId'],
                                                          snap!['albumId'],
                                                          snap!['title'],
                                                          snap!['imagesUrl'][0],
                                                          widget.isGone);
                                                  Navigator.of(context).pop();
                                                },
                                                title: "Attach Album",
                                              )
                                            : ChooseFavoritesScreen(
                                                onClick: (snap) {
                                                  FirestoreMethods()
                                                      .addAlbumOrFavoriteToDestination(
                                                          widget.uid,
                                                          snapshot.data!
                                                                  .docs[index]
                                                              ['destinationId'],
                                                          snap!['favoritesId'],
                                                          snap!['title'],
                                                          snap!['imagesUrl'][0],
                                                          widget.isGone);
                                                },
                                                title: "Attach Favorites"),
                                      ),
                                    ),
                                    child: Container(
                                      decoration: BoxDecoration(
                                        borderRadius: BorderRadius.circular(10),
                                        border: Border.all(
                                          color: Theme.of(context)
                                                  .textTheme
                                                  .bodyMedium
                                                  ?.color ??
                                              Colors.white,
                                          width: 1,
                                        ),
                                      ),
                                      child: Center(
                                          child: Icon(Icons.attach_file,
                                              size: 30)),
                                    ),
                                  ),
                                ),
                              ],
                            ),
                    ),
                  )
              ],
            ),
            GestureDetector(
              onTap: () => Navigator.of(context).push(
                MaterialPageRoute(
                  builder: (context) => (ModifyDestinationScreen(
                      isGone: widget.isGone, locations: [""])),
                ),
              ),
              child: Container(
                decoration: BoxDecoration(
                    color: Theme.of(context).highlightColor,
                    borderRadius: BorderRadius.circular(12)),
                child: Padding(
                  padding: const EdgeInsets.all(12.0),
                  child: Center(
                    child: Icon(Icons.add, size: 30),
                  ),
                ),
              ),
            ),
          ],
        );
      },
    );
  }
}
GotJimmy commented 1 year ago

Your StreamBuilder causes the Accordion to be rebuilt every time and therefore doing its opening sequence. You should only update data (if any) inside an AccordionSection, not rebuild the whole Accordion. See #47 #46 #42 #31 #21

PT10 commented 1 year ago

It's a very common use case. I have faced an issue due to this in mobile browser. In my case I have a MediaQuery call in the parent. In my accordion section there is a textFormField (used for searching). When I tap it the keyboard gets launched. Because of the MediaQuery call after launching the keyboard it recalculates the hight and build the widget again. As a result the accordion also gets rebuilt and the textFormField goes out of focus. Which then makes the keyboard disappear.

Then I used the ExpansionPanelList and it worked. The text field is not losing the focus in that case.

In general, in Flutter, rebuilds are quite common, the component should maintain the state across rebuilds.

GotJimmy commented 1 year ago

@PT10 I just tried putting a TextFromField in an AccordionSection and it works fine. Of course, if you wrap the whole Accordion in another widget that changes the layout then this might be a problem. However, combining all your requirements (textfield & wrapped) is probably out of the scope of this widget. You might want to try setting sectionAnimation to false -- maybe that helps.