Bloc: using built_value with bloc

Created on 4 Apr 2020  路  24Comments  路  Source: felangel/bloc

Model classes in the app implements built_value for json serialization and deserialization. Therefore it's not possible to extend the model class with Equatable. As a result, when an event causes a transition like this

{
    state: 'State1',
    event: 'Event1',
    state: 'State1'
}

there's no changes observed in the app. Is there any solution to this other than overloading == operator?

question

All 24 comments

@DhanaSekharM this is more like a built value related question rather than bloc related. You can either customize built value to treat those instances as different or you could yield an intermediate state, e.g. :

{
    state: 'StateLoaded',
    event: 'Event',
    state: 'StateLoading',
    state: 'StateLoaded'
}

@RollyPeres Thanks for the quick reply. I'm not sure about if customizing built_value is a viable alternative for me, I guess I'll go ahead with yielding an intermediate state.

You can customize individual built value classes.

@override
  bool operator ==(Object other) {
    return false; // or whatever logic you want
  }

Hi @DhanaSekharM 馃憢
Thanks for opening an issue!

This is because built_value overrides == and hashCode. If you don鈥檛 want value based equality then I suggest using json_serializable instead of built_value.

Hello @felangel ,
Thanks for the suggestion but, I don't really have an option to switch to json_serializable. @RollyPeres workaround of adding an intermediate state seems to do the trick. I'm probably gonna stick with that.

@DhanaSekharM that鈥檚 fine but I just want to call out that it鈥檚 more of a hack than a fix. The real solution would be to update how equality comparisons are being done.

@felangel While I am aware that it's more of a hack, I just wanted to know if there is a solution other than updating the equality comparisons.
Could you give an idea of how I could go about updating equality comparisons?

You can just override == to return if this and other are identical.

 @override
 bool operator ==(Object other) =>
  identical(this, other);

It looks like built_value forbids you from overriding == as seen in this error message

Please make the following changes to use BuiltValue:
1.Stop implementing operator ==  it will be generated for you.

Here's a related issue and a merged pr from built_value

@DhanaSekharM Yeah, don't do that. It's already built into built_value.

So if you have a built_value User object, you can do this:

final user = User((b) => b..name = 'Test');
final sameUser = User((b) => b..name = 'Test');

print(user == sameUser) // returns true.

built_collection also supports it, and it will do a deep compare for equality.

@larssn that's exactly what he needs to avoid. He wants to yield two identical states consecutively. Bloc will not yield the second one unless equality comparison returns false.

Ah my bad.

He could just flip a boolean in the state object.

yield state.rebuild((b) => b..showError = true);
yield state.rebuild((b) => b..showError = false);

@larssn thats also not addressing the real problem. Imo you should not use built_value in this case.

You're right, it doesn't, but we're in hacking territory already. The OP describes a scenario where 2 identical states are treated exactly like that: identical; which should not retrigger the bloc, repaint the widgets or anything else.

@DhanaSekharM It's hard to say why you have the requirement you do without more information, but it is possible that you working with an anti-pattern? What I'm reading from the OP is that you want State1 != State1. If you have an event that doesn't change the state, but the UI should react to it, then that event SHOULD change the state - otherwise the UI would remain the same. 馃檪

@larssn this is very subjective. To be honest I've encountered situations where I got the same state twice in a row and I needed to re-render the UI both times. eg: tap a button ->add an event which results in an error state -> show a snackbar; do that again and you won't get the snackbar displayed second time. You can't achieve this without hacking the equality or using an intermediate state.

The situation I'm facing with is this. I have a dropdown(that has objects that haven't extended Equatable) when clicked adds them to a ListView. Now when I repeatedly add items from the dropdown the state(which extends Equatable) essentially remains the same but with its field changed with the new dropdown value. But when I hot reload the app, all the items are added at once. Now I'm guessing since its field doesn't extend Equatable, bloc treats the state as unchanged. Although I'm not sure if I'm doing it the right way since I just started learning bloc

If you want state changes to occur even if the values are the same don鈥檛 extend Equatable or use built_value. By default it will work this way as long as you are always yielding a new state instance 馃憤

@RollyPeres I also do intermediate states for showing snackbars, which I consider to be the most correct as it does require the UI to do something, meaning a state change is required.

@felangel It's just that using built value for your bloc state removes a ton of boilerplate, especially if your state objects are big.

@DhanaSekharM I assume your ListView is bound to a list object on your state object, correct? I can only assume you're using List compared to BuiltList. If you were using BuiltList, then when your DropDownItemClicked event (or whatever you call it 馃槄) is added to your bloc, you would rebuild the state like so:

// In `mapEventToState`
if (event is DropItemClicked) {
  yield state.rebuild((b) => b..myListViewList.add(event.myItem));
}

built_collection will handle the immutability of myListViewList within the callback, meaning the underlying reference to your list will be a new list, which will cause equality to be false, which in turn will update your widget.

Hope that makes sense.

Edit:
If you don't wanna use built_collection, you can probably just use List.of to create new list every time you add a new object to it, but you really should use built_collection and built_value together, as they work great together.

@larssn I agree, I do the same, especially since I'm using freezed and there's no way to juggle with equality there.
With that being said, I highly recommend you have a look at freezed, I believe you'll like it; from my experience built_value is too verbose. But if built_value is a must then definitely needs to be used together with built_collection.

@RollyPeres So I just read all the docs - pretty sweet library I must say. I like how it attempts to stay true to the standard dart syntax, plus being aware of future features such as non nullable types and late.

It doesn't mention how lists and maps are handled though?

Also, I'm using built_value's EnumClass in a lot of places (almost all my enums' values are strings), and I appreciate being able to able to do:

switch (obj.myEnum) {
  case MyEnum.VALUE1: break;
  case MyEnum.VALUE2: break;
  default:
}

Lastly, my objects are usually serialized to Maps because I need to persist it in Firestore. :-)

@larssn lists and maps handled as in...?
I also mostly work with Firestore and string enums can be helpful for an easier data visualization in console, but nothing you can't achieve with a class mimicking string enums.

@RollyPeres I mean, is it possible to make them immutable, and if so, how are they treated in regards to equality?

@larssn freezed is not opinionated with collections, so you can use both standard mutable ones or immutable collections like ones from built_collection or kt_dart. The standard ones are compared using DeepCollectionEquality.
From what I know json_serializable only supports standard collections, so it gets trickier to use freezed with data classes when it involves blt and kt collections, since I believe you'd have to write converters. This is important because under the hood freezed delegates serialization to json_serializable.

@DhanaSekharM can this issue be closed? I'm not sure if there is any actionable output.

Was this page helpful?
0 / 5 - 0 ratings