Hot questions for Using Butter Knife in xml

Question:

fragment in xml

<fragment
    android:id="@+id/parent_fragment"
    android:name="com.app.example.ParentFragment"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

binding in activity

@BindView(R.id.parent_fragment)
ParentFragment parentFragment;

gradle build error message

@BindView fields must extend from View or be an interface

Is there something like @BindFragment for binding XML fragments using @+id?

I am sorry if this is something obvious.


Answer:

Apparently, there is no such annotation in that library.

http://jakewharton.github.io/butterknife/

Since there will not be a lot of fragments in your activity, using a library might not be necessary. Just use the classical approach using FragmentManager

parentFragment = (ParentFragment) getSupportFragmentManager().findFragmentById(R.id.parent_fragment);

Question:

Is it possible to get SomeFragment via an interface? I don't want to use FragmentManager, because in my original code MainActivity is a fragment.

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.some_container)
    FragmentCallback fragment;

    public interface FragmentCallback {
        void test();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        fragment.test();
    }
}

public class SomeFragment extends Fragment implements FragmentCallback {

    public SomeFragment() {
    }

    @Nullable
    @Override
    public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle
            savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_some, container, false);
        ButterKnife.bind(this, view);
        return view;
    }

    @Override
    public void test() {
        Log.d("" , "it works");
    }
}

layouts:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    <fragment
            android:id="@+id/some_container"
            android:name="com.tamtam.myapplication.SomeFragment"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
</LinearLayout>

FATAL EXCEPTION: main Process: com.tamtam.myapplication, PID: 29138 java.lang.RuntimeException: Unable to start activity ComponentInfo{com.tamtam.myapplication/com.tamtam.myapplication.MainActivity}: java.lang.NullPointerException: Attempt to invoke interface method 'void com.tamtam.myapplication.MainActivity$FragmentCallback.test()' on a null object reference at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2665) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2726) at android.app.ActivityThread.-wrap12(ActivityThread.java) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1477) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:154) at android.app.ActivityThread.main(ActivityThread.java:6119) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776) Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'void com.tamtam.myapplication.MainActivity$FragmentCallback.test()' on a null object reference at com.tamtam.myapplication.MainActivity.onCreate(MainActivity.java:21) at android.app.Activity.performCreate(Activity.java:6679) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1118) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2618) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2726)  at android.app.ActivityThread.-wrap12(ActivityThread.java)  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1477)  at android.os.Handler.dispatchMessage(Handler.java:102)  at android.os.Looper.loop(Looper.java:154)  at android.app.ActivityThread.main(ActivityThread.java:6119)


Answer:

It's not possible because

A Fragment is not a View, Butterknife's @BindView is used to bind a View, not a Fragment.

You can use FragmentManager.findFragmentById(int id); to get a fragment, but if you check the implementation, you'll see that FragmentManager doesn't look in the View hierarchy

public Fragment findFragmentById(int id) {
    if (mAdded != null) {
        // First look through added fragments.
        for (int i=mAdded.size()-1; i>=0; i--) {
            Fragment f = mAdded.get(i);
            if (f != null && f.mFragmentId == id) {
                return f;
            }
        }
    }
    if (mActive != null) {
        // Now for any known fragment.
        for (int i=mActive.size()-1; i>=0; i--) {
            Fragment f = mActive.get(i);
            if (f != null && f.mFragmentId == id) {
                return f;
            }
        }
    }
    return null;
}

If you want to do this with ButterKnife

You could extend ButterKnife by adding this functionality, or you could just create a wrapper around it and look for your own annotation. For example, you could create a custom annotation

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindFrament {
    @IdRes int value();
}

create a wrapper with bind(Activity) method, call ButterKnife.bind(Activity) method and then look for your own annotation and set it to the instance field

public class ButterKnifeWrapper {

    public static void bind(Activity activity){
        ButterKnife.bind(activity);

        Class clazz = activity.getClass();
        for(Field field : clazz.getDeclaredFields()){
            if(field.isAnnotationPresent(BindFrament.class)){
                Class fieldType = field.getType();
                if(Fragment.class.isAssignableFrom(fieldType)){
                    int fragmentId = field.getAnnotation(BindFrament.class).value();
                    Fragment fragment = activity.getFragmentManager().findFragmentById(fragmentId);
                    try {
                        field.set(activity, fragment);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

In your activity you could then do this

public class MyActivity extends AppCompatActivity {

    @BindFrament(R.id.my_fragment)
    MyFragment myFragment;

    @BindView(R.id.my_view)
    View myView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.my_activity);
        ButterKnifeWrapper.bind(this);
    }
}

Question:

dependencies {
    implementation 'com.jakewharton:butterknife:10.0.0'
    annotationProcessor 'com.jakewharton:butterknife-compiler:10.0.0'
    implementation 'com.squareup.picasso:picasso:2.71828'
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

}

These are the dependencies, in build.gradle

Manifest merger failed : Attribute application@appComponentFactory value=(android.support.v4.app.CoreComponentFactory) from [com.android.support:support-compat:28.0.0] AndroidManifest.xml:22:18-91 is also present at [androidx.core:core:1.0.0] AndroidManifest.xml:22:18-86 value=(androidx.core.app.CoreComponentFactory). Suggestion: add 'tools:replace="android:appComponentFactory"' to element at AndroidManifest.xml:7:5-21:19 to override.

I wished to add a library to my project, it is called as ButterKnife library, before adding this library the project was fine, but as I added this library. Manifest merger failed error occurred.

What I have tried? I added these lines to my AndroidManifest.xml:

tools:replace="android:appComponentFactory"
android:appComponentFactory="whateverString"

But this generated another set of errors

Caused by: com.android.tools.r8.utils.AbortException: Error: Static interface methods are only supported starting with Android N (--min-api 24): void butterknife.Unbinder.lambda$static$0()

I tried removing butterknife library, and then it builds finely.

I also tried adding only one of those lines:

tools:replace="android:appComponentFactory"

This did nothing and produced yet another error:

Manifest merger failed with multiple errors, see logs

I tried Refractor->migrate to androidx, this created a new problem in Java file, which now says that it "cannot resolve symbol R"

So what should I do, I am following some course online for app development. And the person teaching this course does not seem to have these errors.


Answer:

com.jakewharton:butterknife:10.0.0 is using AndroidX. Check it here.

But you also depend on com.android.support:appcompat-v7:28.0.0.

You shouldn't mix dependencies using AndroidX with non-AndroidX.

You have two options:

  1. Use a lower version for ButterKnife.
  2. Migrate to AndroidX.

To migrate to AndroidX:

Use androidx.appcompat:appcompat:1.0.0 instead of com.android.support:appcompat-v7:28.0.0.

Add the following to your gradle.properties:

android.useAndroidX=true
android.enableJetifier=true

Change imports of your Activity's AppCompatActivity from

import android.support.v7.app.AppCompatActivity;

to

import androidx.appcompat.app.AppCompatActivity;

Check the migration guide here.

Question:

I looked at several Android projects. Why is it common practice to use lowercase with underscores for XML IDs?

in XML:

@+id/name_text <!-- sometimes with "_view" suffix, sometimes without -->

in Java:

TextView nameTextView = // ...

I would propose the following ID: @+id/nameTextView

That is actually how I do it. What would be the downside?

Especially data binding could be done even shorter (for example with ButterKnife) if the XML ID and field name would follow the same pattern. In this case we could just omit the XML ID in the annotation:

@BindView TextView nameTextView;

Answer:

There is no such feature for Java yet, but for Kotlin: Kotlin Android Extensions.

If your view is declared like this (in activity_main.xml):

 <TextView
        android:id="@+id/hello"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Hello World, MyActivity"
        />

you can work with it like this in your Activity:

hello.setText("Hi!")

The only thing that has to be done for this is to add this to the Kotlin file where you want to use the view:

import kotlinx.android.synthetic.main.activity_main.*

and of course adding the dependency in your project-local build.gradle file:

apply plugin: 'kotlin-android-extensions'