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 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.

Sinks & 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' .

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 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 ]
    
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 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' 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' 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 ] ]