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
We will create a folder go-gopher-desktop
for our CLI application and go into it:
$ mkdir go-gopher-desktop
$ cd go-gopher-desktop
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
This will create a go.mod
file like this:
module github.com/scraly/learning-go-by-examples/go-gopher-desktop
go 1.16
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
That's it? Yes, the rest of our code organization will be created shortly ;-).
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
And then its dependencies:
$ go get fyne.io/fyne/v2
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
)
Let's create our Desktop app!
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"
)
Define our constant:
const KuteGoAPIURL = "https://kutego-api-xxxxx-ew.a.run.app"
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()
}
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")
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)
The main menu contains a File and a Help menu:
├── File
│ └── Quit
└── Help
└── About
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
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
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.
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
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)
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()
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
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:
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
or through our task:
$ task run-mobile
task: [run-mobile] GOFLAGS=-mod=mod go run -tags mobile main.go
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
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
This command create an app for the current OS with icons embedded.
I'm on MacOS so, the command generate an app for it:
When you double click on it, the Desktop app is launched with our cute icon:
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
... And package it for Android & iOS!
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
or execute our task:
$ task package-android
task: [package-android] fyne package -os android -appID com.scraly.gopher -icon gopher.png
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
/!\ 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
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
Distribute it!
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
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
Top comments (25)
Fyne is awesome, I’m building all my GUI projects only with fyne. Andrew Is really very helpful in slack.
Yess, Andrew Williams is very active and take time to help people on Slack :-).
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.
Very neat artcile - looks awesome at first. Probably trying this framework out soon.
Are there productive apps built with Fyne already out there?
As well as the app listings there is also a full desktop built using Fyne which is a pretty good demo :).
fyne.io/fynedesk/
Oh Andrew, you are here? ^^
Yes of course, the fynedesk demo is a way to see the Fyne possibilities :-).
Thanks David.
Productive, I don't know, but you can find existing apps done with Fyne here: apps.fyne.io/
on Windows 10
Getting this error in command terminal window. no image is displayed in the windows client. Go compiled fine w/o error. Ran go mod tidy also.
2021/12/21 17:57:10 Cause: XML syntax error on line 2: unquoted or missing attribute value in element
2021/12/21 17:57:10 At: c:/go-workspace/pkg/mod/fyne.io/fyne/v2@v2.0.4/internal/painter/image.go:55
ran the program but keep getting the following error:
Cause: XML syntax error on line 2: unquoted or missing attribute value in element
At: C:/Users/alanh/go/pkg/mod/fyne.io/fyne/v2@v2.1.2/internal/painter/image.go:78
Any help appreciated
Hi,
I haven't tested on a Windows OS yet.
Have you tried to contact Fyne team members on their slack? :)
Here is complete error message.
2021/12/21 18:03:17 Fyne error: SVG Load error:
2021/12/21 18:03:17 Cause: XML syntax error on line 2: unquoted or missing attribute value in element
2021/12/21 18:03:17 At: c:/go-workspace/pkg/mod/fyne.io/fyne/v2@v2.0.4/internal/painter/image.go:55
Oh neat! Curious how fyne performs (cpu, ram, etc) compared to electron and similar web based desktop apps 🤔.
I didn't have compared Fyne with electron, but maybe @andydotxyz already done a benchmark? :-)
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.
Thanks for the info this is good to know :)
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 !
Was about to complain that the stargate was missing earth, but it's right there at the top, just a bit... bent xD
:-D
Yes don't worry, Gopher can come home ^^
Loved the whole series. Thanks.
Thanks for your comment! :-) :-)
Thanks for a great resource, covers just about everything
Thanks @nigel447 :-)