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> <return: #AsyncMapFuture> | 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> <return: #BrLabel> | stream | stream := AsyncImageBehaviorsStream new. ^ BrLabel new aptitude: BrGlamorousLabelAptitude; withAsyncSinkDo: [ :anElementSink | anElementSink sink: AsyncCounterSink new; whenUpdate: [ :aLabel :aCounterSink | aLabel text: aCounterSink count ]; forwardStream: stream "Specify a receiving Sink for the items from the 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> <return: #BrLabel> | 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> <return: #BrLabel> | 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>
<return: #BrVerticalPane>
| 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> <return: #BrFromFuture> ^ 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> <return: #BrFromFuture> ^ 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 ] ]