I'm trying to understand how to use bloc package. While doing so I was looking through documentation and examples. I took flutter_timer example and rewrote it using rxDart package: https://github.com/autolainen/flutter_timer To my surprise I got 30% less code (288 lines in original code and 199 lines as I got rid of bloc package). The code is not just shorter, it is much cleaner.
Then I got a bigger example, flutter_todos and did the same thing - rewrote it using rxDart classes and completely removed dependency on bloc package. https://github.com/autolainen/flutter_todos Again the same result - code 34% off (from 1198 lines to 785 lines), less files, cleaner architecture.
So at the moment what I see is that usage of bloc package produces 30% more code relative to rxDart package. Code is much more complicated, harder to read and understand.
Why a programmer is forced using event and state objects instead of operating with plain data objects (models) and streams? Event and state objects are artificial, they introduce unnecessary complexity. This approach produces many classes and files when, for example, in flutter_todos rxDart version I got just one file of 107 lines containing all application business logic.
I see that bloc package is quite popular but at the moment I have no clue why. You see I spent few hours coding and got some results which are definitely against bloc package. Could you please tell me where I am wrong? In which cases bloc package would be preferable than rxDart?
Using multiple streams per bloc introduces complexity and reduces composibility.
This library allows developers to not think about streams if they are not familiar with Rx.
I can rewrite your example with changenotifiers and say that it is even less complex but the point of this library is to have a pattern that can be repeated regardless of the complexity of your logic.
Hi @autolainen 馃憢
Your bloc take is more or less vanilla bloc(I say more or less since you're violating the core principles by not using sinks); bloc library was created to simplify working with the bloc pattern found in the vanilla bloc.
I personally don't find your rewritten examples to be cleaner, and I say that being a huge RX fan myself. They actually introduce complexity, something that bloc library tries to abstract away.
The purpose of any piece of software shouldn't be writing as less code as possible. If that would be the case then you'd clump up everything in the UI layer with no separation of concerns and without any abstraction/layering, which would make up for a "great" maintenance experience for any developer.
Why a programmer is forced using event and state objects instead of operating with plain data objects (models) and streams?
In vanilla bloc you'd add a value/object to a sink, while with bloc you'd add an event to the bloc, which is nothing but an object, and adding is done guess how...using a sink, With vanilla bloc you'd use multiple sinks while bloc only needs one. Win for bloc.
Nothing stops you from representing your state using a primitive type or a data object.
You're actually not using streams but stream, singular; a bloc is also a Stream and this is how you'd consume your state, through a Stream<State>. I'd say win for bloc once again.
So while you seem to think you're being limited by bloc, the reality is completely opposite. 馃槻
Hi @autolainen 馃憢
Thanks for opening an issue!
As others have pointed out, the goal of the library is to provide a predictable, safe, and scalable way to manage state. The bloc library's goal is not to have the least amount of code (although I believe your rxdart examples could be rewritten with cubit to be even fewer lines of code since you don't have to deal with maintaining multiple BehaviorSubjects and managing their lifecycle). One of the biggest advantages of using bloc imo is the ability to trace all state changes (also known as event sourcing). If events aren't important in a particular use-case I believe you'll find cubit a lot more concise and can scale up to bloc as needed.
Other things to note are the ecosystem provides support for automatic persistence and restoration of state via hydrated_bloc, a way to easily test all blocs/cubits via bloc_test, support for vscode and intelliJ, and much more. In any case, if you prefer to continue to use pure rxdart then that's totally fair -- in the end use whatever tool makes sense for you and your team 馃憤
Closing for now but feel free to comment with additional questions/comments and I'm more than happy to continue the conversation 馃槃
Thank you all for such professional replies! But, to be honest, I expected figures and facts like I gave in my post. As we're talking about technical stuff, we can measure this or at least compare and say 'more' and 'less'. @felangel said
your rxdart examples could be rewritten with cubit to be even fewer lines of code
So, please do that. @mocodesmo wrote
I can rewrite your example with changenotifiers and say that it is even less complex
Please do so and if you keep the same level of separation of business logic and presentation layer - that would be a giant leap for mankind.
Code worth more than thousands words.
Two of you mentioned that amount of code is not the main criteria of good code. Sure, I totally agree here. If we have, say 10...15% overhead, that would be no prob. But when every third line is written just to follow some pattern - that is too much. And in my app versions I didn't turn the code into a mess just to make it shorter. I implemented, as you said, (almost) vanilla bloc, which gives you all those things like readability, maintainability, testability and further ilities.
By the way, I can explain why I didn't implement sinks on bloc as it is proposed in vanilla bloc. The only reason I can think of why architectors said that is not to put much calculations on main thread and keep UI smooth. If you know some other reason please let me know. So if you know for sure that a bloc object method is quite lightweight, there is no sense of introducing sink here.
I've been thinking about what @felangel wrote
One of the biggest advantages of using bloc imo is the ability to trace all state changes
If I understand correctly this is possible because you have event to state mapping so you can make out which event lead to some certain state. So let's see what the price for that is.
Single incoming event channel makes you creating a hierarchy of event objects. Events have a common ancestor but in fact they are totally different objects. They are derived from some class but they don't inherit any functionality because base class is in fact empty. Some of them don't even add their own functionality. It looks like a misuse of object inheritance idea. The only reason I can think of is that you need to properly compare incoming objects (please correct me if I am wrong). To me it looks like you can create any bloc with
activeFilter: state is FilteredTodosLoadSuccess
? state.activeFilter
: VisibilityFilter.all,
Another drawback is pieces of business logic in presentation layer. I mean code like this:
if (state is TodosLoadSuccess) {
bool allComplete =
(BlocProvider.of<TodosBloc>(context).state as TodosLoadSuccess)
.todos
.every((todo) => todo.complete);
or this
buildWhen: (previousState, state) =>
state.runtimeType != previousState.runtimeType,
You need some logic in UI widgets just because you have a single output stream for everything. Every time UI classes receive a state object they should realise what kind of object has come and decide what should they do with it, whether they should rebuild or not.
I want to finish with what @mocodesmo wrote
This library allows developers to not think about streams if they are not familiar with Rx.
I want to tell all the users of bloc package: 'Please get familiar with Rx'.
Hey @autolainen I opened a pull request to refactor your rxdart example using cubit. When comparing the previous bloc implementation with the current cubit implementation I was actually able to reduce the number of lines of code when you don't take the state class into consideration (54 lines for cubit and 61 for the rxdart bloc). I would also argue that having the separation between the business logic and the data class makes the code easier to understand (I don't have to look at the business logic implementation to understand how to consume the bloc/cubit).
I can rewrite your todos example using cubit as well if you really want but I didn't want to spend the time because I think it's fairly clear to see that strictly looking at number of lines of code, cubit will generally be the same as the rxdart approach (although I would argue it's a lot easier to understand due to the separation of state and logic).
Single incoming event channel makes you creating a hierarchy of event objects. Events have a common ancestor but in fact they are totally different objects
It's completely up to you to define how you want to model events. The bloc library itself does not have an opinion on this matter. We could have just as easily used enums for the events or defined methods with cubit.
I don't know will you agree or not but type check is always bad thing.
Type checking is not bad -- the is keyword exists for that exact reason and again the bloc library does not have an opinion on how states should be defined so you could have a single concrete class with an enum (just like I did with the TimerState in the cubit pull request) and switch on status which will eliminate the type checks. Again, this has nothing to do with the bloc library and is purely based on how you choose to model your events and states.
Regarding all of your other critiques of the todos example, that example was written based on the todos example in the flutter architecture samples and much of that code was forked from other examples and adapted to the bloc library. I am planning on doing a complete rewrite and will address most if not all of your feedback.
I want to tell all the users of bloc package: 'Please get familiar with Rx'.
Again, that's totally fair and feel free to use whatever tools work best for you -- no one is forcing you to use the bloc library (I hope) 馃槃
Hello @felangel! Thanks for the pull request!
Unfortunately refactoring of flutter_timer is not informative at all since you have only one type of state there. So there is no those nice type checks in the code and there is no other boilerplate code generated by the design. As for number of lines - you have 201 of them comparing to 199 in my version. I counted all the new lines in all *.dart files under lib.
Type checking is not bad -- the is keyword exists for that exact reason
Excessive type checking is bad and considered as one of design smells called 'Unexploited encapsulation': https://en.wikipedia.org/wiki/Design_smell
I would also argue that having the separation between the business logic and the data class makes the code easier to understand (I don't have to look at the business logic implementation to understand how to consume the bloc/cubit).
Did I understand correctly you said that business logic may appear anywhere in the code just 'for convenience' and not necessarily be incapsulated in business logic layer classes? If so I'll leave this with no comment.
You often refer to cubit as a possible solution for my questions.
cubit will generally be the same as the rxdart approach (although I would argue it's a lot easier to understand due to the separation of state and logic)
I decided to have a closer look at cubit implementation. So cubit is in fact a stream where you can also take the latest value. It is very close to BehaviorSubject from rxDart isn't it? Another feature is that it prevents emitting state objects if the state didn't change. The last feature is really questionable because I may want to have a stream of the same objects in my system. One object is not the same as a sequence of equal objects. No way you can have that using cubit and descendant classes like bloc. When you need only distinct values from dart stream you just call .distinct() and that's it.
I can not say bloc and BehaviorSubject are functionally equal but pairs of classes have exactly the same functionality: (Cubit + BlocBuilder) and (BehaviorSubject + StreamBuilder). Why did you decide to write your own classes but not use existing ones?
Another question:
One of the biggest advantages of using bloc imo is the ability to trace all state changes
where and how this ability is used? The only thing I can imagine is debugging.
Hey @autolainen can you please describe what the goal of this issue is? It's not clear to me so far what this issue is trying to accomplish.
As I mentioned previously, the excessive type checks can be eliminated by refactoring the state representation (stay tuned for the revamped todos example).
I'm not sure what argument is being made with the 201 vs 199 lines of code. I could just as easily have made the cubit implementation 198 lines by omitting some trailing commas like the implementation you provided.
Did I understand correctly you said that business logic may appear anywhere in the code just 'for convenience' and not necessarily be incapsulated in business logic layer classes? If so I'll leave this with no comment.
No, what I said is the bloc library implementation separates the business logic from the model (hence the state class) whereas the rxdart implementation combines the two. If I wanted to understand how to consume the bloc in the example you provided, I would have to look at the implementation itself whereas with the cubit example, I can simply look at the state class to understand what information is being exposed by the cubit (I don't have to bother understanding _how_ the information is being computed).
No way you can have that using cubit and descendant classes like bloc. When you need only distinct values from dart stream you just call .distinct() and that's it.
By default two objects in Dart are not equal unless the references are the same so the behavior you are describing is attainable if you omit the equality and hashCode overrides.
Why did you decide to write your own classes but not use existing ones?
The bloc library was created at BMW because we had very bad experiences with ReactiveX at scale. I won't go into too much detail but basically while ReactiveX is very powerful, it also has a much steeper learning curve and can quickly result in spaghetti code. It quickly became extremely difficult to understand, debug, and test code. The bloc library was created to make writing reactive code safer, simpler and in a more opinionated and scalable way. I highly encourage you to watch my coworker, Jorge Coca's talk on Flutter at Scale. You can also watch Brian Egan, one of the main maintainers of the rxdart package describe how in many cases it overcomplicates things in his talk at Flutter Europe (skip to 37:40).
where and how this ability is used? The only thing I can imagine is debugging.
Debugging, logging, and analytics are all easily achieved with event sourcing. In addition, the entire solution is much more predictable which in my experiencing drastically reduces the number of bugs and also the amount of time spent debugging and tracking down the root cause of a bug.
Again, I don't feel I understand the goal of this issue so in the future please try to clearly describe a specific problem that you would like solved and again feel free to continue using rxdart or whatever solution works best for you, cheers! 馃憤
I would highly encourage you @autolainen to write a larger app using vanilla bloc/rxdart before advertising for how easy and clear things are with it. There's a reason most devs stopped using vanilla bloc and it's simply because it scales terrible.
As soon as you have to deal with a bunch of controllers/subjects and all sorts of combinations between them things are not easy to follow anymore.
Hello @felangel !
stay tuned for the revamped todos example
Have you released new todos example yet?
@autolainen not yet it's next on my list of todos haha and I will try to get it done by this weekend 馃
Most helpful comment
Hey @autolainen can you please describe what the goal of this issue is? It's not clear to me so far what this issue is trying to accomplish.
As I mentioned previously, the excessive type checks can be eliminated by refactoring the state representation (stay tuned for the revamped todos example).
I'm not sure what argument is being made with the 201 vs 199 lines of code. I could just as easily have made the cubit implementation 198 lines by omitting some trailing commas like the implementation you provided.
No, what I said is the bloc library implementation separates the business logic from the model (hence the state class) whereas the rxdart implementation combines the two. If I wanted to understand how to consume the bloc in the example you provided, I would have to look at the implementation itself whereas with the cubit example, I can simply look at the state class to understand what information is being exposed by the cubit (I don't have to bother understanding _how_ the information is being computed).
By default two objects in Dart are not equal unless the references are the same so the behavior you are describing is attainable if you omit the equality and hashCode overrides.
The bloc library was created at BMW because we had very bad experiences with ReactiveX at scale. I won't go into too much detail but basically while ReactiveX is very powerful, it also has a much steeper learning curve and can quickly result in spaghetti code. It quickly became extremely difficult to understand, debug, and test code. The bloc library was created to make writing reactive code safer, simpler and in a more opinionated and scalable way. I highly encourage you to watch my coworker, Jorge Coca's talk on Flutter at Scale. You can also watch Brian Egan, one of the main maintainers of the rxdart package describe how in many cases it overcomplicates things in his talk at Flutter Europe (skip to 37:40).
Debugging, logging, and analytics are all easily achieved with event sourcing. In addition, the entire solution is much more predictable which in my experiencing drastically reduces the number of bugs and also the amount of time spent debugging and tracking down the root cause of a bug.
Again, I don't feel I understand the goal of this issue so in the future please try to clearly describe a specific problem that you would like solved and again feel free to continue using
rxdartor whatever solution works best for you, cheers! 馃憤