DEV Community

Cover image for Learning Go by examples: part 7 - Create a cross-platform GUI/Desktop app in Go
Aurélie Vache
Aurélie Vache

Posted on • Updated on

Learning Go by examples: part 7 - Create a cross-platform GUI/Desktop app in Go

In previous articles we created an HTTP REST API server, a CLI, a Bot for Discord and even a game for Nintendo Game Boy Advance.

Golang is used a lot for CLI and microservices but what about creating a GUI/Desktop and a mobile application?

Initialization

We created our Git repository in the previous article, so now we just have to retrieve it locally:

$ git clone https://github.com/scraly/learning-go-by-examples.git
$ cd learning-go-by-examples
Enter fullscreen mode Exit fullscreen mode

We will create a folder go-gopher-desktop for our CLI application and go into it:

$ mkdir go-gopher-desktop
$ cd go-gopher-desktop
Enter fullscreen mode Exit fullscreen mode

Now, we have to initialize Go modules (dependency management):

$ go mod init github.com/scraly/learning-go-by-examples/go-gopher-desktop
go: creating new go.mod: module github.com/scraly/learning-go-by-examples/go-gopher-desktop
Enter fullscreen mode Exit fullscreen mode

This will create a go.mod file like this:

module github.com/scraly/learning-go-by-examples/go-gopher-desktop

go 1.16
Enter fullscreen mode Exit fullscreen mode

Before starting to code our Desktop application, as good practices, we will create a simple code organization.

Create the following folders organization:

.
├── README.md
└── go.mod
Enter fullscreen mode Exit fullscreen mode

That's it? Yes, the rest of our code organization will be created shortly ;-).

Fyne

Fyne

Fyne is a UI toolkit for building Desktop and mobile applications. Its interface design follows the Material Design principles, providing cross-platform graphics that appear identical on all supported platforms.

Graphical applications are generally more complicated to create than web based or command line applications. Fyne changes this by utilizing the great design of Go to make building beautiful graphical applications simple and fast.

Fyne toolkit support building for iOS and Android devices as well as macOS, Windows, Linux and BSD.

With Fyne, no need to know React, Angular or VueJS framework, we can create GUI and mobile apps in Go, our favorite language ;-).

Fyne provides an executable and dependencies.

In order to use Fyne, we first need to install the fyne executable command:

$ go get fyne.io/fyne/v2/cmd/fyne
Enter fullscreen mode Exit fullscreen mode

And then its dependencies:

$ go get fyne.io/fyne/v2
Enter fullscreen mode Exit fullscreen mode

At this time, the go.mod file should have this following import:

module github.com/scraly/learning-go-by-examples/go-gopher-desktop

go 1.16

require (
    fyne.io/fyne/v2 v2.0.4 // indirect
)
Enter fullscreen mode Exit fullscreen mode

Let's create our Desktop app!

Starate SG1 Gopher

What do we want?

We want to create an app for Desktop/GUI and mobile that display:

  • A menu
  • A text
  • A cute random Gopher
  • A random button

Let's create a main.go file.

We initialize the package, called main, and all dependencies/librairies we need to import:

package main

import (
    "image/color"

    "fyne.io/fyne/v2"
    "fyne.io/fyne/v2/app"
    "fyne.io/fyne/v2/canvas"
    "fyne.io/fyne/v2/container"
    "fyne.io/fyne/v2/dialog"
    "fyne.io/fyne/v2/widget"
)
Enter fullscreen mode Exit fullscreen mode

Define our constant:

const KuteGoAPIURL = "https://kutego-api-xxxxx-ew.a.run.app"
Enter fullscreen mode Exit fullscreen mode

Then, create our main() function:

func main() {
    myApp := app.New()
    myWindow := myApp.NewWindow("Gopher")

    // Main menu
    fileMenu := fyne.NewMenu("File",
        fyne.NewMenuItem("Quit", func() { myApp.Quit() }),
    )

    helpMenu := fyne.NewMenu("Help",
        fyne.NewMenuItem("About", func() {
            dialog.ShowCustom("About", "Close", container.NewVBox(
                widget.NewLabel("Welcome to Gopher, a simple Desktop app created in Go with Fyne."),
                widget.NewLabel("Version: v0.1"),
                widget.NewLabel("Author: Aurélie Vache"),
            ), myWindow)
        }))
    mainMenu := fyne.NewMainMenu(
        fileMenu,
        helpMenu,
    )
    myWindow.SetMainMenu(mainMenu)

    // Define a welcome text centered
    text := canvas.NewText("Display a random Gopher!", color.White)
    text.Alignment = fyne.TextAlignCenter

    // Define a Gopher image
    var resource, _ = fyne.LoadResourceFromURLString(KuteGoAPIURL + "/gopher/random/")
    gopherImg := canvas.NewImageFromResource(resource)
    gopherImg.SetMinSize(fyne.Size{Width: 500, Height: 500}) // by default size is 0, 0

    // Define a "random" button
    randomBtn := widget.NewButton("Random", func() {
        resource, _ := fyne.LoadResourceFromURLString(KuteGoAPIURL + "/gopher/random/")
        gopherImg.Resource = resource

        //Redrawn the image with the new path
        gopherImg.Refresh()
    })
    randomBtn.Importance = widget.HighImportance

    // Display a vertical box containing text, image and button
    box := container.NewVBox(
        text,
        gopherImg,
        randomBtn,
    )

    // Display our content
    myWindow.SetContent(box)

    // Close the App when Escape key is pressed
    myWindow.Canvas().SetOnTypedKey(func(keyEvent *fyne.KeyEvent) {

        if keyEvent.Name == fyne.KeyEscape {
            myApp.Quit()
        }
    })

    // Show window and run app
    myWindow.ShowAndRun()
}
Enter fullscreen mode Exit fullscreen mode

Let's explain the main function, step by step.

First, we create a new application and a new window with a title equals to "Gopher":

    myApp := app.New()
    myWindow := myApp.NewWindow("Gopher")
Enter fullscreen mode Exit fullscreen mode

For a graphical application to work, we first need to create a new application and a window. So, we create a new app with a single window with a title equals to "Gopher".

Then, we create a main menu:


    // Main menu
    fileMenu := fyne.NewMenu("File",
        fyne.NewMenuItem("Quit", func() { myApp.Quit() }),
    )

    helpMenu := fyne.NewMenu("Help",
        fyne.NewMenuItem("About", func() {
            dialog.ShowCustom("About", "Close", container.NewVBox(
                widget.NewLabel("Welcome to Gopher, a simple Desktop app created in Go with Fyne."),
                widget.NewLabel("Version: v0.1"),
                widget.NewLabel("Author: Aurélie Vache"),
            ), myWindow)
        }))
    mainMenu := fyne.NewMainMenu(
        fileMenu,
        helpMenu,
    )
    myWindow.SetMainMenu(mainMenu)
Enter fullscreen mode Exit fullscreen mode

The main menu contains a File and a Help menu:

├── File
│   └── Quit
└── Help
    └── About
Enter fullscreen mode Exit fullscreen mode

When we click on File>Quit, the application is exited.
When we click on Help>About, a dialog box is displayed with an about text.

Inside the window, we place a text "Display a random Gopher!" and we center it.

    // Define a welcome text centered
    text := canvas.NewText("Display a random Gopher!", color.White)
    text.Alignment = fyne.TextAlignCenter
Enter fullscreen mode Exit fullscreen mode

It's time to define our cute Gopher image:

    // Define a Gopher image
    var resource, _ = fyne.LoadResourceFromURLString(KuteGoAPIURL + "/gopher/random/")
    gopherImg := canvas.NewImageFromResource(resource)
    gopherImg.SetMinSize(fyne.Size{Width: 500, Height: 500}) // by default size is 0, 0
Enter fullscreen mode Exit fullscreen mode

For that we creates a new StaticResource in memory from KuteGo API random URL, we define it as a resource to our image and we set the minimum size of the image.

Wait gopher

Oh yes, the blank identifier _ is an anonymous placeholder. It may be used like any other identifier in a variable declaration, but it does not introduce a binding.
In this case, the function LoadResourceFromURLString return a resource and an error, but I don't want to retrieve the error, test it and do something in case of an error. So I use _ instead for this value I don't care.

I recommend you to retrieve errors and do something when an error happens, but for this example I wanted to show you this Golang feature :-).

Let's go back to our main() function.
Then, we define a button with "random" text, in blue (HighImportance level).
When we click on this button, we need to retrieve a new random Gopher and define it as a resource to our image. And we need to refresh the image in order to tell to Fyne to redrawn it.

    // Define a "random" button
    randomBtn := widget.NewButton("Random", func() {
        resource, _ := fyne.LoadResourceFromURLString(KuteGoAPIURL + "/gopher/random/")
        gopherImg.Resource = resource

        //Redrawn the image with the new path
        gopherImg.Refresh()
    })
    randomBtn.Importance = widget.HighImportance
Enter fullscreen mode Exit fullscreen mode

Thanks to the new image resource and refresh method, the screen will be updated to the end user.

After that, we define a vertical box with our three elements and we set it to our window:

    // Display a vertical box containing text, image and button
    box := container.NewVBox(
        text,
        gopherImg,
        randomBtn,
    )

    // Display our content
    myWindow.SetContent(box)
Enter fullscreen mode Exit fullscreen mode

A vertical box layout arranges items in a column. Each item will have its height set to minimum and all the widths will be equal, set to the largest of the minimum widths.

We listen when the user presses the Escape key in the keyboard, we close the application.

And, finally, we run the application and show the window.

    // Close the App when Escape key is pressed
    myWindow.Canvas().SetOnTypedKey(func(keyEvent *fyne.KeyEvent) {

        if keyEvent.Name == fyne.KeyEscape {
            myApp.Quit()
        }
    })

    // Show window and run app
    myWindow.ShowAndRun()
Enter fullscreen mode Exit fullscreen mode

The Window.ShowAndRun() method is a shortcut for Window.Show() and App.Run().

After calling myApp.Run() or myWindow.ShowAndRun(), our application will run and the function will return after the window has been closed.

Test it!

It's time to test our first graphical app, for that we will run it:

$ go run main.go
Enter fullscreen mode Exit fullscreen mode

Random Gopher

Awesome, our desktop app is running!

We can click on "Random" button, cool another cute Gopher appears :-).

And we can click on the menu in Help>About in order to display our about message:

App menu

Help About

We can also press the Escape key in our keyboard, the app should exit ;-).

Perfect, our little Desktop app is working correctly!

Test as mobile device

And do you know that we can also test an app and simulate it in a mobile environment?

With the following command we will see how our app would work on a mobile device:

$ go run -tags mobile main.go
Enter fullscreen mode Exit fullscreen mode

or through our task:

$ task run-mobile
task: [run-mobile] GOFLAGS=-mod=mod go run -tags mobile main.go
Enter fullscreen mode Exit fullscreen mode

Test on mobile

Help>About menu on mobile simulation

As you can see, the menu is displayed differently than in the GUI/Desktop application.

Built/Package it!

Our application is now ready, we just have to build it.
For that, like in the previous articles, we will use Taskfile in order to automate our common tasks.

So, for this app too, I created a Taskfile.yml file with this content:

version: "3"

tasks:
    run: 
        desc: Run the app
        cmds:
        - GOFLAGS=-mod=mod go run main.go

    run-mobile: 
        desc: Run the app on mobile emulator
        cmds:
        - GOFLAGS=-mod=mod go run -tags mobile main.go

    build:
        desc: Build the app for current OS
        cmds:
        # - GOFLAGS=-mod=mod go build -o bin/gopher-desktop main.go 
        - fyne package -icon gopher.png

    package-android:
        desc: Package the app for Android
        cmds:
        - fyne package -os android -appID com.scraly.gopher -icon gopher.png

    package-ios:
        desc: Package the app for iOS
        cmds:
        - fyne package -os ios -appID com.scraly.gopher -icon gopher.png
Enter fullscreen mode Exit fullscreen mode

Thanks to this, we can build our app easily. Before to execute our task, let's explain packaging for GUI and mobile applications.

Packaging for multiple operating systems can be a complex task. Graphical applications typically have icons and metadata associated with them as well as specific formats required to integrate with each environment.

The fyne command provides support for preparing applications to be distributed across all the platforms the toolkit supports. Running fyne package command will create an application ready to be installed on a computer and to be distributed to other computers by simply copying the created files from the current directory.

Let's build/package it:

$ task package
task: [package] fyne package -icon gopher.png
Enter fullscreen mode Exit fullscreen mode

This command create an app for the current OS with icons embedded.
I'm on MacOS so, the command generate an app for it:
app

When you double click on it, the Desktop app is launched with our cute icon:

App in bar

If you run task package command in a Windows environment, you will have an .exe executable file.
On a MacOS computer, you will have an .app bundle (like in this article).
And for Linux, you will have a .tar.xz file that can be installed in the usual manner (or by running make install inside the extracted folder).

And you can also specify the target OS, like this:

$ fyne package -os windows -icon myapp.png
Enter fullscreen mode Exit fullscreen mode

... And package it for Android & iOS!

cross compilation

To run on a real mobile device, it is required that you package the application. To do this, we can use the fyne package command.

Let's package our app for Android:

$ fyne package -os android -appID com.scraly.gopher -icon gopher.png
Enter fullscreen mode Exit fullscreen mode

or execute our task:

$ task package-android
task: [package-android] fyne package -os android -appID com.scraly.gopher -icon gopher.png
Enter fullscreen mode Exit fullscreen mode

And we can do the same thing for iOS:

$ task package-ios
task: [package-ios] fyne package -os ios -appID com.scraly.gopher -icon gopher.png
Enter fullscreen mode Exit fullscreen mode

/!\ Warning: In order to package for Android, you need to install adb in your computer and for iOS you need to install XCode. Please read the following instructions.

If you don't install them, you'll have these kind of error messages:

Android:

$ task package-android
task: [package-android] fyne package -os android -appID com.scraly.gopher -icon gopher.png
no Android NDK found in $ANDROID_HOME/ndk-bundle nor in $ANDROID_NDK_HOME
task: Failed to run task "package-android": exit status 1
Enter fullscreen mode Exit fullscreen mode

iOS:

$ task package-ios
task: [package-ios] fyne package -os ios -appID com.scraly.gopher -icon gopher.png
-os=ios requires XCode
task: Failed to run task "package-ios": exit status 1
Enter fullscreen mode Exit fullscreen mode

Distribute it!

Stargate

We run it, test it, build it, package it, so now what can be the final step? We can distribute our application!

As you know, it can be painful to distribute our applications in Play and App stores. That's the reason, fyne release command exists.

In one command you can bundle your app for Play store:

$ fyne release -os android -appID com.example.myapp -appVersion 1.0 -appBuild 1
Enter fullscreen mode Exit fullscreen mode

Please follow the complete instructions if you are interested to distribute your application.

Conclusion

As we have seen in this article, it's possible to create a simple GUI/Desktop and mobile application in few minutes, with Fyne.

Special thanks to Andrew Williams who helped me on Slack.

But, be careful I do not recommend you to develop all of your web apps, REST, gRPC, games, mobiles (...) and desktop apps in Go, but I think it's interesting to know that you can and how is it possible to do that, concretely :-).

All the code is available in: https://github.com/scraly/learning-go-by-examples/tree/main/go-gopher-desktop

Discussion (20)

Collapse
davidkroell profile image
David Kröll

Very neat artcile - looks awesome at first. Probably trying this framework out soon.

Are there productive apps built with Fyne already out there?

Collapse
andydotxyz profile image
Andy Williams

As well as the app listings there is also a full desktop built using Fyne which is a pretty good demo :).
fyne.io/fynedesk/

Collapse
aurelievache profile image
Aurélie Vache Author

Oh Andrew, you are here? ^^
Yes of course, the fynedesk demo is a way to see the Fyne possibilities :-).

Collapse
aurelievache profile image
Aurélie Vache Author

Thanks David.
Productive, I don't know, but you can find existing apps done with Fyne here: apps.fyne.io/

Collapse
oper profile image
Ilie Soltanici

Fyne is awesome, I’m building all my GUI projects only with fyne. Andrew Is really very helpful in slack.

Collapse
aurelievache profile image
Aurélie Vache Author

Yess, Andrew Williams is very active and take time to help people on Slack :-).

Collapse
andydotxyz profile image
Andy Williams

The whole community is excellent! I think in the summer fewer of the core team are around, but the rest of the year people are available to help in Slack or Discord at all hours.

Collapse
buphmin profile image
buphmin

Oh neat! Curious how fyne performs (cpu, ram, etc) compared to electron and similar web based desktop apps 🤔.

Collapse
aurelievache profile image
Aurélie Vache Author

I didn't have compared Fyne with electron, but maybe @andydotxyz already done a benchmark? :-)

Collapse
andydotxyz profile image
Andy Williams

I have never done a comparison with web tech - it didn't feel fair as native and web are designed to solve different problems.
A while ago I did start a performance comparison, at github.com/fyne-io/fyne/wiki/Perfo..., I will see if I can add electron and react native to round out the table.
In general a Fyne app may be a little larger (around 7MB or 4MB compressed) because it contains the Go interpreter, but it should run faster with much less RAM required.

Thread Thread
buphmin profile image
buphmin

Thanks for the info this is good to know :)

Collapse
mjehanno profile image
mJehanno

I'm currently learning Go and I have a side-project where I need to create an API, a mobile App and a discord bot ... and your articles are exactly what I needed ! Many thanks !

Collapse
darkwiiplayer profile image
DarkWiiPlayer

Was about to complain that the stargate was missing earth, but it's right there at the top, just a bit... bent xD

Collapse
aurelievache profile image
Aurélie Vache Author

:-D
Yes don't worry, Gopher can come home ^^

Collapse
codeanit profile image
Anit Shrestha Manandhar

Loved the whole series. Thanks.

Collapse
aurelievache profile image
Aurélie Vache Author

Thanks for your comment! :-) :-)

Collapse
nigel447 profile image
nigel447

Thanks for a great resource, covers just about everything

Collapse
aurelievache profile image
Aurélie Vache Author

Thanks @nigel447 :-)

Collapse
burneyhoel profile image
Burney Hoel

Love the stargate references!

Collapse
aurelievache profile image
Aurélie Vache Author

Ah ah, I love mixing Gophers and movies or TV show; happy you like Stargate reference :-)