Home > Enterprise >  How to avoid circular dependencies in a GUI application with fyne?
How to avoid circular dependencies in a GUI application with fyne?

Time:03-22

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.

  • Related