Saturday, December 15, 2012

Scala And Android Were Made For Each Other, Part 2

This is the second in a 2 part post.  To read the first post, Click Here.

Now that we've gone over the Service in our application, lets review the Activity.  Just in case you haven't read part 1, here's a 10 second recap:

We're building a countdown timer for the specific case of tracking a Pomodoro, a 25 minute block of time.  For more info, visit the Pomodoro Technique site, or maybe, order Pomodoro Technique Illustrated...

We've reviewed the Service and seen how the Scala language allows us to reduce a lot of boilerplate code.  Now lets look at the Activity:

1:  class ScalaDoro extends FragmentActivity with Actionable with MessageReceiver {  
2:   /** Messenger for communicating with service. */  
3:   var mService: Messenger = null  
4:   /** Flag indicating whether we have called bind on the service. */  
5:   var mIsBound: Boolean = false  
6:   var running = false  
7:   override def onCreate(savedInstanceState: Bundle) {  
8:    super.onCreate(savedInstanceState)  
9:    setContentView(R.layout.activity_main)  
10:    spawn {  
11:     startService(new Intent(ScalaDoro.this, classOf[BackgroundTimer]))  
12:    }  
13:    if (savedInstanceState != null) {  
14:     running = savedInstanceState.getBoolean("running", false)  
15:     val timerString = savedInstanceState.getString("timerText")  
16:     getSupportFragmentManager().findFragmentById(R.id.timer).asInstanceOf[CountdownTimerFragment].updateTime(if (timerString != null) timerString else "")  
17:    }  
18:    val button = findViewById(R.id.foo_button).asInstanceOf[Button]  
19:    //awesome lack of boilerplate code...  
20:    if (!running) {  
21:     button.setOnClickListener(toOnClickListener(startClickListener))  
22:     button.setText(R.string.start)  
23:    } else {  
24:     button.setOnClickListener(toOnClickListener(stopClickLister))  
25:     button.setText(R.string.stop)  
26:    }  
27:    val donate = findViewById(R.id.donate).asInstanceOf[TextView]  
28:    if(donate != null) {  
29:     donate.setText(Html.fromHtml("<a href='https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=2RYS72VS6BJAA'>Is this useful to you? Donate via PayPal.</a>"));  
30:     donate.setMovementMethod(LinkMovementMethod.getInstance());  
31:    }  
32:   }  
33:   override def onSaveInstanceState(bundle: Bundle) = {  
34:    bundle.putBoolean("running", running)  
35:    bundle.putString("timerText", getSupportFragmentManager().findFragmentById(R.id.timer).asInstanceOf[CountdownTimerFragment].getTime.toString())  
36:   }  
37:   def onMessage(m: Message) = {  
38:    Log.i("BAA", "Message in Activity")  
39:    m.what match {  
40:     case BackgroundTimer.TICK =>  
41:      val textFragment = getSupportFragmentManager().findFragmentById(R.id.timer).asInstanceOf[CountdownTimerFragment]  
42:      if (textFragment != null) {  
43:       runOnUiThread {  
44:        textFragment.updateTime(m.obj.asInstanceOf[String])  
45:       }  
46:      }  
47:     case BackgroundTimer.STOPPED =>  
48:      val button = findViewById(R.id.foo_button).asInstanceOf[Button]  
49:      button.setOnClickListener(toOnClickListener(startClickListener))  
50:      button.setText(R.string.start)  
51:    }  
52:   }  
53:   def stopClickLister(v: View): Unit = {  
54:    val button = findViewById(R.id.foo_button).asInstanceOf[Button]  
55:    try {  
56:     running = false  
57:     // Give it some value as an example.  
58:     Log.i("BAA", "Sent message")  
59:     button.setOnClickListener(toOnClickListener(startClickListener))  
60:     val msg = Message.obtain(null, BackgroundTimer.STOP)  
61:     msg.replyTo = mMessenger  
62:     mService.send(msg);  
63:    } catch {  
64:     case ex: Exception => Log.i("BAA", ex.getMessage())  
65:    }  
66:    button.setText(R.string.start)  
67:   }  
68:   def startClickListener(v: View): Unit = {  
69:    val button = findViewById(R.id.foo_button).asInstanceOf[Button]  
70:    try {  
71:     // Give it some value as an example.  
72:     button.setOnClickListener(toOnClickListener(stopClickLister))  
73:     running = true  
74:     val msg = Message.obtain(null,  
75:      BackgroundTimer.START)  
76:     msg.replyTo = mMessenger  
77:     mService.send(msg);  
78:     Log.i("BAA", "Sent message")  
79:    } catch {  
80:     case ex: Exception => Log.i("BAA", ex.getMessage())  
81:    }  
82:    button.setText(R.string.stop)  
83:   }  
84:   //binding code..  
85:   val mConnection = new ServiceConnection() {  
86:    override def onServiceConnected(className: ComponentName, service: IBinder) = {  
87:     Log.i("BAA", "OnServiceConnected called")  
88:     // This is called when the connection with the service has been  
89:     // established, giving us the service object we can use to  
90:     // interact with the service. We are communicating with our  
91:     // service through an IDL interface, so get a client-side  
92:     // representation of that from the raw service object.  
93:     mService = new Messenger(service)  
94:     // We want to monitor the service for as long as we are  
95:     // connected to it.  
96:     try {  
97:      // Register to receive the notifications  
98:      val msg = Message.obtain(null,  
99:       BackgroundTimer.REGISTER)  
100:      msg.replyTo = mMessenger  
101:      mService.send(msg)  
102:      //Now get status  
103:      val status = Message.obtain(null, BackgroundTimer.STATUS)  
104:      status.replyTo = mMessenger  
105:      mService.send(status);  
106:      Log.i("BAA", "Sent message")  
107:     } catch {  
108:      case ex: Exception => Log.i("BAA", ex.getMessage())  
109:     }  
110:     mIsBound = true;  
111:    }  
112:    def onServiceDisconnected(className: ComponentName) = {  
113:     // This is called when the connection with the service has been  
114:     // unexpectedly disconnected -- that is, its process crashed.  
115:     mService = null;  
116:    }  
117:   }  
118:   override def onStart = {  
119:    super.onStart()  
120:    // Bind to the service  
121:    Log.i("BAA", "Binding")  
122:    bindService(new Intent(this, classOf[BackgroundTimer]), mConnection, Context.BIND_AUTO_CREATE);  
123:   }  
124:   @Override  
125:   override def onStop = {  
126:    super.onStop()  
127:    Log.i("BAA", "Unbinding")  
128:    // Unbind from the service  
129:    if (mIsBound) {  
130:     try {  
131:      // Give it some value as an example.  
132:      val msg = Message.obtain(null,  
133:       BackgroundTimer.UNREGISTER)  
134:      msg.replyTo = mMessenger  
135:      mService.send(msg)  
136:      Log.i("BAA", "Sent message")  
137:     } catch {  
138:      case ex: Exception => Log.i("BAA", ex.getMessage())  
139:     }  
140:     unbindService(mConnection)  
141:     mIsBound = false;  
142:    }  
143:   }  
144:  }  

The first thing to notice is that we are using 2 Traits.  Actionable and MessageReceiver.  You can see, even with this simple app, we're composing functionality from small pieces, and we're reusing code from the Service.  I've never NEEDED multiple inheritance, but in this case, it sure is nice.  We've already seen MessageReceiver, but lets take a look at Actionable:


1:  package com.example.scaladoro.activity  
2:  import android.view.View  
3:  trait Actionable {  
4:   implicit def toRunnable[F](f: => F): Runnable = new Runnable() { def run() = f }  
5:   implicit def toOnClickListener(f: View => Unit): View.OnClickListener = new View.OnClickListener() { def onClick(v: View) = f(v) }  
6:  }  

Actionable simply holds 2 implicit definitions.  These allow us to use functions in our Activity instead of having to constantly create new Abstract inner classes.  Now when I want to run something on the UI thread, I just use the syntax on line 43.  No 'new' keyword, no class definition, no methods to mark @Override.  Just the code that will be run.  All the cruft that obfuscates what we're trying to do is gone.

Notice I had to use additional syntax to set the click listeners (line 21).  I defined the click listener functions in the activity, and Scala's compiler didn't like them until I wrapped them in the implicit.  It doesn't add a ton of code and I find it to be a much cleaner implementation of the state/strategy pattern than creating abstract classes, so I'm willing to live with it.

Take another look at the code. Once again, there is is very little 'boilerplate' and nice cohesion.  Once you understand the Scala syntax, its also a lot easier to read and to reason about what is going on than the equivalent Java code.  At least, it is for me.

If you are interested in seeing the app run, go ahead and download it.  I've posted it to the Play store:

Want to try the app out?  Download Campari Pomodoro.


No comments:

Post a Comment