Why I Don't Regret Moving Our Android App to Scala
Gregg Hernandez
Reading time: about 12 min
Topics:
Why Scala?
At Lucid Software, we already have a large back-end code base consisting primarily of Scala micro services. After experimenting with Kotlin for a short time, we ultimately felt it would be a good idea to bring our already well-established skills with Scala to Android. Using sbt for our Android app has also allowed us to leverage our unified build system and easily set up automated builds alongside the rest of our projects. Besides practical considerations, there are a lot of technical reasons for choosing Scala. I want to highlight some of these reasons by way of example below, and hopefully I will be able to illustrate how Scala can dramatically improve your experience while developing Android applications.sbt-android
The heart of developing Android apps with Scala is sbt-android. This plugin provides a number of sbt tasks that allow you to easily build and run your application from an sbt console. It even downloads and updates the Android SDK for you automatically. The README in the github project does a great job of explaining how to set up a new project with sbt-android and how to configure Intellij (or Android Studio). The development workflow can also be sped up by introducing the sbt-android-protify plugin. This allows for behavior that is similar to Android Studio's "Instant Run" but (in my experience) is much more reliable. TypedResource and TypedViewHolder are types provided by sbt-android to simplify and improve the safety of dealing with layouts.TypedResource and TypedViewHolder
During the normal Android build process, a file called R.java is produced that contains identifiers for any static resources like layouts or images (anything in the res/ directory). A common usage of this file is to look up views in layouts. Combined with findViewById, you end up with code that looks like this:findViewById(R.id.sendButton).asInstanceOf[ImageButton]
The obvious downside of this is that findViewById returns a view and requires you to cast to a more specific type that (hopefully) matches the type of view that is actually in the layout. If it doesn't match, you'll get a runtime exception and probably a crash. In addition to the standard R.java, sbt-android also generates a file called TR.scala. The code here is very similar to what is found in R.java except it preserves type information about views. In an activity that implements the TypedFindView trait, you can write the above code like this:
findView(TR.id.sendButton)
This is great! But it will still return a null reference in cases where the view doesn't exist in the current hierarchy. So sbt-android takes it a step further and provides TypedViewHolder, which basically wraps your layout in a type safe case class. Let's say you have roughly the following main.xml layout file:
<LinearLayout>
<TextView android:id="@+id/text" />
<ImageView android:id="@+id/image" />
</LinearLayout>
The following code in an activity will give you structured and typed access to the views:
private lazy val views: TypedViewHolder.main = TypedViewHodler.setContentView(this, TR.layout.main)
override def onCreate(state: Bundle): Unit = {
views.text.addTextchangedlistener(???)
views.image.setImageDrawable(TR.drawable.image.value(this))
...
Now you can use views.text to access the TextView and views.image to access the ImageView instead of calling findViewById or findView. You'll notice that lazy val was leveraged to make sure views wasn't created until the first usage in onCreate. This is a common approach when writing Android apps in Scala. Instead of creating mutable vars and initializing them in onCreate, it is often more convenient to make them immutable lazy vals so they are simply initialized on their first usage. This works well for anything that won't change between onCreate calls.
The call to TR.drawable.image.value(this) is a simple way to inflate a static resource into a concrete Android object without the ceremony of looking up the resource. This line could be simplified further by bringing an implicit context into scope:
implicit lazy val ctx = this
override def onCreate(state: Bundle): Unit = {
views.image.setImageDrawable(TR.drawable.image)
}
It is a common pattern to introduce the context via an implicit and to accept the context as an implicit parameter to avoid the boilerplate of constantly passing it around.
Replacing AsyncTask
Scala has a pretty decent concurrency primitive in Future. In Lucidchart, the primary use case is to make sure that long-running tasks stay off the main thread. Any time we make network requests or read data from disk, we do this off the main thread via a Future.class DocumentListActivity extends Activity {
override def onCreate(state: Bundle): Unit = {
// show loading indicator
val loadingTask = Future {
loadDocumentsFromNetwork()
}
loadingTask.onComplete { case _ =>
// hide loading indicator
}
...
}
}
The Future does need an implicit ExecutionContext in scope, so it knows which thread pool to execute on. You can supply one created from AsyncTask's thread pool like this:
implicit lazy val ec = ExecutionContext.fromExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
This will make it so any Future using ec will run on the same thread pool that an AsyncTask would normally execute on.
Transitioning to sbt
If you already have an Android app, it probably uses Gradle for its build system. While you can use Scala with Gradle, it does not work well in an Android project. gradle-android-scala-plugin exists in an attempt to support Scala for Android, but it hasn't been actively maintained for some time now. The documentation suggests that it only works up to API 23. Thankfully with sbt-android-gradle, you can automatically import Gradle settings into an sbt project. When this plugin is enabled, you can use and maintain your existing Gradle configuration while using sbt for your day-to-day development tasks. Whether or not you end up using Scala, there are still some notable advantages of using sbt in this way. Debugging over Wi-Fi is much simpler. Instead of following the instructions here, you can easily enable Wi-Fi debugging with the adb-wifi command (after selecting a device with device <device-id>). Then any time you get disconnected for some reason, you can easily reconnect Wi-FI debugging with adb-wifi-reconnect. The sbt-android-gradle plugin made transitioning from a Kotlin/Gradle build to a Scala/sbt build a simple incremental process. sbt-android-gradle generates .sbt files that you can base your final sbt configuration on. They aren't the prettiest, but they get the job done, and they are easy enough to modify. When we were transitioning, I initially enabled sbt-android-gradle. Once that was working well, and I got used to the workflow, I basically just dropped all the gradle configuration, slightly modified the generated sbt configuration, and ran with that.Transitioning from Kotlin to Scala
If your app is small-ish, just rewriting your code all at once isn't so bad. However, if you need to make it an incremental process, it becomes a little trickier. The main problem is that Kotlin and Scala can’t reference each other in the same project. This makes it impossible to share code between the two languages the way you can with Java and Kotlin or Java and Scala. The workaround is to create a subproject for one of the languages. Since we started with Kotlin and moved to Scala, we set it up so that the Kotlin project was the root project that gets built, packaged, and shipped. The Scala subproject was included in the Kotlin project as a dependency. This way we could implement new features in Scala and still launch activities or create fragments from inside the Kotlin project. Once this was set up, it was easy enough to port to Scala a class at a time. There are some surprising interoperability issues when calling Scala code from Kotlin. Much of this applies to interoperability with Java as well.Getters
Scala generates public getters for all public members (whether or not you are using case classes). However, Kotlin doesn't automatically understand Scala's getters. The following Scala classclass TheBestClass(val data: String)
gets compiled into roughly
// this won't actually compile because of using the same name for the val and def, but it's more or less equivalent to what the Scala compiler generates
class TheBestClass {
private val data: String = _
def data: String = this.data
}
in Kotlin:
val theBestClass = TheBestClass()
theBestClass.data
This fails with an error because data is private. The fix is to call data as a function: theBestClass.data(). However, it might be kind of annoying to add parentheses everywhere you use data. Kotlin does understand Java Bean style getters. So adding a getData method to the original Scala class will allow the above Kotlin code to work without any problems.
Alternatively, you can have Scala generate the getter for you automatically with the @BeanProperty annotation:
class TheBestClass(@BeanProperty val data: String)
For var, a similar concept applies for generated setters.
Anonymous Functions
I also liberally used wrapper classes and methods to simplify using Scala standard library classes from within Kotlin code. It's fairly difficult to create a Scala-style anonymous function from within Kotlin, so instead when they are needed, I would use a trait with a single method in Scala. Kotlin can automatically convert from an anonymous function to an implementation of the trait. For example, to use Scala's Option.map, you could approach it like this:// Scala
trait Callback[In, Out] {
def call(in: In): Out
}
def map[In, Out](opt: Option[In], f: Callback[In, Out]) = opt.map(f.call _)
// Kotlin
map(opt) { it * 2 }
You could make this even more seamless by creating a Kotlin extension method on Option<A> that calls the map above.
Multiple Parameters Lists
When a Scala method uses multiple parameters lists (including the implicit parameter list), once compiled, the method will have a single parameter list of all parameters lists flattened into one. In Kotlin, you can call these methods by just passing each parameter.// Scala
def f(a: String)(b: Int)(implicit c: Context): Unit = {}
// Kotlin
f("hi", 10, this)
Kotlin and Scala both support default parameters, but they don't understand each other’s default parameters. That means you have to leverage overloading like you would in Java if you want to simulate default parameters. So instead of writing def f(i: Int = 100, j: Int = 10), you would write overloading versions if you want defaults from Kotlin's perspective:
// Scala
def f(i: Int, j: Int): Int = i * j
def f(i: Int): Int = f(i, 10)
def f(): Int = f(100, 10)
// Kotlin
f(1, 2)
f(1)
f()
Traits
Scala's traits get compiled to plain Java interfaces. If you provide default implementations of methods in your trait, they get compiled into an interface with no implementations plus static methods in a companion object that represent the default implementations. The Scala compiler then fills in the implementations as needed. That means, when using a Scala trait from Kotlin, your implementations don't get applied and you would have to manually supply them. You can often just use an abstract class instead. As a general rule, the principle of least power suggests that you favor abstract classes.Other Useful Tools and Libraries
As we've developed our application and added features, we have found (and created) some great tools for building Android apps in Scala. You can of course use all of the common Android libraries like Facebook's Stetho, Picasso, OkHttp, etc. But you also gain the ability to use some really great tools that are primarily for Scala like cats, circe, and nscala-time. We also wanted to use Google's new Room Persistence Library, so I created a plugin that makes this possible while using sbt-android called (very creatively) sbt-android-room. Give Scala on Android a spin. We'd love to hear your feedback in the comments here. I also frequent the sbt-android gitter channel where you can get more or less real-time help with the various Android-related sbt plugins.About Lucid
Lucid Software is a pioneer and leader in visual collaboration dedicated to helping teams build the future. With its products—Lucidchart, Lucidspark, and Lucidscale—teams are supported from ideation to execution and are empowered to align around a shared vision, clarify complexity, and collaborate visually, no matter where they are. Lucid is proud to serve top businesses around the world, including customers such as Google, GE, and NBC Universal, and 99% of the Fortune 500. Lucid partners with industry leaders, including Google, Atlassian, and Microsoft. Since its founding, Lucid has received numerous awards for its products, business, and workplace culture. For more information, visit lucid.co.