Scala is cool. Writing Android applications with Scala is also a cool idea. Because Android exposes Java API, we need some utility library to leverage full power of Scala. Scaloid is a library aimed to simplify your Android code.
For example, the code block shown below:
val button = new Button(context)
button.setText("Greet")
button.setOnClickListener(new OnClickListener() {
def onClick(v: View) {
Toast.makeText(context, "Hello!", Toast.LENGTH_SHORT).show()
}
})
layout.addView(button)
is reduced to:
layout += SButton("Greet", toast("Hello!"))
- Write elegant Android software
Scaloid provides a concise and type-safe way of writing Android application. - Simple to use
This is a single-file project. Just copy-and-pastecommon.scala
into your project! - Compatible with your legacy code
You can use both Scaloid and plain-old Java Android API. You can gradually improve your legacy code. - Maintained actively
Scaloid is a dogfooding software. This is originally created to be used for my own Android apps.
If you want to see how Scaloid can be used in action, check a Scala port of apidemos app.
There is an out-of-the-box solution. Just fork this project and start your Scala-Android app.
- UI Layout without XML
- Implicit conversions
- Shorter representation without context object
- Shorter listeners
- Asynchronous processing
- Smarter logging
- Improved getters/setters
- Concise dialog builder
...and many other things!
Android SDK leverages XML to build UI layouts. However, XML considered still a bit verbose, and lacks programmability. Scaloid composes UI layout in Scala DSL style, therefore achieve both clarity and programmability. For example, suppose a legacy XML layout as shown below:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content" android:padding="20dip">
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content" android:text="Sign in"
android:layout_marginBottom="25dip" android:textSize="24.5sp"/>
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content" android:text="ID"/>
<EditText android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/userId"/>
<TextView android:layout_width="match_parent"
android:layout_height="wrap_content" android:text="Password"/>
<EditText android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/password"
android:inputType="textPassword"/>
<Button android:layout_width="match_parent"
android:layout_height="wrap_content" android:id="@+id/signin"
android:text="Sign in"/>
<LinearLayout android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button android:text="Help" android:id="@+id/help"
android:layout_width="match_parent" android:layout_height="wrap_content"/>
<Button android:text="Sign up" android:id="@+id/signup"
android:layout_width="match_parent" android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
is reduced to:
new SLinearLayout {
orientation = VERTICAL
val id = SEditText()
val pass = SEditText() inputType TEXT_PASSWORD
+=(STextView("Sign in").textSize(24.5 sp).layout.marginBottom(25 dip).end)
+=(STextView("ID")) += id += STextView("Password") += pass
+=(SButton("Sign in"))
+=(new SLinearLayout {
+=(SButton("Help"))
+=(SButton("Sign up"))
})
}.padding(20 dip)
The layout description shown above is highly programmable. You can easily wire your logic into the layout:
new SLinearLayout {
orientation = VERTICAL
val id = SEditText()
val pass = SEditText() inputType TEXT_PASSWORD
+=(STextView("Sign in").textSize(24.5 sp).layout.marginBottom(25 dip).end)
+=(STextView("ID")) += id += STextView("Password") += pass
+=(SButton("Sign in", signin(id.text, pass.text)))
+=(new SLinearLayout {
+=(SButton("Help", openUri("http://help.url")))
+=(SButton("Sign up", openUri("http://signup.uri"))
})
}.padding(20 dip)
Because a Scaloid layout description is a plain Scala code, it is type-safe. Please refer to layout context for more details.
This converter turns an Android XML layout into a Scaloid layout:
Scaloid employs several implicit conversions. Some of available implicit conversions are shown below:
String => Uri
The functions such as play ringtones play()
or open URIs openUri()
takes an instance of Uri
as a parameter. However, we frequently have URIs as a String
. Scaloid implicitly converts String
into Uri
. Therefore, you can freely use String
when you play a ringtone:
play("content://media/internal/audio/media/50")
, open a URI:
openUri("http://google.com")
, or wherever you want.
Int => CharSequence
Int => Drawable
Android API provides two versions of methods for string resources; One for CharSequence
, the other for Int
as a resource ID. If you write a function that handles Android resource, you also have to expose methods for every combinations of two versions of resources:
def alert(titleId:Int, textId:Int)(implicit context:Context) = {
alert(context.getText(titleId), context.getText(textId))
}
def alert(titleId:Int, text:CharSequence)(implicit context:Context) = {
alert(context.getText(titleId), text)
}
def alert(titleId:CharSequence, textId:Int)(implicit context:Context) = {
alert(title, context.getText(textId))
}
def alert(title:CharSequence, text:CharSequence) = ...
This is not a smart way. Write just one method that defines the logic:
def alert(title:CharSequence, text:CharSequence) = ...
Then Scaloid implicit conversions will take care about these resource type conversions.
Units dip
and sp
can be converted into the pixel unit.
val inPixel:Int = (32 dip)
val inPixel2:Int = (22 sp)
( => Any) => Runnable
Runnable
also covered with rich and prefixed classes.
There are more implicit conversions available. Check the source code as needed.
Many methods in Android API requires an instance of a class Context
. Providing this for every method calls results a clumsy code. We employ an implicit parameter to eliminate this. Just declare an implicit value that represents current context:
implicit val context = ...
or just extend trait SContext
, which defines it for you. Then the codes that required Context
becomes much simpler, for example:
new Intent(context, classOf[MyActivity])
is reduced to:
SIntent[MyActivity]
startService(new Intent(context, classOf[MyService]))
stopService(new Intent(context, classOf[MyService]))
is reduced to:
startService[MyService]
stopService[MyService]
Toast.makeText(context, "hi, there!", Toast.LENGTH_SHORT).show()
is reduced to:
toast("hi, there!")
If you want toast longer:
longToast("long toast")
ProgressDialog.show(context, "Dialog", "working...", true)
is reduced to:
spinnerDialog("Dialog", "working...")
PendingIntent.getActivity(context, 0, new Intent(context, classOf[MyActivity]), 0)
PendingIntent.getService(context, 0, new Intent(context, classOf[MyService]), 0)
is reduced to:
pendingActivity[MyActivity]
pendingService[MyService]
PreferenceManager.getDefaultSharedPreferences(context)
is reduced to:
defaultSharedPreferences
Just play the default notification ringtone:
play()
, specify ringtone resources as a String
:
play("content://media/internal/audio/media/50")
, or specify a resource Uri
:
play(alarmSound)
This opens a web browser (or another view assigned to the http protocol).
openUri("http://google.com")
Getting system service objects become much simpler. The following legacy code:
val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE).asInstanceOf[Vibrator]
vibrator.vibrate(500)
is reduced to:
vibrator.vibrate(500)
Under the hood, Scaloid defines a function vibrator
like this:
def vibrator(implicit ctx: Context) = ctx.getSystemService(Context.VIBRATOR_SERVICE).asInstanceOf[Vibrator]
All of the system service accessors available in Android API level 8 are defined (e.g. audioManager
, alarmManager
, notificationManager
, etc.). The name of a system service accessor is the same of its class name, except that the first character is lowercased.
Suppose an Android class Foo
, for example, Scaloid defines an implicit conversion Foo => RichFoo
. The class RichFoo
defines additional methods for more convenient access to Foo
. This is a common pattern in Scala to extend existing API (see pimp-my-library pattern). This section describes various features added on existing Android API classes.
Android API defines many listener interfaces for callback notifications. For example, View.OnClickListener
is used to be notified when a view is clicked:
find[Button](R.id.search).setOnClickListener(new View.OnClickListener {
def onClick(v:View) {
openUri("http://google.com")
}
})
Scaloid provides a shortcut that dramatically reduces the length of the code:
find[Button](R.id.search).onClick(openUri("http://google.com"))
All other listener-appending methods such as .onKey()
, .onLongClick()
, and .onTouch()
are defined.
Some conventions we employed for method naming are:
- We omit
set...
,add...
, and...Listener
from the method name, which is less significant.
For example,.setOnKeyListener()
becomes.onKey()
. - Every method has two versions of parameters overridden. One is a lazy parameter, and another is a function which has full parameters defined in the original Android API. For example, these two usages are valid:
button.onClick(info("touched"))
button.onClick((v:View) => info("touched a button "+v))
- Methods
add...
is abbreviated with a method+=
if it is not a listener-appender.
For example,layout.addView(button)
becomeslayout += button
.
Methods beforeTextChanged()
, onTextChanged()
, and afterTextChanged()
are defined in RichTextView
, which can be implicitly converted from TextView
. It is more convenient than using TextWatcher
directly. For example:
inputField.beforeTextChanged(saveTextStatus())
is equivalent to:
inputField.addTextChangedListener(new TextWatcher {
def beforeTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
saveTextStatus()
}
def onTextChanged(p1: CharSequence, p2: Int, p3: Int, p4: Int) {}
def afterTextChanged(p1: Editable) {}
})
Also, we overrides beforeTextChanged()
with full parameters defined in the original listener:
inputField.beforeTextChanged((s:CharSequence, _:Int, _:Int) => saveText(s))
Other listeners in Android API can also be accessed in this way.
In Android API, layout information is stored into a View
object with a method View.setLayoutParams(ViewGroup.LayoutParams)
. A specific type of parameter passing into that method is determined by a the type of ...Layout
object which contains the View
object. For example, let us see some Java code shown below:
LinearLayout layout = new LinearLayout(context);
Button button = new Button(context);
button.setText("Click");
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams();
params.weight = 1.0f; // sets some value
button.setLayoutParams(params);
layout.addView(button);
Because the button is appended into the LinearLayout
, the layout parameter must be LinearLayout.LayoutParams
, otherwise a runtime error might be occurred. Meanwhile, Scaloid eliminate this burden, while still preserving regorous typing of LayoutParams
. The code shown below is equivalent to the previous Java code:
val layout = new SLinearLayout {
+=(SButton("Click").layout.Weight(1.0f).end)
}
In the anonymous constructor of 'SLinearLayout', Scaloid provides an implicit function called "layout context". This affects a return type of .layout
of the class SButton
. If we use SFrameLayout
as a layout context, .layout
returns FrameLayout.LayoutParams
, so the code below results a syntax error.
val layout = new SFrameLayout {
+=(SButton("Click").layout.Weight(1.0f).end) // Syntax error on Weight()
}
It is a pragmatical progress towards both simplicity and rigorous type checking.
As we noted, the function .layout
returns an object type of ViewGroup.LayoutParams
:
val params = SButton("Click").layout // type LayoutParams
This class provides some setters for chaining:
val params = SButton("Click").layout.bottomMargin(100).leftMargin(10) // type LayoutParams
if we want use the SButton
object again, Scaloid provides .end
method returning back to the object:
val button = SButton("Click").layout.bottomMargin(100).leftMargin(10).end // type SButton
When the layout context is nested, inner-most layout context is applied:
val layout = new SFrameLayout {
+=(new SLinearLayout {
+=(SButton("Click").layout.Weight(1.0f).end) // in context of SLinearLayout
})
}
When we get a LayoutParams
from .layout
, the default values of width
and height
properties are width = MATCH_PARENT
and height = WRAP_CONTENT
. You can override this when you need it:
SButton("Click").layout.Width(MATCH_PARENT).Height(MATCH_PARENT)
This is a very frequently used idiom. Therefore we provide a further shorthand:
SButton("Click").matchLayout
If you want the View
element to be wrapped,
SButton("Click").layout.Width(WRAP_CONTENT).Height(WRAP_CONTENT)
This also be shortened as:
SButton("Click").wrapLayout
Scaloid follows naming of XML attributes of Android API with some improvements.
For XML attributes, layout related properties prefixed with layout_
, while Scaloid does not need it. For boolean type attributes in which the default is false
, Scaloid simply flag it as true
when the attribute is declared explicitly without any parameter. For example:
new SRelativeLayout {
+=(STextView("hello").layout.centerHorizontal.alignParentBottom.end)
}
Scaloid omits unnecessary ="true"
for attribute centerHorizontal
. Equivalent XML layout description for TextView
is:
<TextView
android:id="@+id/helloText"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:text="hello"/>
Trait SContext
includes several shortcuts for frequently used android idioms.
Android API provides runOnUiThread()
only for class Activity
. Trait RunOnUiThread
provides Scala version of runOnUiThread()
for anywhere other than Activity
.
Instead of:
activity.runOnUiThread {
new Runnable() {
def run() {
debug("Running only in Activity class")
}
}
}
, extend trait RunOnUiThread
and use it like this:
runOnUiThread(debug("Running in any context"))
It is a very frequently used pattern that running a job asynchronously and notifying it back to UI thread. Although Android API provides an helper class AsyncTask
, implementing such a simple idea is still painful, even when we use Scala:
new AsyncTask[String, Void, String] {
def doInBackground(params: Array[String]) = {
doAJobTakesSomeTime(params)
}
override def onPostExecute(result: String) {
alert("Done!", result)
}
}.execute("param")
Using runOnUiThread
and importing scala.concurrent.ops._
, the asynchronous job shown above can be rewritten like this:
spawn {
val result = doAJobTakesSomeTime(params)
runOnUiThread(alert("Done!", result))
}
It is a great win as it exposes your idea clearly.
Just like we throw away AsyncTask
, we can also elliminate all other helpers for asynchronous job, such as AsyncQueryHandler
and AsyncTaskLoader
. Compare with the original Java code
and a Scala port of ApiDemos example app.
When you registered BroadcastReceiver
with Context.registerReceiver()
you have to unregister it to prevent memory leak. Trait UnregisterReceiverService handles these chores for you by just extend it for your Service.
class MyService extends UnregisterReceiverService {
def func() {
// ...
registerReceiver(receiver, intentFilter)
// Done! automatically unregistered at UnregisterReceiverService.onDestroy()
}
}
Instead of
findViewById(R.id.login).asInstanceOf[Button]
use a shorthand:
find[Button](R.id.login)
Although we provide this shorthand, Scaloid recommends to programmatically laying out UI, not with XML.
Unlike other logging frameworks, Android Logging API requires a String
tag for every log calls. We elliminate this by introducing implicit parameter. Define an implicit value type of LoggerTag
as shown:
implicit val tag = LoggerTag("MyAppTag")
or, extend trait TagUtil
or SContext
which defines the tag by default. Then you can simply log like this:
warn("Something happened!")
Other functions for every log levels (verbose()
, debug()
, info()
, warn()
, error()
and wtf()
) are available.
info("hello " + world)
A String
parameter passed with info()
is a lazy argument, so it is evaluated only if the logging is possible. Therefore the example shown above is equivalent to:
val tag = "MyAppTag"
if(Log.isLoggable(tag, Log.INFO)) Log.i(tag, "hello " + world)
You can use any type of setters listed below:
obj.setText("Hello")
Java bean styleobj.text = "Hello"
Assignment styleobj text "Hello"
DSL styleobj.text("Hello")
Method calling style
Compared with Java style getters and setters, for example:
new TextView(context) {
setText("Hello")
setTextSize(15)
}
that of Scala style clearly reveals the nature of the operations as shown below:
new STextView {
text = "Hello"
textSize = 15
}
Or, you can also chain the setters:
new STextView text "Hello" textSize 15
, which is a syntactic sugar of a method calling:
new STextView.text("Hello").textSize(15)
We recommend "assignment style" and "DSL style". Use assignment style when you emphasize that you are assigning something, or use DSL style when the code length of the assignee is short and need to be chained.
Note: Using .apply(String)
method on object STextView
, you can further reduce the code above like this:
STextView("Hello") textSize 15
Unlike most setters in Android API, our setters return the object itself. This feature can be used as a syntactic sugar when setters need to be chained or a function returning some object. For example, a snippet of Java code from ApiDemos that is shown below:
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
TextView textView = getGenericView();
textView.setText(getGroup(groupPosition).toString());
return textView;
}
is reduced to:
def getGroupView(groupPosition: Int, isExpanded: Boolean, convertView: View, parent: ViewGroup): View =
getGenericView.text = getGroup(groupPosition).toString
Design considerations on returning values: In C or Java, the assignment operator =
returns a right hand side object. However, chaining assignment operator is very rarely used in these languages. Assigning the same value to multiple variables might means that your code is badly designed (except some context such as involving intensive mathematical computations). However, in Scala DSLs, setters return a left hand side object, and chaining setters are more frequent. For example:
getGenericView text "hello" maxHeight 8
If you want to use scala style getters/setters, implicit conversion do the magic on native Android objects:
val v:TextView = ...
v.text = "Hello" // Valid code. Implicit conversion handles this.
However, if you use it in constructors, compiler cannot find correct implicit conversion:
def getInstance = new TextView(context) {
text = "Hello" // Compilation Error.
}
Therefore, we extended Android classes with the same name prefixed with a 'S' character:
def getInstance = new STextView {
text = "Hello" // OK.
}
These classes explicitly provide the extra methods that was provided implicitly.
Aditionally, prefixed classes supports implicit context value and additional syntactic sugars. For example, many classes has .apply(...)
methods for creating a new instance:
STextView("Hello")
SButton("title", onClickBehavior())
SIntent[MyActivity]
Design considerations on making prefixed classes: In modern programming language, using package (or namespace) is preferred than prefixing. However, when we use both classes from Android API and Scaloid, using package name is more verbose than prefixing class name itself (compare with common.Button
and SButton
) and can be confused when you use both classes at the same code. We choose pragmatism rather than discipline.
If the setter ends with ...Enabled
, Scaloid adds functions named enable...
and disable...
. For example:
SLinearLayout().disableVerticalScrollBar
is equivalent to:
SLinearLayout().verticalScrollBarEnabled(false)
A Scala-style builder for AlertDialog.
new AlertDialogBuilder(R.string.title, R.string.message) {
neutralButton()
}.show()
This displays an alert dialog with given string resources. We provide an equivalent shortcut:
alert(R.string.title, R.string.messag)
Also you can build a more complex dialog:
new AlertDialogBuilder("Exit the app", "Do you really want to exit?") {
positiveButton("Exit", finishTheApplication())
negativeButton("Cancel")
}.show()
The code above is equivalent to:
new AlertDialog.Builder(context)
.setTitle("Exit the app")
.setMessage("Do you really want to exit?")
.setPositiveButton("Exit", new DialogInterface.OnClickListener {
def onClick(dialog: DialogInterface, which: Int) {
finishTheApplication()
}
})
.setNegativeButton("Cancel", new DialogInterface.OnClickListener {
def onClick(dialog: DialogInterface, which: Int) {
dialog.cancel()
}
}).show()
Android API has some protected interfaces which has static fields, and inherited it in public classes. For example android.provider.ContactsContract.Contacts
inherits a protected interface android.provider.ContactsContract.ContactsColumns
, which defines a static field ContactsColumns.DISPLAY_NAME
. In Java code, you can access it with Contacts.DISPLAY_NAME
. However, Scala does not support accessing in this way (please refer this and this). It is a bad news for Android-Scala programmer. So we provide a workaround implementation for this problem. Just copy-and-paste Workaround.java
and declare import org.scaloid.Workarounds._
. Then you can use the interfaces publicly which is originally defined as protected.
For now, Scaloid is a single-file project. Just copy common.scala
and paste it to your project and declare import org.scaloid.common._
. Enjoy!
- This project can be built with Android API level 8 or higher and Scala version 2.9.1 or higher.
This project is in early stages, and I will grow it constantly. If you have any idea to improve Scaloid, feel free to open issues or post patches.
This software is licensed under the Apache 2 license.
-
Cover full Android API versions
Currently, only API level 8 is supported. Scaloid may not be compiled with below that API level, and new features introduced above that level are not covered. Some of the features to be covered are:- Fragment
- New system services
- Action bar
-
Build an example Android app
Finish a Scala port of apidemos app and try another. -
Build a dedicated website
-
Write a complete API document
-
Write the Beginner's guide
-
Build an example of laying out multiple UI
-
Cover full listener shortcuts
Such as EditTextPreference.setOnPreferenceChangeListener -
Cover openGL ES and renderscript
-
Automatically unregister SensorEventListener onStop()
-
Support Google services
Including Google Cloud Messaging (GCM) -
Write a converter that turns XML layout into a Scaloid code
A simple web application is demonstrated. Providing this functionality as an Eclipse or Intellij plugin would also be great.
Xtend natively supports 1) converting Java bean style getter/setters into the assignment style 2) automatically shorten the clutters when calling one-method callback interface by converting it into closure-like style. Because these are language features, Xtend users can enjoy these features without any wrapper library. We hope that Scala also adopt these benefits soon.
However, We think that Scala is a better alternative for Android platform, because Scala is mature than Xtend and has these advanced features:
-
Implicit conversion
Check implicit conversions section to see how Scaloid leverage it. -
Implicit parameter
Hiding some common values (e.g. context object, tag for logging, and layout context) from the parameter list is possible by the Scala's implicit parameters. -
Advanced type system
Scaloid layout parameter is simple, intuitive, and type-safe. -
Traits
Many useful features such as automatic unregistering receivers are implemented as traits which permits multiple inheritance.
-
Type-safety
-
Runtime performance (and your precious battery)
See benchmarks