Photo by Wilhelm Gunkel on Unsplash
Intro
In this article, we will describe how we can achieve a fancy type-writer effect:

Author: DEV Community
Although there are some packages available on pub.dev, like the one described on this article, the usual question is: “do we really need another package in our beloved pubspeck just for this..?” Let’s explore some features like:
- Generators
- StreamBuilders
and then we will see!
Generators
Generator functions in Dart are used to provide a series of values, sequentially, one after the other.
The values provided by a generator object are not “cooked” before-hand: in fact, these values are created lazily, on demand.
In Dart, you can create 2 types of generators:
- Synchronous generator: it blocks the execution until the sequence is provided.
- Asynchronous generator: provides the sequence concurrently, while performing other tasks at (almost) the same time.
Although they are implemented in a different way, both types have some features in common:
- they are marked with * before the body of the function, in order to state they are “generator” methods
- the keyword “yield” is used in order to provide the current value generated by the sequence
Synchronous generator
Synchronous generator functions are marked with sync* before their body. Returned data type in the function signature must be a Iterable<T>, where T is the generic data type for the object contained in the series. So we can have, for instance:
Iterable<String> generateNames() sync* {
...
}
Iterable<int> generateNums() sync* {
...
}
Nevertheless, in the function body, these methods do not explicitly return any value. Instead, they “provide” some element using the yield statement.
The following code show a synchronous generator that provides a set of numbers:
Iterable<int> generateNumsSync() sync* {
var i = 0;
while (i < 10) {
yield i++;//XXX: use yield to "push" the current value...
}
}
Asynchronous generator
On the other hand, asynchronous generator functions use the async* modifier and return a Stream<T> instead. So:
Stream<int> generateNumsAsync() async* {
var i = 0;
while (i < 10) {
//XXX: here we could wait for some time...
yield i++;
}
}
As shown on the previous snippet, asynchronous generators do not return a value explicitly either.
One more thing: if you ever want to create a recursive generator, then you can use the modifier yield*.
Now we’ve seen the structure of generators, let’s see how we can “hook” their results into the widget tree.
StreamBuilder
The StreamBuilder widget listens for events on a given stream and updates its child every time a new item is provided down the stream. So you can think about it as some sort of implementation of the observer pattern.
This widget builds its child using the builder pattern. Among the parameters received, it gets a “picture” of the current state of the stream through an AsyncSnapshot object:
StreamBuilder<int>(
stream: ...,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
}
);
This snapshot object contains the item emitted by the stream, but also other info, such as the state of the connection between the widget and the stream, any error that may have happened… Luckily for us, this blog is an bug-free environment 🙂 but when working on production apps, we should check all this additional info before accessing the stream.
The type-writer text
Now that we know how generators and streambuilders work, we can use both of them for our type-writer effect! So let’s put all the moving pieces together:
- the text we want to animate can be encapsulated on a model class, so one of its methods provide us with a stream of values:
class StreamableTextModel {
final String msg;
const StreamableTextModel({this.msg = ""}) : assert(msg != null);
...
Stream<String> toStream() async* {
for (var i = 0; i < msg.length; i++) {
yield msg.substring(0, i + 1);
}
}
}
- execution can be “paused” between emissions using Future.delayed()
- a streambuilder widget can be used to listen to the previous stream and update the UI accordingly
@override
Widget build(BuildContext context) {
return StreamBuilder<String>(
initialData: "",
stream: ...,
builder: (BuildContext cntxt, AsyncSnapshot<String> snap) {
if (snap.hasData) {
return Text(snap.data);
} else {
return Text("");
}
});
}
And that would be the result (you better skip the initial blank seconds…):
Recap
By combining generators and streambuilders, we can build dynamic widgets that are updated automatically when some data changes. This allow us to create “fake” animations like the one we described.
Write you next time! As usual, full source code is available at: