React native support works fine but this section has not been updated with the most
recent API. Also, some sbt plugins have been published to help copy and manage
the scala.js artifacts without defining your plugins like shown below.
You can easily create react native applications using this library. react native
uses a "runner" application (unlike flutter) to host the javascript execution
engine. You will need the runner skeleton to build a react native
application. The best way to do that is to follow the instructions for
installing
react-native-cli
and then overlaying the scalajs library on top of the generated project.
To overlay the scala.js parts (until I create a nice g8 template with this
already in it):
Create build.sbt as you normally would.
Create the project folder as you normally would.
Create source folder mkdir -p src/main/scala content at the top-level.
Include the core libraries:
scalajsReactVersion = ...
libraryDependencies ++= Seq(
"ttg" %%% "react" % scalajsReactVersion,
"ttg" %%% "native" % scalajsReactVersion
)
in your library dependencies. Create your build.sbt build file as you would for
any scala project including scala flags, etc. You can copy portions of the
build.sbt file at scala.js react template.
Alter the index.js in the top level directory to either contain the "exported to
js" component or you can call AppRegistry.registerComponent inside of
scalajs. The scala.js function that you would call in index.js must be top level
exported (@JSExportTopLevel). In either case, the index.js should either use
your exported JS "app" component or call a function that calls
registerComponent.
You may want to include "org.scala-js" %%% "scala-js-dom" % "latest.version"
to include scala support for some of the available polyfills such as
fetch. There is no DOM on the mobile devices, so the DOM parts of this library
are not available and you should not use them.
If you want to copy your scalajs output to a well known location so that you
only need to change your index.js (located in the toplevel directory) once, you
can set up a file copy task that copies the output of fullOptJS or
fastOptJS. You would run this task after the scala.js full/fast
processing. index.js is the default starting point for javascript bundling when
using the react-native cli such as react-native run-android.
The metro bundler looks for index.js to create the javascript resource
graph. Instead of modifying the output of sbt, you can also use build vars in
javascript to switch between targets--a typical approach in javascript
e.g. include min if you are in a dev build. Below is the copy approach. If your
final scala.js project that creates the linked javascript artifact is always the
same, it is probably easier to hardcode the path with conditionals in index.js.
However, let's just assume that you want to keep index.js unchanged and want to
map full or fast builds to the same output file.
You will want to scope the task to the respective scala.js linkage tasks,
fastOptJS or fullOptJS so that the correct artifactPath is picked up.
There are many ways to do this in sbt including using artifactPath (scoped to
the fast or full task inside Compile) to copy the file and standardize its output
name for react-native.
If you need to copy per task you could also create a plugin or do something
small like:
def copyTask(odir: String) = {
//lazy val copyOutputDir = settingKey[String]("target directory for copying output to")
lazy val copyJSOutput = taskKey[Unit]("copy scala.js linker outputs to another location")
Seq(
copyJSOutput := {
println(s"Copying artifact ${scalaJSLinkedFile.in(Compile).value.path} to [${odir}]")
val src = file(scalaJSLinkedFile.in(Compile).value.path)
Managing task running in sbt is covered in the
manual as there are a few
different ways to set the above copy operation up some of which are more simple
then defining a task. A really great blog on sbt tasks is
here, you should read
it just to be smarter about sbt.
Regardless of whether you call registerComponent in scala or JS, you need to
create the application component. That's easy. Don't forget to "wrap" it for JS
use.
In the code below, the component itself is exported so you can call
registerComponent in index.js. If you call registerComponent in scala as
show above, you do not need to export the anything related to the component but
it still need to be wrapped.
@JSExportTopLevel("App")
object App {
val Name = "App"
val c = statelessComponent(Name)
import c.ops._
def apply() = render { self =>
View()(
Text()("This is some text.")
)
}
@JSExport("JS")
val JS = c.wrapScalaForJs[js.Object](_ => App())
}
The top level is usually a stateless component as it is called by the
react-native framework and does not take any arguments.
If you have your exports setup using either approach, your index.js should
include the scala.js output:
// index.js
// Adjust for your output or use the copy method described above.
// We assume you used the "copy to a well know location" below.
// Scala.js runs registerComponent inside Main.
import{Main}from"./Scala.js"
// JS calls registerComponent inside index.js.
import{App}from"./Scala.js"
// Let Main run...
Main.main()
// or let javascript run...
AppRegistry.registerComponent(appName,()=>App.JS)
If you want to switch on the build type in javascript and skip the sbt
configuration above, use an ES6 feature with dynamic imports (make sure its
enabled in your environment if you need to):
if(process.env.NODE_ENV === "production") then {
const Main = import("./target/scala-2.12/app-opt.js")
}
else {
const Main = import("./target/scala-2.12/app-fastopt.js")
}
If you don't have ES6 support you could use the legacy "require(...)" which
should be supported for some time to come with most bundlers. The default
react-native metro should allow the dynamic import using import. If you use
this approach, make sure any imported resources such as images are available
based on a path relative to the js file as your scala or js imports inside of
app-opt.js/app-fastopt.js will now be relative to the target/scala-2.12
directory.
Run sbt as you normally would and during dev and use ~fastOptJS if you used
the triggered approach to perform the scala.js output copy. That allows you to
recompile as needed. react native uses its own JS packager, called metro, that
restructures your JS similar to webpack.
metro is not as feature-rich as webpack. When you run react-native run-android
it first runs gradle to build the java part of the project and it starts up a JS
server similar to the way that webpack-dev-server works. It is suppose to detect
changes in js files and do a hot reload. You may need to turn on hot reloading
using Ctrl+M (linux/windows hosted emulators).
While you should install android studio because you will probably need to write
some interop at some point, you can start the emulator without starting android
studio:
# List emulator devices.
$ emulator -list-devs
# Start an emulator I defined in android studio.
$ emulator -avd Pixel_2_XL_API_28 -no-boot-anim
You will want to add:
export ANDROID_HOME=$HOME/Android/Sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/tools/bin
export PATH=$PATH:$ANDROID_HOME/platform-tools
to a shell script you can import into your current terminal.
There are some ports of common libraries, all WIP and some have no code yet :-):
react-navigation (working)
sideswipe (working)
nativebase (no code yet)
react-native-elements (no code yet)
Creating facades is easy. It only took 3 hours to create the entire facade for
react-native from scratch when I did not even know react-native. I just look at
the typescript definitions and develop a scala.js friendly API from there. I
believe that a good combination is to use scala.js and typescript together.