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

Remove RxDart dependency from bloc package #821

Merged
merged 15 commits into from
Feb 2, 2020

Conversation

mozhaiskyi
Copy link
Contributor

@mozhaiskyi mozhaiskyi commented Jan 30, 2020

Status

READY

Breaking Changes

NO

Description

BloC is architecture library which used as the foundation of the application. In my opinion it should be independent of any other libraries like RxDart.

Rx code was used only in two places inside bloc package.

  • bloc.dart (PublishSubject, BehaviorSubject replaced with two StreamController. Also added variable for storing current state which called _currentState)
  • complex_bloc.dart (File is test helper. It still using RxDart because package is enable as a test dependency)

@felangel felangel added enhancement New feature or request pkg:bloc This issue is related to the bloc package labels Jan 31, 2020
@felangel felangel added this to In progress in bloc via automation Jan 31, 2020
Copy link
Owner

@felangel felangel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just had a few minor comments but overall awesome PR! Given the recent changes to rxdart (migration to extensions on Stream) I think this makes a lot of sense. Thanks so much for opening this 💯

@tenhobi
Copy link
Collaborator

tenhobi commented Jan 31, 2020

I am not sure atm, but using BehaviorSubject you are given the last emitted state (from cache) when you subscribe (was _stateSubject.listen) even after some changes to the bloc have been made (emitting a new state). What part of the code does this now? Not sure it's part of the tests to cover this issue.

BehaviorSubject - A broadcast StreamController that caches the latest added value or error. When a new listener subscribes to the Stream, the latest value or error will be emitted to the listener. Furthermore, you can synchronously read the last emitted value.

What I mean:

  1. subscribe to BlocA with X
  2. add an event1
  3. that emits a state1
  4. subscribe to BlocA with Y
  5. does the Y gets state1 now ??

@jorgecoca
Copy link
Collaborator

I am not sure atm, but using BehaviorSubject you are given the last emitted state (from cache) when you subscribe (was _stateSubject.listen) even after some changes to the bloc have been made (emitting a new state). What part of the code does this now? Not sure it's part of the tests to cover this issue.

BehaviorSubject - A broadcast StreamController that caches the latest added value or error. When a new listener subscribes to the Stream, the latest value or error will be emitted to the listener. Furthermore, you can synchronously read the last emitted value.

What I mean:

  1. subscribe to BlocA with X
  2. add an event1
  3. that emits a state1
  4. subscribe to BlocA with Y
  5. does the Y gets state1 now ??

@felangel is there a test case for this? If not, I think that would be a good addition ;)

packages/bloc/pubspec.yaml Show resolved Hide resolved
packages/bloc/lib/src/bloc.dart Outdated Show resolved Hide resolved
@mozhaiskyi
Copy link
Contributor Author

PR contains problems. Changed status to IN DEVELOPMENT. I will try to find better solution.

@tenhobi you are right. Solution with StreamController does not include previous value when new subscription established. It is critical (missing initial state when widget was rebuilded). Also _states should be a broadcast to allow multi subscriptions.

I propose to add test to cover this issue in separate PR. It will be very useful.

  1. subscribe to BlocA with X
  2. add an event1
  3. that emits a state1
  4. subscribe to BlocA with Y
  5. does the Y gets state1 now ??

@tenhobi thank you. Good catch 👍

@mozhaiskyi
Copy link
Contributor Author

Implemented StateSubject (please propose different name if it is necessary). It is lightweight analog of BehaviorSubject. This approach fixed issues related to last item emit. Also BLoC support several listeners as well.

Added tests which cover StateSubject logic such as emits, seed item emit, errors, null-values. Test cases with several listeners are included.

FYI @felangel @tenhobi

@felangel
Copy link
Owner

felangel commented Feb 1, 2020

@mmozhaiskyi thanks for your efforts! I have a few concerns about this approach and would love to hear your thoughts:

  1. StateSubject should not be publicly exposed in my opinion as it's just an implementation detail.
  2. I don't like the idea of copying the BehaviorSubject implementation straight from rxdart just to remove the dependency. It's not a trivial implementation (especially in it's current form) and I think it could be further simplified because it will be used in a very specific context.
  3. We now run the risk of having to maintain the same implementation in multiple places which isn't beneficial for the community and doesn't feel right to me.

Thoughts? (cc @brianegan)

@felangel
Copy link
Owner

felangel commented Feb 2, 2020

Was just messing around on my own branch and managed to simplify the implementation a bit more (I think). What do y'all think?

@mozhaiskyi
Copy link
Contributor Author

@felangel Thank you for your feedback.

Your proposal is awesome and useful. Really, we don’t need full implementation of BehaviourSubject from Rx because bloc context is very specific. I simplified solution as it possible (see last commit).

Changes:

  • current approach uses _StartWithStreamTransformer only.
  • logic of last value emit moved into bloc (StateSubject abstraction was removed).

Co-Authored-By: Felix Angelov <felangelov@gmail.com>
felangel
felangel previously approved these changes Feb 2, 2020
Copy link
Owner

@felangel felangel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 💯
Thanks for your patience and for incorporating all of the feedback so quickly 👍

@jorgecoca, @brianegan thoughts? It ended up being almost identical to Brian's PR which was by far the most simple approach.

@brianegan
Copy link

Wait, my bad -- was coding too quickly. My implementation has a big problem: You can't listen to the state stream twice! This test would fail with this current implementation. I would go back to using a StreamController<State>.broadcast() for the stateStream. It's also probably a good idea to add a test case that verifies the Stream can be listened to more than once.

      test('close does not emit new states over the state stream', () {
        final expectedStates = [equals(''), emitsDone];

        expectLater(
          simpleBloc,
          emitsInOrder(expectedStates),
        );

        expectLater(
          simpleBloc,
          emitsInOrder(expectedStates),
        );

        simpleBloc.close();
      });

This gets a bit hairy: I changed that state StreamController from a broadcast to a normal StreamController to prevent any errors using an async* for the startWith implementation, which were displayed by @mmozhaiskyi in his test code above. Of course, that means you can no longer listen to the Stream more than once 🤦‍♂

The cause of that issue in the tests? Events were added to the StreamController before the async* function could start listening (as the function is run asynchronously).

The Bloc class will not have that same problem, because events are not synchronously added to the stateController, but rather added asynchronously after being transformed by transformEvent and transformState. Therefore, the async* function has time to execute and start listening to the broadcast state Stream before any events would be sent. That's why this test actually works fine using the async* function for startsWith + a broadcast Stream on the Bloc class:

      test('should map single event to correct state', () {
        final expectedStates = ['', 'data'];

        expectLater(
          simpleBloc,
          emitsInOrder(expectedStates),
        );

        expectLater(
          simpleBloc,
          emitsInOrder(expectedStates),
        );

        expectLater(
          simpleBloc,
          emitsInOrder(expectedStates),
        );

        simpleBloc.add('event');
      });

@brianegan
Copy link

Updated my branch with those changes!

@@ -10,7 +10,7 @@ import '../bloc.dart';
/// {@endtemplate}
abstract class Bloc<Event, State> extends Stream<State> implements Sink<Event> {
final _eventController = StreamController<Event>.broadcast();
final _stateController = StreamController<State>();
final _stateController = StreamController<State>.broadcast();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I'd just make sure to add the test to catch this bug as well.

@felangel
Copy link
Owner

felangel commented Feb 2, 2020

Going to merge and will apply the testing updates right after. Thanks everyone!

@codecov
Copy link

codecov bot commented Feb 2, 2020

Codecov Report

❗ No coverage uploaded for pull request base (master@8d31770). Click here to learn what that means.
The diff coverage is 100%.

Impacted file tree graph

@@           Coverage Diff           @@
##             master   #821   +/-   ##
=======================================
  Coverage          ?   100%           
=======================================
  Files             ?     16           
  Lines             ?    247           
  Branches          ?      0           
=======================================
  Hits              ?    247           
  Misses            ?      0           
  Partials          ?      0
Impacted Files Coverage Δ
packages/bloc/lib/src/bloc.dart 100% <100%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8d31770...a2fca17. Read the comment docs.

@felangel felangel merged commit bc45e4d into felangel:master Feb 2, 2020
bloc automation moved this from In progress to Done Feb 2, 2020
@felangel felangel mentioned this pull request Feb 2, 2020
3 tasks
@sla-000
Copy link

sla-000 commented Aug 19, 2022

Hi @felangel @mozhaiskyi So if I understand it right, though BehaviorSubject was removed from bloc/cubit but some other mechanism was implemented to keep it work as it was with BehaviorSubject?
I added listener to the cubit example from repo (see below) and I can see that listener does not get last value from cubit right after it connects to states stream. Listener only gets a new state if I call increment() after connection.
Is it supposed to be so or I do something wrong?

Future<void> cubitMain() async {
  print('----------CUBIT----------');

  /// Create a `CounterCubit` instance.
  final cubit = CounterCubit();

  /// Access the state of the `cubit` via `state`.
  print(cubit.state); // 0

  /// Interact with the `cubit` to trigger `state` changes.
  cubit.increment();

  await Future<void>.delayed(Duration.zero);

  final _sub = cubit.stream.listen((state) {
    print('Look, the state=$state');
  });

  await Future<void>.delayed(Duration.zero);

  cubit.increment();

  await Future<void>.delayed(Duration.zero);

  /// Access the new `state`.
  print(cubit.state);

  /// Close the `cubit` when it is no longer needed.
  await cubit.close();
  await _sub.cancel();
}

Log:

----------CUBIT----------
onCreate -- bloc: CounterCubit
0
onChange -- bloc: CounterCubit, change: Change { currentState: 0, nextState: 1 }
onChange -- bloc: CounterCubit, change: Change { currentState: 1, nextState: 2 }
Look, the state=2
2
onClose -- bloc: CounterCubit

@felangel
Copy link
Owner

Hi @sla-000 this is working as expected and has been this way since bloc v6.0.0. See https://bloclibrary.dev/#/migration?id=%e2%9d%97bloc-does-not-emit-last-state-on-subscription for more information.

@sla-000
Copy link

sla-000 commented Aug 20, 2022

Hi @felangel Thanks for quick response and explanation!
So from v6.0.0 if I want to subscribe a bloc to changes in other bloc I have to pass both current state value and a stream to this bloc and then
1 - process state, if there is one and it's must be checked
2- subscribe to stream and process new data in a listener

This creates boilerplate code. Is there any chance this ever will be changed back to BehaviorSubject or something similar? I understand you are trying to keep code pure with as little dependencies as possible but may be it is simplified too much and we are throwing the baby out with bath water 🙂

@maximveksler
Copy link
Contributor

@sla-000 have you considered using a shared repository to implement communication between the blocks? This way it might simplify your use case of needing to cope with the duality of the bloc behaviour.

Note that BlocBuilder and BlocListener will perform correctly, for both 1st event and followup events.

@sla-000
Copy link

sla-000 commented Aug 20, 2022

@maximveksler Could you provide some example of that shared repository, please?

@maximveksler
Copy link
Contributor

maximveksler commented Aug 20, 2022

Yes, however I can't testify that it's the canonical example.

I will reference you the 2 examples, 1st is the firebase login where the authentication repository is shared between the App, Login and Signup BloC's. Pay attention that the App BloC and the Login Cubit listen to the user stream from the login repository.

flutter_firebase_login

flutter_todos

The 2nd would be the Todo's example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request pkg:bloc This issue is related to the bloc package
Projects
bloc
  
Done
Development

Successfully merging this pull request may close these issues.

None yet

7 participants