Skip to content

async coding with dart

source: https://www.youtube.com/playlist?list=PLjxrf2q8roU2HdJQDjJzOeO6J3FoFLWr2

1754901323.png

  1. isolates and event loops: https://www.youtube.com/watch?v=vl_AaCgudcY
  2. futures: https://www.youtube.com/watch?v=OTS-ap9_aXc
  3. streams: https://www.youtube.com/watch?v=nQBpOIHE4eE
  4. async/await
  5. generators

isolates and event loop

source: https://www.youtube.com/watch?v=vl_AaCgudcY

ANDREW BROGDON: Hey, everybody. I'm Andrew from the Flutter team, and welcome to the "Flutter in Focus" miniseries on a asynchronous programming in Dart. This is the first in a run of videos covering the ways Dart, despite being a single-threaded language, offers support for futures, streams, background work, and all the other things you need to write in a modern, asynchronous, and, in the case of Flutter, reactive way. Since this is the first video in the series, I'm going to start all the way down at the foundation of what makes asynchrony possible with Dart, and that's the isolate. An isolate is what all Dart code runs in. It's like a little space on the machine with its own private chunk of memory and a single thread running an event loop. In a lot of other languages like C++, you could have multiple threads sharing the same memory and running whatever code they want. In Dart, though, each thread is in its own isolate with its own memory and it just processes events. More on that in a minute. Many Dart apps run all their code in a single isolate, but you can have more than one if you need it. If you have a computation to perform that's so enormous it could cause you to drop frames if it were run in the main isolate, you can use Isolate.spawn() or Flutter's compute function (5), both of which create a separate isolate to do the number crunching, leaving your main one free to rebuild and render the widget tree in the meantime. That new isolate will get its own event loop and its own memory, which the original isolate, even though it's the parent of this new one, isn't allowed to access. That's the source of the name isolate. These little spaces are kept isolated from one another. In fact, the only way they can work together is by passing messages back and forth. One isolate will send a message to the other, and that receiving isolate process is the message using its event loop. This lack of shared memory may sound kind of strict, especially if you're coming from a language like Java or C++, but it has some key benefits for Dart coders. For example, memory allocation and garbage collection in an isolate don't require locking. There's only one thread, so if it's not busy, you know the memory's not being mutated. That works out really well for Flutter apps, which sometimes need to build up and tear down a bunch of widgets really quickly. All right. So that's a basic introduction to isolates. Now let's dig into what really makes async code possible, the event loop. Imagine the life of an app stretched out on a timeline (6). Here you start, there you stop, and in between, there are all these little events, like I/O from the disk, or finger taps from the user-- all kinds of stuff. Your app can't predict when these events will happen or in what order, and it has to handle all of them with a single thread that never blocks, so it runs an event loop (7). Simple as can be. It grabs the oldest event from the event queue, processes it, goes back for the next one, processes that one, and so on until the event queue is empty. The whole time the app is running-- you're tapping on the screen, things are downloading, a timer goes off-- that event loop is just going around and around, processing those events one at a time. Whenever there's a break in the action, the thread just kind of hangs out, waiting for the next event. It can trigger the garbage collector, get some coffee, whatever. All of the high level APIs we're used to for asynchronous programming-- futures, streams, async and await-- they're all built on and around this simple loop. For example, say you have a button that initiates a network request, like this one. (1) You run your app and Flutter builds the button and puts it on screen, then it waits. The event loop just sort of idles, waiting for the next thing to process. Other events not related to the button might come in and get handled while the button just sits there waiting for the user to tap on it. Eventually they do, and a tap event enters the queue (8). That event gets picked up for processing, Flutter looks at it, and the rendering system says, hey, those coordinates match the RaisedButton, so Flutter executes the onPressed function. That code initiates a network request, which returns a Future and registers a completion handler for the future by using then. And that's it. The loop is finished processing that tap event and it's discarded. Now, onPressed was a property on RaisedButton, and here we're talking about a callback for a future. But both of those techniques are doing basically the same thing. They're both a way to tell Flutter, hey, later on, you might see a particular type of event come in. When you do, please execute this piece of code. onPressed is waiting for a tap and the future is waiting for network data. But from Dart's perspective, those are both just events in the queue. And that's how asynchronous coding works in Dart. futures, streams, async, and await-- these APIs are all just ways for you to tell Dart's event loop, here's some code. Please run it later. If we look back at the code example, you can now see exactly how it's broken up into blocks for particular events. There's the initial build (2), the tap event (3), and the network response event (4). Once you get used to working with async code, you'll start recognizing these patterns all over the place. And understanding the event loop is going to help as we move on to the higher level APIs. All right. So that's a quick look at isolates, the event loop, and the foundation of async coding in Dart. In our next video, we're going to talk about futures, a simple API you can use to take advantage of these capabilities without a ton of code. In the meantime, leave a comment below if you have a question, and come see us at flutter.io. [MUSIC PLAYING] Hey, if you enjoyed that video, try these others, or subscribe to the Flutter channel. It's Google's new portable UI toolkit. There's a button around here somewhere.

  1. 1754942468.png
  2. 1754943313.png
  3. 1754943406.png
  4. 1754943419.png
  5. 1755115353.png
  6. 1755115652.png
  7. 1755115680.png
  8. 1755115926.png

Threads

In a typical Linux system with a 16-thread CPU running Chrome with 16 tabs, Flutter development, and Neovim, you'd likely see several hundred to over a thousand kernel threads being managed simultaneously. ~ Claude

The program in memory (in memory since instantiation: HDD -> RAM/memory), there might be the section in the memory that defines the current state of the app: eg- how buttons responds, how things look in the ui. By design, this part of the memory can only be accessed by one thread (this is how dart isolates are designed). So one thread has to deal with or coordinate the entire reactive behavior. This thread is the app's main event loop.

futures

source: https://www.youtube.com/watch?v=OTS-ap9_aXc

introduction

ANDREW BROGDON: Hey, everybody, and welcome to the second video in our Flutter in Focus series on asynchronous coding patterns in Dart. Today we're going to cover one of the most basic APIs Dart has for async-- Futures. Most modern languages have some kind of support for asynchronous programming. Many offer a futures API, and some call them Promises. And for the most part, Dart's Futures are very similar to those found in other languages. I like to think of them as little gift boxes for data. Somebody hands you one, and it starts off closed. Then a little while later, it pops open. And inside, there's either a value or an error. So those are the three states a Future can be in. First, the box is closed. We call that uncompleted. Then the box opens, and it's completed with a value or completed with an error. Most of the code you're about to see revolves around dealing with these three states. (1) You know, one of your functions gets a Future, it needs to decide, OK, what do I do if the box isn't open yet? What do I do when it opens later and I have a value? And what about an error? And so on. So you're to see that one, two, three pattern a lot. You might also remember this guy(2) from our previous video about the Dart Event Loop. A good thing to know about Futures is they're really just an API built to make using the Event Loop easier. The Dart code you write is executed by a single thread. The whole time your app is running, that one little thread just keeps going around, picking up events from the Event Queue and processing them. Futures work with the Event Loop to simplify things. Say you have some code for a download button. (3) The user taps, and it starts downloading a picture of a cupcake or something. Well, first, the tap event occurs. (4) The Event Loop gets it, and your tap handler gets called. It uses the http library to make a request, and it gets a future in return. So now you've got your little box, right? It starts off closed, so your code uses then to register a callback for when it opens. (5) Then you wait. (6) Maybe some other events come in. The user does some stuff, and your little box just sits there while the Event Loop keeps going around. Eventually, data for the image arrives. (7) And the https library says, great. I've got this Future right here. It puts the data in the box and pops it open, which triggers your callback. Now that little piece of code executes and displays the image. (8) Throughout that process, your code never had to touch the Event Loop directly. Didn't care what else was going on, what other events came in. All it needed to do was get the Future from the https library and then say what it was going to do when the future completed. If I were a better coder, I probably would have added a code in case it completed with an error. But this series is a safe space.

  1. 1755117451.png
  2. 1755121988.png
  3. 1754950584.png
  4. 1754950810.png
  5. 1754950884.png
  6. 1754950925.png
  7. 1754950970.png
  8. 1754951025.png

Future gives us a way to wire up and queue values into the dart event loop.

  • enter value: Future.value(5)
  • wire up: Future.value(5).then((input) => print("I know we've recieved 5: $input"))

For example, the below code queues the integer 5 into the event loop after a delay of 5 seconds and, using then, wires it up to action to perform once it reaches event loop.

1
2
3
4
5
6
7
8
import 'dart:async';

void main() {
  // print(5);
  Future.delayed(Duration(seconds: 5), () => 5).then((prevResult) {
    print(prevResult);
  });
}

In the code below, note the return value of the function passed the Future.delayed. This return value will enter the event loop.

Q&A with Claude AI

Question: There's a thread, from the perspective of the kernel/scheduler that runs the dart event loop. This event loop gets events.

The origin of those events that enter the event loop (be it data from the network, disk read or even from Future.delayed) involved the working of other threads right?

Claude: The Dart event loop itself runs on a single thread, but the events that feed into it almost always originate from work happening on other threads.

Here's how it typically works:

Network I/O: When you make an HTTP request, the actual network operations happen on background threads managed by the operating system or Dart's native extensions. When data arrives or the connection completes, those background threads signal the event loop thread, which then processes the corresponding Future completion.

File I/O: Similarly, disk reads/writes are handled by OS threads. When the operation completes, the result gets queued as an event for the main isolate's event loop.

Timers/Future.delayed: Even something as simple as Future.delayed(Duration(seconds: 1)) relies on the OS timer system, which runs on separate threads. When the timer expires, it signals the Dart event loop to process the timer callback.

Isolate communication: Messages between isolates are handled by the Dart VM's threading infrastructure before appearing as events in the receiving isolate's event loop.

The beauty of this design is that from the Dart developer's perspective, everything appears to happen on a single thread (the event loop), which eliminates many concurrency issues. But under the hood, there's a whole ecosystem of threads doing the actual I/O work and feeding completed operations back to the event loop as events to be processed.

The event loop thread spends most of its time either processing events from this queue or waiting (blocked) for new events to arrive from those background threads.

getting a Future

All right-- first question. How to get an instance of a Future? Most of the time, you're probably not going to be creating Futures directly. That's because many of the common async tasks already have libraries that generate Futures for you. Like network communication returns a Future (1). Accessing shared preferences returns a Future (2). But there are also constructors you can use. The simplest is the default, which takes a function and returns a Future with the same type (3). Then later, it runs the function asynchronously and uses the return value to complete the Future. Let me add a couple print statements here to make clear the asynchronous part. (4) Now when I run this, you can see the entire main method finishes before the function I gave to the Future constructor. That's because the Future constructor just returns an uncompleted Future at first. It says, here's this box. You hold onto that for now, and later, I'll go run your function and put some data in there for you. If you already know the value for the future, you can use the Future.value named constructor. (5) The Future still completes asynchronously, though. I've used this one when building services that use caching. Sometimes you've already got the value you need, so you can just pop it right in there. Future.value also has a counterpart for completing with an error, by the way. It's called Future.error, and it works essentially the same way. But it takes an error object and an optional stack trace. (6) The constructor I probably use the most, though, is Future.delayed. (7) It works just like the default one, only it waits for a specified length of time before running the function and completing the Future. I use this one all the time when creating mock network services for testing. If I need to make sure my little loading spinner is displaying right and then goes away, somewhere, there's a delayed Future helping me out. All right. So that's where Futures come from.

  1. 1754951837.png
  2. 1754951867.png
  3. 1754951894.png
  4. 1754951936.png
  5. 1754952045.png
  6. 1754952098.png
  7. 1754952126.png

You can input a value to the event loop using Future:

Future.value(5) // (1)
  1. here 5 was the input to the event lop

You can wire up the event loop to respond to the input using then:

1
2
3
4
5
6
7
8
import 'dart:async';

void main() {
  Future.value(5).then((input) {
    print(input + 5);
  });
  print('This will be output before the asynchronous task');
}
Output
This will be output before the asynchronous task
10

You can delay this input by using Future.delayed, this constructor expects duration and a function that returns input value.

Future.delayed(Duration(seconds: 5), () => 5);

This will, after 5 seconds, enter 5 into the event loop.

how to use Future(s)

Now let's talk about how to use them. As I mentioned earlier, it's mostly about accounting for the three states a Future can be in-- uncompleted, completed with a value, or completed with an error. Here's a Future.delayed creating a Future that will complete three seconds later with a value of 100. (1) Now when I execute this, main runs from top to bottom, creates the Future, and prints "waiting for a value". That whole time, the Future is uncompleted. It won't complete for another three seconds. So if I want to use that value, I'll use then. (2) This is an instance method on each Future that you can use to register a callback for when the Future completes with a value. You give it a function that takes a single parameter matching the type of the Future. And then once the Future completes with a value, your function executes with that value. So if I run this, I still get "waiting for a value" first. And then three seconds later, my callback executes and prints the value. In addition, then returns a Future of its own matching the return value of whatever function you give it. (3) So if you have a couple asynchronous calls you need to make, you can chain them together, even if they have different return types. value?

  1. 1754952192.png
  2. 1754952236.png
  3. 1754952325.png

Back to our first example (1), though, What if it completes with an error? then expects a value. We need a way to register another callback in case of an error. And you could do that with catchError (2). Catcherror works just like then, only it takes an error instead of a value, and it executes if the Future completes with an error. Just like then, it returns a Future of its own. So you can build a whole chain of thens and catch errors and thens and catch errors that wait on one another. You can even give it a test method (3) to check the error before invoking the callback. You can have multiple catch error methods this way, each one checking for a different kind of error. Now that we've gotten this far, hopefully you can see what I mean about how the three states of a Future are often reflected by the structure the code. There are three blocks here. The first creates an uncompleted Future. Then there's a function to call when the Future completes with a value and another if it completes with an error.

  1. 1754952409.png
  2. 1754952497.png
  3. 1755127345.png

I do have one more method to show you, though, which is whencomplete (1). You can use this to execute a method when the future is completed, no matter whether it's with a value or an error. It's like the finally block in a try catch finally. There's code executed if everything goes right, code for an error, and then code that runs no matter what. So that's how you create Futures and a bit about how you can use their values.

  1. 1755127616.png
void main() {
  // final testFuture = Future.delayed(Duration(seconds: 5), () => 12);
  final testFuture = Future.delayed(Duration(seconds: 5), () {
    throw Exception(); // (1)
  });

  testFuture
      .then((data) { // (2)
        print("completed with value: $data");
      })
      .catchError((error) { // (3)
        print("completed with error: $error");
      });

  print('working...'); // (4)
}
  1. https://dart.dev/language/error-handling
  2. then: what handles when the future completes with data
  3. catchError: what handles when the future completes with error
  4. the printing happens before any status update of the future (completion-data or completion-error), there fore it corresponds to the "working" or "in progress" state of future.

using Future(s) in Flutter

Now let's talk putting them to work in Flutter. This will probably be the least complicated section of this video. Let me show you why. Say you have a network service that's going to return some JSON, and you want to display it. You could create a stateful widget that will create the Future, check for completion or error, call set state, and generally handle all that wiring manually. Or you can use FutureBuilder (1). It's a widget that comes with the Flutter SDK. You give it a Future and a builder method, and it will automatically rebuild its children when the Future completes. It does that by calling its builder method, which takes a context and a snapshot of the current state of the Future. You can check the snapshot (2) to see if the Future completed with an error and report it. Otherwise, you can check the has data property to see if it completed with a value. (3) And if not, you know you're still waiting. (4) So you can output something for that as well. Even in Flutter code, you can see how those three states keep popping up-- uncompleted, completed with value, and completed with error. All right. That's all we've got for this video, but there are more coming in the series. Next up, we'll be talking about streams. They're a lot like Futures, in that they can either provide values or errors. But where Futures just give you one and stop, streams keep right on going. So be on the lookout for that and head to dart.dev and flutter.dev for more info on Dart and Flutter. [MUSIC PLAYING] Hey, if you enjoyed that video, try these others. Or subscribe to the Flutter channel. It's Google's new portable UI toolkit. There's a button around here somewhere.

  1. 1754952681.png
  2. 1754952732.png
  3. 1754952770.png
  4. 1754952796.png

This is a simple screen that displays the result (stdout) of a particular command.

Note

The Future instance can be in 3 states:

  1. data
  2. error
  3. loading

We can tell if Future is in one of the state in the builder passed to FutureBuilder using snapshot argument:

  1. snapshot.hasData : true if data was yielded
  2. snapshot.hasError: true if error was yielded
  3. the else when both of the above is fale.

Compare this with how Future is used in pure dart:

import 'dart:io';
import 'package:flutter/material.dart';

void main() {
  runApp(ProcessResultViewer());
}

class ProcessResultViewer extends StatelessWidget {
  ProcessResultViewer({super.key})
    // : processResult = Process.run('find', ['/tmp']);
    : processResult = Future.delayed(
        Duration(seconds: 5),
        () => Process.run('find', ['/tmp']),
      ); //

  final Future<ProcessResult> processResult;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text("Process Result Viewer")),
        body: FutureBuilder(
          future: processResult,
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              return SelectableText(snapshot.data?.stdout.toString() ?? "");
            } else if (snapshot.hasError) {
              return SelectableText(snapshot.error.toString());
            } else {
              return Text("Processing...");
            }
          },
        ),
      ),
    );
  }
}
  1. Doing this so that we can see what the FutureBuilder displays when there is no data.
void main() {
  // final testFuture = Future.delayed(Duration(seconds: 5), () => 12);
  final testFuture = Future.delayed(Duration(seconds: 5), () {
    throw Exception(); ()
  });

  testFuture
      .then((data) {
        print("completed with value: $data");
      })
      .catchError((error) {
        print("completed with error: $error");
      });

  print('working...');
}

streams

source: https://www.youtube.com/watch?v=nQBpOIHE4eE

Hey, everybody, and welcome to the third video in our "Flutter in Focus" series on asynchronous coding patterns in Dart. In this episode, I'm going to cover one of the fundamentals of reactive programming, streams.

If you saw our previous video on futures, you may remember that each Future represents a single value, either an error or data that it delivers asynchronously. Streams work similarly, only instead of a single thing, they can deliver zero or more values and errors over time. If you think about the way a single value relates to an iterator of the same type, that's how a future relates to a stream (1). Just like with futures, the key is deciding in advance here's what to do when 1. a piece of data is ready, 2. when there's an error, and 3. when the stream completes. And the Dart event loop is still running the show. If you're using files.openRead method to read data from a file, for example, it returns a stream. Chunks of data are read from disk and arrive at the event loop. dart:io looks at them and says, "Ah, I've got somebody waiting for this," adds the data to the stream, and it pops out in your app's code. When another piece of data arrives, in it goes and out it comes. Timer-based streams, streaming data from a network socket-- they work with the event loop too using clock and network events.

  1. 1755076550.png Within the body of an iterator (eg: for loop) the iteration count is available. Likewise in the body of a stream, you get the particular type of the stream. For Stream its int.

how to work with data provided by stream

Okay, let's talk about how to work with data provided by a stream. Say I have a class that will give me a stream that kicks out a new integer once per second-- one, two, three, four, five (1). I can use the listen method to subscribe to the stream (2). I give it a function, and every time a new value is emitted by the stream, my function gets called and prints it (3). That's how listen works.

  1. 1755082968.png
  2. 1755082999.png
  3. 1755083039.png

One important thing to note is that, by default, streams are set up for single subscription. They hold onto their values until someone subscribes (through listen method), and they only allow a single listener for their entire lifespan. If you try to listen to one twice, you'll get an exception. (1)

  1. 1755083086.png

Fortunately, Dart also offers broadcast streams (1). You can use the asBroadcastStream method to make a broadcast stream from a single subscription one. They work the same as single subscription streams, but they can have multiple listeners. And if nobody's listening when a piece of data is ready, it gets tossed out.

  1. 1755083895.png

Broadcast streams vs Regular streams

Regular streams buffer the values entering it even when there is no listener set up. One a listener gets setup with using the stream's listen method, the handler gets all the data at once. BUT in a broadcast stream the handler only gets the data from when it was set up. The previous values were discarded.

Let's go back to that first listen call though because there are a couple more things to talk about (1). I mentioned earlier that streams can produce errors just like futures can-- by adding an onError method you can catch and process any errors (2). There's also a cancelOnError property (3) that's true by default, but can be set to false to keep the subscription going even after an error. And there's an onDone method you can use to execute some code when the stream is finished sending data, such as when a file has been completely read (4). With all four of those properties combined, you can be ready in advance for whatever happens (5).

  1. 1755083933.png
  2. 1755198977.png
  3. 1755083962.png
  4. 1755084040.png
  5. 1755084081.png

Before moving on to the next section, I should mention that the little subscription object (1) that's so far gone unnoticed has some useful methods of its own. You can use it to pause, resume and even cancel the flow of data (2). Okay, so that's a quick look at how you can use listen to subscribe to a stream and receive data events. Now we get to talk about what makes streams really cool: manipulating them.

  1. 1755084131.png
  2. 1755199115.png

manipulating streams

Once you've got data in a stream, there are a lot of operations that suddenly become fluent and elegant. Let's go back to that number stream from earlier (1). I can use a method called map (2) to take each value from the stream and convert it, on the fly, into something else. I give map a function to do the conversion, and it returns a new stream, typed to match the return value of my function. Instead of a stream of ints, I now have a stream of strings. I can throw a listen call on the end, give it the print function and now I'm printing strings directly off the stream, asynchronously, as they arrive (3).

  1. 1755199564.png
  2. 1755084221.png
  3. 1755084346.png

There's a ton of methods you can chain up like this (1). If I only want to print the even numbers, for example, I can use where to filter the stream. I give it a test function that returns a Boolean for each element, and it returns a new stream that only includes values that pass that test. distinct is another good one. If I have an app that uses a ReduxStore, that store emits new app state objects in an onChange stream (2). I can use map (3) to convert that stream of state objects to a stream of ViewModels for one particular part of my app. Then I can use the distinct method to get a stream that filters out consecutive identical values— in case the store kicks out a change that doesn't affect the subset of data in MyViewModel. Then I can listen and update my UI whenever I get a new ViewModel (5).

  1. 1755084394.png
  2. 1755200109.png
  3. 1755200155.png
  4. 1755084472.png
  5. 1755084521.png

This is a command line which expects file contents to be provided via its stdin (via pipes), but if not you can enter the data line by line until you press <Ctrl-D>

import 'dart:convert';
import 'dart:io';

void main() {
  var subscription;
  if (stdin.hasTerminal) {
    stdin.lineMode = false;
    stdin.echoMode = true;
    List<int> input = [];
    subscription = stdin.listen((data) {
      if (data.contains(4)) {
        subscription.cancel();
        print("\n${utf8.decode(input)}");
      } else {
        input.add(data[0]);
      }
    });
  } else { // if there is no terminal
    stdin.map((data) => utf8.decode(data)).listen((data) {
      print(data);
    });
  }
}
import 'dart:convert';
import 'dart:io';

void main() async {
  final process = await Process.start('cat', []);
  stdin.listen((data) => process.stdin.write(utf8.decode(data)));
  // process.stdout.listen((line1) => print("$line1\n${utf8.decode(line1)}"));
  process.stdout.listen((line1) => print(line1));
}

There are a bunch of additional methods built into Dart that can use to shape and modify your streams. Plus, when you're ready for even more advanced stuff, there's the Async package, maintained by the Dart team and available on Pub. It has classes that can merge two streams together, cache results and perform other types of stream-based wizardry.

how to create streams

Alright, there's one more advanced topic that deserves a mention here, and that's how to create streams of your own. Just like with futures, most of the time, you're going to be working with streams created for you by network libraries, file libraries, state management and so on. But you can make your own as well using StreamController. Let's go back to that number creator we'd been using so far (1). Here's the actual code for it (2). As you can see, it keeps a running count (3), and it uses a timer to increment that count each second (4). The interesting bit though is the stream controller (5). A StreamController creates a brand new stream from scratch and gives you access to both ends of it. There's the stream end (6) itself where data arrives. We've been using that one throughout this video, and there's the sink (7), which is where new data gets added to the stream. NumberCreator here uses both of them. When the timer goes off, it adds the latest count to the controller's sink, and then it exposes the controller's stream with a public property so other objects can subscribe to it.

  1. 1755085323.png
  2. 1755085346.png
  3. 1755085387.png
  4. 1755085420.png
  5. 1755085449.png
  6. 1755085500.png
  7. 1755085527.png

When we think about stream creation, we should remember this:

We don't create the stream, we create the controller with StreamController(). The stream is just comes as part of the package, the other being sink. We access these with controller.stream and controller.sink respectively.

We've already covered how to work with streams. But with wrt sink, the most important method is sink.add(), which allows us to add elements to the stream which appear as events in the event loop.

class NumberCreator {
    NumberCreator() {
        Timer.periodic(Duration(seconds:1), (t) { 
            _controller.sink.add(_count);
            _count++;
        });
    }

    var _count = 1;

    final _controller = StreamController<int>();

    Stream<int> get stream => _controller.stream;
}

creating widgets from streams

Now that we've covered creating, manipulating, and listening to streams, let's talk about how to put them to work building widgets in Flutter. If you saw the previous video on futures, you may remember FutureBuilder (1). You give it a future and a builder method, and it builds widgets based on the state of the future. For streams, there's a similar widget called StreamBuilder (2). Give it a stream, like the one from NumberCreator and a builder method (3), and it will rebuild its children whenever a new value is emitted by the stream. The snapshot parameter (4) is an async snapshot just like with FutureBuilder. You can check its connectionState property to see if the stream hasn't yet sent any data, or if it's completely finished (5). And you can use the hasError property (6) to see if the latest value is an error and handle data values as well (7). The main thing is just to make sure your builder knows how to handle all the possible states of the stream. Once you've got that, it can react to whatever the stream does. Okay, that's all we've got for this video, but there are more coming in the series. Next up, we'll be talking about Async and Await. They're two key words Dart offers to help you keep your asynchronous code tight and easy to read. So be on the lookout for that, and head to Dart.dev and Flutter.dev for more info on Dart and Flutter. ♪ (music) ♪ Hey, if you enjoyed that video, try these others or subscribe to the Flutter channel, it's Google's new portable UI toolkit. There's a button around here somewhere. ♪ (music) ♪

  1. 1755085587.png
  2. 1755085618.png
  3. 1755086069.png
  4. 1755085644.png
  5. 1755085914.png
  6. 1755086148.png
  7. 1755086184.png

The Future delivers a single value asynchronously. Asynchronously here means it is not proessed by the main thread which is the event loop. All asynchronous operation end up as another thread distinct from the event loop thread.

Just some commands

Finding process ID of main flutter thread
ps aux | grep "flutter" | grep -v 'grep' | grep -v 'lsp' | grep -F -- '-d linux'

We should look at 2 aspects of a stream:

  1. what does it enter into the event loop
  2. how does it handle it
1
2
3
4
5
6
7
import 'dart:io';

void main() {
  stdin.listen((input) {
    print(input);
  });
}

the stdin stream provides a list of integers, or List. With the handler (passed to listen), you can configure how to handle it.

async/await

source: https://www.youtube.com/watch?v=SmTCmDMi4BY

Hey everybody, and welcome to the fourth video in our Flutter in Focus series on asynchronous coding in Dart. In this episode, I'm going to show you how to use Dart's async and await keywords to simplify your code. A lot of languages have async and await in their syntax, and the first time I saw them I remember being weirded out. I knew you tagged a function as async, the return type changed, and somewhere in the middle there was a break where it would wait. But at the time it just seemed magical and weird. I have some good news for you all watching this, though. If you've seen the first few videos in this series, you already know the things you need to fully understand async and await. And that's because, at the end of the day, they're really just an alternate syntax for using futures and streams that can help you write cleaner, more readable code. Let's start with a simple example.

example

Say I have a class that represents some processed data (1). I can give it a string and it'll do some business logic that I need done. I also have a method that'll load an id value from disk and another that'll fetch some network data that I can use with my class. (2) Network and File I/O are asynchronous operations, so they return futures. I'd like to write a single method that will put these pieces together. First, it should load an id from disk, then use that id to make a network call, then make a ProcessedData object with the result (3). With Dart's futures API, I can use then to chain callbacks together so that the completed value of the first future becomes the parameter for the next callback, and so on. This technique was covered earlier in the series, and it works great. The code isn't as readable as it would be if this were all synchronous, though. If it weren't for the futures, that code could be written like this (4), right? Make some calls, in order, no big whoop. Well, the big deal about async and await is that you can have code that looks like this, and uses futures.

  1. 1755068762.png
  2. 1755068866.png
  3. 1755068937.png
  4. 1755068985.png

AsyncAwait

First, add the async keyword, just before the opening brace (1). This is just a way of telling Dart, "Hey! I plan to use the await keyword in here." Speaking of which, next up is placing the await keyword in front of each future the function needs to wait for. It can't call fetchNetworkData without the id from loadFromDisk, so there needs to be an await there. And it can't create a ProcessedData object without the data from the network. So, there needs to be an await there (2). The last change is to make the return type a future (3). That might look weird at first, because the return statement on this function just uses a regular ProcessedData object. But before createData here completes, it has to wait on two futures. That means it's going to start executing, then stop, and wait for a disk event, then keep going, then stop and wait for a network event. And only after that can it provide a value. So when createData starts running and hits that first await, right then it returns a future to the calling function. It says, "Hey! Looks like I'm going to have to wait on some stuff. Here's this empty box, you hold onto that, and when I'm done waiting for this disk in the network, I'll call the return statement and put some data in there for you. Go put it in a FutureBuilder or something." And that's how this works.

  1. 1755072332.png
  2. 1755072398.png
  3. 1755072418.png

event loop

Before moving on, let's take a quick look back at the event loop, and how it works with both versions of the code you just saw (1). We started with this one which uses the futures API. And one of the nice things about the futures API is that you can easily see how the code is broken down for the events involved (2). First, the function starts running and calls in to loadFromDisk (3). Then it waits (4). It waits for some data from the disk to arrive at the event loop (5). That completes the future returned by loadFromDisk. So, the callback from the first event statement is invoked and a network request goes out (6). Then createData waits again (7). It waits for some network data to arrive at the event loop. That completes the future returned by fetchNetworkData, so the second callback is invoked (8), some process data is created, and now the future that was returned by this createData is completed with that value (9).

  1. 1755072504.png
  2. 1755072538.png
  3. 1755072563.png
  4. 1755072611.png
  5. 1755072802.png
  6. 1755072753.png
  7. 1755072949.png
  8. 1755073004.png
  9. 1755073042.png

async

Now, let's do the same thing again, using the async/await version of the code (1). Spoiler Alert! It's the exact same process. Before, we could use the calls to then to imagine how the code is broken down, event by event (2). Here, you can do the same thing by breaking the code after each await expression. So we get this nice line-by-line progression. Let's run it. createData starts executing and hits that first await (3). At that point, it returns its own future to the calling function, and invokes loadFromDisk. Then, just like before, it waits for that File I/O event from the disk (4). That completes the future returned by loadFromDisk, which means createData is done awaiting on it and can go on to the rest of the code (5). Next, it calls fetchNetworkData, and waits again (6). Eventually, the network data arrives at the event loop, that completes the future return by fetchNetworkData, and so createData is free to move on again. It creates and returns an instance of ProcessedData (7), which completes the future (8) that createData gave to its caller way back at the beginning.

  1. 1755073077.png
  2. 1755073122.png
  3. 1755073157.png
  4. 1755073190.png
  5. 1755073219.png
  6. 1755073243.png
  7. 1755073274.png
  8. 1755073304.png

error handling

As you can see, in both cases, the same event loop controls the action, and the same futures are involved. The only real change is that with async/await the function is smaller and looks more like synchronous code. Hopefully, at this point, some of you are thinking, "Hey, I watched a couple of the other videos in this series and you said futures could complete either with data or an error. What's up with async/await and errors?" The answer is that async and await also help make your error handling look more like what it would be with synchronous code. If we go back to that first example based on the futures API (1), error handling code might look like this (2). It uses catchError to test and respond to errors, and, when complete, to execute a callback at the very end, whether there's an error or not. With async and await (3), on the other hand, rather than using additional callbacks, you can use try-catch (4). Inside a function tagged with the async keyword, try-catch blocks will handle asynchronous errors the same way they handle synchronous ones in normal functions. You can use the on and catch keywords to trap specific types of exceptions and finally will execute its code block at the end as you would expect.

  1. 1755073372.png
  2. 1755073399.png
  3. 1755073430.png
  4. 1755073457.png

stream processing

Okay, there's one last thing to cover, and that's how to use await with a for loop to process data from a stream. This is a much less common use case for the await keyword, but it is something you can do. Say I have a function that can take in a list of numbers and add them all up (1). That's pretty straightforward, right? I can just use a for loop to iterate over the values. What if I wanted this function to take a stream of numbers instead (2), add them up asynchronously as they arrive, and then, when the stream is finished, return that sum? Just like with futures, async/await helps me make that happen without changing the basic structure of the code. First, I tag the function as async, then I change the return type to a future, and then I add the await keyword in front of for, and I'm done! (3) Just like with futures, the await keyword is separating my function into the parts that execute before and after waiting on events. First, it starts executing and gets all the way to that await (4). Then it returns its future to the calling function and waits for a piece of data to arrive. When it does, the loop executes once to process that piece of data and then stops and waits for the next one (5). Maybe the app runs off and does some other things, garbage collects, whatever. Eventually, though, another piece of data arrives and the loop goes around again. This keeps happening until the stream is finished and closes. When that happens, the function exits the loop and executes its return statement (6). That completes the future that getTotal here gave to its caller way back at the beginning (7). One important thing to keep in mind when using await for is that you should only use it with streams you know are going to complete. If you try to use this with a stream of clicks coming from an HTML button, for example, that stream lasts as long as the button's around, which means your loop could just keep right on waiting. Alright. That's all we have for this video, but there's one more left in the series, and we'll be talking about generator functions. These are functions that can return multiple values over time, creating a stream of data on the fly. So, be on the lookout for that, and head to dart.dev and flutter.dev for more info on Dart and Flutter. ♪ (music) ♪ Hey! If you enjoyed that video, try these others. Or, subscribe to the Flutter channel. It's Google's new portable UI toolkit. There's a button around here somewhere. ♪ (music) ♪

  1. 1755073519.png
  2. 1755073559.png
  3. 1755075569.png
  4. 1755075614.png
  5. 1755075649.png
  6. 1755075690.png
  7. 1755075718.png

Note the pattern here. main is not an async function. What it does is wire up the event loop and run print statement. Note that while the event loop is being wired up here in this synchronous function, the thread isn't free to execute the loop itself.

import 'dart:io';

void main() {
  Future.value().then((value) {
    stdin.readLineSync();
  });
  Future.delayed(Duration(seconds: 10), () {
    print("OK. Now it beings. Wait 3 seoncs");

    Future.delayed(Duration(seconds: 3), () => print("This gets done"));
  });

  print("main function done");
}

generator functions

source: https://www.youtube.com/watch?v=TF-TBsgIErY

Introduction

Andrew Brogdon: Hey, everybody, and welcome to the final episode in our Flutter in Focus series on asynchronous coding in Dart. In this episode, we're going to cover generators, which are functions that can produce not one, but multiple values. They can do that either synchronously or asynchronously.

Synchronous vs Async

You may remember this chart (1) from one of the earlier videos. A synchronous generator will have a return type of Iterable, while an asynchronous one has a return type of Stream. As you'll see in a second, the code for both looks mostly the same. The big difference is that synchronous generators are expected to produce values on demand, right away, so they can't wait on Futures or Streams. Async generators, on the other hand, are allowed to take their sweet, sweet time producing values, so they can use the await keyword. One other thing to note before going on, is that while generators are really handy when you need them, you're probably not going to need them very often. In the past year of coding with Flutter, I've used an async generator just once. That said, when you do see that right spot and think, "This is it, this is the place where a generator can save me a bunch of boilerplate, and I know how to do it because I am a Dart rockstar"-- it's very satisfying. So, let's start with synchronous generators, but first, a quick review of Iterables and Iterators.

  1. 1756554581.png

Iterables and Iterators

An Iterator (1) is a thing that lets you iterate, one at a time, over a series of values. It's a very simple interface. There's a current property, which returns the current value, the most recent one you've iterated to, and the moveNext method, which tells the Iterator, you're done with the most recent value, so forget it and load the next one into current. Iterable (2) is also a simple interface. It just means a class that can give you an Iterator of a particular type. In this case, since MyStrings extends Iterable String, it needs to have an iterator property that returns an Iterator String. One of the cool things about Iterables is that you can stick them in a for/in loop. If I have one of these MyStrings objects (3), for instance, I can use for/in (4) to loop over each of its values, one after the other. Dart will automatically call moveNext and current for me, so I can just write a simple loop. Some of the handy methods you're used to with Streams, like where and map, can also be used with Iterators (5). They just return a new Iterator, which you can also loop over using for/in.

  1. 1756554771.png
  2. 1756554859.png
  3. 1756554916.png
  4. 1756554942.png
  5. 1757313181.png

Yield and Sync Generators

So, how do you create a generator that returns one of these Iterables? Well, first, declare a function, and make sure it has a return type of Iterable. Here I'll define one that's going to return a range of numbers from start to finish (1). Next, use the sync* keyword to mark the function as a synchronous generator (2). This is just a way to tell Dart that the function is going to produce multiple values on demand. Once that's done, I just need to use the yield keyword to yield-- each of the values in order (3). yield is kind of like return, but it doesn't end the function. Instead, it provides a single value and waits for the caller to request the next one. Then, it picks up executing again. In this case, doing another round of the loop. When it hits yield, it returns that value and waits again. Interestingly, this function doesn't really begin executing at all, until someone starts iterating over the Iterable that it returns. It's providing values synchronously, on demand, so the first time someone tries to get one of those values, that's when it kicks on and runs until it hits yield. And since that return value is an Iterable, you can do all the usual stuff, like looping over the values. This code will print out the numbers from 1 to 10, for example. You can also use the normal methods Iterable has, like where, if you decide you only want the even numbers, or forEach, if you decide you can't be bothered with an actual loop. One last thing before we move on to async generators. The yield keyword has a variant for yielding other Iterables. If I had chosen to make getRange recursive, for example-- and I'll give you a second to read the code, because that's a big change-- I have to loop over the Iterable returned by the inner call to getRange just so I can yield values one at a time. In the first call, that loop executes nine times. In the next one, eight. Then seven, six. And you can see how my simple function just became quadratic. Fortunately, there's yield, which you can use to yield a whole Iterable, one value at a time-- no loop required. All right, so that's yield, yield*, and synchronous generators.

  1. 1757395560.png
  2. 1757395604.png
  3. 1757395650.png

Async Generators

Next up are async generators, which work almost the same, only they return Streams and they can yield values when they decide they're ready. Let's say, hypothetically, we lived in a world where all math had to be done on a server, and so we had a function like this that would make a network call to double a number for us. Horrifying as that may be, if you saw the other episodes in the series, you know how to call that method once, and use then, to take action when the Future completes. But what if I want to get a bunch of those values, one after another, in a Stream? That's where I can use an async generator function. First, I declare a function that returns a Stream of the right type. Then, I add the async* keyword to mark the function as an async generator. Then, I use yield to yield the values as I receive them. Notice that the function is calling await to wait on the Future from fetchDouble. If this were a synchronous generator, that wouldn't compile. Now I've got a method that produces the doubles of a range of numbers, one at a time, waiting on fetchDouble for each one. And just like any Stream, I can use where, map, forEach, listen-- whatever I want-- in order to take advantage of those values. Also, if I wanted to rewrite my fetchDoubles function to be recursive, I can still use yield* with a Stream to keep my code tight and efficient. All right. That's all we have for this video, and all we have for this Flutter in Focus series. Hopefully, it's demystified some of the core concepts of async coding for you, because once you get used to handling Futures and Streams, designing our apps to react to asynchronous data, you will wonder how you ever got by without it. In the meantime, head to dart.dev and flutter.dev for more info on Dart and Flutter, and we'll see you next time. ♪ (music) ♪ Hey, if you enjoyed that video, try these others or subscribe to the Flutter channel. It's Google's new portable UI tool kit. There's a button around here somewhere. ♪ (music) ♪

First we note that generators are functions. A synchronous generator is a function that returns an iterable while an asynchronous generator is a function that returns a stream. Generators are contrasted to functions that return a single value, be it a Future or an int.

You can iterate over an iterable (2) because an iterable possesses an iterator (1).

  1.     abstract class Iterator<E> {
            bool moveNext();
            E get current;
        }
    
  2. abstract class Iterable<E> {
       Iterator<E> get iterator;
    }
    

In python i remember we had a range() function. The dart getRange example here seems to be similar. Here, we actually implement the range using a synchronous generator.

Iterable<int> getRange(int start, int finish) sync* {

}

We know that a list of strings is an iterable, and we can move through the different elements of that list with:

void main() {
  for (var color in [
    "orange white",
    "orange",
    "green-white",
    "blue",
    "blue-white",
    "green",
    "brown-white",
    "brown",
  ]) {
    print(color);
  }
}

The example presented in the videos shows us how we can create a iterable. What we need is to create a class that extends Iterable and implement a iterator getter.

1
2
3
4
5
6
7
8
 class MyStrings extends Iterable<String> {
    MyStrings(this.strings);

    final List<String> strings;

    @override
    Iterator<String> get iterator => strings.iterator;
}

You could get the lsp to write the getter for you with code actions (in neovim). But its worth paying attention to the syntax.

First, the diagnostic error message when we don't implement the getter:

Missing concrete implementation of 'getter abstract mixin class Iterable<E>.iterator'.
Try implementing the missing method, or make the class abstract. [non_abstract_class_inherits_abstract_member]

First, let's talk about getters. Getters make it look like you are accessing a field. Dart is very particular about fields, by default they are non-nullable and has to be initialized right in the declaration, or in the constructor body using this or before the constructor body gets executed with initializer list.

But if its a getter, its a "field" that computed on the fly. Note the syntax of creating a getter:

double get area => length * breadth;

...where length and breadth are some fields of the class.

This is something worth noting, how Iterables can be defined through a class and through a function:

Creating an iterable by extending class
1
2
3
4
5
6
7
8
 class MyStrings extends Iterable<String> {
    MyStrings(this.strings);

    final List<String> strings;

    @override
    Iterator<String> get iterator => strings.iterator;
}
Returning an iterable?
Iterable<int> getRange(int start, int stop) sync* {
  for (int i = start; i <= stop; i++) {
    yield i;
  }
}

void main() {
  for (int value in getRange(0, 10)) {
    print("The value is $value");
  }
}
1
2
3
4
5
6
7
8
Iterable<int> getRange(int start, int stop) sync* {
    if (start <= stop) {
        yield start;
        for (final val in getRange(start + 1, stop)) {
            yield val;
        }
    }
}

The first thing to note here how we are calling getRange within getRange. (1)

  1. 1757652208.png

Let's move step by step. getRange returns an Iterable<int>. I think of the yield statement as how it computes the individual elements of that Iterable. You can see that the first element is computed: yield start, the second one is the result of this computation:

 for (final val in getRange(start + 1, stop)) {
     yield val;
 }
 ```

 The 2 vals in getRange(start + 1, stop) are going to be start + 1 and another:
for (final val in getRange(start + 1, stop)) { yield val; }
The key is noting that in order to compute the next item of the iterable<int>, we
have to iterate over a range that keeps decreasing. At some point it might look Iterable<int>
might look like:
1, 2, 3, for (i in range(4,5)) yield i;, (...) ```

WHAT THE HELL IS GOING ON?? 😭

Iterable<int> getRange(int start, int stop) sync* {
  if (start <= stop) {
    yield start;
    for (final val in getRange(start + 1, stop)) {
      yield val;
      print('iteration inside for');
    }
  }
}

Iterable<int> getRangeNormal(int start, int stop) sync* {
  while (stop >= start) {
    yield start;
    start++;
  }
}

void main() {
  for (var i in getRange(1, 10)) {
    print(i);
  }
}
dart run recursive_test.dart
1
2
iteration inside for
3
iteration inside for
iteration inside for
4
iteration inside for
iteration inside for
iteration inside for
5
iteration inside for
iteration inside for
iteration inside for
iteration inside for
6
iteration inside for
iteration inside for
iteration inside for
iteration inside for
iteration inside for
7
iteration inside for
iteration inside for
iteration inside for
iteration inside for
iteration inside for
iteration inside for
8
iteration inside for
iteration inside for
iteration inside for
iteration inside for
iteration inside for
iteration inside for
iteration inside for
9
iteration inside for
iteration inside for
iteration inside for
iteration inside for
iteration inside for
iteration inside for
iteration inside for
iteration inside for
10
iteration inside for
iteration inside for
iteration inside for
iteration inside for
iteration inside for
iteration inside for
iteration inside for
iteration inside for
iteration inside for

We can begin to make sense of it using this example instead:

main.dart
Iterable<int> getRange(int start, int stop) sync* {
  if (start <= stop) {
    yield start;
    for (final val in getRange(start + 1, stop)) {
      yield val * 10;
    }
  }
}

void main() {
  for (var i in getRange(1, 5)) {
    print(i);
  }
}
shell output
1
2
3
4
5
6
dart run recursive_test.dart
1
20
300
4000
50000

We know that generators only produce values on demand, i.e it stops after the first yield it encounters and only computes whatever follows the yield when the next element is accessed: and executes until finding the next yield.

Here we start with getRange(1, 5).

It returns 1 (from the first yield), and when the next element is requested, we reach this:

for (final val in getRange(2, stop)) {
  yield val * 10;
}

The result of this is:

10 * (2, getRange(3, 5))

This pattern keeps repeating:

10 * (2, 10*(3, getRange(4,5)))
10 * (2, 10*(3, 10*(4, getRange(5,5))))
10 * (2, 10*(3, 10*(4, 10*(5)))

So the combined final result:

1, 20, 300, 4000, 50,000

This multiplier clearly reveals some compounding effect. The foremost yield is just one yield. But the next one that follows is yield twice, the one that follows that thrice and so on.

Now let's examine the old print problem again:

Iterable<int> getRange(int start, int stop) sync* {
  if (start <= stop) {
    yield start;
    for (final val in getRange(start + 1, stop)) {
      yield val;
      print("loop $val");
    }
  }
}

void main() {
  for (var i in getRange(1, 5)) {
    print(i);
  }
}

The key to solving this, for me, was to remember until the recursion would keep stacking until function finally returns/yields a value.

We start with getRange(1, 5). We note that it involves computing getRange(2,5), which in turn involves computing getRange(3,5) and so on. We write them all down until reaching the step that actually just yields a value which is getRange(5,5). From them we back trace.

getRange(1,5)
yield 1

for (final val in getRange(2, 5)) {
  yield val;
  print("loop $val");
}

getRange(2,5):
yield 2
for (final val in getRange(3, 5)) {
  yield val;
  print("loop $val");
}

getRange(3,5):
yield 3
for (final val in getRange(4, 5)) {
  yield val;
  print("loop $val");
}

getRange(4,5):
yield 4
for (final val in getRange(5, 5)) {
  yield val;
  print("loop $val");
}

getRange(5,5):
yield 5 // (1)
  1.     getRange(5,5):
        yield 5
        for (final val in getRange(6, 5)) {
          yield val;
          print("loop $val");
        }
    
        getRange(6,5): <nothing>
    
getRange(1,5)
yield 1

for (final val in getRange(2, 5)) {
  yield val;
  print("loop $val");
}

getRange(2,5):
yield 2
for (final val in getRange(3, 5)) {
  yield val;
  print("loop $val");
}

getRange(3,5):
yield 3
for (final val in getRange(4, 5)) {
  yield val;
  print("loop $val");
}

getRange(4,5):
yield 4
for (final val in <5>) {
  yield val;
  print("loop $val");
}
getRange(1,5)
yield 1

for (final val in getRange(2, 5)) {
  yield val;
  print("loop $val");
}

getRange(2,5):
yield 2
for (final val in getRange(3, 5)) {
  yield val;
  print("loop $val");
}

getRange(3,5):
yield 3
for (final val in <4, 5, print(loop 5)>) {
  yield val;
  print("loop $val");
}

getRange(4,5):
yield 4
for (final val in <5>) {
  yield val;
  print("loop $val");
}
getRange(1,5)
yield 1

for (final val in getRange(2, 5)) {
  yield val;
  print("loop $val");
}

getRange(2,5):
yield 2
for (final val in getRange(3, 5)) {
  yield val;
  print("loop $val");
}

getRange(3,5):
yield 3
for (final val in <4, 5, print(loop 5)>) {
  yield val;
  print("loop $val");
}
getRange(1,5)
yield 1

for (final val in getRange(2, 5)) {
  yield val;
  print("loop $val");
}

getRange(2,5):
yield 2
for (final val in <3, 4, print(loop 4), 5, print(loop 5), print(loop 5)>) {
  yield val;
  print("loop $val");
}

getRange(3,5):
yield 3
for (final val in <4, 5, print(loop 5)>) {
  yield val;
  print("loop $val");
}
getRange(1,5)
yield 1

for (final val in <2, 3, print(loop 3), 4, print(loop 4), print(loop 4), 5, print(loop 5), print(loop 5), print(loop 5)>) {
  yield val;
  print("loop $val");
}

getRange(2,5):
yield 2
for (final val in <3, 4, print(loop 4), 5, print(loop 5), print(loop 5)>) {
  yield val;
  print("loop $val");
}
getRange(1,5)
yield 1

for (final val in <2, 3, print(loop 3), 4, print(loop 4), print(loop 4), 5, print(loop 5), print(loop 5), print(loop 5)>) {
  yield val;
  print("loop $val");
}
getRange(1,5)
<1, 2, print(loop 2), 3, print(loop 3) print(loop 3), print(loop 4), 4, print(loop 4), print(loop 4), print(loop 4), 5, print(loop 5), print(loop 5), print(loop 5), print(loop 5)>

Comments