Sung-Ho Lee edited this page Jun 25, 2014 · 18 revisions

Implicit conversions

Scaloid employs several implicit conversions. Some of the available implicit conversions are shown below:

Uri conversion
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://scaloid.org")

, or wherever you want.

Unit conversion

Units dip and sp can be converted into the pixel unit.

val inPixel:Int = 32.dip
val inPixel2:Int = 22.sp

Reversely, pixel unit can also be converted into dip and sp unit.

val inDip:Double = 35.px2dip
val inSp:Double = 27.px2sp
Resource IDs

Scaloid provides several implicit conversions that convert from Int type resource ID to CharSequence, Array[CharSequence], Array[String], Drawable and Movie. For example:

def toast(msg:CharSequence) = ...

toast(R.string.my_message) // implicit conversion works!

Although Scaloid provides these conversions implicitly, explicit conversion may be required in some context. In this case, methods r2... are provided for the Int type:

warn("Will display the content of the resource: " + R.string.my_message.r2String)

Currently, r2Text, r2TextArray, r2String, r2StringArray, r2Drawable and r2Movie is provided.

Why implicit conversion of Resource ID is cool?

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 combination 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(title: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.

Runnable
(_ => Any) => Runnable

Runnable also covered with rich and prefixed classes.

There are more implicit conversions available. Check the source code as needed.

IntentFilter

String can be converted into IntentFilter:

implicit string2IntentFilter(str: String) = new IntentFilter(str)

Context as an implicit parameter

Many methods in the Android API require an instance of a class Context. Providing this for every method call results in clumsy code. We employ an implicit parameter to eliminate this. Just declare an implicit value that represents current context:

implicit val ctx = ...

or just extend trait SContext, which defines it for you. Then the code that required Context becomes much simpler, for example:

Intent
new Intent(context, classOf[MyActivity])

is reduced to:

SIntent[MyActivity]

When a method takes an Intent as a first parameter in which we want to pass the newly created intent object, the parameter can be omitted. For example:

startService(new Intent(context, classOf[MyService]))
stopService(new Intent(context, classOf[MyService]))

is reduced to:

startService[MyService]
stopService[MyService]
Toast
Toast.makeText(context, "hi, there!", Toast.LENGTH_SHORT).show()

is reduced to:

toast("hi, there!")

If you want a longer toast:

longToast("long toast")
Dialog
ProgressDialog.show(context, "Dialog", "working...", true)

is reduced to:

spinnerDialog("Dialog", "working...")

When you call toast, longToast or spinnerDialog from non-UI thread, you don't have to mind about threading. The toast example shown above is equivalent to the following Java code:

activity.runOnUiThread(new Runnable() {
    public void run() {
        Toast.makeText(activity, "hi, there!", Toast.LENGTH_SHORT).show();
    }
});
Pending intent
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]
DefaultSharedPreferences
PreferenceManager.getDefaultSharedPreferences(context)

is reduced to:

defaultSharedPreferences
Play ringtones

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)
Open URIs

This opens a web browser (or another view assigned to the http protocol).

openUri("http://scaloid.org")
System services

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 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 as its class name, except that the first character is lowercased.

Enriched Implicit classes

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.

Listeners

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://scaloid.org")
  }
})

Scaloid provides a shortcut that dramatically reduces the length of the code:

find[Button](R.id.search).onClick(openUri("http://scaloid.org"))

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) becomes layout += button.
Multiple method listeners

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 override 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.

Asynchronous task processing

Android API provides runOnUiThread() only for class Activity. Scaloid provides a Scala version of runOnUiThread() for anywhere other than Activity.

Instead of:

activity.runOnUiThread {
  new Runnable() {
    def run() {
      debug("Running only in Activity class")
    }
  }
}

In Scaloid, use it like this:

runOnUiThread(debug("Running in any context"))

Running a job asynchronously and notifying the UI thread is a very frequently used pattern. Although Android API provides a 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]) = {
    doAJobTakeSomeTime(params)
  }

  override def onPostExecute(result: String) {
    alert("Done!", result)
  }
}.execute("param")

Using scala.concurrent.Future, the asynchronous job shown above can be rewritten like this:

Future {
  val result = doAJobTakeSomeTime(params)
  runOnUiThread(alert("Done!", result))
}

When you don't want to build sophisticate UI interactions, but just want to display something by calling a single Scaloid method (e.g. alert, toast, and spinnerDialog), Scaloid handles runOnUiThread for you. Therefore, the code block shown above is reduced to:

Future {
  alert("Done!", doAJobTakeSomeTime(params))
}

It is a great win as it exposes your idea clearly.

Just like we thrown away AsyncTask, we can also eliminate all other Java helpers for asynchronous job, such as AsyncQueryHandler and AsyncTaskLoader. Compare with the original Java code and a Scala port of ApiDemos example app.

Using Future is just an example of asynchronous task processing in Scaloid. You can freely use any modern task management utilities.

Further reading: Refer to this blog post for an important consideration when using Future in Android.

Traits

Trait UnregisterReceiver

When you registere BroadcastReceiver with Context.registerReceiver() you have to unregister it to prevent memory leak. Trait UnregisterReceiver handles these chores for you. All you need to do is append the trait to your class.

class MyService extends SService with UnregisterReceiver {
  def func() {
    // ...
    registerReceiver(receiver, intentFilter)
    // Done! automatically unregistered at UnregisterReceiverService.onDestroy()
  }
}

Trait SContext

Trait SContext includes several shortcuts for frequently used android idioms, and inherits TagUtil.

Starting activity
startActivity(new Intent(context, classOf[MyActivity]))

is reduced to:

startActivity[MyActivity]

Trait SActivity

Instead of

findViewById(R.id.login).asInstanceOf[Button]

use a shorthand:

find[Button](R.id.login)

Although we provide this shorthand, Scaloid recommends programmatically laying out UI, not with XML.

Activity as an implicit parameter

Similar to the implict context, an Activity typed implicit parameter is also required for some methods. Therefore, you have to define an activity as an implicit value:

implicit val ctx: Activity = ...

Because the class Activity is a subclass of Context, it can also be an implicit context. When you extend SActivity, object this is assigned as the implicit activity by default.

Here we show some example cases of using the implicit activity:

Automatically allocate a unique View ID

Often, Views are required to have an ID value. Although Android API document specifies that the ID need not be unique, allocating unique ID is virtually mandatory in practice. Scaloid provides a package scope function getUniqueId, which returns Int type ID that is not allocated by any existing View components for given implicit activity.

val newUniqueIdForCurrentActivity = getUniqueId

Using this, Scaloid also extended View class to add a method uniqueId, that assigns a new unique ID if it is not already allocated.

val uniqueIdOfMyView = myView.uniqueId

One of the good use case of uniqueId is SRelativeLayout. Some of the methods in this layout context, such as below, above, leftOf and rightOf, takes another View object as an anchor:

new SRelativeLayout {
  val btn1 = SButton(R.string.hi)
  SButton("There").<<.below(btn1)
}

Here we show the implimentation of the below function:

def below(anchor: View)(implicit activity: Activity) = {
  addRule(RelativeLayout.BELOW, anchor.uniqueId)
  this
}

A new unique ID is assigned to the anchor if it is not assigned already, and passes it to addRule function.

Logging

Unlike other logging frameworks, Android Logging API requires a String tag for every log call. We elliminate this by introducing an 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 level (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)

Scala getters and setters

You can use any of the setters listed below:

  • obj.setText("Hello") Java bean style
  • obj.text = "Hello" Assignment style
  • obj text "Hello" DSL style
  • obj.text("Hello") Method calling style

Compared to 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 for:

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 needs to be chained.

Note: Using .apply(String) method on object STextView, you can further reduce the code above like this:

STextView("Hello") textSize 15

Return value of setters

Unlike most setters in the 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 pos, boolean expanded, View v, ViewGroup parent) {
  TextView textView = getGenericView();
  textView.setText(getGroup(pos).toString());
  return textView;
}

is reduced to:

def getGroupView(pos: Int, expanded: Boolean, v: View, parent: ViewGroup): View =
  getGenericView.text = getGroup(pos).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

Prefixed classes

If you want to use scala style getters/setters, implicit conversions 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, the compiler will not find the correct implicit conversion:

def getInstance = new TextView(context) {
  text = "Hello"    // Compilation Error.
}

Therefore, we extended Android classes with the same name prefixed with the 'S' character:

def getInstance = new STextView {
  text = "Hello"    // OK.
}

These classes explicitly provide the extra methods that was provided implicitly.

Aditionally, prefixed classes support implicit context value and additional syntactic sugar. For example, many classes have .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 packages (or namespaces) are preferred than prefixing. However, when we use both classes from Android API and Scaloid, using a package name is more verbose than prefixing the 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.

Sweet-little sugar

If the setter ends with ...Enabled, Scaloid adds functions named enable... and disable.... For example:

layout1.setVerticalScrollBarEnabled(true)
layout2.setVerticalScrollBarEnabled(false)

is equivalent to:

layout1.enableVerticalScrollBar
layout2.disableVerticalScrollBar

And,

button1.setEnabled(false)
button2.setEnabled(true)

is equivalent to:

button1.enable
button2.disable

Because setting the property orientation = VERTICAL for SLinearLayout is frequently used, we provide a shorthand:

new SVerticalLayout()

that is equivalent to:

new SLinearLayout().orientation(LinearLayout.VERTICAL)

Classes

Class AlertDialogBuilder

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.message)

Also you can build a more complex dialog:

new AlertDialogBuilder("Exit the app", "Do you really want to exit?") {
  positiveButton("Exit", finishTheApplication())
  negativeButton(android.R.string.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(android.R.string.cancel, new DialogInterface.OnClickListener {
    def onClick(dialog: DialogInterface, which: Int) {
      dialog.cancel()
    }
  }).show()

When you call show() or alert from non-UI thread, you don't have to mind about threading.

Class SArrayAdapter

Suppose you want to let the user selects a string from spinner, and larger font should be displayed in the dropdown list. Then the plain-old Android code is consisted of a chunk of XML and its wiring:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    style="?android:attr/spinnerDropDownItemStyle"
    android:id="@+id/spinner_textview"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
	android:textSize="25 dip" />
val adapter = new ArrayAdapter(context, android.R.layout.simple_spinner_item, 
                               Array("One", "Two", "Three"))
adapter.setDropDownViewResource(R.layout.spinner_dropdown)

In Scaloid, a directly equivalent code is:

SArrayAdapter("One", "Two", "Three").dropDownStyle(_.textSize(25 dip))

If you want to let the text color in the spinner be blue, use the style method:

SArrayAdapter("Quick", "Brown", "Fox").style(_.textColor(Color.BLUE))

Can it be simpler?

Static fields on protected interfaces

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 to it with Contacts.DISPLAY_NAME. However, Scala does not support accessing in this way (please refer this and this). It is bad news for an Android-Scala programmer. So we provide a workaround implementation for this problem. Declare import org.scaloid.Workarounds._. Then you can use the interfaces publicly which is originally defined as protected.

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.