The Desktop Companion-App Pattern – Java Edition
Provide an enhanced experience to your web app users with a desktop companion app.
In 2004, it was “Desktop first”. Your website would essentially be a brochure to promote your desktop app that you’d written in Swing or JavaFX, and it would direct the user to download your app. In 2022 this has shifted to “web first”, where the website is the app, but that doesn’t have to mean “web only”. Many web sites (Slack and Zoom to name a couple), provide a desktop version of their apps for users who prefer it. This pattern, known as “the desktop companion app pattern”, is an effective way to provide an enhanced experience or functionality that requires better performance and/or tighter integration with the local environment than a web app can provide.
How it works
The desktop companion pattern works by associating your desktop app with a custom URL scheme so that, when users click on a link with this scheme in their browser, it will automatically open your app (if it is installed), with the URL being passed to the app so that it can respond appropriately.
For example, suppose you want to give the user the option to edit their user profile using a desktop companion app. You could achieve this by creating a custom URL scheme for your app. In this case we want URLs like “profilemaker://new” to open your app, and show a “new profile” form. In the website, we might have a link like:
<a href="profilemaker://new">Create Profile in Desktop App</a>
You could also use Javascript to programmatically open your app with code like:
window.location.href='profilemaker://new';
You could also trigger the app to open from the server-side using the Location header. E.g.
<?php
header("Location: profilemaker://new");
In order for this to work, you need to associate the “profilemaker” URL scheme with your app. This is trivial if you’ve deployed your app using jDeploy, as I’ll describe in a moment.
If the user has already installed your app, then when the user clicks on this link (or you try to programmatically open such a link), the browser will prompt them to ask if they want to open the link in your app. If the user clicks “Yes”, then your app will be opened, and the URL itself will be passed to the app so that it can respond appropriately. In our case, we would want the app to show a “New profile” form.
If the user has not installed your app yet, then clicking on this link will do nothing.
The trickiest part of this pattern is crafting a workflow that will let the user know to download your app if it isn’t installed yet, but to automatically open your app if it is installed. If you browse through Stack Overflow there are many hacks listed to try to detect if an app is installed using Javascript, but none of these will work reliably, and even techniques that work today, may not work tomorrow, because browsers don’t “want” javascript to be able to know this for security reasons. You probably don’t want random web pages to be able to sniff around to see if you have a program installed.
The most common work-flow that provides a relatively seamless user experience for all users is to just add message in the web page that says something like:
If the desktop app doesn’t open within 5 seconds, you may need to install it. Download the latest version here.
You could also simply add a note below or beside the link. E.g.
<a href="profilemaker://new">
Create New Profile in Desktop App
</a>
<br/>
Requires Profile Maker Desktop to be installed.
<a href="https://www.jdeploy.com/~profilemaker">
Download now
</a>
Alternatively, you can create a “Launching” webpage that will programmatically try to open your app using a Javascript redirect, and provides instructions on how to install the app. For example, the following image shows Zoom’s “launch” page:
Supporting Custom URL Schemes in Java Desktop Apps
Adding custom URL support in your Java app is trivial if you are deploying your app using jDeploy.
There are two parts:
Registering the custom URL scheme
Handling the custom URLs inside your app
You can register the custom URL scheme using either the jDeploy GUI, or by adding the configuration manually to your app’s package.json file.
TIP: Custom URL schemes are documented in detail in the jDeploy developer guide.
In the jDeploy GUI, you would click on the “URLs” tab, and enter the custom URLs you want to support in the field provided.
The following screenshot shows an app that supports the “jdtext” URL scheme, meaning that the app would be launched in response to urls like “jdtext://foobar/bazz/foo”
The equivalent configuration in the package.json file (if you prefer to do it manually) would look like:
"jdeploy" : {
...
"urlSchemes" : ["jdtext"]
...
}
Handling the URLs inside your app works slightly differently on Linux and Windows than it does on Mac. On Linux and Windows, the URL will be provided as the first argument in your app’s main()
method.
E.g.
public static void main(String[] args) {
if (args.length > 0) {
if (args[0].equals("profilemaker://new")) {
showNewProfileForm();
}
}
}
On Mac, we’ll need to use the OpenURIHandler interface that was added to the java.awt.desktop
package in JDK 9.
import java.awt.Desktop;
import java.awt.desktop.OpenURIEvent;
import java.awt.desktop.OpenURIHandler;
public class HelloApplication {
static {
try {
Desktop.getDesktop().setOpenURIHandler(new URIHandler());
} catch (Exception ex){}
}
...
public static class URIHandler implements OpenURIHandler {
@Override
public void openURI(OpenURIEvent e) {
if (e.getURI().toString().equals("profilemaker://new") {
showNewProfileForm();
}
}
}
...
}
The reason why we do this differently on Mac than on Windows and Linux is because on the Mac, there is only ever one instance of your app running at a time. If the user launches your app and it’s already opened, it doesn’t call the main()
method again, so you won’t be able to get the URL from the args
. Instead it triggers the OpenURIHandler
callback in your app with the URL that was used to launch your app.
On Windows and Linux, your app can be launched multiple times, and each launch will open a different instance of your app in its own process, so the main()
method is called each time. If you prefer the Mac behaviour of running a single instance, there are a few techniques that you can use to achieve this, but they all boil down to detecting if your app is already running when it is launched, and if so, pass a message to the already running instance of your app to let it know that the user has requested it to open, and then exit the new instance. I’ll write about this in more detail in a future post.
Keeping your Desktop App Version in Sync with the Website
The biggest problem with the companion app pattern is that it requires users to install your desktop app. In some cases, you may also need to make sure that the desktop app version stays in sync with the web site. jDeploy can help to smooth over these pain points with its auto-update feature. As soon as you deploy a new version of your desktop app, it will be available to users, and will automatically download the next time they launch your app. If keeping the desktop app version synchronized with the web version is a hard requirement, you should also have your app periodically check the version, and prompt the user to relaunch to receive updates when they are available.
Highlighting Java’s Awesomeness as a Desktop Platform in 2022
I’m writing this newsletter to help raise awareness to the awesomeness that Java still has to offer on the desktop. Articles are a mix of editorials, tutorials, and historical melodramas related to Java and the desktop. I try to post a new one every week. If you enjoy reading this newsletter, please consider subscribing, so that you will be informed first when new articles are published. Also, please comment below if this post has inspired any thoughts, memories, or rants. I always enjoy reading other’s perspectives on these topics.