0

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'],
    );
  }
}
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Raj A
  • 544
  • 9
  • 21
  • From what I understood of your code, `streamDemo()` is only called on the `onInit`, is this correct? If so, it makes sense that nothing gets updated, since the stream is never reinstantiated with the new `mainDocId` value. Can you confirm if calling this function again in your execution path fixed the issue? – Ralemos May 24 '21 at 14:19
  • Greetings Lemos, Yes I'm using streamDemo() method only in onInit() method. I tried calling the streamDemo() in the onpressed callback of BottomNavbar as well as in the changeDocid() method. It doesn't update. It prints the changed value but doesn't update. With Steambuilder it does update but not with models and Getx. – Raj A May 24 '21 at 15:12
  • well, I think you are going to need to use a streambuilder for that, I recommend you to apply the logic proposed in this [community question](https://stackoverflow.com/questions/60663818/flutter-how-to-refresh-streambuilder), as it's pretty similar to yours (at least has the same objective) and explains how to use streambuilder for that end. – Ralemos May 25 '21 at 13:46
  • 1
    I do know how to achieve the objective with streambuilder but I was curious how can I use it with this model concept. As streambuilder is expensive as per reads. Anyways Thank you for the response. – Raj A May 25 '21 at 14:11

2 Answers2

0

Try this:

changeDocId(int index) {
   mainDocId.value = docIdList[index];
 }

instead of

changeDocId(int index) {
   mainDocId.value(docIdList[index]);
 }

I believe updating an Rx by calling method (rxVar()) doesn't trigger widget rebuild but updating with assigning values does (rxVar.value=)

Hope this fixes or at least gets you closer to fixes.

S. M. JAHANGIR
  • 4,324
  • 1
  • 10
  • 30
0

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

Loren.A
  • 4,872
  • 1
  • 10
  • 17
  • It worked Loren. Thank you for your extra tips. Also, should I use two `StreamBuilder` in two separate screens or use this version for the displaying data on UI? Does two `StreamBuilder` with one `stream()` in `onInIt()` will have less reads or model version will have less read? – Raj A May 31 '21 at 18:48
  • No problem. It's possible that every time you re-bind the stream that adds another document read, but I'm not sure about that. You could also create a separate `RxList` per page and bind them to each `docId` and that would be the same as a `StreamBuilder` on each page, without having to actually use a `StreamBuilder`. That way you get all your reads in when the app starts, then the only time you get dinged for more reads is when something in the cloud changes and Firebase pushes a real time update of to a users device. – Loren.A May 31 '21 at 19:34