Reactive state with flow
The flow package
holds state as typed values that tell listeners when they change. It is plain
Go with generics and imports nothing from Bellui, so you can keep your state
in a package that has no UI code and test it on its own.
Values and derived values
first := flow.New("Ada")
last := flow.New("Lovelace")
shout := flow.New(false)
full := flow.Derive(func() string {
s := first.Get() + " " + last.Get()
if shout.Get() {
s = strings.ToUpper(s)
}
return s
}, first, last, shout)flow.Newcreates aflow.Valuewith a starting value. The type is inferred from it and must be comparable.Getreads it andSetchanges it.flow.Derivecreates aflow.Derivedvalue. It runs the function once straight away, then again each time one of the listed sources changes. A derived value can itself be a source for another one.
List every value the function reads. Derive does not discover them by
itself, and a source you leave out will not trigger a recompute.
Set does nothing when the new value equals the current one, and a derived
value only notifies its listeners when the result actually changes. For a
read-modify-write, use Modify, which does both steps under one lock:
clicks := flow.New(0)
counter := widget.NewLabel("")
widget.BindLabel(counter, widget.FormatValue(clicks, "Clicked %d times"))
button := widget.NewButton("Click", func() {
clicks.Modify(func(n int) int { return n + 1 })
})Connect values to widgets
The widget package has helpers that connect a flow value to a widget:
firstEntry := widget.NewEntry()
widget.BindEntry(firstEntry, first) // two-way
lastEntry := widget.NewEntry()
widget.BindEntry(lastEntry, last)
shoutCheck := widget.NewCheck("Shout", nil)
widget.BindCheck(shoutCheck, shout)
result := widget.NewLabel("")
widget.BindLabel(result, widget.StringDerived{Derived: full}) // one-way| Helper | Widget | Direction |
|---|---|---|
widget.BindEntry | Entry and Value[string] | both ways |
widget.BindCheck | Check and Value[bool] | both ways |
widget.BindSlider | Slider and Value[float64] | both ways |
widget.BindProgressBar | ProgressBar from Value[float64] | value to widget |
widget.BindLabel | Label from a StringSource | value to widget |
BindLabel takes a
widget.StringSource,
not a flow value. Wrap the value to get one:
widget.StringValue{Value: v}for a*flow.Value[string],widget.StringDerived{Derived: d}for a*flow.Derived[string],widget.FormatValue(v, format)for any*flow.Value, formatted withfmt.Sprintf.
Each helper sets the widget from the value straight away and returns a
function that disconnects them. The two-way helpers install their own
OnChanged on the widget. They keep the OnChanged you set before calling
them and call it too, and restore it when you disconnect.
Goroutines
flow values are safe to read and set from any goroutine. Listeners,
however, run on the goroutine that called Set, straight away. When a value
is connected to widgets, its listeners change those widgets, so set it on
the UI goroutine. From background work, wrap the change in
bellui.Do:
go func() {
time.Sleep(3 * time.Second)
bellui.Do(func() { // listeners update widgets, so Set on the UI goroutine
last.Set("Byron")
})
}()Data binding does this for you; see which one to use.
Listen to changes
OnChange registers a function to call after each change and returns a
function that unregisters it:
stop := full.OnChange(func() {
fmt.Println("name is now", full.Get())
})
defer stop()Unlike a binding listener, it does not run when you register it.
Value and Derived also have Listen(func(old, new T)). In this version
both arguments receive the new value. If you need the previous value, keep a
copy yourself in an OnChange listener.
A derived value keeps listening to its sources until you call its Close
method. Call it when a derived value is no longer needed but its sources live
on.
The full program:
// Reactive state with the flow package: values, derived values and the
// widget helpers that connect them.
package main
import (
"fmt"
"strings"
"time"
"github.com/belluikit/bellui"
"github.com/belluikit/bellui/app"
"github.com/belluikit/bellui/container"
"github.com/belluikit/bellui/flow"
"github.com/belluikit/bellui/widget"
)
func main() {
a := app.New()
w := a.NewWindow("Flow")
first := flow.New("Ada")
last := flow.New("Lovelace")
shout := flow.New(false)
full := flow.Derive(func() string {
s := first.Get() + " " + last.Get()
if shout.Get() {
s = strings.ToUpper(s)
}
return s
}, first, last, shout)
firstEntry := widget.NewEntry()
widget.BindEntry(firstEntry, first) // two-way
lastEntry := widget.NewEntry()
widget.BindEntry(lastEntry, last)
shoutCheck := widget.NewCheck("Shout", nil)
widget.BindCheck(shoutCheck, shout)
result := widget.NewLabel("")
widget.BindLabel(result, widget.StringDerived{Derived: full}) // one-way
clicks := flow.New(0)
counter := widget.NewLabel("")
widget.BindLabel(counter, widget.FormatValue(clicks, "Clicked %d times"))
button := widget.NewButton("Click", func() {
clicks.Modify(func(n int) int { return n + 1 })
})
go func() {
time.Sleep(3 * time.Second)
bellui.Do(func() { // listeners update widgets, so Set on the UI goroutine
last.Set("Byron")
})
}()
stop := full.OnChange(func() {
fmt.Println("name is now", full.Get())
})
defer stop()
w.SetContent(container.NewVBox(firstEntry, lastEntry, shoutCheck, result, counter, button))
bellui.ShowAndRun(w)
}