2

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

Christian
  • 1,589
  • 1
  • 18
  • 36
  • I am not familiar with fyne, but you what about keeping model as simple as possible and returning value instead ? `go UpdateCounterLabel(model.DoTimeConsumingStuff())` – medasx Mar 20 '22 at 12:56
  • That's a goroutine that runs async. As far as I am aware, it cannot return a value, otherwise it would be blocking the ui thread... see https://stackoverflow.com/questions/20945069/catching-return-values-from-goroutines - But maybe making use of channels would solve the problem :-/ – Christian Mar 20 '22 at 13:36
  • Exactly, what I meant was to make `model` completely independent from caller. It was just an example, you could assign value to new variable and call `UpdateCounterLabel` afterwards. I'd recommend to use channel for this, just wanted to make a point :) – medasx Mar 20 '22 at 13:57
  • Yes, keep things simple. A model should always be sync, so async can be added as needed at GUI layer etc. Returning value works in this case, but callbacks would likely be needed for more complex data flow. – andy.xyz Mar 21 '22 at 09:23
  • 1
    All the answers here are great, but you are right the Fyne `databinding` would be another way to do it as it handles the callbacks for you, though your data model would depend on Fyne which may not always be desirable. The default data bindings are simple but it's fully extensible to do whatever you would like :). – andy.xyz Mar 21 '22 at 09:24

1 Answers1

5

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.

The Fool
  • 16,715
  • 5
  • 52
  • 86
  • Thanks mate! I'll look into all of these options. Highly appreciated. I'll leave the question open for another day for additional suggestions :-) – Christian Mar 20 '22 at 16:17
  • Quick question @The Fool: Is it possible to store a reference to a callback function as a member of a struct or as a variable inside a package, i.e. have something like `var myCallback func(int)` and then a setter like `func SetCallback(callback func(int)) {myCallback = callback}`? I tried that but the compiler would refuse my definition of `var myCallback func(int)`... – Christian Mar 20 '22 at 19:36
  • `var myCallback func(int)`, works. You might have another issue. But I don't think that is good practice. Why cant you just pass it when you call the model func? – The Fool Mar 20 '22 at 19:51
  • I probably/most likely can :-) ... I was just wondering what went wrong when I tried the above. – Christian Mar 20 '22 at 20:42
  • @Christian, there are actually some package in the std lib that have a callback as struct field. https://pkg.go.dev/net/http/httputil#ReverseProxy, check out the Director and ModifyResponse fields, for example. – The Fool Mar 21 '22 at 07:02