Cross-Platform Mobile dev with Scala and Capacitor

Antoine Doeraene
Jul 4 · 7 min read

These last years, the trend has been to export web technologies out of the browser, into the desktop (e.g., electron) and more and more into Mobile Applications (e.g., Ionic). Whether or not we like this trend, it is in any case a bargain for any technology that can produce HTML, CSS and JavaScript. One such technology is Scala, via its Scala-to-JavaScript compiler, Scala.js.

In this blog post, we will start from scratch and arrive at a convenient setup for building cross-platform mobile application, using Scala and Capacitor. However, if you prefer to TLDR, you can directly check the final result.

Step 1: The sbt project for Scala.js

The first thing we need to do is to setup the barebones project for using Scala.js. We will use sbt, and in that case the minimal project structure looks like this:

root
├── build.sbt
├── project
| ├── plugins.sbt
| └── build.properties
└── src
└── main
└── scala
└── main
└── Main.scala

Where the contents of build.properties is sbt.version=1.5.4 (latest version as of today) and the contents of the build.sbt is

val theScalaVersion = "2.13.6"

lazy val root = project
.in(file("."))
.enablePlugins(ScalaJSPlugin)
.settings(
name := "ScalaJS-Capacitor",
version := "0.1.0",
scalaVersion := theScalaVersion,
scalaJSUseMainModuleInitializer := true,
scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) },
)

In the plugins.sbt file, we need

addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.6.0")

Finally, the contents of the Main.scala file could be

package mainobject Main {
def main(args: Array[String]): Unit = {
println("Hello!")
}
}

With this setup, we can do sbt run and we will see “Hello!” printed to the console. What we don’t see here is that it was actually a Node.js process who ran this line of code. Note: you need to have Node.js installed on your machine. The generated JavaScript code will be available at target/scala-2.13/scalajs-Capacitor-fastopt/main.js .

Step 2: Setup Snowpack (or any other packager)

Scala-js generates the JavaScript that corresponds to the Scala code. However, in order to integrate via the JavaScript ecosystem, it’s a good thing to have a dedicated node packager. Indeed, if we want to use some capacitor plugin (and we do!), it’s handier to rely on such tool.

The most celebrated one is probably Webpack, but one that is particularly suited for working with Scala-js is Snowpack. Snowpack’s setup is quite small. We need two additional files for Snowpack itself (a package.json and a snowpack.config.js ) with the following contents.

In package.json :

{
"name": "snowpack-capacitor",
"devDependencies": {
"snowpack": "3.1.0"
}
}

And insnowpack.config.js :

module.exports = {
buildOptions: {
out: "./target/build",
},
mount: {
public: "/",
"target/scala-2.13/scalajs-capacitor-fastopt": "/",
"src/main/resources": "/"
},
}

What we just did in the first file is to tell npm to use the version 3.1.0 of Snowpack (only as a development dependencies since we don’t need it when we ship the code). We thus need to run npm install in order to install Snowpack. In the second file, we instructed Snowpack to consider as the root directory, all files inside the public directory, the directory where our Scala code gets compiled and, for the sake of it, also the resources folder of our project (so that it feels like a JVM Scala project in that regard).

The last thing we need before the Snowpack setup is complete is to create an HTML file, as public/index.html and fill it with the following (minimal) content:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<title>Test Capacitor</title>
</head>
<body>
<div id="root"></div>
<script src="/main.js" type="module"></script>
</body>
</html>

After that, we can do sbt fastLinkJS and then npx snowpack dev and the browser will open to a blank page, where “hello” has been printed out in the JS console.

Step 3: Adding a UI Framework

Web technologies are all about manipulating the HTML and CSS that the user will see on the screen. And for that, we need a UI Framework (nowadays, no one manipulates the dom by hand). The best in class in the Scala.js ecosystem is probably Laminar.

Adding Laminar to the project is done by adding the line

libraryDependencies += "com.raquo" %%% "laminar" % "0.13.0"

in the build.sbt file, in the project settings. We can now change the Main.scala file in order to actually see something on the screen. We can add the lines

import com.raquo.laminar.api.L._
import org.scalajs.dom
val app = h1("Hello world!")
render(dom.document.getElementById("root"), app)

If the Snowpack dev server is still running, we can issue sbt fastLinkJS again and the browser page should refresh, showing the greeting message.

Step 4: Adding Capacitor

Up until know, we haven’t done any mobile at all. We have a nice setup for making an application in the browser, but we can’t use it for mobile. For that, we need to add the npm dependency to Capacitor. We do so by adding the following lines to the package.json file

"dependencies": {
"@capacitor/cli": "3.0.0",
"@capacitor/core": "3.0.0"
},

and then running npm install once again. After that, we can use the Capacitor cli to configure the barebones of the app. Following here, we need to run npx cap init (defaults are good, except for the “Web asset directory”, which needs to be target/build since this is how we configured Snowpack). That command will merely create a file capacitor.config.json with minimal contents in it.

Step 5: Adding Android support

We still didn’t achieve actual mobile development. This is what this step is about, by following instructions from here. In the dependencies of the package.json file, we add the line "@capacitor/android": "3.0.0" and we run npm install once again.

Finally, we can run our app on mobile. We need to run the three last commands

npx cap add android
npx snowpack build
npx cap run android

whose role are to add an Android app to the project, building the application using snowpack and finally run the Android emulator (you will need to have Android Studio installed).

Note: Similar steps would be required to build the app for iOS, but I refer to the official doc for that.

This is great! However, we still did not win anything with respect to simply making a responsive web page. In the two last steps, we will use one of Capacitor’s plugin to access native feature of our device.

Step 6: Adding ScalablyTyped

Capacitor plugins are distributed as TypeScript modules. In order to use those, we need to tell Scala of their existence (and the classes/functions/stuff they contain). This can be done automatically for us by ScalablyTyped.

Note: you can also write this by hand if you prefer, but relying on ScalablyTyped has advantages such as ensured correctness, discoverability…

We need three things for that. First the plugin. In project/plugins.sbt we add the line

addSbtPlugin("org.scalablytyped.converter" % "sbt-converter" % "1.0.0-beta32")

Then, we need TypeScript. We can add it as a devDependencies in the package.json file (of course, we also need to install it):

"typescript": "4.1.3"

and then we need to add the plugin to our project via

.enablePlugins(ScalablyTypedConverterExternalNpmPlugin)

together with a few lines in the settings of our project inside the build.sbt

import scala.sys.process.Process
[...]
externalNpm
:= {
Process("npm", baseDirectory.value).!
baseDirectory.value
},
stIgnore ++= List(
"@capacitor/android",
"@capacitor/cli",
"@capacitor/core"
)

And we’re all set for this. You can verify that everything is in place by running sbt compile .

Note: on MacOS, if you try to refresh the project inside IntelliJ, you will likely hit this, but the fix works fine.

Step 7: Using the Geolocation Plugin

As an example of using a Capacitor plugin, we will display the position of the device when the page loads. This will require the Geolocation Plugin.

Add the following to the dependencies of the package.json (and install it):

"@capacitor/geolocation": "1.0.0"

You can once sbt compile in order for ScalablyTyped to kick in, which will take a little bit while but after that, you will be able to use the plugin freely. As an example, let’s add the functionality to display the coordinates of the user when the page loads. For that, we can change our Main.scala file with the following lines:

import typings.capacitorGeolocation.definitionsMod.PositionOptions
import typings.capacitorGeolocation.mod.Geolocation
[...]
val app = div(
h1("Hello world!"),
child <-- EventStream .fromJsPromise(Geolocation.getCurrentPosition(PositionOptions().setEnableHighAccuracy(true))
)
.map { position =>
s"Your position: ${position.coords.latitude}, ${position.coords.longitude}"
}
)

If you run sbt fastLinkJS and npx snowpack dev once more, you should see your position displayed in the browser (it will probably ask for your permission).

For the Android application, however, it’s not enough. We need to tell the AndroidManifest.xml (inside android/app/src/main ) that we will use the Geolocation capabilities, and that is done by adding the lines

<!-- Geolocation API -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature android:name="android.hardware.location.gps" />

Now we can finish with the following commands:

npx snowpack build
npx cap sync
npx cap run android

and voilà! We have a working Android application, written entirely in Scala and using native features of the device.

Other improvements

There are many things that we could improve to enhance this setup. Among others, here are a few ideas:

  • Having hot reload (see, e.g., here)
  • currently the build is made using the “fast optimisation” mode of Scala.js, that we could/should change to using the “full optimisation”
  • Handling capacitor commands directly within sbt

Closing words and related works

We are now in a good position to do mobile development in Scala! And this with a relatively straightforward and minimal shenanigan setup. The only strong dependance we have is on Capacitor, which is an open source and independent (from Google and Apple) project. For the rest, it is a usual Scala project, coming with all the goodies that we like..

Before closing, we would like to mention this blog post for an overview of the history of Scala for mobile development, presenting also an alternative using React Native.

The result of this blog post can be found in the accompanying repo.

Geek Culture

Proud to geek out. Follow to join our +1.5M monthly readers.