460

Pre-Honeycomb (Android 3), each Activity was registered to handle button clicks via the onClick tag in a Layout's XML:

android:onClick="myClickMethod"

Within that method you can use view.getId() and a switch statement to do the button logic.

With the introduction of Honeycomb I'm breaking these Activities into Fragments which can be reused inside many different Activities. Most of the behavior of the buttons is Activity independent, and I would like the code to reside inside the Fragments file without using the old (pre 1.6) method of registering the OnClickListener for each button.

final Button button = (Button) findViewById(R.id.button_id);
button.setOnClickListener(new View.OnClickListener() {
    public void onClick(View v) {
        // Perform action on click
    }
});

The problem is that when my layout's are inflated it is still the hosting Activity that is receiving the button clicks, not the individual Fragments. Is there a good approach to either

  • Register the fragment to receive the button clicks?
  • Pass the click events from the Activity to the fragment they belong to?
DilithiumMatrix
  • 17,795
  • 22
  • 77
  • 119
smith324
  • 13,020
  • 9
  • 37
  • 58
  • 1
    Can't you handle registering listeners within the onCreate of the fragment? – Jodes May 22 '11 at 22:24
  • 26
    @jodes Yes, but I don't want to have to use `setOnClickListener` and `findViewById` for each button, that's why `onClick` was added, to make things simpler. – smith324 May 22 '11 at 22:35
  • 4
    Looking at the accepted answer I think using setOnClickListener is more loosely coupled than sticking to the XML onClick approach. If the activity has to 'forward' each click to the right fragment this means that code will have to change each time a fragment is added. Using an interface to decouple from the fragment's base class does not help with that. If the fragment registers with the correct button itself, the activity remains completely agnostic which is better style IMO. See also the answer from Adorjan Princz. – Adriaan Koster Dec 10 '14 at 14:11
  • @smith324 have to agree with Adriaan on this one. Have a go of Adorjan's answer and see if life isn't any better after that. – user1567453 Sep 24 '17 at 04:54

19 Answers19

619

I prefer using the following solution for handling onClick events. This works for Activity and Fragments as well.

public class StartFragment extends Fragment implements OnClickListener{

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {

        View v = inflater.inflate(R.layout.fragment_start, container, false);

        Button b = (Button) v.findViewById(R.id.StartButton);
        b.setOnClickListener(this);
        return v;
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
        case R.id.StartButton:

            ...

            break;
        }
    }
}
Lorenzo Polidori
  • 10,332
  • 10
  • 51
  • 60
Adorjan Princz
  • 11,708
  • 3
  • 34
  • 25
  • 1
    I'd do this instead of accepted answer, but I use `android:onClick` for buttons placed in rows of listview and then I just do `getPositionForView(v)` to get the position. I don't know how would I use this to achieve it. – prostynick Mar 20 '13 at 11:39
  • 9
    In onCreateView, I loop through all the child items of the ViewGroup v and set the onclicklistener for all the Button instances that I find. It's much better than manually setting the listener for all buttons. – A Person Mar 21 '13 at 15:24
  • 18
    Voted. This makes the fragments reusable. Otherwise why using fragments? – boreas May 29 '13 at 16:00
  • 2
    @NathanOsman granted this is the solution I would recommend and use but the question asks not to register an on click listener – Blundell Jun 11 '13 at 09:44
  • 1
    It is acceptable solution, but is is quite puzzling for me why you can not achieve same result, when registering onClick in fragment layout xml. – apocalypz Sep 24 '13 at 11:39
  • I do not prefer to register the event handlers in the xml file because there are a lot of events which do not have xml attributes so if you want to register all events in the same place, the only possible solution is to register them in the code. – Adorjan Princz Sep 25 '13 at 20:22
  • Its a shame android doesn't have a responder chain like iOS's `UIResponder` class, however for fragments this solution is as close as it will come. – Leon Storey Oct 17 '13 at 01:23
  • 47
    Isn't this the same technique advocated by [Programming Windows](http://www.charlespetzold.com/pw5/ProgWinEditions.html) back in 1987? Not to worry. Google moves fast and is all about developer productivity. I'm sure it won't be long until event handling is as good as 1991-eara Visual Basic. – Edward Brey Oct 18 '13 at 14:06
  • 7
    witch import have you used for OnClickListener? Intellij suggests me android.view.View.OnClickListener and it doesn't work :/ (onClick never runs) – Lucas Jota Nov 29 '13 at 13:26
  • 1
    @LucasJota it is `android.view.View.OnClickListener`. For me, for some reason, I have to actually say `implements View.OnClickListener`, so that may be your issue. – Kevin McCarpenter Mar 10 '14 at 04:31
  • 1
    works nice but click event is not fired from `XML` ... which is the PO's issue. Thanks though – S.Thiongane Nov 05 '14 at 15:02
  • 4
    @NathanOsman I think Question was related to the xml onClick, so the accepted ans provide the exact solution. – Naveed Ahmad Nov 19 '14 at 12:43
  • This sort of solution is now suggested in the official docs at [Android Guide > UI > Input Events > Event Listeners](http://developer.android.com/guide/topics/ui/ui-events.html#EventListeners), albeit with an example for an Activity. – John Bentley Dec 12 '14 at 19:59
  • 2
    I am using API 21 and this solution does not work...any suggestions? thanks – Demian Flavius Mar 10 '15 at 09:48
  • Same here, I'm using API 21 and my app crashes the moment the fragment is loaded, if I follow this approach (I do agree that this looks far more elegant and less cluttered than the accepted answer). Any suggestions how to make this work please? – Darth Coder Mar 10 '15 at 17:31
  • @AdorjanPrincz your solution looks very clean and to-the-point but it doesn't seem to be working for me. Could you please take a look at my problem as well: stackoverflow.com/questions/29021441/unable-to-get-onclick-method-in-fragment-to-work-using-existing-solutions – Darth Coder Mar 12 '15 at 22:27
  • @EdwardBrey the answer made no claims to being original. – Jon Cairns Jun 08 '15 at 11:13
  • 1
    @joonty Agreed. My comment was a jab at Google, not the SO answer. – Edward Brey Jun 08 '15 at 13:37
  • This does not work on API level 20, onClick never fired, why so many up votes? – zdd Jun 09 '15 at 07:08
  • all encapsulated into the same component, I love it! – QuarK Jun 09 '15 at 11:19
  • 2
    implements OnClickListener.... which one do we have to import to make this implement work? – A. Adam Jul 18 '15 at 23:23
  • In case anyone gets here and is still confused, yes this doesn't work (at least I sure couldn't get it to and I followed it to the T and have this method working in activities). For fragments, go below and look at the barely upvoted method answered by Brill Pappin that works perfectly. – StackAttack Dec 01 '17 at 05:15
  • 1
    This worked for me on Android Studio 4.x. Just a slight cleanup I'd recommend to avoid a switch: put your logic into a method in your fragment's class, then use a lambda instead of an anonymous object as the listener, e.g., `view.findViewById(.R.id.donut).setOnClickListener(v -> showDonutOrder(v));` – Jordan Deyton May 17 '21 at 23:18
180

You could just do this:

Activity:

Fragment someFragment;    

//...onCreate etc instantiating your fragments

public void myClickMethod(View v) {
    someFragment.myClickMethod(v);
}

Fragment:

public void myClickMethod(View v) {
    switch(v.getId()) {
        // Just like you were doing
    }
}    

In response to @Ameen who wanted less coupling so Fragments are reuseable

Interface:

public interface XmlClickable {
    void myClickMethod(View v);
}

Activity:

XmlClickable someFragment;    

//...onCreate, etc. instantiating your fragments casting to your interface.
public void myClickMethod(View v) {
    someFragment.myClickMethod(v);
}

Fragment:

public class SomeFragment implements XmlClickable {

//...onCreateView, etc.

@Override
public void myClickMethod(View v) {
    switch(v.getId()){
        // Just like you were doing
    }
}    
Daniel Nugent
  • 43,104
  • 15
  • 109
  • 137
Blundell
  • 75,855
  • 30
  • 208
  • 233
  • 52
    That's what I'm doing now essentially but it is a lot messier when you have multiple fragments that each need to receive click events. I'm just aggravated with fragments in general because paradigms have dissolved around them. – smith324 Jun 09 '11 at 00:47
  • It doesn't get much messier, as you just pass it the click even to all your fragments and only the one with the ID will react, but yeah they have thrown a fly in the oitment – Blundell Jun 09 '11 at 07:37
  • Lovely! This isn't messy at all, even makes it easier for other people to read the code (as they don't have to guess where the actual call ends up). :-) – karllindmark Jan 31 '12 at 18:50
  • I think, you should name the one in the fragment something like `myClickMethod_actual` or something like that. Since there might be an ambiguity on which method is being called at a later point of time. I know that at this point of time the method in the activity is just a stub but you may add something to it later. – Dheeraj Bhaskar Feb 02 '13 at 21:05
  • 133
    I'm running into the same issue, and even though I appreciate your response, this is not clean code from a software engineering point of view. This code results in the activity being tightly coupled with the fragment. You should be able to re-use the same fragment in multiple activities without the activities knowing the implementation details of the fragments. – Ameen Feb 08 '13 at 02:20
  • @Ameen all you would need to do is let your Fragment implement a `XmlClickable` interface – Blundell Mar 21 '14 at 11:21
  • 1
    Should be "switch(v.getId()){" and not "switch(v.getid()){" – eb80 Jun 25 '14 at 16:42
  • If using the first pass through method, you do not need to make the method in the fragment public. – Steve Waring Jul 20 '14 at 08:11
  • 5
    Instead of defining your own Interface, you can use the already existing OnClickListener as mentioned by Euporie. – fr00tyl00p Sep 07 '14 at 11:50
  • I think this is really the correct answer. The belower answer (with too much upvotes) doesn't solve PO's issue ... he wanted to fire click event from `XML`, not from `JAVA` ! – S.Thiongane Nov 05 '14 at 15:01
  • You could also use the existing View.OnClickListener interface on your fragment! – jrmgx Dec 20 '14 at 12:05
  • 1
    I about cried when I read this, it is SO MUCH BOILERPLATE... The below answer from @AdorjanPrincz is the way to go. – Wjdavis5 Jan 02 '15 at 01:10
  • "instantiating your fragments casting to your interface." how ? – Milad Faridnia Aug 15 '16 at 11:16
  • 1
    @milad `XmlClickable someFragment = (XmlClickable) getFragmentManager().findFragment....` – Blundell Aug 16 '16 at 07:18
  • @DarthCoder What do you mean didnt work? Was there an error or app didnt respond for onclick? did you register the onclick listener for the ui element? – Hanu Apr 29 '17 at 08:24
  • @Blundell This is very bad practice. Even XmlClickable is tightly coupled architecture. Everyone should scroll down to the other answer (500+ upvotes and will save you headaches) – user1567453 Sep 24 '17 at 04:51
  • @user1567453 whilst I answered this in 2011, did you read the question? They specifically asked not to register a click listener – Blundell Sep 24 '17 at 12:40
  • This is actually correct. When a Fragment is added to an Activity, all its views are added to the View Hierarchy of the Activity. this is why only activities can handle view events like clicks. As a subview, the fragment should really be "dumb". If you want a reusable code, just call another class to handle the click and do it everywhere the fragment goes. PS: for clean code, just don't use XML binding, do it in code :) you could set a click listener when you create the view, in the fragment. – Matei Suica Mar 01 '18 at 08:12
31

I've recently solved this issue without having to add a method to the context Activity or having to implement OnClickListener. I'm not sure if it is a "valid" solution neither, but it works.

Based on: https://developer.android.com/tools/data-binding/guide.html#binding_events

It can be done with data bindings: Just add your fragment instance as a variable, then you can link any method with onClick.

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.example.testapp.fragments.CustomFragment">

    <data>
        <variable android:name="fragment" android:type="com.example.testapp.fragments.CustomFragment"/>
    </data>
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_place_black_24dp"
            android:onClick="@{() -> fragment.buttonClicked()}"/>
    </LinearLayout>
</layout>

And the fragment linking code would be...

public class CustomFragment extends Fragment {

    ...

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_person_profile, container, false);
        FragmentCustomBinding binding = DataBindingUtil.bind(view);
        binding.setFragment(this);
        return view;
    }

    ...

}
Alex Mamo
  • 130,605
  • 17
  • 163
  • 193
Aldo Canepa
  • 1,791
  • 2
  • 16
  • 16
  • 1
    I just have a feeling, some details are missing since I'm not able to get this solution work – Tima Apr 13 '17 at 08:50
  • Maybe your are missing something from the "Build Environment" in the documentation: https://developer.android.com/tools/data-binding/guide.html#binding_events – Aldo Canepa Apr 25 '17 at 14:33
  • @Aldo For the onClick method in XML I believe you should have android:onClick="@{() -> fragment. buttonClicked()}" instead. Also for others, you should declare buttonClicked() function inside the fragment and put your logic inside. – Hayk Nahapetyan Jan 28 '19 at 11:47
  • in the xml it should be `android:name="fragment" android:type="com.example.testapp.fragments.CustomFragment"/>` – COYG Jun 07 '19 at 03:31
  • 1
    I just tried this and the `name` and `type` attributes of the `variable` tag should _not_ have the `android:` prefix. Perhaps there is an old version of Android layouts that does require it? – JohnnyLambada Feb 05 '20 at 19:32
31

The problem I think is that the view is still the activity, not the fragment. The fragments doesn't have any independent view of its own and is attached to the parent activities view. Thats why the event ends up in the Activity, not the fragment. Its unfortunate, but I think you will need some code to make this work.

What I've been doing during conversions is simply adding a click listener that calls the old event handler.

for instance:

final Button loginButton = (Button) view.findViewById(R.id.loginButton);
loginButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(final View v) {
        onLoginClicked(v);
    }
});
Ziem
  • 6,579
  • 8
  • 53
  • 86
Brill Pappin
  • 4,692
  • 1
  • 36
  • 36
  • 1
    Thanks - I used this with one slight modification in that I'm passing the fragment view (ie. the result of inflater.inflate(R.layout.my_fragment_xml_resource)) to onLoginClicked() so that it can access the fragments sub-views, such as an EditText, via view.findViewById() (If I simply pass through the activity view, calls to view.findViewById(R.id.myfragmentwidget_id) returns null). – Michael Nelson Jan 01 '13 at 10:20
  • This doesn't work with API 21 in my project. Any thoughts on how to use this approach? – Darth Coder Mar 10 '15 at 17:45
  • Its pretty basic code, used in pretty much every app. Can you describe what is happening for you? – Brill Pappin Mar 11 '15 at 02:56
  • 1
    Make my upvote to this answer for explanation of the problem which occurred because fragment's layout is attached to activity's view. – Blo Jan 26 '16 at 22:39
7

I would rather go for the click handling in code than using the onClick attribute in XML when working with fragments.

This becomes even easier when migrating your activities to fragments. You can just call the click handler (previously set to android:onClick in XML) directly from each case block.

findViewById(R.id.button_login).setOnClickListener(clickListener);
...

OnClickListener clickListener = new OnClickListener() {
    @Override
    public void onClick(final View v) {
        switch(v.getId()) {
           case R.id.button_login:
              // Which is supposed to be called automatically in your
              // activity, which has now changed to a fragment.
              onLoginClick(v);
              break;

           case R.id.button_logout:
              ...
        }
    }
}

When it comes to handling clicks in fragments, this looks simpler to me than android:onClick.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ron
  • 24,175
  • 8
  • 56
  • 97
7

ButterKnife is probably the best solution for the clutter problem. It uses annotation processors to generate the so called "old method" boilerplate code.

But the onClick method can still be used, with a custom inflator.

How to use

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup cnt, Bundle state) {
    inflater = FragmentInflatorFactory.inflatorFor(inflater, this);
    return inflater.inflate(R.layout.fragment_main, cnt, false);
}

Implementation

public class FragmentInflatorFactory implements LayoutInflater.Factory {

    private static final int[] sWantedAttrs = { android.R.attr.onClick };

    private static final Method sOnCreateViewMethod;
    static {
        // We could duplicate its functionallity.. or just ignore its a protected method.
        try {
            Method method = LayoutInflater.class.getDeclaredMethod(
                    "onCreateView", String.class, AttributeSet.class);
            method.setAccessible(true);
            sOnCreateViewMethod = method;
        } catch (NoSuchMethodException e) {
            // Public API: Should not happen.
            throw new RuntimeException(e);
        }
    }

    private final LayoutInflater mInflator;
    private final Object mFragment;

    public FragmentInflatorFactory(LayoutInflater delegate, Object fragment) {
        if (delegate == null || fragment == null) {
            throw new NullPointerException();
        }
        mInflator = delegate;
        mFragment = fragment;
    }

    public static LayoutInflater inflatorFor(LayoutInflater original, Object fragment) {
        LayoutInflater inflator = original.cloneInContext(original.getContext());
        FragmentInflatorFactory factory = new FragmentInflatorFactory(inflator, fragment);
        inflator.setFactory(factory);
        return inflator;
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        if ("fragment".equals(name)) {
            // Let the Activity ("private factory") handle it
            return null;
        }

        View view = null;

        if (name.indexOf('.') == -1) {
            try {
                view = (View) sOnCreateViewMethod.invoke(mInflator, name, attrs);
            } catch (IllegalAccessException e) {
                throw new AssertionError(e);
            } catch (InvocationTargetException e) {
                if (e.getCause() instanceof ClassNotFoundException) {
                    return null;
                }
                throw new RuntimeException(e);
            }
        } else {
            try {
                view = mInflator.createView(name, null, attrs);
            } catch (ClassNotFoundException e) {
                return null;
            }
        }

        TypedArray a = context.obtainStyledAttributes(attrs, sWantedAttrs);
        String methodName = a.getString(0);
        a.recycle();

        if (methodName != null) {
            view.setOnClickListener(new FragmentClickListener(mFragment, methodName));
        }
        return view;
    }

    private static class FragmentClickListener implements OnClickListener {

        private final Object mFragment;
        private final String mMethodName;
        private Method mMethod;

        public FragmentClickListener(Object fragment, String methodName) {
            mFragment = fragment;
            mMethodName = methodName;
        }

        @Override
        public void onClick(View v) {
            if (mMethod == null) {
                Class<?> clazz = mFragment.getClass();
                try {
                    mMethod = clazz.getMethod(mMethodName, View.class);
                } catch (NoSuchMethodException e) {
                    throw new IllegalStateException(
                            "Cannot find public method " + mMethodName + "(View) on "
                                    + clazz + " for onClick");
                }
            }

            try {
                mMethod.invoke(mFragment, v);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new AssertionError(e);
            }
        }
    }
}
sergio91pt
  • 1,459
  • 18
  • 21
6

This is another way:

1.Create a BaseFragment like this:

public abstract class BaseFragment extends Fragment implements OnClickListener

2.Use

public class FragmentA extends BaseFragment 

instead of

public class FragmentA extends Fragment

3.In your activity:

public class MainActivity extends ActionBarActivity implements OnClickListener

and

BaseFragment fragment = new FragmentA;

public void onClick(View v){
    fragment.onClick(v);
}

Hope it helps.

Euporie
  • 1,896
  • 1
  • 21
  • 23
  • 1 year, 1 month and 1 day after your answer: Is there any reason other than not repeating implementation of OnClickListener on every Fragment class to create the abstract BaseFragment? – Dimitrios K. Nov 24 '14 at 22:09
5

In my use case, I have 50 odd ImageViews I needed to hook into a single onClick method. My solution is to loop over the views inside the fragment and set the same onclick listener on each:

    final View.OnClickListener imageOnClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            chosenImage = ((ImageButton)v).getDrawable();
        }
    };

    ViewGroup root = (ViewGroup) getView().findViewById(R.id.imagesParentView);
    int childViewCount = root.getChildCount();
    for (int i=0; i < childViewCount; i++){
        View image = root.getChildAt(i);
        if (image instanceof ImageButton) {
            ((ImageButton)image).setOnClickListener(imageOnClickListener);
        }
    }
Chris Knight
  • 24,333
  • 24
  • 88
  • 134
3

As I see answers they're somehow old. Recently Google introduce DataBinding which is much easier to handle onClick or assigning in your xml.

Here is good example which you can see how to handle this :

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="handlers" type="com.example.Handlers"/>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       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:text="@{user.firstName}"
           android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"
           android:onClick="@{user.isFriend ? handlers.onClickFriend : handlers.onClickEnemy}"/>
   </LinearLayout>
</layout>

There is also very nice tutorial about DataBinding you can find it Here.

Amir
  • 16,067
  • 10
  • 80
  • 119
3

You can define a callback as an attribute of your XML layout. The article Custom XML Attributes For Your Custom Android Widgets will show you how to do it for a custom widget. Credit goes to Kevin Dion :)

I'm investigating whether I can add styleable attributes to the base Fragment class.

The basic idea is to have the same functionality that View implements when dealing with the onClick callback.

Mathieu K.
  • 283
  • 3
  • 14
Nelson Ramirez
  • 7,864
  • 7
  • 28
  • 34
1

Adding to Blundell's answer,
If you have more fragments, with plenty of onClicks:

Activity:

Fragment someFragment1 = (Fragment)getFragmentManager().findFragmentByTag("someFragment1 "); 
Fragment someFragment2 = (Fragment)getFragmentManager().findFragmentByTag("someFragment2 "); 
Fragment someFragment3 = (Fragment)getFragmentManager().findFragmentByTag("someFragment3 "); 

...onCreate etc instantiating your fragments

public void myClickMethod(View v){
  if (someFragment1.isVisible()) {
       someFragment1.myClickMethod(v);
  }else if(someFragment2.isVisible()){
       someFragment2.myClickMethod(v);
  }else if(someFragment3.isVisible()){
       someFragment3.myClickMethod(v); 
  }

} 

In Your Fragment:

  public void myClickMethod(View v){
     switch(v.getid()){
       // Just like you were doing
     }
  } 
amalBit
  • 12,041
  • 6
  • 77
  • 94
1

If you register in xml using android:Onclick="", callback will be given to the respected Activity under whose context your fragment belongs to (getActivity() ). If such method not found in the Activity, then system will throw an exception.

Isham
  • 424
  • 4
  • 14
1

You might want to consider using EventBus for decoupled events .. You can listen for events very easily. You can also make sure the event is being received on the ui thread (instead of calling runOnUiThread.. for yourself for every event subscription)

https://github.com/greenrobot/EventBus

from Github:

Android optimized event bus that simplifies communication between Activities, Fragments, Threads, Services, etc. Less code, better quality

programmer
  • 3,043
  • 1
  • 22
  • 30
1

I'd like to add to Adjorn Linkz's answer.

If you need multiple handlers, you could just use lambda references

void onViewCreated(View view, Bundle savedInstanceState)
{
    view.setOnClickListener(this::handler);
}
void handler(View v)
{
    ...
}

The trick here is that handler method's signature matches View.OnClickListener.onClick signature. This way, you won't need the View.OnClickListener interface.

Also, you won't need any switch statements.

Sadly, this method is only limited to interfaces that require a single method, or a lambda.

Community
  • 1
  • 1
Dragas
  • 1,140
  • 13
  • 29
1

Though I've spotted some nice answers relying on data binding, I didn't see any going to the full extent with that approach -- in the sense of enabling fragment resolution while allowing for fragment-free layout definitions in XML's.

So assuming data binding is enabled, here's a generic solution I can propose; A bit long but it definitely works (with some caveats):

Step 1: Custom OnClick Implementation

This will run a fragment-aware search through contexts associated with the tapped-on view (e.g. button):


// CustomOnClick.kt

@file:JvmName("CustomOnClick")

package com.example

import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import java.lang.reflect.Method

fun onClick(view: View, methodName: String) {
    resolveOnClickInvocation(view, methodName)?.invoke(view)
}

private data class OnClickInvocation(val obj: Any, val method: Method) {
    fun invoke(view: View) {
        method.invoke(obj, view)
    }
}

private fun resolveOnClickInvocation(view: View, methodName: String): OnClickInvocation? =
    searchContexts(view) { context ->
        var invocation: OnClickInvocation? = null
        if (context is Activity) {
            val activity = context as? FragmentActivity
                    ?: throw IllegalStateException("A non-FragmentActivity is not supported (looking up an onClick handler of $view)")

            invocation = getTopFragment(activity)?.let { fragment ->
                resolveInvocation(fragment, methodName)
            }?: resolveInvocation(context, methodName)
        }
        invocation
    }

private fun getTopFragment(activity: FragmentActivity): Fragment? {
    val fragments = activity.supportFragmentManager.fragments
    return if (fragments.isEmpty()) null else fragments.last()
}

private fun resolveInvocation(target: Any, methodName: String): OnClickInvocation? =
    try {
        val method = target.javaClass.getMethod(methodName, View::class.java)
        OnClickInvocation(target, method)
    } catch (e: NoSuchMethodException) {
        null
    }

private fun <T: Any> searchContexts(view: View, matcher: (context: Context) -> T?): T? {
    var context = view.context
    while (context != null && context is ContextWrapper) {
        val result = matcher(context)
        if (result == null) {
            context = context.baseContext
        } else {
            return result
        }
    }
    return null
}

Note: loosely based on the original Android implementation (see https://android.googlesource.com/platform/frameworks/base/+/a175a5b/core/java/android/view/View.java#3025)

Step 2: Declarative application in layout files

Then, in data-binding aware XML's:

<layout>
  <data>
     <import type="com.example.CustomOnClick"/>
  </data>

  <Button
    android:onClick='@{(v) -> CustomOnClick.onClick(v, "myClickMethod")}'
  </Button>
</layout>

Caveats

  • Assumes a 'modern' FragmentActivity based implementation
  • Can only lookup method of "top-most" (i.e. last) fragment in stack (though that can be fixed, if need be)
d4vidi
  • 2,407
  • 26
  • 27
0

This has been working for me:(Android studio)

 @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        View rootView = inflater.inflate(R.layout.update_credential, container, false);
        Button bt_login = (Button) rootView.findViewById(R.id.btnSend);

        bt_login.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                System.out.println("Hi its me");


            }// end onClick
        });

        return rootView;

    }// end onCreateView
Vinod Joshi
  • 7,696
  • 1
  • 50
  • 51
0

Best solution IMHO:

in fragment:

protected void addClick(int id) {
    try {
        getView().findViewById(id).setOnClickListener(this);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public void onClick(View v) {
    if (v.getId()==R.id.myButton) {
        onMyButtonClick(v);
    }
}

then in Fragment's onViewStateRestored:

addClick(R.id.myButton);
Opal
  • 81,889
  • 28
  • 189
  • 210
0

Your Activity is receiving the callback as must have used:

mViewPagerCloth.setOnClickListener((YourActivityName)getActivity());

If you want your fragment to receive callback then do this:

mViewPagerCloth.setOnClickListener(this);

and implement onClickListener interface on Fragment

JAAD
  • 12,349
  • 7
  • 36
  • 57
0

The following solution might be a better one to follow. the layout is in fragment_my.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="listener"
            type="my_package.MyListener" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
        <Button
            android:id="@+id/moreTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{() -> listener.onClick()}"
            android:text="@string/login"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

And the Fragment would be as follows

class MyFragment : Fragment(), MyListener {
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
            return FragmentMyBinding.inflate(
                inflater,
                container,
                false
            ).apply {
                lifecycleOwner = viewLifecycleOwner
                listener = this@MyFragment
            }.root
    }

    override fun onClick() {
        TODO("Not yet implemented")
    }

}

interface MyListener{
    fun onClick()
}
sadat
  • 4,004
  • 2
  • 29
  • 49