Introduction

“Listener pattern” is one of the most commonly used patterns used in application development. The working principle is based on the fact that in one class we define an event and the moment when the execution of the event starts (“trigger event”), while in another class we define an object (so-called listener object) that listens to that event and reacts to it.
There are quite a number of built-in listeners within Android itself, but in addition to them, we can also create our own custom listeners and thus enable us to define callback methods for events that are triggered from other parts of our code. Custom listeners are used in the following cases:
- Communication between fragment and activity
- Communication between two fragments via activities
- Communication between adapter and activity
- Communication between dialog and activity
Example of a standard listener built into the operating system
This pattern is often used within the operating system, and the best example of the built-in interface in android core is OnClickListener which is responsible for the “click” event. This listener is defined within the View.java class.
|
1 2 3 |
public static interface OnClickListener() { void onClick(View view); } |
A setter method named setOnClickListener() is defined within the View.java class. Calling this setter method in another class defines a listener object that listens to that event, and through it a callback method that reacts to the event. In this example, the object “btnNekiButton” is the successor of the View.java class, and therefore it has inherited all interfaces, including the one mentioned, so it can simply call its setter method:
|
1 |
btnSomeButton.setOnClickListener(listeningObject); |
Or if we show the known code with an “on the fly” created object:
|
1 2 3 4 5 6 |
btnSomeButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { // This defines what happens when the event is "triggered". } }); |
EXPLANATION:
Instantiating “on the fly” an anonymous object that implements an interface
An anonymous class is a class that does not have a name, so if we wanted to instantiate an object from this class it would look like this:
|
1 |
new { } |
We need a specific anonymous class, one that implements a specific interface, so although we don’t need to define a class name, we do need to define which interface to implement. If the interface is defined in its own file, this is achieved by inserting the name of the interface before the brackets:
|
1 |
new NazivInterfejsa () { }; |
We also need to implement interface methods:
|
1 2 3 4 5 6 |
new NazivInterfejsa () { @Override public void nekaCallBacKMetoda() { // the code that performs the action after the event is triggered } }; |
Procedure
The interface can be defined independently in a separate file in the communication between two classes is defined in one class for practical reasons. In these examples, we will use the terms “First Class” (the place where the interface is defined, i.e. the event) and “Second Class” (the place where the object that listens and defines the callbackMethod is registered, i.e. the action that should be performed after “triggering” the event).
a)Interface (first class):
As with ready-made listeners within the Android operating system, the interface must first be created here. In the previous example, this part of the work was written as part of android, while in this case we will do it. The interface ensures that every object that implements the interface has a callback method:
|
1 2 3 |
public interface NekiInterfejs { void callbackMetoda(); } |
b) Setter method (first class)
In addition to the interface, a setter method is needed, by calling which one defines which object is the listener (that is, the object that listens to the event). This method allows us to define that object anywhere, because it is enough to pass it as a parameter when we call that method (see more about it here).
|
1 2 3 4 5 |
private NekiInterfejs mListener; public void setListener(NekiInterfejs value){ this.mListener = value; } |
c) Triggering the event = calling the callback method (first class)
Calling the callback method can be considered as an event trigger, so call the callback method somewhere in the class and that way the switch will be “triggered” and the event will be started:
|
1 |
mListener.callbackMetoda(); |
Although exception:
should be avoided
|
1 2 3 |
if(mListener != null){ mListener.callbackMetoda(); } |
You can view the entire code from the first class here.
d) Defining the object that listens and the callback method
All previous parts are created in one class that created the event, while this part of the code is in another class and is in charge of defining a listener object that listens to the event in that other class. By defining the listener object, we also practically define a callback method that reacts to the event. There are several ways to do this:
d1) Defining the listener object and the callback method through the setter method
Defining the callbackMethod can be done in two ways depending on what the listener is.
d1.a) Listener = anonymous object
In this case, defining the callback method is done by calling the setter method and passing an “anonymous” object that implements the interface and thus the callback method. In order to be able to call a method from another class, we need to call it through an object of the first class or an object that implements an interface (more about “objectImplementingInterface” see here).
|
1 |
objekatKojiImplementiraInterfejs.setListener(anonimniObjekatKojiImplementiraInterfejs) |
The object that we pass through the parameter is created as an instance of an anonymous class that implements an interface (how to “on the fly” create an object from an anonymous class, see here).
|
1 2 3 4 5 6 |
objekatKojiImplementiraInterfejs.setListener(new PrvaKlasa.NekiInterfejs() { @Override public void callbackMetoda() { // reaction to an event } }); |
d1.b) Listener = Whole other class
In this case the whole class implements the interface, so it is necessary to register the whole class as a listener by passing the class through the setter method using the keyword “this” and then override the callbackMethod:
|
1 2 3 4 5 6 7 8 9 |
PrvaKlasa objectImplementingInterface = new PrvaKlasa(LayoutInflater.from(this), null); objectImplementingInterface.setListener(this); . . . @Override callbackMetoda () { // Some action after triggering the event } |
d2) Defining the listener object and the callback method through the constructor
If the listener is extremely important for the class itself, then the setter method is replaced by the constructor method of the class itself:
|
1 2 3 4 5 6 7 8 9 10 11 |
public class PrvaKlasa { public interface NekiInterfejs { void callbackMetoda(); } private NekiInterfejs mListener; // First class constructor instead of setter method: public PrvaKlasa (NekiInterfejs listener) { this.mListener = listener; } } |
In the second class, we create an object based on the constructor method of the First Class, by passing an “on the fly” created anonymous object that implements a listener through a parameter:
|
1 2 3 4 5 6 |
PrvaKlasa objekatOdPrveKlase = new PrvaKlasa(new PrvaKlasa.NekiInterfejs() { @Override public void callbackMetoda() { // reaction to an event }); }); |
d3) Defining the listener object and the callback method through the lifecycle method
This approach isused in communication between fragments and activities.
Fragment
The procedure within the fragment is similar to the procedure (code) from FirstClass, so that within the fragment there are all three previously described steps: creating an interface, setter methods and triggering events.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class NekiFragment extends Fragment { public interface NekiInterfejs { void callbackMetoda(); } private NekiInterfejs mListener; public void setOnNekiInterfejsListener(Activity activity) { mListener = activity; } . . . public void nekaMetoda() { // Triggering an event: mListener.callbackMetoda(); } } |
Activity
In the case of Activity, the procedure is partially different from that of the Second Class, because it must be checked in the onFragmentAttach() method whether the activity implements the interface. Only when we are sure that the activity implements the interface is it defined that the activity becomes a listener object. By validating the callbackMethod, the action after triggering the event is defined:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public static class MainActivity extends Activity implements NekiFragment.OnHeadlineSelectedListener{ . . . @Override public void onAttachFragment(Fragment fragment) { if (fragment instanceof NekiFragment) { NekiFragment nekiFragmentObjekat = (NekiFragment) fragment; // Defining the activity (this) as a listener object: nekiFragmentObjekat.setOnNekiInterfejsListener(this); } } . . . // Defining an action when an event is triggered @Override public void callbackMetoda() { // Some action that is executed when the event is triggered } } |
There is also another way when checking within the fragment itself whether the activity implements the interface or not. Then the entire validation code is executed in the fragment within the onAtach() method (see the full code here).
NOTE:
If you need to register more than one listener, then you need to adapt the code to work with an array of listeners:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// OVAJ DEO OSTAJE ISTI: public interface NekiInterfejs { void callbackMetoda(); } // STARO: private NekiInterfejs mListener; // NEW: Instead of mListener (most often named "mListener") a list of objects (mListeners) is used private final ArrayList<NekiInterfejs> mListeners = new ArrayList<>(); // STARO: public void setListener(NekiInterfejs value){ this.mListener = value; } // NEW: The setter method now adds a new listener to the Array public void registerListener (NekiInterfejs value){ if(!mListeners.contains(value)) mListeners.add(value); } // NEW: New method that evicts the listener when no longer needed from the Array public void unregisterListener (NekiInterfejs value){ if(mListeners.contains(value)) mListeners.remove(value); } // The OLD way to trigger an event is to call the callbackMethod: mListener.callbackMetoda(); // NEW now for each listener in the array a method must be called therefore a new method containing a loop is needed public void pozivanjeCallbackMetode(){ for(NekiInterfejs listener : mListeners){ listener.callbackMetoda(); } } // Now instead of calling the callback method to trigger the event, we call the callingCallbackMethod() method pozivanjeCallbackMetode(); |
See the full new code here.
Communication Fragment – Activity
In this example, the interaction between fragments and activities will be explained. Within the creation fragment, the interface “triggers” the custom event, while the callback body is defined in the activity, i.e. reaction to the execution of that custom event from the fragment.
MyListFragment
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import android.support.v4.app.Fragment; public class MyListFragment extends Fragment { // The listener variable is an object that represents an instance of a fragment private OnItemSelectedListener listener; // Defining the interface ie. events that the fragment will use for communication public interface OnItemSelectedListener { // callback method, by calling which the event is triggered later public void onRssItemSelected(String link); } @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof OnItemSelectedListener) { // the listener object will be the entire activity to which the fragment is attached listener = (OnItemSelectedListener) context; } else { throw new ClassCastException(context.toString() + " must implement MyListFragment.OnItemSelectedListener"); } } // An event is "triggered" inside this method, so calling this method immediately fires the event public void onSomeClick(View v) { listener.onRssItemSelected("some link"); } } |
Activity:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Activity implements the interface defined in the fragment public class RssfeedActivity extends AppCompatActivity implements MyListFragment.OnItemSelectedListener { DetailFragment fragment; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_rssfeed); fragment = (DetailFragment) getSupportFragmentManager().findFragmentByTag(Detailfragment.TAG)); } // Implementation of the method from the interface and within it the action that should be performed after the event is triggered @Override public void onRssItemSelected(String link) { if (fragment != null && fragment.isInLayout()) { fragment.setText(link); } } } |
Communication Fragment – Fragment
Communication between them can be achieved in two ways:
- Using activity
- Using a shared “ViewModel” (see more about this in the article: “Communication between fragments using ViewModel and LiveData”.
Since the topic of this article is the listener pattern, in the next example we will show how to communicate between two fragments using listeners, although using a shared “ViewModel” is a simpler approach.
The working principle is as follows: a message from the FirstFragment is sent to the Activity, after which the Activity sends a message to the SecondFragment through the callback method.
FirstFragment
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public class PrviFragment extends Fragment { // Interface public interface OnPrviFragmentListener { void onMessageFromPrviFragment(String text); } // Interface object a.k.a. listener private OnPrviFragmentListener mListener; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_prvi, container, false); Button button = v.findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String message = "Hello, Blue! I'm Green."; // Event triggering mListener.onMessageFromPrviFragment(message); } }); return v; } @Override public void onAttach(Context context) { super.onAttach(context); // Defining a listener object that listens as an activity if (context instanceof OnPrviFragmentListener) { mListener = (OnPrviFragmentListener) context; } else { throw new RuntimeException(context.toString() + " must implement OnPrviFragmentListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; } } |
Activity
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public class MainActivity extends AppCompatActivity implements PrviFragment.OnPrviFragmentListener { private static final String PRVI_TAG = "prvi"; private static final String DRUGI_TAG = "drugi"; PrviFragment mPrviFragment; DrugiFragment mDrugiFragment; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FragmentManager fragmentManager = getSupportFragmentManager(); mPrviFragment = (PrviFragment) fragmentManager.findFragmentByTag(PRVI_TAG); if (mPrviFragment == null) { mPrviFragment = new PrviFragment(); fragmentManager.beginTransaction().add(R.id.prvi_fragment_container, mPrviFragment, PRVI_TAG).commit(); } mDrugiFragment = (DrugiFragment) fragmentManager.findFragmentByTag(DRUGI_TAG); if (mDrugiFragment == null) { mDrugiFragment = new DrugiFragment(); fragmentManager.beginTransaction().add(R.id.drugi_fragment_container, mDrugiFragment, DRUGI_TAG).commit(); } } // Defining the callback method ie. actions after triggering the event @Override public void onMessageFromPrviFragment(String message) { mDrugiFragment.youveGotMail(message); } } |
SecondFragment
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class DrugiFragment extends Fragment { private TextView mTextView; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_drugi, container, false); mTextView = v.findViewById(R.id.textview); return v; } // The public method that the Activity uses to pass the message to the other fragment public void youveGotMail(String message) { mTextView.setText(message); } } |
Communication between Dialogue and Activity is solved in a similar way.
Communication Adapter – Activity
In this example, the interface is defined by default, followed by the setter method, as well as event triggering within the adapter class. Although in the examples on the net you can often find that the triggering of the event is done within the “onBindViewHolder() method, it is recommended that the triggering of the event is done within the ViewHolder class, i.e. within its constructor.
Adapter
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public class nekiAdapter extends RecyclerView.Adapter<nekiAdapter.ViewHolder> { // Interface public interface nekiListener { void onItemClick(ControllerMap controller); } nekiListener mListener; // Setter method public void setListener(nekiListener value){ mListener = value; } . . . public class ViewHolder extends RecyclerView.ViewHolder { ... public ViewHolder(final View itemView) { super(itemView); ButterKnife.bind(this, itemView); itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(mListener != null){ // Event triggering mListener.onItemClick(mMap); } } }); } } |
After defining the interface,it is necessary to register a listener and define a callback method in the fragment or activity that uses the adapter:
Fragment or Activity
Here we will define an anonymous listener object “on the fly” and in it define the action to trigger the event:
|
1 2 3 4 5 6 |
mAdapter.setListener(new nekiAdapter.nekiListener() { @Override public void onItemClick(ControllerMap map) { // Some action that is executed when an event is fired } }); |
This could have been done in another way: when the Activity implements the interface, it is enough to validate the callback method for defining the action after the event is fired.
Instantiating an anonymous object that implements the interface “on the fly”:
An anonymous class is a class that does not have a name, so if we wanted to instantiate an object from this class it would look like this:
|
1 |
new { } |
We need a specific anonymous class, one that implements a specific interface, so although we don’t need to define a class name, we do need to define which interface to implement. If the interface is defined in its own file, this is achieved by inserting the name of the interface before the brackets:
|
1 |
new NazivInterfejsa () { }; |
We also need to implement interface methods:
|
1 2 3 4 5 6 |
new NazivInterfejsa () { @Override public void nekaCallBacKMetoda() { // the code that performs the action after the event is triggered } }; |
Communication between two classes:
When the interface is needed for communication only between two classes, then it is usually defined and created within the so-called. first class and it is necessary to call the interface through the first class:
|
1 |
new PrvaKlasa.NazivInterfejsa () { }; |
In addition to this, we need to implement all the abstract methods of this interface:
|
1 2 3 4 5 6 |
new PrvaKlasa.NazivInterfejsa () { @Override public void interfejsMetoda() { // some code in the callback method } }; |
In this example, using the “anonymous class” we created an “anonymousListenerObject” (that is, an object that listens), but the “listenerObject” does not have to be anonymous, it can be stored in a variable and used multiple times:
|
1 2 3 4 5 6 |
PrvaKlasa.NazivInterfejsa listenerObjekat = new PrvaKlasa.NazivInterfejsa() { @Override public void callBackMetoda() { // the code that performs the action after the event is triggered } }; |
Instantiating an anonymous object that implements the interface “on the fly”:
An anonymous class is a class that does not have a name, so if we wanted to instantiate an object from this class it would look like this:
|
1 |
new { } |
We need a specific anonymous class, one that implements a specific interface, so although we don’t need to define a class name, we do need to define which interface to implement. If the interface is defined in its own file, this is achieved by inserting the name of the interface before the brackets:
|
1 |
new NazivInterfejsa () { }; |
We also need to implement interface methods:
|
1 2 3 4 5 6 |
new NazivInterfejsa () { @Override public void nekaCallBacKMetoda() { // the code that performs the action after the event is triggered } }; |
|
1 2 3 |
public void setOnClickListener(View.OnClickListener onClickListener) { this.onClickListener = onClickListener; } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public interface NekiInterfejs { void callbackMetoda(); } private NekiInterfejs mListener; public void setListener(NekiInterfejs value){ this.mListener = value; } . . . // somewhere in the first class, the callback method is called and thus defines the moment when the event is "triggered". if(mListener != null){ mListener.callbackMetoda(); } |
If “objectImplementingInterface” is a descendant of View class then it can be inserted into View of other class as “custom view”.
1) Inserted statically (directly in .xml)
If “objectWhichImplementInterfejs” is inserted statically (as a custom view in the layout) then we target it as a regular view using the method indViewById():
|
1 2 3 4 5 6 7 |
PrvaKlasa nekiCustomView; nekiCustomView = findViewById(R.id.ubacenViewPrveKlase); . . . // Calling a setter method using an embedded view nekiCustomView.setListener(anonimniObjekatKojiImplementiraInterfejs); |
2) Inserted programmed
2.a) By creating a First Class object
|
1 2 |
PrvaKlasa nekiCustomView = new PrvaKlasa(); nekiCustomView.setListener(listenerObjekat); |
“listenerObject” is actually an anonymous object created “on the fly”:
|
1 2 3 4 5 6 |
nekiCustomView.setListener(new PrvaKlasa.nekiInterfejs() { @Override public void callbackMetoda() { // action after triggering the event } }); |
2.b) DrugaKlasa implements the interface
In case the whole class implements the interface then it is used:
|
1 |
PrvaKlasa nekiCustomView = new PrvaKlasa(LayoutInflater.from(this), null); |
First class
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public interface NekiInterfejs { void callbackMetoda(); } private ArrayList<NekiInterfejs> mListeners = new ArrayList<>(); // setter method public void registerListener (NekiInterfejs value){ if(!mListeners.contains(value)) mListeners.add(value); } public void unregisterListener (NekiInterfejs value){ if(mListeners.contains(value)) mListeners.remove(value); } // preparatory method for triggering the event public void pozivanjeCallbackMetode(){ for(NekiInterfejs listener : mListeners){ listener.callbackMetoda(); } } . . . // Somewhere in the first class, indirectly call callbackMethod() by calling the callbackMethod() method, because that's how we'll fire the event pozivanjeCallbackMetode(); |
Second class
|
1 2 3 4 5 6 7 |
// registering an object that will be a listener objectImplementingInterface.registerListener(listeningObject); . . . // Now instead of calling the callback method to trigger the event, we call the callingCallbackMethod() method pozivanjeCallbackMetode(); |
With this approach, within the Activity, we do not define which object will be the listener, but we do it within the fragment itself through the onAttach() method. So that the listener object becomes any activity to which the fragment is attached, while implementing the interface:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public class NekiFragment extends Fragment { public interface NekiInterfejs { void callbackMetoda(); } private NekiInterfejs mListener; . . . // @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof NekiInterfejs) { // Assigning "mListener" activity when uploading a fragment mListener = (NekiInterfejs) context; } else { throw new ClassCastException(context.toString() + "must implement NekiFragment.NekiInterfejs"); } } } . . . public void nekaMetoda() { // Triggering an event: mListener.callbackMetoda(); } } |
In the activity that implements the interface, only the callbackMethod is overridden to define the action when the event is triggered:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public static class MainActivity extends Activity implements NekiFragment.OnHeadlineSelectedListener{ NekiFragment fragment; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); fragment = (NekiFragment) getSupportFragmentManager().findFragmentById(R.id.neki_fragment); } . . . // Defining an action when an event is triggered @Override public void callbackMetoda() { if (fragment != null && fragment.isInLayout()) { // Some action that is executed when the event is triggered } } } |
