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