Lesson Tuesday

Currently, we are creating a new View.onClickListener for our findRestaurantsButton, like so:

MainActivity.java
...
        mFindRestaurantsButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String location = mLocationEditText.getText().toString();
                Intent intent = new Intent(MainActivity.this, RestaurantsActivity.class);
                intent.putExtra("location", location);
                startActivity(intent);
            }
        });
...

However, eventually we'll add another button to take the user to an account registration page. This second button will need its own View.onClickListener too! And we'll need a sign-in button for the user to log in once they create an account. That will also need its ownView.onClickListener! And what if we wanted to add a link to an About page? Yep! You guessed, it, we'd need anotherView.onClickListener! All of these listeners would confound our onCreate() method! Not to mention that our code would be redundant and difficult-to-follow.

Thankfully, there's a way we can refactor. Instead of creating a new View.onClickListener for each individual button, let’s implement something called a View.OnClickListener interface. The interface is more reusable, and will result in much cleaner code.

Interfaces

But what's an interface anyway? Before we begin, let's explore interfaces in general. While Android uses interfaces, they're certainly not Android-specific. They're not even specific to Java. Interfaces are a concept used in many programming languages, including C#, Visual Basic, Python, Java, PHP, and Swift.

In simple terms, an interface is a group of methods multiple different classes may inherit.

For instance, say we had an interface called Noisy:

public interface Noisy {
    void angryNoise(); 
    void happyNoise();  
}

The Noisy interface contains two methods: One for making a happy noise, and one for making an angry noise. Now, we know many things are capable of being noisy. So, many classes can implement this interface. For instance, an Elephant could be pretty noisy:

public class Elephant implements Noisy {
    @Override
    public void angryNoise() {
         Log.d(TAG, "rumble");
    }
    @Override
    public void happyNoise() {
         Log.d(TAG, "trumpet");
    }
}

And a Dog can be pretty loud, too:

public class Dog implements Noisy {
    @Override
    public void angryNoise() {
         Log.d(TAG, "growl");
    }
    @Override
    public void happyNoise() {
         Log.d(TAG, "bark");
    }
}

However, despite both Dog and Elephant classes implementing the same interface with the same methods, Dogs and Elephants make different noises; so the interface's methods are personalized to each animal's class.

Interfaces allow us to separate what a class does (make angry noises, for instance) from how it does it (either rumbling or. growling, depending on the animal).

Rules for Interfaces

There are several rules to consider when creating and using interfaces:

  • Interface methods can only be public (and are by default).
  • Member variables can only be public, static, and final (and are by default).
  • A class must implement all methods of an interface, unless the class is declared as abstract. That means, using the example above, any other People or Animal classes that implement the interface Noisy must have methods for both happyNoise() and angryNoise() defined in their class.

For more information, read the Oracle documentation on Creating Interfaces.

Built-In Android Interfaces

In the example above, we created a rudimentary Noisy interface from scratch. However, Android comes with many of the most commonly-used interfaces built right in! The OnClickListener is one of many interfaces included in Android's View class.

Implementing an Interface

Our MainActivity should currently look something like this:

MainActivity.java
public class MainActivity extends AppCompatActivity {
    @Bind(R.id.findRestaurantsButton) Button mFindRestaurantsButton;
    @Bind(R.id.locationEditText) EditText mLocationEditText;
    @Bind(R.id.appNameTextView) TextView mAppNameTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        Typeface ostrichFont = Typeface.createFromAsset(getAssets(), "fonts/ostrich-regular.ttf");
        mAppNameTextView.setTypeface(ostrichFont);

        mFindRestaurantsButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String location = mLocationEditText.getText().toString();
                Intent intent = new Intent(MainActivity.this, RestaurantsActivity.class);
                intent.putExtra("location", location);
                startActivity(intent);
            }
        });
    }
}

To begin using the View.OnClickListener interface, we'll first need to provide the class access to it. We do this by adding implements View.OnClickListener to the end of the class declaration, like so:

MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
...

implements is a keyword specific to interfaces, whereas extends (also seen here) is used to inherit classes. Whenever we add an interface, we always use implements.

As soon as you add this, Android Studio will immediately underline this line in red, and present you with the following error message:

android-studio-error-on-click-listener

Remember, the rules for implementing interfaces we just covered? A class must implement all methods of an interface, unless the class is declared as abstract. Therefore, we will need to include every method in the View.OnClickListener interface here in our MainActivity. If we check the documentation for the View.OnClickListener interface we can see that it only has one method: onClick().

You may notice that our MainActivity actually already has an onClick() method; but it's not part of the class! It's currently nested in onCreate(). Let's move it outside of this method:

MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    @Bind(R.id.findRestaurantsButton) Button mFindRestaurantsButton;
    @Bind(R.id.locationEditText) EditText mLocationEditText;
    @Bind(R.id.appNameTextView) TextView mAppNameTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        Typeface ostrichFont = Typeface.createFromAsset(getAssets(), "fonts/ostrich-regular.ttf");
        mAppNameTextView.setTypeface(ostrichFont);

        mFindRestaurantsButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        String location = mLocationEditText.getText().toString();
        Intent intent = new Intent(MainActivity.this, RestaurantsActivity.class);
        intent.putExtra("location", location);
        startActivity(intent);
    }
}

Now, instead of creating individual click listeners for anything clickable, the View.OnClickListener interface is available to the entire class. Therefore, when we attach a listener to mFindRestaurantsButton, we can pass the current context as an argument: mFindRestaurantsButton.setOnClickListener(this);, because the current context (MainActivity) now includes the View.OnClickListener it requires.

Additionally, because the interface applies to the whole class, its onClick() method will be called whenever any click listener is triggered. So, let's include a conditional that will execute different code depending on what is clicked:

MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    @Bind(R.id.findRestaurantsButton) Button mFindRestaurantsButton;
    @Bind(R.id.locationEditText) EditText mLocationEditText;
    @Bind(R.id.appNameTextView) TextView mAppNameTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        Typeface ostrichFont = Typeface.createFromAsset(getAssets(), "fonts/ostrich-regular.ttf");
        mAppNameTextView.setTypeface(ostrichFont);

        mFindRestaurantsButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        if(v == mFindRestaurantsButton) {
            String location = mLocationEditText.getText().toString();
            Intent intent = new Intent(MainActivity.this, RestaurantsActivity.class);
            intent.putExtra("location", location);
            startActivity(intent);
        }
    }
}

Now, as we add more links and buttons we can simply call .setOnClickListener() on the View element we'd like to attach a click listener to, and add another if statement to the onClick() method. Awesome!

From now on, we'll implement our View.OnClickListeners in this fashion.


Example GitHub Repo for MyRestaurants

Terminology


  • Interface: A group of methods multiple different classes may inherit. They allow us to separate what a class does from how it does it.

Tips


Interface Rules

  • Interface methods can only be public (and are by default).
  • Member variables can only be public, static, and final (and are by default).
  • A class must implement all methods of an interface, unless the class is declared as abstract. That means, using the example above, any other People or Animal classes that implement the interface Noisy must have methods for both happyNoise() and angryNoise() defined in their class.

  • implements is a keyword specific to interfaces, whereas extends (also seen here) is used to inherit classes. Whenever we add an interface, we always use implements.

Examples


Interfaces

For instance, say we had an interface called Noisy:

public interface Noisy {
    void angryNoise(); 
    void happyNoise();  
}

The Noisy interface contains two methods: One for making a happy noise, and one for making an angry noise. Now, we know many things are capable of being noisy. So, many classes can implement this interface. For instance, an Elephant could be pretty noisy:

public class Elephant implements Noisy {
    @Override
    public void angryNoise() {
         Log.d(TAG, "rumble");
    }
    @Override
    public void happyNoise() {
         Log.d(TAG, "trumpet");
    }
}

And a Dog can be pretty loud, too:

public class Dog implements Noisy {
    @Override
    public void angryNoise() {
         Log.d(TAG, "growl");
    }
    @Override
    public void happyNoise() {
         Log.d(TAG, "bark");
    }
}

However, despite both Dog and Elephant classes implementing the same interface with the same methods, Dogs and Elephants make different noises; so the interface's methods are personalized to each animal's class.

MyRestaurants MainActivity Refactored with View.OnClickListener

MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    @Bind(R.id.findRestaurantsButton) Button mFindRestaurantsButton;
    @Bind(R.id.locationEditText) EditText mLocationEditText;
    @Bind(R.id.appNameTextView) TextView mAppNameTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        Typeface ostrichFont = Typeface.createFromAsset(getAssets(), "fonts/ostrich-regular.ttf");
        mAppNameTextView.setTypeface(ostrichFont);

        mFindRestaurantsButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        if(v == mFindRestaurantsButton) {
            String location = mLocationEditText.getText().toString();
            Intent intent = new Intent(MainActivity.this, RestaurantsActivity.class);
            intent.putExtra("location", location);
            startActivity(intent);
        }
    }
}

Example GitHub Repo for MyRestaurants

Additional Resources