Asynchronous widgets & futures
Responsive and non-blocking user interface is a key to a good user experience. It is therefore a responsibility of a UI developer to ensure that most if not all actions do not block the UI thread. That can be acomplished by asynchronously performing time consuming operations in a different worker thread and then refreshing or updating the UI once the data is available. While waiting for the background task to finish, the UI should inform a user that there is a pending operation and the UI is not yet in its final state.
Futures
A future represents an asynchronous computation. Any object that implements TAsyncFuture
trait can be used as a future. For example, a BlockClosure
can be transformed into a future by sending BlockClosure>>#asAsyncFuture
and later be composed as part of a more complex future using future combinators api. One such future combinator is AsyncMapFuture
:
mapFuture <gtExample> | future | future := self simpleFuture asAsyncFuture. future := future map: [ :x | x * 2 ]. ^ future
More information about asynchronous programming can be found in Futures, Promises & Streams.
Sinks & Streams
TAsyncSink
is a receiver of a TAsyncStream
.
Counting items
The following example shows how to create a label that receives a stream and counts live how many items it received:
countSink <gtExample> | stream | stream := AsyncImageBehaviorsStream new. ^ BrLabel new aptitude: BrGlamorousLabelAptitude; withAsyncSinkDo: [ :anElementSink | anElementSink "Specify a receiving Sink for the items from the stream" sink: AsyncCounterSink new; whenUpdate: [ :aLabel :aCounterSink | aLabel text: aCounterSink count ]; forwardStream: stream ]
Showing each received item
Just like Stream combinators, there are different types of Sinks.
For example, AsyncPeekSink
only records the last received item, here is an example:
peekSink <gtExample> | stream | stream := AsyncImageBehaviorsStream new. ^ BrLabel new aptitude: BrGlamorousLabelAptitude; withAsyncSinkDo: [ :anElementSink | anElementSink sink: AsyncPeekSink new; whenUpdate: [ :aLabel :aPeakSink | aPeakSink peek ifSome: [ :aBehavior | aLabel text: aBehavior name ] ]; forwardStream: stream ]
Folding the items
Sinks allow developers to fold received items as they are received. The following example sums up the items from the stream and updates the result live:
foldSink <gtExample> | stream | stream := AsyncImageBehaviorsStream new collect: [ :each | each linesOfCode ]. ^ BrLabel new aptitude: BrGlamorousLabelAptitude; withAsyncSinkDo: [ :anElementSink | anElementSink sink: (AsyncFoldSink inject: 0 into: [ :sum :each | sum + each ]); whenUpdate: [ :aLabel :aSink | aLabel text: aSink value ]; forwardStream: stream ]
Redirecting items to multiple sinks
Compared to streams, sinks can fanout items to multiple receivers. Here is how we can take one stream and send the items to multiple sinks processing them differently:
fanoutSink <gtExample> | stream labelA labelB | stream := AsyncImageBehaviorsStream new collect: [ :each | each linesOfCode ]. labelA := BrLabel new aptitude: BrGlamorousLabelAptitude; withAsyncSinkDo: [ :anElementSink | anElementSink sink: (AsyncFoldSink inject: 0 into: [ :sum :each | sum + each ]); whenUpdate: [ :aLabel :aSink | aLabel text: aSink value ] ]. labelB := BrLabel new aptitude: BrGlamorousLabelAptitude; withAsyncSinkDo: [ :anElementSink | anElementSink sink: (AsyncFoldSink inject: 0 into: [ :max :each | max max: each ]); whenUpdate: [ :aLabel :aSink | aLabel text: aSink value ] ]. ^ BrVerticalPane new fitContent; addChildren: { labelA . labelB }; withAsyncSinkDo: [ :anElementSink | anElementSink sink: (AsyncFanoutSink forSinkA: labelA asyncSink sinkB: labelB asyncSink); forwardStream: stream ]
Brick widgets from the Future
While TAsyncFuture
represents a foundation brick of an asynchronous programming, Brick
widgets are foundation of the user interface. To glue them together developers can use BrFromFuture
widget (read as brick from the future
) to display values coming from asynchronous computations.
The following examples shows how to wrap a BrLabel
into an element that gets data from a future and assigns the text of a label depending on the state:
fromFutureLabel <gtExample> ^ BrFromFuture new fitContent; future: [ 0.5 seconds wait . 'Alice' ] initialValue: 'placeholder'; stencil: [ BrLabel new aptitude: BrGlamorousLabelAptitude ]; dataBinder: [ :aLabel :aDataSnapshot | aDataSnapshot ifSuccess: [ :aName | aLabel text: aName ] ifError: [ :anError | aLabel text: (anError description asRopedText foreground: Color red) ] ifPending: [ :anInitialValue | aLabel text: anInitialValue ] ]
BrFromFuture
allows developers to gracefully handle
pending
state and even
errors
:
fromFutureLabelWithError <gtExample> ^ BrFromFuture new fitContent; future: [ 0.5 seconds wait . 1/0 ] initialValue: 'placeholder'; stencil: [ BrLabel new aptitude: BrGlamorousLabelAptitude ]; dataBinder: [ :aLabel :aDataSnapshot | aDataSnapshot ifSuccess: [ :aName | aLabel text: aName ] ifError: [ :anError | aLabel text: (anError description asRopedText foreground: Color red) ] ifPending: [ :anInitialValue | aLabel text: anInitialValue ] ]