fluttergoogle-cloud-firestoreflutter-streambuilder

Flutter streambuilder not updating UI when listening to changes in a single firestore document


I have a screen that listens for changes in a document using a stream builder. If the document has data then it shows Widget1, otherwise it will show Widget2.

The problem: When the stream builder builds for the first time it uses Widget2 since there is no data, but when it receives the data it does not update the widget to Widget2.

Here is my simplified implementation:




class Test extends ConsumerStatefulWidget {
  const Test({ Key? key }) : super(key: key);

  @override
  ConsumerState<Test> createState() => _TestState();
}

class _TestState extends ConsumerState<Test> {
  @override
  Widget build(BuildContext context) {
    // final match = ref.watch(matchedEventProvider);
    final user = ref.read(currentUserProvider);

    return Scaffold(
      body:StreamBuilder<Object>(
        stream: db
            .collection(kMatchedCollection)
            .doc(user.currentMatch)
            .snapshots(),
        builder: (context, snapshot) {
          if (snapshot.data != null) {
            kLogger.d('snapshot data: ${snapshot.data.toString()}');
            final snap =
                snapshot.hasData ? snapshot.data as DocumentSnapshot : null;
            matchData = snap?.data() as Map<String, dynamic>;

            kLogger.d("Got real match data: $matchData");
            matchData['CreatedAt'] = DateTime.fromMillisecondsSinceEpoch(
                    matchData['CreatedAt'].seconds * 1000)
                .toString();
            final Event event = Event.fromJson(matchData);
            final String status = event.EventStatus;
            if (event.AssignedTo == ref.read(currentUserProvider).id) {
              //only push new screens if current user is assigned.
              if (status == Status.dateCancelled) {
                Navigator.pop(context);
              } else if (status == Status.dateConfirmed) {
                Navigator.pushReplacementNamed(
                    context, FinalConfirmationScreen.id);
              } else if (status == Status.deciding) {
                Navigator.pushReplacementNamed(
                    context, FinalConfirmationScreen.id);
              }
            }

            return Widget1();
          } else {
            kLogger.d("has data ${snapshot.hasData}");
            return Widget2();
          }
        },
      ),
    );
  }
}

Here are the corresponding logs:

/flutter (  988): ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter (  988): │ #0   _LocationWaitScreenState.build.<anonymous closure> (package:app/screens/location_wait_screen.dart:154:21)
I/flutter (  988): │ #1   StreamBuilder.build (package:flutter/src/widgets/async.dart:437:81)
I/flutter (  988): ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
I/flutter (  988): │ 🐛 has data false
I/flutter (  988): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter (  988): ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter (  988): │ #0   _LocationWaitScreenState.build.<anonymous closure> (package:app/screens/location_wait_screen.dart:44:21)
I/flutter (  988): │ #1   StreamBuilder.build (package:flutter/src/widgets/async.dart:437:81)
I/flutter (  988): ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
I/flutter (  988): │ 🐛 snapshot data: Instance of '_JsonDocumentSnapshot'
I/flutter (  988): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter (  988): ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
I/flutter (  988): │ #0   _LocationWaitScreenState.build.<anonymous closure> (package:app/screens/location_wait_screen.dart:49:21)
I/flutter (  988): │ #1   StreamBuilder.build (package:flutter/src/widgets/async.dart:437:81)
I/flutter (  988): ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
I/flutter (  988): │ 🐛 Got real match data: {StartedWith: bCO38DzmjrU9DsT4FTvU7uqR4r42, AssignedTo: bCO38DzmjrU9DsT4FTvU7uqR4r42, DocID: a1776e3d180543b084, Resets: 3, CreatedAt: Timestamp(seconds=1703706807, nanoseconds=365138000), EventStatus: Start, Time: , Participants: [RaLpzb2BZBUnn8AYYJ5UiApmmtx2, bCO38DzmjrU9DsT4FTvU7uqR4r42], Location: }
I/flutter (  988): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

From the logs we can observe that it first builds Widget2 as it says 🐛 has data false and then when the snapshot receives data it tries to build Widget1 but these build changes are not reflected in the UI.

Any help on this would be appreciated


Solution

  • To all the new people stumbling on this question, the problem is with an animation library that I was using. Don't try to modify the Text Widget child inside the animation widget and instead only use one animation widget while conditionally rendering.

    So the code will go from this:

     if (snapshot.hasData) {
                return const Animation(child: Text('Widget 1'));
              } else {
                return const Animation(child: Text('Widget 2'));
              }
            },
    

    to :

     if (snapshot.hasData) {
                return const Animation(child: Text('Widget 1'));
              } else {
                return Text('Widget 2');
              }
            },
    

    Removing the Animationwidget while rendering conditionally fixed this issue for me.

    Note: Here Animation widget could be any generic animation library widget from pub.dev .