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.
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.
TAsyncSink
is a receiver of a TAsyncStream
.
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 ]
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 ]
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 ]
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 ]
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 ] ]