~gioverse/skel

Experimental application framework for https://gioui.org
stream: document rebuild-elements helpers
sqlitestream,mattn_sqlx_stream: test to ensure transactionality
stream/sqlitestream: only notify on write tx with lock held

refs

main
browse  log 

clone

read-only
https://git.sr.ht/~gioverse/skel
read/write
git@git.sr.ht:~gioverse/skel

You can also use your local clone with git send-email.

#skel

Go Reference

Skel is an experimental application framework for Gio developed on behalf of Plato Team and generously open-sourced for the benefit of the ecosystem.

It has been through a number of revisions as we've worked to learn how to build larger and more composable Gio applications.

The current iteration adds package stream, providing a general purpose way to consume data from asynchronous sources directly within layout.

Additionally, stream/sqlitestream provides logic for building reactive data streams atop SQLite databases. Combined with stream, it provides an end-to-end system for consuming and updating SQLite databases from Gio UIs.

#Streams

Your GUI wants to display information from asynchronous sources sometimes, but it can be difficult to connect asynchronous data with the layout goroutine without generating race conditions or displaying stale data. A stream is essentially a special varible that you can read from the layout goroutine without blocking. The variable will always provide the most recent results of an asynchronous computation pipeline. This allows you to write layout code depending upon the results from a stream and receive the latest data each frame.

Streams powering visible widgets must be read from every frame. We don't want to waste resources on streams that update widgets which aren't being displayed, so streams are designed to shut down when their widget is not visible. We determine this by checking (once per frame) which streams have been read since the last check. Any stream that has not been read during the frame will go inert until it is read again.

Streams for any given application window are powered by a stream.Controller. This type connects the managed streams to the frame lifecycle of the window.

Construct a controller with:

func NewController(ctx context.Context, invalidator func()) *Controller

The provided ctx can be cancelled to shut down all streaming work for the window, and the provided invalidator function will be used by the controller to ensure the window generates a new frame when active streams emit new values. Proper use might look like this;

func loop(w *app.Window) error {
	// Make a context that lives as long as the window.
	windowCtx, cancel := context.WithCancel(context.Background())
	defer cancel()
	// Make a controller for this window.
	controller := stream.NewController(windowCtx, w.Invalidate)

	var ops op.Ops
	for {
		switch event := w.Event().(type) {
		case app.DestroyEvent:
			return event.Err
		case app.FrameEvent:
			gtx := app.NewContext(&ops, event)

			// Your layout here, passing the controller so that code can instantiate streams with it.
			layoutUI(gtx, controller)

			event.Frame(gtx.Ops)

			// Transition any active unread streams to be inactive.
			controller.Sweep()
		}
	}
	return nil
}

A stream is created like so:

// Make a stream that will emit increasing integers every second.
myStream := stream.New(controller, func(ctx context.Context) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		ticker := time.NewTicker(time.Second)
		defer ticker.Stop()
		ticks := 0
		for {
			select {
			case <-ticker.C:
				ticks++
				out <- ticks
			case <- ctx.Done():
				return
			}
		}
	}()
	return out
})

The parameters for the constructor are the controller which will manage the stream's lifecycle (connecting this stream to the parent window and ensuring that it shuts down correctly when not in use) and a function used to start the stream when it is active. That function is provided with a context that will be cancelled when the stream goes inert, and is expected to return a receive-only channel of values that will be closed when the context is cancelled. Within this function, you can perform arbitrary asynchronous computation, returning the final channel in a complex asynchronous pipeline.

To read a stream, you can invoke one of several methods prefixed with Read.

ticks, status := myStream.Read(gtx)
if status == stream.Waiting {
	// We haven't received a value over the stream yet.
} else {
	// We have a value, so do something with ticks.
}

If the exact status of the stream isn't important for your purposes, the ReadDefault and ReadInto methods are more ergonomic.

ticks := myStream.ReadDefault(gtx, 0)
var ticks int // Assume we declared this elsewhere, perhaps as a field.
myStream.ReadInto(gtx, &ticks, 0)

Here's a complete example that displays a label counting up once each second:

func tickStreamProvider(ctx context.Context) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		ticker := time.NewTicker(time.Second)
		ticks := 0
		for {
			select {
			case <-ticker.C:
				ticks++
				out <- ticks
			case <-ctx.Done():
				return
			}
		}
	}()
	return out
}

func loop(w *app.Window) error {
	// Make a context that lives as long as the window.
	windowCtx, cancel := context.WithCancel(context.Background())
	defer cancel()
	// Make a controller for this window.
	controller := stream.NewController(windowCtx, w.Invalidate)
	tickStream := stream.New(controller, tickStreamProvider)

	th := material.NewTheme()
	th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
	ticks := 0

	var ops op.Ops
	for {
		switch event := w.Event().(type) {
		case app.DestroyEvent:
			return event.Err
		case app.FrameEvent:
			gtx := app.NewContext(&ops, event)

			tickStream.ReadInto(gtx, &ticks, 0)
			material.H1(th, strconv.Itoa(ticks)).Layout(gtx)

			event.Frame(gtx.Ops)

			// Transition any active unread streams to be inactive.
			controller.Sweep()
		}
	}
}

You can try this example yourself with go run ./example/readme/stream.

For a more sophisticated example (simulating the users list of a chat application with users going on/offline, changing their usernames, etc...) see go run ./example/readme/stream2 and the corresponding source code.

See the package godoc for more detailed usage info and special types making error handling easier.

The stream API is a result of collaboration among Pedro Leite Rocha, Jack Mordaunt, and Chris Waldon. Thanks to Pedro for asking why we couldn't have nicer things.

Do not follow this link