async coding with dart
source: https://www.youtube.com/playlist?list=PLjxrf2q8roU2HdJQDjJzOeO6J3FoFLWr2
- isolates and event loops: https://www.youtube.com/watch?v=vl_AaCgudcY
- futures: https://www.youtube.com/watch?v=OTS-ap9_aXc
- streams: https://www.youtube.com/watch?v=nQBpOIHE4eE
- async/await
- 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.
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.
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.
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.
You can input a value to the event loop using Future:
- here 5 was the input to the event lop
You can wire up the event loop to respond to the input using then
:
You can delay this input by using Future.delayed
, this constructor
expects duration
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?
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.
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.
- https://dart.dev/language/error-handling
then
: what handles when the future completes with datacatchError
: what handles when the future completes with error- 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.
This is a simple screen that displays the result (stdout) of a particular command.
Note
The Future instance can be in 3 states:
- data
- error
- loading
We can tell if Future is in one of the state in the builder passed to FutureBuilder
using snapshot
argument:
- snapshot.hasData : true if data was yielded
- snapshot.hasError: true if error was yielded
- the else when both of the above is fale.
Compare this with how Future is used in pure dart:
- 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.
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.
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)
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.
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).
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.
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).
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).
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>
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.
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.
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) ♪
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
We should look at 2 aspects of a stream:
- what does it enter into the event loop
- how does it handle it
the stdin stream provides a list of integers, or List
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.
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.
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).
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.
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.
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) ♪
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.
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.
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.
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.
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
You can iterate over an iterable (2) because an iterable possesses an iterator (1).
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.
We know that a list of strings is an iterable, and we can move through the different elements of that list with:
The example presented in the videos shows us how we can create a iterable.
What we need is to create a class that extends Iterableiterator
getter.
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:
...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:
The first thing to note here how we are calling getRange within getRange. (1)
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:
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:
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:
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:
The result of this is:
This pattern keeps repeating:
So the combined final result:
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:
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)
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 <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");
}