Stream not updating when docId is changed dynamically

Issue

The streamDemo() is not updating the value of doc(‘$mainDocId’) when the value of mainDocId is updated dynamically. I want to update the widget HomeBody() when the document Id is changed dynamically so that I can retrieve the data as per documents selected by users.

I’m using getx as SM. I tried to update the value with update() method but not working.

The codes are as follows.

Controller:

class Controller extends GetxController {

  // onInit
  @override
  void onInit() {
    finalNewsModel.bindStream(streamDemo());
    super.onInit();
  }

 
  // list of document ids.
  List docIdList = [
    'USA',
    'New York',
    'Canada',
  ];

  //
  RxString mainDocId = 'USA'.obs;

  // method to change document id based on docId index.
  changeDocId(int index) {
    mainDocId(docIdList[index]);
  }

  //
  Rxn<List<NewsModel>> finalNewsModel = Rxn<List<NewsModel>>();

  //
  List<NewsModel> get newsModelList => finalNewsModel.value;

  //
  Stream<List<NewsModel>> streamDemo() {
    return FirebaseFirestore.instance
        .collection('news')
        .doc('$mainDocId')
        .snapshots()
        .map((ds) {
      var mapData = ds.data();
      List mapList = mapData['list'];
      List<NewsModel> modelList = [];
      mapList.forEach((element) {
        modelList.add(NewsModel.fromMap(element));
      });
      return modelList;
    });
  }
}

// UI

class HomeBody extends StatefulWidget {
  @override
  _HomeBodyState createState() => _HomeBodyState();
}

class _HomeBodyState extends State<HomeBody> {
//
  final Controller _controller = Get.put<Controller>(Controller());

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Obx(() {
        if (_controller.newsModelList == null) {
          return Center(
              child: Text(
            'Please try later!',
          ));
        } else if (_controller.newsModelList.isEmpty) {
          return Text('Empty List');
        } else {
          return ListView.builder(
            itemCount: _controller.newsModelList.length,
            itemBuilder: (context, index) {
              final NewsModel _newsModel = _controller.newsModelList[index];
              return MyContainer(
                title: _newsModel.title,
                titleImage: _newsModel.titleImage,
                index: index,
              );
            },
          );
        }
      }),
    );
  }
}

BottomNavBar:

bottomNavigationBar: Container(
        color: Colors.grey[300],
        height: 60.0,
        child: Padding(
          padding: EdgeInsets.all(8.0),
          child: GetBuilder<Controller>(
            builder: (context) => ListView.builder(
              shrinkWrap: true,
              scrollDirection: Axis.horizontal,
              itemCount: _controller.docIdList.length,
              itemBuilder: (context, index) {
                return FavCategoryTags(
                  tagName: _controller.docIdList[index],
                  onpress: () =>_controller.changeDocId(index),
                );
              },
            ),
          ),
        ),
      ),

Model:

class NewsModel {
  String title, titleImage, brief, source;
  List aList;

  NewsModel({this.title, this.titleImage, this.brief, this.aList, this.source});

  factory NewsModel.fromMap(dynamic fieldData) {
    return NewsModel(
      title: fieldData['title'],
      titleImage: fieldData['titleImage'],
      brief: fieldData['brief'],
      aList: fieldData['mediaDescList'],
      source: fieldData['source'],
    );
  }
}

Solution

No StreamBuilder needed here. Once a regular Stream is binded to an RxType you’re good to go. It will update when the stream data changes. You just need to update the binding call as demonstrated below.

One issue is that you should just initialize to a regular RxList.

Instead of this

  Rxn<List<NewsModel>> finalNewsModel = Rxn<List<NewsModel>>();

Initialize it like this.

  RxList<NewsModel> finalNewsModel = <NewsModel>[].obs;

You can then lose this getter

  List<NewsModel> get newsModelList => finalNewsModel.value;

because .value isn’t needed and won’t work on a properly initialized RxList. You can treat an RxList list like a regular list, as opposed to RxString, RxInt etc… that need .value.

Your Obx can now build on finalNewsModel and you can lose the null check because finalNewsModel is initialized to an empty list, and will never be null.

Obx(() {
            if (_controller.finalNewsModel.isEmpty) {
              return Text('Empty List');
            } else {
              return Expanded(
                child: ListView.builder(
                  itemCount: _controller.finalNewsModel.length,
                  itemBuilder: (context, index) {
                    final NewsModel _newsModel =
                        _controller.finalNewsModel[index];
                    return MyContainer(
                      title: _newsModel.title,
                      titleImage: _newsModel.titleImage,
                      index: index,
                    );
                  },
                ),
              );
            }
          }),

As for what you’re trying to do with your BottomNavBar:

Here you’re trying to change the parameter of the Stream itself, by changing the Document Id. When you binded to the Stream in onInit it binded to whatever Document Id was set to at the time. So it will only update for changes within that Document unless you bind it to a new Stream. So in your case, just call finalNewsModel.bindStream(streamDemo()); again in the changeDocId() method to update the Stream parameters.

  void changeDocId(int index) {
    mainDocId(docIdList[index]);
    finalNewsModel.bindStream(streamDemo()); // only needed because you're updating the Document Id
  }

You also don’t need a GetBuilder in your BottomNavBar unless you need something visually to change on the BottomNavBar itself. All you’re doing is updating the value of an RxString based on the value of a hard coded List. Assuming you did need something in the BottomNavBar to rebuild, that would be the only scenario you would need to call update().

I don’t have your full Firebase collection structure, but I tested it with a simplified NewsModel and updating a String in the Firebase console updates the Obx widget immediately. And calling changeDocId() immediately returns the value of the updated Document Id.

EDIT: Also, for what you’re doing, mainDocId doesn’t need to be an observable string. A stream will always be more expensive than a primitive data type so unless you can justify it, best just to make it a regular string like below. It works exactly the same.

 String mainDocId = 'USA';

  // method to change document id based on docId index.
  void changeDocId(int index) {
    mainDocId = docIdList[index];
    finalNewsModel.bindStream(streamDemo());
  }

Answered By – Loren.A

Answer Checked By – David Goodson (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *