felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.72k stars 3.38k forks source link

Using generics if Bloc has different entities but the same logic #3200

Closed Arthur-Zaripov closed 2 years ago

Arthur-Zaripov commented 2 years ago

Hello, everyone! First of all I want to thank whole Bloc team for such an amazing library!

As already noted in this topic #1631, using generic types of Entities in Bloc (Cubit) can be problematic.

I have a small water delivery service. We deliver water to apartments and offices. I am not a professional programmer myself, but I was able to learn Dart and Flutter in a couple of years. This knowledge was enough for me to create a small CRM for my business. I am using a Bloc for the state management . However, after studying hundreds of articles about this library, I was surprised to find that most solutions involve using one bloc for one entity.

But, what if the logic is absolutely the same for all entities? What if we using BloC for table data (CRM system)? For example it has 3 entities: Customer, Order, Product. Here is an example. Entities All these entities have absolutely identical logic: add, copy, delete, filter, sort, create, delete all, select all, etc. And all this logic can be processed with a single Cubit, which will have a Generic type of entity.

class EntitesTableCubit<T> extends Cubit<EntitesTableState<T>> {
  getEntities(List<T>entities ) => emit(newState);
  selectEntities(List<T>entities) => emit(newState)
  sortEntities(List<T>entities) => emit(newState)
  filterEntities(List<T>entities) => emit(newState)
}

But as Felix Angelov said in #1631: "logging/analytics perspective it might be tricky" in this case. So everytime the BlocObserver would be printed a transition, it would write the same for all entities. Do we really have to abandon good code reuse practices in this case?

Recently, the use of Bloc has become more problematic. It's all the fault of banal code duplication. Every time I try to reuse blocs using inheritance, composition, or generics, I encounter certain problems. But I have only 3 entities in my CRM system: Order, Customer, Product. Now imagine if there were 10 of them? Of course, this is 100% my fault. There are probably ways to reuse a blocs that I just don't know about. I think it would be nice for the authors of this wonderful package to expand the documentation, where more attention will be paid to the reuse of blocs.

At the moment, in my CRM, each entity has its own Bloc, as professionals advise. But adding functionality is extremely problematic in this case. Everything that I added (changed) in a bloc for one entity, I have to duplicate each time in another bloc. This is extremely difficult.

Otherwise, I really like Bloc, it's a very powerful thing, thanks again for supporting!

Gene-Dana commented 2 years ago

Hi there! You are treading in super opinionated territory, and seeing that you value our feedback I'm really glad you too the time to raise this issue and field our opinions on these matters.

So there is some finer points that the documentation has avoided discussing as some things are just not in our scope.

One of the big understated things in my opinion is the 'One bloc/cubit per feature' mantra.

And it took me a long time to understand why this was so important.

I've come to realize that most questions like this come from that fundamental assumption, that our users are working in a feature-driven practice, and that alone is a whole subject in-and-of-itself.

I say all this to say that it's difficult to describe how to scope models and better architect an app because every case differs, and this understanding truly comes from doing it a few times before truly getting it.

What was done for me and many others here was someone patiently sat down and showed us the way. And if you link us a repo, we can offer pointers.

Also, if you want to accelerate your understanding, feel free to join the bloc discord and/or visit https://l2t.dev for hands on learning

Big thing : one bloc per feature (not one bloc per entity)

Next thing, there are tools like freezed, json serializable, and plugins that generate data classes for you. That may make your life easier!

Arthur-Zaripov commented 2 years ago

Hello @Gene-Dana! Thank you for your detailed answer! So based on recommendation "one bloc per feature", what is considered an feature? There are 2 possible options:

// 1. EntitiesTable as a feature. So there is 1 cubit for 1 feature.
class EntitesTableCubit<T> extends Cubit<EntitesTableState<T>> {
  getEntities(List<T>entities ) => emit(newState);
  selectEntities(List<T>entities) => emit(newState)
  sortEntities(List<T>entities) => emit(newState)
  filterEntities(List<T>entities) => emit(newState)
}
// 2. OrdersTable,  UsersTable, ProductsTable as a separate features. So there is 3 cubit for 3 feature.
class OrdersTableCubit extends Cubit<OrdersTableState> {
  getOrders(List<Order>orders ) => emit(newState);
  selectOrders(List<Order>orders ) => emit(newState)
  sortOrders(List<Order>orders ) => emit(newState)
  filterOrders(List<Order>orders ) => emit(newState)
}

class UsersTableCubit extends Cubit<UsersTableState> {
  getUsers(List<User>users ) => emit(newState);
  selectUsers(List<User>users ) => emit(newState)
  sortUsers(List<User>users ) => emit(newState)
  filterUsers(List<User>users ) => emit(newState)
}

class ProductsTableCubit extends Cubit<ProductsTableState> {
  getProducts(List<Product>products ) => emit(newState);
  selectProducts(List<Product>products ) => emit(newState)
  sortProducts(List<Product>products ) => emit(newState)
  filterProducts(List<Product>products ) => emit(newState)
}

Here enother example. Simple Cubit for loading or watching entities.

//1 cubit for 1 feature
class EntitiesOverviewCibit<T> extends Cubit<EntitiesOverviewState<T>> {
  final IRepository<T> _repository;
  EntitiesOverviewCibit(this._repository)

 //here some code where we listen or load entities from _repository and update state

  watchEntities() => emit(newState); 
  watchFiltredEntities() => emit(newState)
  loadEntities(Filter filter) => emit(newState);
  loadFiltredEntities(Filter filter) => emit(newState)

}
//3 cubits for 3 features.
class OrdersOverviewCibit extends Cubit<OrdersOverviewState> {
  final IOrderRepository _orderRepository;
  OrdersOverviewCibit (this._orderRepository)

 //here some code where we listen or load orders from _orderRepository and update state

  watchOrders() => emit(newState); 
  watchFiltredOrders() => emit(newState)
  loadOrders(Filter filter ) => emit(newState);
  loadFiltredOrders(Filter filter ) => emit(newState)

}

class UsersOverviewCibit extends Cubit<UsersOverviewState> {
  final IUserRepository _userRepository;
  UsersOverviewCibit (this._userRepository)

 //here some code where we listen or load orders from _userRepository and update state

  watchUsers() => emit(newState); 
  watchFiltredUsers() => emit(newState)
  loadUsers(Filter filter) => emit(newState);
  loadFiltredUsers(Filter filter ) => emit(newState)
}

class ProductsOverviewCibit extends Cubit<ProductsOverviewState> {
  final IProductRepository _productRepository;
  ProductsOverviewCibit (this._productRepository)

 //here some code where we listen or load orders from _productRepository and update state

  watchProducts() => emit(newState); 
  watchFiltredProducts() => emit(newState)
  loadProducts(Filter filter) => emit(newState);
  loadFiltredProducts(Filter filter ) => emit(newState)
}

I'm not sure that such a concept as "Presentation logic" would be appropriate, but I hope you will understand what I will say next. In my opinion, such table methods as sort, select, filter or some simple loaders like EntitiesOverviewWatcher are not a business logic at all. This is "presentation logic". Business logic is usually something unique and very complex. And presentation logic is often common and very simple. Please correct me if I'm wrong.

So, if someone in their projects came across the fact that their Blocs differ only in the types of entities they work with, and nothing else, it would be interesting to know what patterns they used to reduce code duplication.

Gene-Dana commented 2 years ago

Do you have a design you can share?

Gene-Dana commented 2 years ago

And.. May we continue this chat on the discord? I can share with you some more examples and help you break down some of the design in to features.

https://discord.gg/qx8sfdCa Gene#7475

Arthur-Zaripov commented 2 years ago

Ok, yes, I will share the code in the discord

rouuuge commented 2 years ago

@Gene-Dana @popay34 is there a link to the discord thread? I got a similar question and the start of that conversation is very interesting.

thx

Arthur-Zaripov commented 2 years ago

@rouuuge , hello! This was not discussed much on discord. @Gene-Dana explained it to me personally. Thank him very much for that. Everything related to the functionality of the table (a list with elements, filter, sorting, pagination, etc.) should be in one bloc. But there may be different entities in the table (Users, Orders, Products) and whether it is necessary to use generics in this case is not yet clear. I wrote to Gene. When he answers, I will write here.

Arthur-Zaripov commented 2 years ago

@Gene-Dana's solution

class RecordsOveriewState extends Equatable {
  const RecordsOverviewState({
    this.status = RecordsOverviewStatus.initial;
    this.activeRecords = [],
  });

  final RecordsOverviewStatus status;
  final List<Record> activeRecords;
Gene-Dana commented 2 years ago

@popay34 My solution is a little overstated here ! I proposed a solution that was exactly like the new todos example. Feel free to reach out to us in the discord if you have any more questions, @rouuuge and @popay34.