felangel / bloc

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

Share data between blocs #461

Closed ftognetto closed 5 years ago

ftognetto commented 5 years ago

Hi @felangel ! Hope you're doing well.

I am building a production app in flutter using your library, so thank you :)

Since it's a social network app for me it's important to share many information across screen in a reactive way.

A simple example can be the number of likes or comments of a post, that must be synchronized across different screen (for example I have two feed pages one with last posts and one with most popular).

I managed this by providing a Posts bloc at the top of the app that is maintaining an updated map of posts, but this is becoming hard to maintain because I have always to remember to update this bloc from other blocs (when fetching new or most popular posts in the relative blocs or when updating one with a comment or a like).

This is the bloc I'm using:

posts_state.dart

abstract class PostsState extends Equatable {
  PostsState([List props = const []]) : super(props);
}

class PostsUninitialized extends PostsState {
}

class PostsLoaded extends PostsState {
  final Map<int, Post> postsMap;
  PostsLoaded({@required this.postsMap}) : super([postsMap]);
}

posts_event.dart

abstract class PostsEvent extends Equatable {
  PostsEvent([List props = const []]) : super(props);
}

class PostsMergeToMap extends PostsEvent {
  final List<Post> posts;
  PostsMergeToMap({@required this.posts}): super([posts]);
}

class PostsAddToMap extends PostsEvent {
  final Post post;
  PostsAddToMap({@required this.post}): super([post]);
}

class PostsRemoveFromMap extends PostsEvent{
  final int idPost;
  PostsRemoveFromMap({@required this.idPost}): super([idPost]);
}

class PostsLike extends PostsEvent {
  final Post post;
  final User user;
  PostsLike({this.post, this.user}): super([post, user]);
}

posts_bloc.dart

class PostsBloc extends Bloc<PostsEvent, PostsState> {

  final PostRepository postRepository;

  PostsBloc({@required this.postRepository});

  @override
  PostsState get initialState => PostsUninitialized();

  @override
  Stream<PostsState> mapEventToState(event) async* {
    if (event is PostsMergeToMap) {
      yield* _mapMergeMapEventToState(event);
    }
    else if(event is PostsAddToMap) {
      yield * _mapUpdateMapEventToState(event);
    }
    else if (event is PostsRemoveFromMap) {
      yield * _mapRemoveFromMap(event);
    }
    else if (event is PostsLike) {
      yield * _mapLikeEventToState(event);
    }
  }

  /* Aggiorna tutta la mappa dei post */
  Stream<PostsState> _mapMergeMapEventToState(PostsMergeToMap event) async* {
    Map<int, Post> map = <int, Post>{};
    if(currentState is PostsLoaded){
      final PostsLoaded loadedState = currentState;
      map = loadedState.postsMap; 
    }
    for(Post p in event.posts){
      map[p.id] = p;
    }
    yield PostsLoaded(postsMap: map);
  }

  /* Aggiorna un singolo record della mappa dei post */
  Stream<PostsState> _mapUpdateMapEventToState(PostsAddToMap event) async* {
    Map<int, Post> map = <int, Post>{};
    if(currentState is PostsLoaded){
      final PostsLoaded loadedState = currentState;
      map = Map.from(loadedState.postsMap); 
    }
    map[event.post.id] = event.post;
    yield PostsLoaded(postsMap: map);
  }

  /* Rimuove un singolo record della mappa dei post */
  Stream<PostsState> _mapRemoveFromMap(PostsRemoveFromMap event) async* {
    if(currentState is PostsLoaded){
      Map<int, Post> map = <int, Post>{};
      final PostsLoaded loadedState = currentState;
      map = Map.from(loadedState.postsMap); 
      map.remove(event.idPost);
      yield PostsLoaded(postsMap: map);
    }
  }

  /* Aggiunge o rimuove un like da un post */
  Stream<PostsState> _mapLikeEventToState(PostsLike event) async* {
    if(currentState is PostsLoaded){
      final PostsLoaded pState = currentState;
      final Post post = pState.postsMap[event.post.id];
      final int presentIndex = post.postLikeList.map((pl) => pl.idUser.id).toList().indexOf(event.user.id);
      final int idPostLike = presentIndex >= 0 ? post.postLikeList[presentIndex].id : null;
      final pList = post.postLikeList.toList();

     //optimistic update
      if (presentIndex >= 0) { //remove like
        pList.removeAt(presentIndex);
      }
      else { //add like
        pList.add(PostLike(idPost: post, idUser: event.user));
      }
      Post updatedPost = post.copyWith(postLikeList: pList);
      dispatch(PostsAddToMap(post: updatedPost));

     //TODO try - catch with rollback
      if(presentIndex >= 0){
        await postRepository.removeLike(idPost: post.id, idPostLike: idPostLike);
      }
      else{
        final PostLike pl = await postRepository.insertLike(idPost: post.id, idUser: event.user.id);
        final pList2 = post.postLikeList.toList();
        pList2.add(pl);
        updatedPost = post.copyWith(postLikeList: pList2);
        dispatch(PostsAddToMap(post: updatedPost));
      }

    }
  }

}

So other blocs (for example, RecentsBloc which control the last posts feed) can update this one by calling

recents_bloc.dart

final posts = await postRepository.getAllPosts(start: loadedState.posts.length, limit: limit);
postsBloc.addAll(posts);
yield RecentLoaded(posts: posts);

And Widgets (like the post comment count widget) can listen to PostsBloc change, so they can update themselves.

I found this solution working but now I'm thinking that it will not be scalable anymore.

So I started thinking that the repository layer, which is the only gateway for blocs to get data, should manage this and pass down to the blocs layer always updated data. Every repository must become singleton and expose a stream with an updated list of data which blocs can use by reducing what they need.

PostRepository.dart

class PostRepository extends Repository{

  final PostApiProvider _postApi = PostApiProvider();

  List<Post> _posts = [];

  final _posts$ = BehaviorSubject<List<Post>>();
  Observable<List<Post>> get posts$ => _posts$.stream; //this is the only output of repository
  List<Post> get posts => _posts$.value;

  /// get

  Future<List<int>> getAllPosts({int start, int limit}) async { //could be Future<void> too
    final Response response = await _postApi.getAll(start, limit); 
    final List<Post> posts = response.data.map<Post>((json) => Post.fromJson(json)).toList();
    _emit(posts);
    return posts.map((p) => p.id).toList();
  }

  Future<List<int>> getPostsOfFollowings({int idUser, int start, int limit}) async {
    final Response response = await _postApi.getPostsOfFollowings(idUser: idUser, start: start, limit: limit); 
    final List<Post> posts = response.data.map<Post>((json) => Post.fromJson(json)).toList();
    _emit(posts);
    return posts.map((p) => p.id).toList();
  }

/// post
...

/// put
...

/// delete
...

  void _emit(List<Post> posts){
    _posts = ListUtils.mergeLists(_posts, posts);
    _posts$.sink.add(_posts);
  }

}

I'm having some difficulties managing different kind of post lists with this approach, but do you think this could lead to a clean architecture?

Thank you,

Fabrizio

felangel commented 5 years ago

Hi @quantosapplications πŸ‘‹ Thanks for opening an issue!

Regarding your question, I think you're on the right path. The repositories should ideally interface with your data providers and expose streams of models that the blocs can consume. In your case, you can probably expose a posts stream in the repository which the PostBloc subscribes to and reacts to. Then whenever you have any features that need access to the posts, you would simply use the PostBloc. If you want to modify posts, you can have other blocs that interface with the PostRepository calling methods like addPost, updatePost etc...

This should scale nicely because you'll have a unidirectional data flow (posts will always flow down from the data-provider, to the repository, to the bloc, and to the UI).

You can use RepositoryProvider to provide a single instance of your repository to your application and then consume the repository when creating your blocs via BlocProvider

BlocProvider(
  builder: (context) => MyBloc(repository: RepositoryProvider.of<MyRepository>(context)),
  child: MyChild(),
),

Regarding having different kinds of post lists, you can have different post blocs which listen to the root postBloc's stream and filter/modify the posts similarly to the FilteredTodosBloc in the Todos Example.

Hope that helps and I'm more than happy to answer any questions you have as you go on gitter as well as do some live coding sessions to work through obstacles πŸ‘

ftognetto commented 5 years ago

hi @felangel thank you!

Yes, I was reading this https://medium.com/flutter-community/whats-new-in-flutter-bloc-0-19-0-bf58a7154661 you are the best!! πŸ’― πŸ₯‡

And yes I am filtering different lists of posts using the same implementation you were using in the todo example.

The only thing I cannot afford with this approach is to get exactly the posts I need.

For example in the recent posts feed bloc, for retrieving new posts, I would dispatch a FetchRecentPosts event which would call await postRepository.getMostRecent( page: 1, limit: 20 ) and get back (from repo to bloc) a list of integers that represent the id of the objects while the repository would update it's internal post list adding the new posts.

Then always in the bloc I would listen to the post stream in the repository and filter them accordingly with the list of ids that I retrieved before.

But the repository is emitting the new list before I can afford the ids list.

PostRepository.dart

Future<List<int>> getAllPosts({int start, int limit}) async {

    /// fetching new posts from api
    final Response response = await call(api.getAll(start, limit)); 
    final List<Post> posts = response.data.map<Post>((json) => Post.fromJson(json)).toList();

    /// let stream emit a new list of post with the new ones
    _posts$.sink.add([...oldPosts, ...posts]);

    /// return the list of ids so who is calling knows which posts has been fetched
    return posts.map((p) => p.id).toList();
 }

Without doing all this I can simply reduce the repository list stream in the recent bloc by sorting posts with it's creation date on listening (like the filteredTodosBloc).

But this will lead to some strange behavior: In the recent list, for example, the first time I open it (and while the bloc is fetching recent posts for the first time) I would find in the ListView many posts that I fetched somewhere else in the app.

raulmabe commented 2 years ago

@ftognetto Hi Fabrizio!, I am really interested in how you solved this problem as I am dealing with the same exact one. Would you mind to share your experience? πŸ™πŸΌ

ftognetto commented 2 years ago

Hi @raulmabe I would not be rude to this library which is great but I am currently using another library of state management πŸ˜ƒ Anyway the concepts are similar and I ended up solving this feature by having an api from the backend that gives the ids of the posts you liked, and store them in a place where you can access from the whole app. Then when you like a post you can add the id there and when you load the post in another page you know yet if you liked that post or not. Hope this can help!

RyanGSampson commented 1 year ago

Looking for clarification if anyone knows thank you.

Question: If multiple blocs have open stream listeners on the same repository, does that cause the application to open multiple streams to the db? Take Firebase for ex.

Follow Up: If so, that would be inefficient. And so what is the appropriate way to have a header bloc open a stream listener, and then action cubits / blocs depend on the header bloc for the models / state.

In other words, what is the pattern for a cubit or bloc to subscribe to a stream of data coming from another bloc.

Or am i wrong to want to do this, and each cubit regardless of function should open its own stream listener directly with the repository?