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 named: #TAsyncFuture instanceVariableNames: '' package: 'Futures-Base - Futures' trait can be used as a future. For example, a BlockClosure Object variableSubclass: #BlockClosure instanceVariableNames: 'outerContext compiledBlock numArgs' classVariableNames: '' package: 'Kernel-Methods' 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 subclass: #AsyncMapFuture uses: TAsyncFuture instanceVariableNames: 'future mapBlock' classVariableNames: '' package: 'Futures-Base - Futures' :

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 Trait named: #TAsyncSink instanceVariableNames: 'parentSink' package: 'Futures-Base - Sinks' is a receiver of a TAsyncStream Trait named: #TAsyncStream instanceVariableNames: '' package: 'Futures-Base - Streams' .

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 Object subclass: #AsyncPeekSink uses: TAsyncSink instanceVariableNames: 'item' classVariableNames: '' package: 'Futures-Base - Sinks' 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 Trait named: #TAsyncFuture instanceVariableNames: '' package: 'Futures-Base - 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 subclass: #BrFromFuture uses: TBrLayoutResizable instanceVariableNames: 'future futureExecution dataSnapshot elementStencil elementDataBinder element updater configuration' classVariableNames: '' package: 'Brick-Future - Support' widget (read as brick from the future) to display values coming from asynchronous computations.

The following examples shows how to wrap a BrLabel BlElement subclass: #BrLabel uses: TBrLayoutResizable + TBrLayoutAlignable + TBrSizeAdjustable instanceVariableNames: '' classVariableNames: '' package: 'Brick-Label - UI' 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 BlElement subclass: #BrFromFuture uses: TBrLayoutResizable instanceVariableNames: 'future futureExecution dataSnapshot elementStencil elementDataBinder element updater configuration' classVariableNames: '' package: 'Brick-Future - Support' 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 ] ]