Kuan

January 9, 2023

Dart Stream journey 1: Stream everywhere

I have been enjoying writing "dot chaining" dart codes recently, something I really adore when I was building fp-svelte. The beauty of the codes is their declarative nature of them, as opposed to imperative codes. Let shows them here.

Stream, Stream everywhere


final _contacts = BehaviorSubject<List<Contact>>();
_contacts.addStream(remote.getContacts());

Let us start with a simple one. We create a BehaviorSubject: _contacts, to store and pass down the list of contacts we got from the remote service, remote.getContacts(). This remote call returns a Stream which sends an event when it got the response from an HTTP call. To capture this Stream's event and put it into _contacts, I used addStream here.

Usually, in a more common way of programming, _contacts is a normal variable. i.e. 

List<Contact> _contacts;

And we simply store the result of calling remote.getContacts(). If it returns a Future, we use await. If it returns a Stream, we listen to that Stream and put the event into _contacts. e.g.

_contacts = await remote.getContacts(); // if Future is returned.

remote.getContacts().listen((event) => _contacts = event); // if Stream is returned.

But we do not do these here. In this new world, everything is a Stream (BehaviourSubject is a Stream). All states are Stream event, all state manipulations are Stream operation, all results are delivered in a Stream. Streams everywhere.

Note: BehaviorSubject is Dart's StreamController with powerful upgrades. It is a broadcast Stream that always sends out the latest event to new subscribers. We will be using it for many other Streams.

Stay with me now. Hold all your "whyyyy???". I am sure you will understand later.

Next.

If-else in Stream world


final BehaviorSubject<String?> _passedInContactId = BehaviorSubject<String?>();
final BehaviorSubject<String?> _initialContactId = BehaviorSubject<String?>();

_initialContactId.addStream(
  _passedInContactId.stream
    .switchIfEmpty(
      _contacts.stream
        .map((contacts) => contacts.isNotEmpty
          ? contacts.first.contactNumber.toString()
          : null)));

Things just got ten times scarier! Stay with me. It isn't that nasty.

The main idea here is to figure out an initial ContactId. We want to display a default Contact in the page, and it should be whatever passed in from the URL's query parameter. If there isn't one from the URL, then we should use the first contact from _contacts we acquired in the first part.

Let walk through the codes.

_initialContactId.addStream(
  _passedInContactId.stream
    .switchIfEmpty(

_initialContactId is a BehaviourSubject. We start by adding a Stream to _initialContactId
  1. First thing we check is there any event in _passedInContactId.
  2. switchIfEmpty() here means if _passedInContactId was closed with no event sent, switch to another Stream.

This is a simple if-else situation here (without writing if-else!).

The 'else' Stream's job is to return the ID of the first contacts, or null if there isn't any contacts.

    .switchIfEmpty(
      _contacts.stream
        .map((contacts) => contacts.isNotEmpty
          ? contacts.first.contactNumber.toString()
          : null)));

  1. switchIfEmpty() expects a Stream, so, we start with _contacts.stream.
  2. _contacts.stream send out a List<Contact> event. But, we need the ID of the first item from the list.
  3. So, we use .map() to transform the event, a list of Contact, to a single ID. Or null if the list is empty.

End of journey 1

Scary or beauty? I will try my best to continue into the next journey asap. Stay with me!

About Kuan

Web developer building with Flutter, Svelte and JavaScript. Recently fell in love with functional programming.

Malaysian. Proud Sabahan. Ex game developer but still like playing games.

New found hobby is outdoor camping with my love.