I want to add a GUI to a command line application that I have written in Go but I'm running into problems with fyne and circular dependencies.
Consider this simple example to illustrate the problem I am facing: Assume that a button triggers a time-consuming method on my model class (say fetching data or so) and I want the view to update when the task has finished.
I started by implementing a very naive and not at-all-decoupled solution, which obviously runs into a circular dependency error raised by the go compiler. Consider the following code:
main.go
package main
import (
"my-gui/gui"
)
func main() {
gui.Init()
}
gui/gui.go
package gui
import (
"my-gui/model"
//[...] fyne imports
)
var counterLabel *widget.Label
func Init() {
myApp := app.New()
myWindow := myApp.NewWindow("Test")
counterLabel = widget.NewLabel("0")
counterButton := widget.NewButton("Increment", func() {
go model.DoTimeConsumingStuff()
})
content := container.NewVBox(counterLabel, counterButton)
myWindow.SetContent(content)
myWindow.ShowAndRun()
}
func UpdateCounterLabel(value int) {
if counterLabel != nil {
counterLabel.SetText(strconv.Itoa(value))
}
}
model/model.go
package model
import (
"my-gui/gui" // <-- this dependency is where it obviously hits the fan
//[...]
)
var counter = 0
func DoTimeConsumingStuff() {
time.Sleep(1 * time.Second)
counter++
fmt.Println("Counter: " + strconv.Itoa(counter))
gui.UpdateCounterLabel(counter)
}
So I am wondering how I could properly decouple this simple app to get it working. What I thought about:
use fyne data binding: That should work for simple stuff such as the label text in the example above. But what if I have to update more in a very custom way according to a model's state. Say I'd have to update a button's enabled state based on a model's condition. How can this be bound to data? Is that possible at all?
use interfaces as in the standard MVC design pattern: I tried this as well but couldn't really get my head around it. I created a separate module that would provide an interface which could then be imported by the model class. I would then register a view that (implicitly) implements that interface with the model. But I couldn't get it to work. I assume that my understanding of go interfaces isn't really sufficient at this point.
short polling the model: that's just meh and certainly not what the developers of Go and/or fyne intended :-)
Can anyone please point me to an idiomatic solution for this problem? I'm probably missing something very, very basic here...
Return Value
You could return the value.
Then on button click you spawn an anonymous goroutine, in order to not block the UI.
Callback
You could pass the
UpdateCounterLabel
function to your model function aka callback.Channel
Maybe you could also pass a channel to your model function. But with the above approach, this doesn't seem required. Potentially, if you have more than one counter value coming.
In the GUI you can then receive from the channel, again in a goroutine in order to not block the UI.
Of course, you could also use, again, a callback that you call on each iteration.