Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Streambuilder not rebuilding after BLOC event while using Built value #1707

Closed
MatthewCoetzee412 opened this issue Sep 7, 2020 · 8 comments
Assignees
Labels
question Further information is requested
Projects

Comments

@MatthewCoetzee412
Copy link

I am trying to implement pagination in my application but I have not been successful in doing so.

I am using Firebase, specifically Firestore with the BLOC pattern alongside Built Value which I started using recently to make pagination easier.

I would really appreciate any help or referral links how to use these technologies together.

My application architecture is as follows:

https://i.stack.imgur.com/MgNjx.png

I have tried to keep to the BLOC pattern as much as possible but this in turn has made it really difficult to paginate largely ,because of using built value as built value make it really difficult to use Streams and Futures. I have looked all over the Internet but I could not find any tutorial or docs to use built value with Firestore and BLOC specifically to paginate.

The problem is that when I do any of the CRUD functions for example delete an Category from a list, the Stream Builder is not updating the list despite the pagination and everything else working.

Currently I have tried using the a Listview builder by itself which obviously didn't work at all,so I moved to a Stream Builder and tryed both Streams and Futures(.asStream) but it is not updating.

Below is some of the code:

The model:

abstract class CategoryCard
    implements Built<CategoryCard, CategoryCardBuilder> {
  String get category;
  String get icon;
  double get budget;
  double get spent;
  String get categoryRef;
  DocumentSnapshot get document;

  CategoryCard._();

  factory CategoryCard([updates(CategoryCardBuilder b)]) = _$CategoryCard;

  static Serializer<CategoryCard> get serializer => _$categoryCardSerializer;
}

The query:

  Future<Stream<fs.QuerySnapshot>> getMoreCategoryAmounts(
      fs.DocumentSnapshot documentSnapshot) async {
    var user = await getCurrentUser();

    print(currentMonth);

    fs.Query categoryAmountQuery = _instance
        .collection('users')
        .document(user.uid)
        .collection('amounts')
        .where('year', isEqualTo: currentYear)
        .where('month', isEqualTo: currentMonth)
        .orderBy('category', descending: false)
        .limit(7);

    return documentSnapshot != null
        ? categoryAmountQuery.startAfterDocument(documentSnapshot).snapshots()
        : categoryAmountQuery.snapshots();
  }

The BLOC:

class CategoryCardBloc extends Bloc<CategoryCardEvents, CategoryCardState> {
  final BPipe bPipe;
  final FirebaseRepository firebaseRepository;

  CategoryCardBloc({@required this.bPipe, @required this.firebaseRepository})
      : assert(bPipe != null),
        assert(firebaseRepository != null);

  @override
  CategoryCardState get initialState => CategoryCardState.intial();

  @override
  Stream<CategoryCardState> mapEventToState(CategoryCardEvents event) async* {
    if (event is LoadCategoryCardEvent) {
      yield* _mapToEventLoadCategoryCard(event);
    }
  }

  Stream<CategoryCardState> _mapToEventLoadCategoryCard(
      LoadCategoryCardEvent event) async* {
    if (event.amountDocumentSnapshot == null) {
      yield CategoryCardState.loading();
    }
    try {
      Future<BuiltList<CategoryCard>> _newCategoryCards =
          bPipe.getMoreCategoryCards(event.amountDocumentSnapshot);

      yield CategoryCardState.loaded(
          FutureMerger()
              .merge<CategoryCard>(state.categoryCards, _newCategoryCards));
    } on NullException catch (err) {
      print('NULL_EXCEPTION');
      yield CategoryCardState.failed(err.objectExceptionMessage,
          state?.categoryCards ?? Stream<BuiltList<CategoryCard>>.empty());
    } on NoValueException catch (_) {
      print('NO VALUE EXCEPTION');
      yield state.rebuild((b) => b..hasReachedEndOfDocuments = true);
    } catch (err) {
      print('UNKNOWN EXCEPTION');
      yield CategoryCardState.failed(
          err != null ? err.toString() : NullException.exceptionMessage,
          state.categoryCards);
    }
  }
}

The state:

abstract class CategoryCardState
    implements Built<CategoryCardState, CategoryCardStateBuilder> {
  Future<BuiltList<CategoryCard>> get categoryCards;
  //*Reached end indicator
  bool get hasReachedEndOfDocuments;
  //*Error state
  String get exception;
  //*Loading state
  @nullable
  bool get isLoading;
  //*Success state
  @nullable
  bool get isSuccessful;
  //*Loaded state
  @nullable
  bool get isLoaded;

  CategoryCardState._();

  factory CategoryCardState([updates(CategoryCardStateBuilder b)]) =
      _$CategoryCardState;

  factory CategoryCardState.intial() {
    return CategoryCardState((b) => b
      ..exception = ''
      ..isSuccessful = false
      ..categoryCards =
          Future<BuiltList<CategoryCard>>.value(BuiltList<CategoryCard>())
      ..hasReachedEndOfDocuments = false);
  }

  factory CategoryCardState.loading() {
    return CategoryCardState((b) => b
      ..exception = ''
      ..categoryCards =
          Future<BuiltList<CategoryCard>>.value(BuiltList<CategoryCard>())
      ..hasReachedEndOfDocuments = false
      ..isLoading = true);
  }
  factory CategoryCardState.loaded(Future<BuiltList<CategoryCard>> cards) {
    return CategoryCardState((b) => b
      ..exception = ''
      ..categoryCards = cards
      ..hasReachedEndOfDocuments = false
      ..isLoading = false
      ..isLoaded = true);
  }
  factory CategoryCardState.success(Future<BuiltList<CategoryCard>> cards) {
    return CategoryCardState((b) => b
      ..exception = ''
      ..categoryCards =
          Future<BuiltList<CategoryCard>>.value(BuiltList<CategoryCard>())
      ..hasReachedEndOfDocuments = false
      ..isSuccessful = true);
  }
  factory CategoryCardState.failed(
      String exception, Future<BuiltList<CategoryCard>> cards) {
    return CategoryCardState((b) => b
      ..exception = exception
      ..categoryCards = cards
      ..hasReachedEndOfDocuments = false);
  }
}

The event:

abstract class CategoryCardEvents extends Equatable {}

class LoadCategoryCardEvent extends CategoryCardEvents {
  final DocumentSnapshot amountDocumentSnapshot;

  LoadCategoryCardEvent({@required this.amountDocumentSnapshot});

  @override
  List<Object> get props => [amountDocumentSnapshot];
}

The pagination screen(Contained inside a stateful widget):

//Notification Handler
  bool _scrollNotificationHandler(
      ScrollNotification notification,
      DocumentSnapshot amountDocumentSnapshot,
      bool hasReachedEndOfDocuments,
      Future<BuiltList<CategoryCard>> cards) {
    if (notification is ScrollEndNotification &&
        _scollControllerHomeScreen.position.extentAfter == 0 &&
        !hasReachedEndOfDocuments) {
      setState(() {
        _hasReachedEnd = true;
      });

      _categoryCardBloc.add(LoadCategoryCardEvent(
          amountDocumentSnapshot: amountDocumentSnapshot));
    }
    return false;
  }


BlocListener<CategoryCardBloc, CategoryCardState>(
                    bloc: _categoryCardBloc,
                    listener: (context, state) {
                      if (state.exception != null &&
                          state.exception.isNotEmpty) {
                        if (state.exception == NullException.exceptionMessage) {
                          print('Null Exception');
                        }  else {
                          ErrorDialogs.customAlertDialog(
                              context,
                              'Failed to load',
                              'Please restart app or contact support');

                          print(state.exception);
                        }
                      }
                    },
                    child: BlocBuilder<CategoryCardBloc, CategoryCardState>(
                        bloc: _categoryCardBloc,
                        builder: (context, state) {
                          if (state.isLoading != null && state.isLoading) {
                            return Center(
                              child: CustomLoader(),
                            );
                          }

                          if (state.isLoaded != null && state.isLoaded) {
                            return StreamBuilder<BuiltList<CategoryCard>>(
                              stream: state.categoryCards.asStream(),
                              builder: (context, snapshot) {

                                if (!snapshot.hasData) {
                                  return Center(
                                    child: CustomLoader(),
                                  );
                                } else {
                                  BuiltList<CategoryCard> categoryCards =
                                      snapshot.data;

                                  _hasReachedEnd = false;

                                  print(state.hasReachedEndOfDocuments &&
                                      state.hasReachedEndOfDocuments != null);

                                  return Container(
                                    height: Mquery.screenHeight(context),
                                    width: Mquery.screenWidth(context),
                                    child: NotificationListener<
                                        ScrollNotification>(
                                      onNotification: (notification) =>
                                          _scrollNotificationHandler(
                                              notification,
                                              categoryCards.last.document,
                                              state.hasReachedEndOfDocuments,
                                              state.categoryCards),
                                      child: SingleChildScrollView(
                                        controller: _scollControllerHomeScreen,
                                        child: Column(
                                          children: [
                                            CustomAppBar(),
                                            Padding(
                                              padding: EdgeInsets.all(
                                                  Mquery.padding(context, 2.0)),
                                              child: Row(
                                                children: [
                                                  Expanded(
                                                    flex: 5,
                                                    child: Padding(
                                                      padding: EdgeInsets.all(
                                                          Mquery.padding(
                                                              context, 1.0)),
                                                      child:Container(
                                                          width: Mquery.width(
                                                              context, 50.0),
                                                          height: Mquery.width(
                                                              context, 12.5),
                                                          decoration:
                                                              BoxDecoration(
                                                            color: middle_black,
                                                            borderRadius: BorderRadius
                                                                .circular(Constants
                                                                    .CARD_BORDER_RADIUS),
                                                            boxShadow: [
                                                              BoxShadow(
                                                                  color: Colors
                                                                      .black54,
                                                                  blurRadius:
                                                                      4.0,
                                                                  spreadRadius:
                                                                      0.5)
                                                            ],
                                                          ),
                                                          child: Padding(
                                                            padding: EdgeInsets.fromLTRB(
                                                                Mquery.padding(
                                                                    context,
                                                                    4.0),
                                                                Mquery.padding(
                                                                    context,
                                                                    4.0),
                                                                Mquery.padding(
                                                                    context,
                                                                    2.0),
                                                                Mquery.padding(
                                                                    context,
                                                                    1.0)),
                                                            child: TextField(
                                                              textInputAction:
                                                                  TextInputAction
                                                                      .done,
                                                              style: TextStyle(
                                                                  color: white,
                                                                  fontSize: Mquery
                                                                      .fontSize(
                                                                          context,
                                                                          4.25)),
                                                              controller:
                                                                  searchController,
                                                              decoration:
                                                                  InputDecoration(
                                                                border:
                                                                    InputBorder
                                                                        .none,
                                                                hintText: Constants
                                                                    .SEARCH_MESSAGE,
                                                                hintStyle: TextStyle(
                                                                    fontSize: Mquery
                                                                        .fontSize(
                                                                            context,
                                                                            4.25),
                                                                    color:
                                                                        white),
                                                              ),
                                                            ),
                                                          ),
                                                    ),
                                                  ),
                                                  Expanded(
                                                      flex: 1,
                                                      child: Padding(
                                                        padding: EdgeInsets.all(
                                                            Mquery.padding(
                                                                context, 1.0)),
                                                        child: Container(
                                                          decoration:
                                                              BoxDecoration(
                                                            boxShadow: [
                                                              BoxShadow(
                                                                  color: Colors
                                                                      .black54,
                                                                  blurRadius:
                                                                      4.0,
                                                                  spreadRadius:
                                                                      0.5)
                                                            ],
                                                            color: middle_black,
                                                            borderRadius: BorderRadius
                                                                .circular(Constants
                                                                    .CARD_BORDER_RADIUS),
                                                          ),
                                                          width: Mquery.width(
                                                              context, 12.5),
                                                          height: Mquery.width(
                                                              context, 12.5),
                                                          child: IconButton(
                                                            splashColor: Colors
                                                                .transparent,
                                                            highlightColor:
                                                                Colors
                                                                    .transparent,
                                                            icon: Icon(
                                                              Icons.search,
                                                              color: white,
                                                            ),
                                                            onPressed: () {
                                                              _onSearchButtonPressed();
                                                            },
                                                          ),
                                                        ),
                                                      ))
                                                ],
                                              ),
                                            ),
                                            ListView.builder(
                                              shrinkWrap: true,
                                              itemCount: categoryCards.length,
                                              physics:
                                                  NeverScrollableScrollPhysics(),
                                              itemBuilder: (context, index) {
                                                return GestureDetector(
                                                    onTap: () {
                                                      //Navigate
                                                    },
                                                    child:
                                                        CategoryCardWidget(
                                                            categoryCount:
                                                                categoryCards
                                                                    .length,
                                                            categoryCard:
                                                                categoryCards[
                                                                    index]));
                                              },
                                            ),
                                            _hasReachedEnd
                                                ? Padding(
                                                    padding: EdgeInsets.all(
                                                        Mquery.padding(
                                                            context, 4.0)),
                                                    child: CustomLoader(),
                                                  )
                                                : Container()
                                          ],
                                        ),
                                      ),
                                    ),
                                  );
                                }
                              },
                            );
                          }

                          return Container();
                        }))

Thank you for you time and sorry for being so verbose
-Matt

@felangel felangel self-assigned this Sep 7, 2020
@felangel felangel added question Further information is requested waiting for response Waiting for follow up labels Sep 7, 2020
@felangel felangel added this to To do in bloc via automation Sep 7, 2020
@felangel
Copy link
Owner

felangel commented Sep 7, 2020

Hi @MatthewCoetzee412 👋
Thanks for opening an issue!

I'm guessing the issue is the related to the fact the new state being emitted is considered equal to the previous state as described by the FAQs. Are you able to share a link to a sample app which illustrates the issue? It would be much easier for me to help/provide suggestions if I am able to reproduce/debug the issue locally, thanks! 👍

@MatthewCoetzee412
Copy link
Author

Hi Felangel, thank you for the swift response! I attempted to make a mock of my app to reproduce the problem but the problem comes from the Streambuilder not refreshing with the updated Firestore data after a BLOC event which I couldn't model accurately enough to reproduce the problem, so I would have to make an app using Firebase. I will post the link tomorrow to the repository, thanks for your help!

@MatthewCoetzee412
Copy link
Author

Hi Falangel,

Below is a replication of the problem:
https://github.com/MatthewCoetzee412/built_value_bloc.git

If you add a Food item, the Streambuilder does not update itself.
I hypothesise this might be due to my architecture with the BVPipe. That is a necessary part of my architecture as it converts the QuerySnapshot to a custom model and checks for errors. I'm not sure how else to make an effective, robust error handling layer.

Any help would be appreciated!
Thanks

@narcodico
Copy link
Contributor

narcodico commented Sep 8, 2020

Hi @MatthewCoetzee412 👋

  • you're extending equatable on your events, but you're also using a field of type DocumentSnapshot; your equatable is not gonna work here since DocumentSnapshot doesn't extend equatable, not to mention that a data source type shouldn't be present inside your model.

  • the last observation also applies to state classes containing BuiltList<Food>.

  • instead of using BVPipe, your repo should expose a Stream<YourModel>, since as already mentioned, you don't want to have your data source models leaking inside your other layers.

  • error handling is easily achievable using Stream operators like handleError, rxdart's onErrorReturnWith or even create your own custom StreamTransformer based on your requirements. You would either throw app specific exceptions or even opt in to return valid values(this being done inside repos).

  • I highly recommend you think twice whether built_value bring something valuable to you, since I find it quite verbose; have a look at freezed, at least you're gaining things like pattern matching, unions.

Hope this gives you a couple of ideas ✌

@felangel felangel assigned felangel and narcodico and unassigned felangel Sep 9, 2020
@MatthewCoetzee412
Copy link
Author

Hi @RollyPeres and @felangel , thank you so much for the advice! 🙏 💯

I have switched from built_value to freezed and its made it a lot easier to implement everything I need too plus much less boiler plate. I also toke your advice for handling errors in Streams and I am using the dartz package now which makes it really easy and lastly I removed the Pipe Layer completely. I have managed to implement everything but I just have one caveat which I'm not sure how to fix, which is the Food Items are not updating once I add an Item after I have scroll down and fetched more results. I don't mind closing the issue and I don't want to waste your time but if you wouldn't mind taking a look at the updated repository and provide any suggestions perhaps on how to fix it I would appreciate it a lot. Any help would be appreciated but I don't mind closing and figuring it out for myself.

Repo Link:
https://github.com/MatthewCoetzee412/built_value_bloc.git

My thanks again!

  • Matt

@narcodico
Copy link
Contributor

narcodico commented Sep 11, 2020

I've opened a PR with some minor updates mostly related to updating bloc to latest version.

The problem you're facing is called firestore live pagination 🤦‍♂️
The thing is that when you're loading more items you're only getting live updates on the latest batch: startAfterDocument(lastSnapshot). So firestore will only send you updates for that batch.
In order to get real time updates on all your items, you need to keep subscriptions alive for all your batches instead of cancelling them: await _streamSubscription?.cancel();.

@MatthewCoetzee412
Copy link
Author

Thanks @RollyPeres 🙏 🙏 🙏 ,

Really, genuinely , appreciate all the help, thank you very much!

Kind Regards
-Matt

bloc automation moved this from To do to Done Sep 11, 2020
@felangel felangel removed the waiting for response Waiting for follow up label Sep 11, 2020
@MatthewCoetzee412
Copy link
Author

Hi, for anyone in the future wanting to know how to fix the problem of realtime pagination as metioned above,

Below is the solution:

class FoodLoadBloc extends Bloc<FoodLoadEvent, FoodLoadState> {
  final FirebaseRepository _repository;

  FoodLoadBloc(this._repository);

  //Create a list of stream subscriptions
  List<StreamSubscription> _subscriptions = [];

  @override
  FoodLoadState get initialState => FoodLoadState.intial();

  @override
  Stream<FoodLoadState> mapEventToState(FoodLoadEvent event) async* {
    yield* event.map(load: (_) async* {
      StreamSubscription<Either<ItemFailure, List<Food>>> _streamSubscription =
          _repository.getMoreItems(_.items, _.documentSnapshot).listen(
              (foodItems) => add(FoodLoadEvent.itemRecieved(foodItems)));

      //Add a new stream subscription to the list each time the event is called
      _subscriptions.add(_streamSubscription);
    }, itemRecieved: (event) async* {
      yield event.items
          .fold((l) => FoodLoadState.error(l), (r) => FoodLoadState.success(r));
    });
  }

  @override
  Future<void> close() {
    //Lastly, dispose the stream subscriptions
    for (StreamSubscription sub in _subscriptions) sub.cancel();
    return super.close();
  }
}

Hope that helps someone!

  • Matt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
bloc
  
Done
Development

No branches or pull requests

3 participants