Button click handler
Download the source or a presentation of this article.
Introduction
Once the Android app "hello world" has been written, the next app very likely has a button. Buttons are clicked, and then some piece of code should be executed. This is done by a click handler. Android has too many ways to link a handler to a button. This page tries to document them.
1 | On click in XML | Looks best, but only works for "on click". |
2a | Activity is listener | Clumsy: all click are funneled to single handler (if-then-else chain) |
2b | Inline listener | Clumsy: all click handlers in onCreate (which gets too large) |
2c | Listener in variable | Separate handlers, sharing (but: no xml, extra obj, multi func) |
2d | Listener class | Nobody does this |
3 | Subclassing | Nobody does this |
Option 1: On click in XML
So, we have this activity, and it has a button. The xml file that contains the layout for the activity, has an entry for the button. A button is a so-called view. Views are the building blocks for user interface components (in other environments views are known as widgets or controls). Every view has a set of properties (for a full list rightclick on the view and chose Show In | Properties). One of the properties of every view is "On click", which is called android:onClick in xml.
One can assign this property the "Name of the method in this View's context to invoke when the view is clicked" as the android developer site explains. This name must correspond to a public method that takes exactly one parameter of type View.
So, in practice, the xml file looks like (button in blue, on click handler in red):
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hint" /> <Button android:id="@+id/But1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="But1" android:onClick="ButOnClickXml" /> </LinearLayout>
The associated Java code uses a so-called toast (a pop-up), in the click handler.
public class ButtonClickActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } public void ButOnClickXml(View v) { Toast.makeText( getApplicationContext(), "ButOnClickXml", Toast.LENGTH_SHORT ).show(); } }
This approach offers the flexability to add a second button with its own handler. For example, add the fragment below to the xml layout file (changes in red).
<Button android:id="@+id/But2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="But2" android:onClick="But2OnClickXml" />
and the following fragment to the Java class
public void But2OnClickXml(View v) { Toast.makeText( getApplicationContext(), "But2OnClickXml", Toast.LENGTH_SHORT ).show(); }
We now have two buttons, But1 and But2, each which their own click hanlder, ButOnClickXml respectively But2OnClickXml, in the activity.
It is also possible to use one handler for two buttons. For example, add the fragment below to the xml layout file (changes in red). This maps a new button 3 to the same handler that button 1 is using.
<Button android:id="@+id/But3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="But3" android:onClick="ButOnClickXml" />
The handler can determine which button was clicked by checking the passed view.
public class ButtonClickActivity extends Activity { Button But1; Button But2; Button But3; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); But1= (Button)findViewById(R.id.But1); But2= (Button)findViewById(R.id.But2); But3= (Button)findViewById(R.id.But3); } public void ButOnClickXml(View v) { if( v==But1 ) Toast.makeText( getApplicationContext(), "ButOnClickXml(But1)", Toast.LENGTH_SHORT ).show(); else if( v==But3 ) Toast.makeText( getApplicationContext(), "ButOnClickXml(But3)", Toast.LENGTH_SHORT ).show(); else Toast.makeText( getApplicationContext(), "ButOnClickXml(?)", Toast.LENGTH_SHORT ).show(); } public void But2OnClickXml(View v) { Toast.makeText( getApplicationContext(), "But2OnClickXml", Toast.LENGTH_SHORT ).show(); } }
I believe this option has many benefits. It is the "delegation pattern" used by Delphi since 1995. It is concise, it allows multiple buttons to have their own handler, and it allows multiple buttons to share a handler (and stil determine which button was clicked).
One thing is not possible in Android right now. Where most xml properties have a Java equivalent, the android:onClick has not (I don't see any fundamental problem, it just seems an oversight). This means that it is not possible to hook a button at runtime to a handler. If that's what you want, you have to use any of the options described next.
A much bigger problem is that currently, only the "on click" has an xml attribute. So, if you want any other event, you also must have to use any of the options described next.
Option 2a: Activity is listener
The "delegation pattern" seems "not done" in Java, it is not OO (object orientation) pur sang. In the delegation pattern, the button has a private field (mOnClick) that stores a callback method (this is a combination of a pointer to an object, and a pointer to a function) to be invoked when the button is clicked. Note, the actual implementation does not have mOnClick.
The official OO way to go is to use a listener. The button than has a private field (mOnClickListener) that stores a callback interface to be invoked when the button is clicked. The interface is implemented by a so called listener object. This can be any type of object, as long as it implements the interface typed OnClickListener.
Just to rephrase, there is an interface type OnClickListener. Any class can implement that interface. An instance (object) of such a class can be registered as listener of some view by setting the view's mOnClickListener member variable to the object, using view.setOnClickListener(object).
One object that could implement interface OnClickListener is the activity object. This way, we get an approach which is similar to option 1; the click handler is a method of the activity.
Since there is no xml equivalent for the Java method setOnClickListener, the xml definitions of two new buttons can not yet refer to the object that implements the interface. We add the following to the layout xml file.
<Button android:id="@+id/But4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="But4" /> <Button android:id="@+id/But5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="But5" />
The code below shows the accompanying Java code. Note the red items in the code. Firstly, there is an import of the interface definition. Next, we see that the activity claims to implement the interface OnClickListener. Thirdly, the onCreate method sets the mOnClickListener field of both buttons; it sets them to refer to the activity itself (it does claim to implement OnClickListener, so it is a acceptable candidate). Finally, we see that the activity class indeed implements the interface (the interface only contains one function onClick(View v) as documented on the android website).
import android.view.View.OnClickListener; public class ButtonClickActivity extends Activity implements OnClickListener { ... Button But4; Button But5; @Override public void onCreate(Bundle savedInstanceState) { ... But4= (Button)findViewById(R.id.But4); But5= (Button)findViewById(R.id.But5); But4.setOnClickListener(this); But5.setOnClickListener(this); } ... @Override public void onClick(View v) { if( v==But4 ) Toast.makeText( getApplicationContext(), "onClick(But4)", Toast.LENGTH_SHORT ).show(); else if( v==But5 ) Toast.makeText( getApplicationContext(), "onClick(But5)", Toast.LENGTH_SHORT ).show(); else Toast.makeText( getApplicationContext(), "onClick(?)", Toast.LENGTH_SHORT ).show(); } }
I don't like this approach. The click handling is now scattered over three places: the layout xml file for the buttons, the onCreate for the binding, and the actual onClick function. Furthermore, the single onClick function of the activity must handle all buttons (actually all views that have an on click), leading to ugly if-then-else chaining. Another issue with this approach is that some callback interfaces contain more than one functions. So as soon as you need one function, the actvity needs to implement the whole interface (all functions need to be implemented, likely using a do nothing stub).
I did try to have android:onClick="But5OnClickXml" in the layout xml file as well as But5.setOnClickListener(this) in the Java file, but then the But5OnClickXml() is never called.
Option 2b: Inline listener
Some of the negative aspects of the previous approach can be solved by using an inline (anonymous) class. Most notably, the code is more contralized and we can have one handler per button. However, inline classes are a bit cryptic, see another page for the Java details.
The OnClickListener will again be set in Java, so the xml fragment for the new button is short again.
<Button android:id="@+id/But6" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="But6" />
All the magic is now in Java, in the onCreate method. Note that we lookup the button and then set its listener. The listener object we pass, is created on the spot (all blue code in the fragment below), using an inline extension of the abstract class OnClickListener; its onClick method is overriden.
@Override public void onCreate(Bundle savedInstanceState) { ... But6= (Button)findViewById(R.id.But6); But6.setOnClickListener( new OnClickListener() { @Override public void onClick(View v) { String s= "inline(But6), not "+But1.toString(); Toast.makeText( getApplicationContext(), s, Toast.LENGTH_SHORT ).show(); } }); }
I'm not too enthusiastic about this approach either. It is much more centralized, but it is cryptic. It is not possible to handle multiple buttons in one place. And, when the interface has multiple functions, they all need to be implemented. Java has a feature (each nested-class object has a link to its surronding class object) which makes it possible to refer to all fields in the activity class from withing the listener object (for example, see the red But1).
Option 2c: Listener in variable
This option is a variation on the previous. It solves the issue of sharing one handler over multiple buttons. It's less cryptic. Since it is still based on implementing interfaces, we still have stubs when there are multiple functions in the interface.
We add But7 and But8 to the layout xml file as before. All code is in the Java file again. This time we create a listener object and store it in a member variable.
public class ButtonClickActivity extends Activity ... { ... Button But7; Button But8; OnClickListener ButListener = new OnClickListener() { @Override public void onClick(View v) { String s= v==But7 ? "But7" : (v==But8?"But8":"?"); Toast.makeText( getApplicationContext(), s, Toast.LENGTH_SHORT ).show(); } }; @Override public void onCreate(Bundle savedInstanceState) { ... But7= (Button)findViewById(R.id.But7); But8= (Button)findViewById(R.id.But8); But7.setOnClickListener(ButListener); But8.setOnClickListener(ButListener); } ... }
The activity class now has a member variable ButListener that contains a listener object. Its class is anonymous since it is created via an inline extension. But the object is stored in a variable so that it can be used to setOnClickListener() for multiple buttons.
Here too, it is possible to refer to all fields in the activity class from within the listener object (see for example But7 and But8).
Options 2b and 2c actually create a fresh listener object. This does cost some (ram) resources.
I believe this option is the best choice in practice.
Note that we can create an instance of an interface; the red OnClickListener. I expect this is just syntactic sugar for creating an object of the empty class having this interface.
Also note that setOnClickListener() can not be called twice on a button in order to register two handlers (as is possible in C#). The last setOnClickListener "wins" (overwrites the previous).
Option 2d: Listener class
The previous option used an anonymous class, but an explicit handle to the listener object (in a variable). The reverse is also possible: an explicit class for the listener, but an anonymous (inlined) listener object.
public class ButtonClickActivity extends Activity implements OnClickListener { Button But8; class But8OnClickListener implements OnClickListener { public void onClick(View v) { Toast.makeText( getApplicationContext(), "But8", Toast.LENGTH_SHORT ).show(); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); But8= (Button)findViewById(R.id.But8); But8.setOnClickListener( new But8OnClickListener() ); }
Personally, I do not think this makes sense. Defining an explicit class is useful when multiple instances are created; but this class its onClick is taylored to one specific button.
Option 3: Subclassing
A totally different option is to make once own button class, and override its click behavior. We dynamically create a button of that new type, and it automatically behavse on clicks as we want.
The first thing we should know, is that every view has a method performClick(), that calls the view's OnClickListener, if it is defined (as explained on the android site).
Button But9= new Button(this) { @Override public boolean performClick() { Toast.makeText( getApplicationContext(), "But9", Toast.LENGTH_SHORT ).show(); return false; } }; But9.setText("But9"); LinearLayout layout = (LinearLayout)findViewById(R.id.LL); layout.addView(But9);
In the first line, we create a But9, passing it the activity (this) as context. The class we instantiate is Button, however, it has an inline extension. In the extension, we override the method that normally invokes the listener performClick(). It contains our click action (in this case popping up a toast). We set the caption of the button (and should set its layout width), and we add it to the linear layout.
Of course, we could make the sub class explicit, instead of inline. However, then we lose the capability to access all members of the actvity.
I would not suggest this click handler option in real applications.
Download the source or a presentation of this article.