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...
CodePudding user response:
Return Value
You could return the value.
func DoTimeConsumingStuff() int {
time.Sleep(1 * time.Second)
counter
return counter
}
Then on button click you spawn an anonymous goroutine, in order to not block the UI.
counterButton := widget.NewButton("Increment", func() {
go func() {
counter := model.DoTimeConsumingStuff(counterChan)
UpdateCounterLabel(counter)
}()
})
Callback
You could pass the UpdateCounterLabel
function to your model function aka callback.
func DoTimeConsumingStuff(callback func(int)) {
time.Sleep(1 * time.Second)
counter
callback(counter)
}
counterButton := widget.NewButton("Increment", func() {
go model.DoTimeConsumingStuff(UpdateCounterLabel)
})
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.
func DoTimeConsumingStuff(counterChan chan int) {
for i := 0; i < 10; i {
time.Sleep(1 * time.Second)
counter
counterChan <- counter
}
close(counterChan)
}
In the GUI you can then receive from the channel, again in a goroutine in order to not block the UI.
counterButton := widget.NewButton("Increment", func() {
go func() {
counterChan := make(chan int)
go model.DoTimeConsumingStuff(counterChan)
for counter := range counterChan {
UpdateCounterLabel(counter)
}
}()
})
Of course, you could also use, again, a callback that you call on each iteration.