Create a new stream from a stream in Dart

Viewed 1682

I suspect that my understanding of Streams in Dart might have a few holes in it...

I have a situation in which I'd like a Dart app to respond to intermittent input (which immediately suggests the use of Streamss -- or Futures, maybe). I can implement the behavior I want with listener functions but I was wondering how to do this in a better, more Dartesque way.

As a simple example, the following (working) program listens to keyboard input from the user and adds a div element to the document containing what has been typed since the previous space, whenever the space bar is hit.

import  'dart:html';

main() {
  listenForSpaces(showInput);
}

void  listenForSpaces(void  Function(String) listener) {
  var input =  List<String>();
  document.onKeyDown.listen((keyboardEvent) {
    var key = keyboardEvent.key;
    if (key ==  " ") {
      listener(input.join());
      input.clear();
    } else {
      input.add(key.length >  1  ?  "[$key]"  : key);
    }
  });
}

void  showInput(String message) {
  document.body.children.add(DivElement()..text = message);
}

What I'd like to be able to do is to create a new Stream from the Stream that I'm listening to (in the example above, to create a new Stream from onKeyDown). In other words, I might set the program above out as:

var myStream = ...
myStream.listen(showInput);

I suspect that there is a way to create a Stream and then, at different times and places, insert elements to it or call for it to emit a value: it feels as though I am missing something simple. In any case, any help or direction to documentation would be appreciated.

3 Answers

Creating a new stream from an existing stream is fairly easy with an async* function. For a normal stream, I would just do:

Stream<String> listenForSpaces() async* {
  var input = <String>[];
  await for (var keyboardEvent in document.onKeyDown) {
    var key = keyboardEvent.key;
    if (key == " ") {
      yield input.join();
      input.clear();
    } else {
      input.add(key.length > 1 ? "[$key]" : key);
    }
  }
}

The async* function will propagate pauses through to the underlying stream, and it may potentially pause the source during the yield. That may or may not be what you want, since pausing a DOM event stream can cause you to miss events. For a DOM stream, I'd probably prefer to go with the StreamController based solution above.

There are several methods and there is a whole package rxdart to allow all kinds of things.

Only the final consumer should use listen and only if you need to explicitly want to unsubscribe, otherwise use forEach

If you want to manipulate events like in your example, use map.

I wasn't originally planning to answer my own question but I have since found a very simple answer to this question in the dartlang creating streams article; in case it's helpful to others:

Specifically, if we'd like to create a stream that we can insert elements into at arbitrary times and places in the code, we can do so via the StreamController class. Instances of this class have an add method; we can simply use the instance's stream property as our stream.

As an example, the code in my question could be rewritten as:

import 'dart:html';
import 'dart:async';

main() async {
  // The desired implementation stated in the question:
  var myStream = listenForSpaces();
  myStream.listen(showInput);
}

Stream<String> listenForSpaces() {
  // Use the StreamController class.
  var controller = StreamController<String>();

  var input = List<String>();
  document.onKeyDown.listen((keyboardEvent) {
    var key = keyboardEvent.key;
    if (key == " ") {
      // Add items to the controller's stream.
      controller.add(input.join());
      input.clear();
    } else {
      input.add(key.length > 1 ? "[$key]" : key);
    }
  });

  // Listen to the controller's stream.
  return controller.stream;
}

void showInput(String message) {
  document.body.children.add(DivElement()..text = message);
}

(As mentioned in the article, we need to be careful if we want to set up a stream from scratch like this because there is nothing to stop us from inserting items to streams that don't have associated, active subscribers; inserted items would in that case be buffered, which could result in a memory leak.)

Related