本页尚未翻译,因此以英文显示。 参与翻译

fx

Wraps a sequence in a lazy, chainable pipeline — the typed heart of FxDart.

Fx<T> fx<T>(Iterable<T> iterable) FxAsync<T> fxAsync<T>(FxAsyncIterable<T> iterable) FxAsync<T> fxStream<T>(Stream<T> stream) FxEvents<T> fxEvents<T>(Stream<T> stream) Fx<T> Iterable<T>.fx FxAsync<T> FxAsyncIterable<T>.fx FxAsync<T> Stream<T>.fx FxAsync<T> Iterable<FutureOr<T>>.fxAsync FxEvents<T> Stream<T>.fxEvents

Lecture

Everything in this course builds toward one idea: a chain. fx(iterable) wraps any Iterable<T> in an Fx<T> — an object with FxTS-style methods like .map(), .filter(), and .take() hung off it. Every one of those calls returns a new Fx wrapping a bit more lazy computation. None of it runs yet. Fx only starts doing work when you call a terminal operatortoList(), each(), consume(), reduce(), and friends — which pulls values through the whole chain, one at a time, from the terminal all the way back to the source.

This laziness is why FxDart can safely chain over huge or infinite sequences (range, cycle, repeat): as long as something downstream — usually take(n) — decides how many values to actually pull, the upstream steps only ever run that many times.

fx is the sync half of the chain. Its async counterparts are fxAsync, which wraps an FxAsyncIterable (the thing you get from toAsync, fromStream, or any *Async function), and fxStream, a shortcut that wraps a Dart Stream directly. Both return an FxAsync<T> chain whose methods accept functions that may return a Future, and whose terminal operators all return a Future you await. Switch from sync to async mid-chain with .toAsync().

Why does this exist at all, instead of just calling top-level functions like map(f, iterable)? Because Dart cannot type a variadic pipe the way FxTS's TypeScript can (see the next lesson) — fx() chaining is how FxDart gets fully typed, autocompletable pipelines instead.

0.8.0 breaking change: Fx<T> is now an extension type that erases to the wrapped Iterable<T> at runtime. All documented APIs stay identical — chains work exactly as before. What breaks: x is Fx<T> checks (the type doesn't exist at runtime), and code that tried to extend or implement Fx directly (use the top-level functions instead). If you're using fx() the normal way, your code needs no changes.

Demo 1 · Nothing runs until the terminal op

Watch calls stay at 0 right after building the chain, then jump once toList() actually pulls the 5 values:

Demo 2 · fxAsync and fxStream

fxAsync wraps an FxAsyncIterable (here, from toAsync); fxStream wraps a Stream directly. Both give you the same chain methods, async-flavored:

The getter spelling

Every entry point also exists as a getter: .fx on an Iterable, an FxAsyncIterable or a Stream, .fxAsync on an iterable of futures, and .fxEvents on a Stream. They build exactly the same chain — the difference is only which end of the expression you read from:

// the function: you go back to the front to open the paren
fx(orders.where(isPaid)).groupBy((o) => o.customerId);

// the getter: left to right, the way .toList() reads
orders.where(isPaid).fx.groupBy((o) => o.customerId);

This is the Dart-idiomatic spelling, and it is free: Fx is an extension type, so the wrapper erases to the iterable itself, and a getter whose body is this is a static call the compiler deletes. Over a million-element map + filter + sum, the two forms measure 12.640 ms and 12.665 ms — the same number twice.

These pages use fx() throughout. It is the name FxTS uses, and it is the one that takes an explicit type argument, which a getter cannot do postfix — fx<num>(xs) works where xs.fx<num> does not parse. Pick whichever reads better in your own code; they compile to the same thing.

One asymmetry is worth knowing. Over an Iterable<Future<T>>, .fx gives you an Fx<Future<T>> — a chain over the futures rather than their values, which compiles and quietly does the wrong thing. That is what .fxAsync is for: it awaits them, so T is the resolved type and concurrent(n) has something to work with.

await responses.fxAsync.map(parse).concurrent(4).toList();

A Stream carries both getters, because it is the one source that belongs to both worlds. .fx gives the pull chain, the same as fxStream; .fxEvents gives the push chain, the same as fxEvents — debouncing, throttling, switching. Cross from push back to pull with .pull(). The two are compared side by side in Stream bridges.

keystrokes.fxEvents
    .debounce(const Duration(milliseconds: 160))
    .switchMap((q) => search(q).asStream())
    .pull()
    .toList();

Every getter spelling

The convention is one rule: an entry point carries fx in its name. It says which library you are stepping into, and it keeps the bare name — toAsync, shuffle, debounce — free for whatever else a project puts on that type.

ReceiverGetterSame as
Iterable<T>.fxfx(xs)
FxAsyncIterable<T>.fxfxAsync(it)
Stream<T>.fxfxStream(s)
Iterable<FutureOr<T>>.fxAsynctoAsync(xs)
Stream<T>.fxEventsfxEvents(s)
Stream<T>.fxLiveLiveValue.from(s)
Stream<T>.fxLiveSeededLiveValue.seededFrom(v, s)
Iterable<T>.fxShuffleshuffle(xs)
FxAsyncIterable<T>.fxShuffleshuffleAsync(it)
void Function(T).fxDebouncedebounce(f, w)
void Function(T).fxThrottlethrottle(f, w)

The operators themselves are not on this list, and will not be. Fifteen of them — map, where, take, fold and friends — share a name with a member Iterable already has, and an instance member always beats an extension, so those calls could never reach fxdart. The chain is where the operators live: xs.fx.map(f), not xs.map(f).

Try it yourself

Exercise: build a chain that keeps scores of 60 or above, doubles them as bonus points, and takes only the first 2 results.

Related: pipe — the dynamically-typed alternative · toList — the most common terminal op · each — terminal op for side effects · consume — terminal op that discards results