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 << #TAsyncFuture tag: 'Base - Futures'; package: 'Futures' trait can be used as a future. For example, a BlockClosure Object << #BlockClosure layout: VariableLayout; slots: { #outerContext . #compiledBlock . #numArgs }; tag: 'Methods'; package: 'Kernel-CodeModel' can be transformed into a future by sending BlockClosure>>#asAsyncFuture asAsyncFuture ^ AsyncPollFuture pollBlock: self and later be composed as part of a more complex future using future combinators api. One such future combinator is AsyncMapFuture Object << #AsyncMapFuture traits: {TAsyncFuture}; slots: { #future . #mapBlock }; tag: 'Base - Futures'; package: 'Futures' :

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 Trait << #TAsyncSink slots: { #parentSink }; tag: 'Base - Sinks'; package: 'Futures' is a receiver of a TAsyncStream Trait << #TAsyncStream tag: 'Base - Streams'; package: 'Futures' .

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 Object << #AsyncPeekSink traits: {TAsyncSink}; slots: { #item }; tag: 'Base - Sinks'; package: 'Futures' 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 Trait << #TAsyncFuture tag: 'Base - Futures'; package: 'Futures' represents a foundation brick of an asynchronous programming, Brick widgets are foundation of the user interface. To glue them together developers can use BrFromFuture BlElement << #BrFromFuture traits: {TBrLayoutResizable}; slots: { #future . #futureExecution . #dataSnapshot . #elementStencil . #elementDataBinder . #element . #updater . #configuration . #futureCancellation }; tag: 'Future - Support'; package: 'Brick' widget (read as brick from the future) to display values coming from asynchronous computations.

The following examples shows how to wrap a BrLabel BlElement << #BrLabel traits: {TBrLayoutResizable + TBrLayoutAlignable + TBrSizeAdjustable}; slots: {}; tag: 'Label - UI'; package: 'Brick' 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 BlElement << #BrFromFuture traits: {TBrLayoutResizable}; slots: { #future . #futureExecution . #dataSnapshot . #elementStencil . #elementDataBinder . #element . #updater . #configuration . #futureCancellation }; tag: 'Future - Support'; package: 'Brick' 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 ] ]