Creating Desktop Apps with Clojure and Cljfx


Published: 2026-04-23, Last Updated: 2026-05-02

Intro

I've been building Backdrop, a desktop app, in Clojure for the last few months. I never thought I would build a desktop app, since I mainly did backend work. But I did, with the help of Clojure!

If you're curious what Backdrop is, here is a quick demo video I made the other day.

Why the Project

It all started when I wanted to share some screenshots of other side projects I was working on on social media.

To make my screenshots best fit social media so that the thumbnails aren't cropped randomly by those platforms, and to have a tool to polish screenshots by adding shadows and rounded corners, and placing them over beautiful backgrounds, so the resulting image ratio meets the target platform's best practices.

For example, images are best suited to the 16:9 aspect ratio on LinkedIn.

But I can't find such a tool on Linux. There are quite a few screenshot options on Linux, such as gnome-screenshot and Flameshot. But none can polish screenshots that way.

After some time, I decided to make this type of tool for myself. For one, it can meet my own requirements. Second, I also wanted to see if I could use Clojure to develop this.

Building Process

Choosing Clojure Stack

Having a product idea in my head, next I needed to figure out how to implement it in Clojure.

I did my research on Google. Somehow, only two frameworks came into my sight at that time: cljfx and Humble UI.

Cljfx is a declarative, functional, and extensible wrapper for JavaFx inspired by the best parts of React and re-frame.

Humble UI is a desktop UI framework for Clojure. No Electron. No JavaScript. Only JVM and native code.

I wasn't sure at first, but I found cljfx better for me.

  1. It's more mature, which means there are relatively more resources for me to learn, and AI can work better with it as well.
  2. It's declarative and reactive as it's inspired by Reagent and Re-frame, among others, which I was familiar with.
  3. JavaFx UI styles are OS-native which users are more used to.

Interactive Development

An edge Clojure has is interactive development, which is a big deal for Clojurists' development experience (DX).

Cljfx has an example on this, but it didn't work well on my side. Thankfully, Vlad (the author) was supportive in the #cljfx Slack community; he advised me to use the ext-watcher machinery to achieve better results.

It did work better. I was excited and even put a demo video on YouTube after that; you can find the demo code on GitHub.

In Backdrop, I polished it a bit with a helper function. Whenever I felt like restarting rendering the UI, I evaluated (show-ui!):

(defonce *state (atom nil))
(defonce *component (atom nil))

(defn show-ui!
  "Create or recreate the root component."
  []
  (when @*component
    (fx/on-fx-thread
      (fx/delete-component @*component)))

  (reset!
   ,*component
   @(fx/on-fx-thread
      (fx/create-component
       {:fx/type fx/ext-watcher
        :ref *state
        ;; data will be passed as {:value @*state}
        :desc {:fx/type window-dispatcher}}
       {:fx.opt/map-event-handler #'map-event-handler
        :fx.opt/type->lifecycle (some-fn fx/keyword->lifecycle
                                         fx/fn->lifecycle)}))))

The Reactive Approach

I like using the reactive approach to build UI. It's clean to maintain UI components in pure functions. Especially with Reagent/Cljfx, all those functions return are data (maps or vectors), we can test it in REPL even without UI, which is a huge advantage.

A trivial example looks like this:

(defn text-input [{:keys [label]}]
  {:fx/type :v-box
   :children [{:fx/type :label :text label}
              {:fx/type :text-field}]})

You can test it with something like (text-input {: label "hello"}) to verify the results without all the UI stuff.

The DX is similar to Reagent. But compared to Reagent, one thing I missed sometimes was React's useEffect. It was a bit tricky to show a component for a few seconds, for example.

Cross-Platform Abilities

Thanks to JavaFX's cross-platform capabilities, overall, the program ran well on different operating systems.

But for Backdrop, there was a headache with taking screenshots across different OSes, especially on Linux, due to the diversity of desktop environments. But it's none of JavaFx or cljfx's business to some extent.

Packaging

After I've finished all the features I want, it's time to think about how to package it for users.

Uberjar

The simplest approach was to run the command lein uberjar. It would produce an uberjar as the command suggests. Then users can just run it with something like java -jar target/uberjar/backdrop-0.3.0-SNAPSHOT-standalone.jar.

It worked fine, but I wanted to challenge myself a bit more: obfuscation. If we're going to release a client app seriously, we should obfuscate it.

Obfuscation

Thanks to Clojure being a hosted language, we can leverage rich Java toolchains. ProGuard is one of the main tools used to obfuscate jar files. Beyond obfuscation, it can even shrink and optimize Java bytecode.

ProGuard could break things if it's too aggressive. To keep things simple, I only obfuscated my own namespaces.

In practice, I found that some Clojure-generated code shouldn't be obfuscated, such as defrecord and reify.

The main part of my ProGuard configuration is as follows:

# CheckBoxOption is a defrecord
-keep class backdrop.core.CheckBoxOption

# Keep all non-app stuff
-keep class !backdrop.**, !backdrop_common.** { *; }

# Keep our load functions like backdrop.core__init class, other public members are also obfuscated
-keep class backdrop.**__init {
    public static void load();
}
-keep class backdrop_common.**__init {
    public static void load();
}
# keep classes implementing Clojure's reify
-keep class backdrop.**$reify** {
    ,*;
}

# Keep classes that contain a main method (otherwise we won't be able to run the jar)
-keepclasseswithmembers public class * {
    public static void main(java.lang.String[]);
}

Packaging for Linux/Windows

Uberjars are easy to run for tech-savvy users, but not so good for normal users. So I want to provide an installer for users.

Thankfully, Java has a tool named jpackage specifically for that purpose. I used it to produce installers for both Linux and Windows.

Here was the recipe for Linux RPM:

jpackage --input target/obfuscated \
	 --name backdrop \
	 --verbose \
	 --description "Backdrop: beautifying screenshots in seconds" \
	 --app-version "0.3" \
	 --icon ./resources/misc/icon.png \
	 --main-jar $(JAR) \
	 --main-class backdrop.core \
	 --type rpm \
	 --linux-shortcut \
	 --linux-menu-group "Graphics;Office;Utility;" \
	 --resource-dir ./build/ \

But it was trickier for Windows, as jpackage needed to use another external tool called Wix to build the installer. Here was the recipe for Windows:

jpackage --input target\obfuscated ^
  --dest c:/jpackage/ ^
  --name backdrop ^
  --verbose ^
  --description "Backdrop: beautifying screenshots in seconds" ^
  --app-version "0.3" ^
  --icon assets/images/logo-transparent.ico ^
  --main-jar %JAR% ^
  --main-class backdrop.core ^
  --type msi ^
  --win-shortcut ^
  --win-menu ^
  --win-menu-group "Backdrop"

Pros and Cons

The benefits of cljfx development are that it's reactive and supports interactive development.

On the flip side, the startup time is very slow. Backdrop takes about 8 seconds to launch in my normal Linux box (without any optimization), which is definitely too long for end users. Another minor issue was that the jar file and, thus, the installers were too large. As a small app, the Backdrop rpm file takes more than 100MB of disk space.

I don't want to have this type of slowness in the future. And recently, I did a bit more research and found that there were two more options for building desktop and mobile apps in Clojure: ClojureDart (cljd) and ClojureScript + React Native (cljsrn). Cljsrn has a complicated toolchain, so I may use ClojureDart for future projects.


See also

comments powered by Disqus