12

In a simple app project at GitHub I have only 2 custom Java-files:

  1. MainActivity.java contains Bluetooth- and UI-related source code
  2. DeviceListAdapter.java contains an Adapter and ViewHolder for displaying Bluetooth devices in a RecyclerView

app screenshot

The MainActivity.java contains a method to be called, when user taps on a Bluetooth device in the RecyclerView:

public void confirmConnection(String address) {
    final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);

    AlertDialog.Builder builder = new AlertDialog.Builder(this);
    builder.setMessage("Do you want to pair to " + device + "?");
    builder.setPositiveButton(R.string.button_ok, 
      new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
            device.createBond();
        }
    });
    builder.setNegativeButton(R.string.button_cancel, null);
    builder.show();
}

And in the ViewHolder class (in the DeviceListAdapter.java) the click listener is defined:

public class DeviceListAdapter extends
  RecyclerView.Adapter<DeviceListAdapter.ViewHolder> {

  private ArrayList<BluetoothDevice> mDevices = new ArrayList<BluetoothDevice>();

  protected static class ViewHolder
        extends RecyclerView.ViewHolder
        implements View.OnClickListener {

    private TextView deviceAddress;

    public ViewHolder(View v) {
        super(v);
        v.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        String address = deviceAddress.getText().toString();

        Toast.makeText(v.getContext(),
                "How to call MainActivity.confirmConnection(address)?",
                Toast.LENGTH_SHORT).show();
    }
  }

My problem:

How to call confirmConnection(address) method from ViewHolders onClick method?

I keep moving ViewHolder class declaration between the 2 Java files and also tried putting it into its own file - and just can't find the right way.

Should I maybe add a field to ViewHolder class and (when?) store a reference to MainActivity instance there?

UPDATE:

This works for me, but seems to be a workaround (and also I was thinking of using LocalBroadcastReceiver - which would be an even more hackish workaround) -

    @Override
    public void onClick(View v) {
        String address = deviceAddress.getText().toString();

        try {
            ((MainActivity) v.getContext()).confirmConnection(address);
        } catch (Exception e) {
            // ignore
        }
    }
Alexander Farber
  • 21,519
  • 75
  • 241
  • 416
  • 3
    Your ViewHolder contains a View. This View has a Context. This Context is most probably your activity. So cast the Context to MainActivity and you should be fine. Using interfaces is the better approach though. ((MainActivity)v.getContext()).confirmConnection() should be it. – ElDuderino Sep 22 '15 at 15:22

5 Answers5

30

To keep your classes decoupled, I'd suggest defining an interface on your adapter, something like:

public interface OnBluetoothDeviceClickedListener {
    void onBluetoothDeviceClicked(String deviceAddress);
}

Then add a setter for this in your adapter:

private OnBluetoothDeviceClickedListener mBluetoothClickListener;

public void setOnBluetoothDeviceClickedListener(OnBluetoothDeviceClickedListener l) {
    mBluetoothClickListener = l;
}

Then internally, in your ViewHolder's onClick():

if (mBluetoothClickListener != null) {
    final String addresss = deviceAddress.getText().toString();
    mBluetoothClickListener.onBluetoothDeviceClicked(address);
}

Then just have your MainActivity pass in a listener to the Adapter:

mDeviceListAdapter.setOnBluetoothDeviceClickedListener(new OnBluetoothDeviceClickedListener() {
    @Override
    public void onBluetoothDeviceClicked(String deviceAddress) {
        confirmConnection(deviceAddress);
    }
});

This way you can reuse the adapter later without it being tied to that particular behavior.

Kevin Coppock
  • 133,643
  • 45
  • 263
  • 274
  • 2
    What if I'm getting an "Non-static method cannot be referenced from a static context" error? – user3484582 Jun 30 '16 at 17:31
  • @user3484582 Probably means you're trying to call a method in the enclosing class from a static inner class (e.g. if you have a static inner class in your Activity and are trying to call a method on your Activity from there) – Kevin Coppock Jun 30 '16 at 18:40
  • 2
    How can mBluetoothClickListener non static field be used in static ViewHolder class? – MBDevelop Aug 26 '16 at 09:19
  • @MBDevelop, I wrote an answer below: http://stackoverflow.com/a/40563598/2914140. – CoolMind Nov 12 '16 at 14:02
7

For those who are looking for invoking a callback from a static ViewHolder do the following. Let you have an adapter:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
    private final int resource;
    private final List<Item> items;
    private final LayoutInflater inflater;
    ...
    private Callback callback;

    private static class ViewHolder extends RecyclerView.ViewHolder {
        ...
    }

    public interface Callback {
        void onImageClick(int position);
        void onRemoveItem(int position);
    }
}

Then you should add a setCallback method and call it from activity/fragment. Also you shouldn't make a callback static (it may lead to problems when you use the same adapter in many classes). You should create a field inside the ViewHolder. So:

    public MyAdapter(Context context, int resource, List<Item> items, Callback callback) {
        super();
        this.resource = resource;
        this.items = items;
        this.inflater = LayoutInflater.from(context);
        this.callback = callback;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        final ViewHolder viewHolder = (ViewHolder) holder;
        final Item item = this.items.get(position);
        viewHolder.caption.setText(item.caption);
        viewHolder.callback = callback;
    }

    // A method to set a callback from activity/fragment.
    public void setCallback(Callback callback) {
        this.callback = callback;
    }

    public static class Item {
        public long id;
        public String number;
        public String caption;

        ...
    }

    private static class ViewHolder extends RecyclerView.ViewHolder {
        protected final TextView caption;
        // A reference to an adapter's callback.
        protected Callback callback;

        public ViewHolder(View view) {
            super(view);
            this.caption = (TextView) view.findViewById(R.id.caption);
        }

        private View.OnClickListener onClickListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int id = v.getId();
                if (id == R.id.image) {
                    // Invoke the callback here.
                    if (callback != null) {
                        callback.onImageClick(getLayoutPosition());
                    }
                }
            }
        };
    }
}

After you have made the adapter you can invoke it so:

adapter = new MyAdapter(getActivity(), R.layout.item,
        new ArrayList<MyAdapter.Item>(), null);

adapterListener = new MyAdapter.Callback() {
    @Override
    public void onImageClick(int position) {
        // Some actions.
    }

    @Override
    public void onRemoveItem(int position) {
        // Some actions.
    }
};

adapter.setCallback(adapterListener);
CoolMind
  • 26,736
  • 15
  • 188
  • 224
3

You could pass the MainActivity as a constructor-parameter for the Adapter and store it in a field. Or you use a event-bus - there are multiple ways to do it - I would go for the field

ligi
  • 39,001
  • 44
  • 144
  • 244
2

In your adapter, create an interface that will provide a callback to the main activity

public interface MyCallback{
    void onItemClicked();
}

private MyCallback listener;

public setOnItemClickListener(MyCallback callback){
    listener = callback;
}

Have your main activity implement it.

public class MainActivity extends AppCompatActivity implements MyCallback

then implement the callback

@Override
public void onItemClick(){
    //do work
}

then just set the callback from the adapter

mDeviceListAdapter.setOnItemClickListener(this);
Nikhil
  • 1,212
  • 13
  • 30
tyczj
  • 71,600
  • 54
  • 194
  • 296
1

You can call Activity method by using instance of Activity like this, inside MainActivity write below code

mDeviceListAdapter = new DeviceListAdapter(MainActivity.this);

Inside Adapter

 private MainActivity _mainActivity;
 public DeviceListAdapter(MainActivity activity){
 this._mainActivity=activity;
 }

Inside your onClick method

 _mainActivity.yourActivityMethod(address);
Ajit Kumar Dubey
  • 1,383
  • 1
  • 19
  • 33