Thursday, 1 September 2016

Custom listeners with interfaces

In my little experiments, I often find myself using custom dialogs to display information to the user or to get input. Therefore, I needed a way to create a callback method to detect what the user selected once the dialog has been dismissed. In order to do so, we can use interfaces to create custom listeners.

This will allow us to create callback functions for custom dialogs and set listener objects that, well listen to what the user selected and react accordingly. Very much how you set an click listener for buttons.

In this episode we are going to create a custom dialog and set a listener in order to follow the choices made by the user.

Let's first create a layout for our dialog:

 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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="Main text"
        android:id="@+id/custom_dialog_textview"
        android:layout_gravity="center_horizontal" />

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Confirm"
            android:layout_weight="0.5"
            android:id="@+id/custom_dialog_confirmbutton" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Cancel"
            android:layout_weight="0.5"
            android:id="@+id/custom_dialog_cancelbutton" />
    </LinearLayout>
</LinearLayout>

In your design window, will look like so:

We can now go on and create a class called CustomDialog, which will be responsible for spawning our dialog to the screen.


I will post the class code below:

 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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class CustomDialog implements View.OnClickListener {

    ICustomDialogListener listener;

    Button confirmButton;
    Button cancelButton;
    TextView maintext;

    Dialog dialog;

    public enum ButtonResults
    {
        BUTTON_CONFIRM,
        BUTTON_CANCEL

    }

    public CustomDialog(Context c, String text,ICustomDialogListener listener)
    {

        this.listener = listener;

        dialog = new Dialog(c);

        dialog.setContentView(R.layout.custom_dialog_layout);
        dialog.setCancelable(false);

        WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
        Window dialogWin = dialog.getWindow();

        lp.copyFrom(dialogWin.getAttributes());

        lp.width = WindowManager.LayoutParams.MATCH_PARENT;

        dialogWin.setAttributes(lp);

        maintext = (TextView) dialog.findViewById(R.id.custom_dialog_textview);
        maintext.setText(text);

        confirmButton = (Button)dialog.findViewById(R.id.custom_dialog_confirmbutton);
        cancelButton = (Button)dialog.findViewById(R.id.custom_dialog_cancelbutton);

    confirmButton.setOnClickListener(this);
        cancelButton.setOnClickListener(this);

        dialog.show();
    }

    @Override
    public void onClick(View v) {

        switch (v.getId())
        {

            case R.id.custom_dialog_confirmbutton:
                listener.OnDialog(ButtonResults.BUTTON_CONFIRM,dialog);
                break;

            case R.id.custom_dialog_cancelbutton:
                listener.OnDialog(ButtonResults.BUTTON_CANCEL,dialog);
                break;
        }
    }
}

The class itself is pretty simple. The dialog is created in the constructor (line 23) and we pass it in the custom layout we previously set up (line25). From line 28 to line 37 we simply tell the dialog window to extend its width as far as the parent view. I found that MATCH_PARENT doesn't work well when set in the xml file, so this is how you can achieve the same thing in Java.

As you can see, the constructor accepts a parameter of type ICustomDialogListener, which is an interface used to define a listener for this dialog. The object listener is then notified of the button(s) pressed by the user in the onClick method on line 50. To identify which button has been pressed, I used an enumeration. To notify the listener, we call the interface method OnDialog which accepts an enumeration type variable, which identifies the button, and the dialog itself.

Below is the interface ICustomDialogListener:


1
2
3
4
public interface ICustomDialogListener {

    void OnDialog(CustomDialog.ButtonResults result, Dialog dialog);
}

Simple and efficient.

The advantage to use interfaces is that we can implement this in every class which displays a dialog and requires a callback. We can also use it in an anonymous class.

Here's an example. In the MainActivity, I jusst create a dialog in the onCreate method, so the window will pop up immediately as soon as the app boots. You will see that, according to which button is pressed, an appropriate toast message will appear.

This is the code for the onCreate method in MainActivity:

 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
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        CustomDialog dialog = new CustomDialog(MainActivity.this, "Hi", new ICustomDialogListener() {
            @Override
            public void OnDialog(CustomDialog.ButtonResults result, Dialog dialog) {

                switch (result)
                {
                    case BUTTON_CONFIRM:
                        Toast.makeText(MainActivity.this,"You pressed Confirm",Toast.LENGTH_SHORT).show();
                        break;

                    case BUTTON_CANCEL:
                        Toast.makeText(MainActivity.this,"You pressed Cancel",Toast.LENGTH_SHORT).show();
                        break;

                }

              
            }
        });

    }

Remember, the dialog will show because in the constructor of CustomDialog we call dialog.show() on line 46. If you omit that, nothing will show up.

As you can see, in the constructor call for the dialog, we create an anonymous class for the listener (line 8), so we can deal with the callback function straight in the onCreate method.

We could very much pass this, which will require the MainActivity to implement ICustomDialogInterface:

1
   CustomDialog dialog = new CustomDialog(MainActivity.this,"Hi",this);


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class MainActivity extends AppCompatActivity implements ICustomDialogListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

//Rest of the code

.........
........
.......
}

And you would have to implement the method OnDialog(...) required by the interfrace:


 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
   @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public void OnDialog(CustomDialog.ButtonResults result, Dialog dialog) {

        switch(result)
        {
            case BUTTON_CONFIRM:
                Toast.makeText(MainActivity.this,"You pressed Confirm",Toast.LENGTH_SHORT).show();
                break;

            case BUTTON_CANCEL:
                Toast.makeText(MainActivity.this,"You pressed Cancel",Toast.LENGTH_SHORT).show();
                break;

        }

    }

This works exactly like when you set a listener for a button or any other view.

Just as I did in the anonymous class earlier, I use a switch to deal with the result obtained.

Below, a gif will show the final result.



Notice how the dialog is not dismissed after pressing a button. That is because we simply did not tell it to. If you need the dialog to disappear after the user has chosen, add the line dialog.dismiss() in the switch and there you go.

Conclusion

Creating a good callback system is a good way to maintain the code readable and organized. Using interfaces gives us great flexibility as we can implement as many as we want in a single class, in case you would need many different listeners. Of course, this concept can be applied to different scenarios. Perhaps you will need a callback method for a GPS app which will callback when the user has reached a particular location or you could use this technique in a database class, to notify a ListView that the database has been edited and it need to update its views.

Finally, you can also set multiple listeners. Instead of having a single variable listener in the CustomDialog class, we could use a static ArrayList<ICustomDialogListener>. In the constructor we would. It needs to be static of course, as we do not want a new list for each dialog. We can then add new listeners any time the constructor is called and, when it's time to notify them, we simply iterate through the list itself calling the method OnDialog(..) for each of its elements.