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?
setOnClickListener
and findViewById
for each button, that's why onClick
was added, to make things simpler.
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;
}
}
}
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
}
}
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);
}
});
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;
}
...
}
android:name="fragment" android:type="com.example.testapp.fragments.CustomFragment"/>
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?
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);
}
}
}
}
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
.
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.
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);
}
}
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.
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.
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
}
}
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.
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
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.
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)
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
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);
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
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()
}
Success story sharing