Maarten Pennings'
Android development: Button click handler
fampennings / maarten / android / 02buttonclick

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 XMLLooks best, but only works for "on click".
2aActivity is listenerClumsy: all click are funneled to single handler (if-then-else chain)
2bInline listenerClumsy: all click handlers in onCreate (which gets too large)
2cListener in variableSeparate handlers, sharing (but: no xml, extra obj, multi func)
2dListener classNobody does this
3 SubclassingNobody 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.

home
button click
Button click




Downloads:
Source
Presentation





Editor:
Maarten Pennings
Laatste wijziging:
21 aug 2011
File:
error