Sinks vs. functions when using Bloc pattern

658 views
Skip to first unread message

maido...@gmail.com

unread,
Aug 24, 2018, 3:40:54 AM8/24/18
to Flutter Dev
So I've been using Bloc pattern a lot in our app (about 20 bloc classes with multiple sinks and streams in each), really enjoying the streams part of it, combining and transforming is really powerful, but I'm struggling with the sinks.
Using them make sense but it makes writing code and tests a bit difficult, so I've been exposing functions instead of sinks and I'm wondering what kind of problems could this bring that I'm not noticing right now?

So to illustrate, instead of doing this:
class Bloc {
  final _name = BehaviorSubject<String>();

  Bloc() {
    _name.listen(_handleName);
  }

  Sink<String> get addName => _name.sink.add;

  void _handleName(String name) async {
    await dosomethingwithname;
  }
}

class BlocTest {
  test('sink', () {
    final bloc = Bloc();
    bloc.addName.add('name');
    //How to test or wait until it has processed??
  });
}

I do:
class Bloc {
  final _name = BehaviorSubject<String>();
 
  void addName(String name) async {
    await dosomethingwithname;
  }
}

class BlocTest {
  test('sink', () {
  final bloc = Bloc();
  await bloc.addName('name');
  //addName finished!
  });
}

Easy to test, more readable (no .add method call), really easy to react to the results, but not really Bloc pattern since blocs can only expose Sinks/Streams. Any reason I should stop doing this?

Brian Egan

unread,
Aug 24, 2018, 5:55:54 AM8/24/18
to Flutter Dev
This is how I've been doing it as well... I've emailed the authors of the Bloc pattern for clarification on why they explicitly recommend Sinks and don't allow Functions, but overall I also prefer the function route.

When it comes to testing, you can definitely test the `sinks` version using the Stream Matchers provided by the test package. It doesn't look like you're exposing a Stream anywhere from your bloc, but if you were, it'd be something like:

test('sink', () {
   
final bloc = Bloc();
    bloc.addName.add('name');
    expect(bloc.nameStream, emits('name'));
  });

More docs on the stream matchers here, which are really important to testing blocs / stream-based patterns in general: https://github.com/dart-lang/test#stream-matchers

maido...@gmail.com

unread,
Aug 24, 2018, 7:26:19 AM8/24/18
to Flutter Dev
Sometimes  this helps, but it breaks encapsulation. I try not to expose implementation details only to help testing, even though Uncle Bob says it is ok :)
I think where I mostly see this problem is when I want to do some navigation after the result was successful.
So using the functions it is really easy to just await on the Future and then navigate, but when I used sinks, only solution I saw was to inject the navigatorState to the Bloc and navigate inside bloc, which I don't like because IMO navigation is more about presentation and less about business.

Peter LoBue

unread,
Nov 26, 2018, 8:05:21 PM11/26/18
to Flutter Dev
I'm wondering this same thing. In this video: https://youtu.be/PLHln7wHgPE?t=1363 ...the "rule" of only using sinks and streams is described. In the minute beforehand, the speaker shows an image representing spaghetti code. He seems to suggest the BLoC rules as a way to prevent your code from getting messy.

I guess if you are using functions instead of streams, you might allow the functions to return a result, and then use that instead of listening to a stream. (The stream is of course better because it forces you to expose the results to other widgets that might be interested.) Maido... brings up a good point about navigation as a case when you would actually want a return value. In that case, you should probably restrict the return value to a simple success/fail boolean, or optional error type.

Also if you are using functions, you might pass multiple parameters into the function. Using sinks forces you to encapsulate what would be multiple parameters into one, thereby minimizing the interface between your UI code and business logic.

I still haven't found a really compelling reason to use sinks instead of function calls. The sinks need so much boilerplate code. I'm just trying to stay rigorous about the functions, keeping them as simple as possible.
Reply all
Reply to author
Forward
0 new messages