Fyne (Golang) desktop app with goroutines

Golang is really not the best choice when it comes to developing desktop apps, but it is still possible. One example of viable use case for Golang desktop app would be to quickly add graphical interface for existing CLI app. And Fyne is great framework for that purpose. As stated on their official GitHub page, it’s an easy-to-use UI toolkit and app API written in Go. It is designed to build applications that run on desktop and mobile devices with a single codebase.

Few months ago I was building an desktop app using Fyne framework. That app was using background goroutine to do some work and report back results to main goroutine via channel. Background worker was running without interruption and main goroutine would just receive and display results until background worker is done with it’s job. Everything was working fyne (pun intended), until I had to interrupt background worker in the middle of the run to send it some additional data that was not available at the start. Idea was to have background worker send signal to main goroutine to get required data. Main goroutine would then show dialog to user to enter data. And finally, main goroutine would send entered data back to background worker. My first solution was something like this (simplified example):

package main

import (
	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/dialog"
	"fyne.io/fyne/v2/widget"
)

type myChan chan string

var mainWindow fyne.Window

func main() {
	// create new Fyne app
	myApp := app.NewWithID("blog.letscode.fynegoroutines")

	// create main window
	mainWindow = myApp.NewWindow("Fyne goroutines")

	// create button which will create new channel, start background worker and listen for results
	mainWindow.SetContent(widget.NewButton("Click Me", func() {
		// create channel to communicate with background goroutine
		chn := make(myChan)

		// start background worker in separate goroutine
		go backgroundWorker(chn)

		// listen on data received from background worker
		listen(chn)
	}))

	// set window size and start app
	mainWindow.Resize(fyne.Size{
		Width:  400,
		Height: 225,
	})
	mainWindow.ShowAndRun()
}

// listen function to receive data from background worker
func listen(chn myChan) {
	for {
		select {
		case msg := <-chn:
			if msg == "get" {
				showDialog(chn)
			}
			if msg == "ok" {
				return
			}
		}
	}
}

// helper function to spawn new dialog and send data back to background worker
func showDialog(chn myChan) {
	items := make([]*widget.FormItem, 1)
	pinEntry := widget.NewEntry()
	items[0] = widget.NewFormItem("PIN: ", pinEntry)
	dialog.ShowForm("Enter PIN", "OK", "Cancel", items,
		func(bool) {
			// send data back to background worker
			chn <- pinEntry.Text
		},
		mainWindow,
	)
}

// background worker running in separate goroutine
func backgroundWorker(chn myChan) {
	for {
		chn <- "get"
		pin := <-chn
		if pin == "1234" {
			chn <- "ok"
			return
		}
	}
}

If you try to run this code, when you click that big button, dialog to enter data will be spawned, but then whole app will stuck and there is nothing you can do but kill process. I was stuck (just like my app) and had no idea how to proceed. With the help of Fyne framework creator on Fyne official Slack channel, I managed to find solution. It was so simple to fix it, but I didn’t saw it. So I decided to share it here, if someone else will have the same problem. All that’s needed to be done is to change line 30, from listen(chn) to go listen(chn) so listen function will also run in separate goroutine which will stop it from blocking the events goroutine. And that’s it, now everything works as expected 🙂

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: