ButterKnife 8.2.1 大圣归来

零.前言

ButterKnife是一个视图注入的框架,主要帮我们解决无脑的findViewById、设置监听事件等等体力劳动。

一.引入

好消息是ButterKnife终于使用apt生成代码了,首先在buildscript增加插件。

buildscript {
    repositories {
        maven { url "https://plugins.gradle.org/m2/" }
        jcenter()
        mavenLocal()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.2'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
        classpath 'com.jakewharton:butterknife-gradle-plugin:8.2.0'
    }
}

其次在application模块或library模块增加ButterKnife依赖,apt的plugin等。

apply plugin: 'com.neenbedankt.android-apt'
apply plugin: 'com.jakewharton.butterknife'

android {
    ...
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.jakewharton:butterknife:8.2.1'
    apt 'com.jakewharton:butterknife-compiler:8.2.1'
}

二.使用

用法看代码,下面例子内会对比使用了视图注入与不使用的区别。功能说明移动到代码里看。

使用视图注入:


/**
 * Created by nielongyu on 16/7/18.
 */
public class TestButterKnifeActivity extends BaseActivity {


    @BindView(R.id.password)
    AppCompatEditText mPassword;
    @BindView(R.id.login)
    Button mLogin;
    
    // you can bind a string
    @BindString(R.string.app_name)
    String mAppName;

    // custom use bind view
    @BindView(R.id.username)
    AppCompatEditText mUsername;
    
    // and views
    @BindViews({R.id.username, R.id.password})
    List<AppCompatEditText> mEditTexts;

    // By default, both @Bind and listener bindings are required.
    // An exception will be thrown if the target view cannot be found.
    @BindView(R.id.recycle_view)
    RecyclerView doNotExistRecyclerView;

    // To suppress this behavior and create an optional binding,
    // add a @Nullable annotation to fields or the @Optional annotation to methods.
    @Nullable
    @BindView(R.id.nav_send)
    View mIDoNotExist;
    
    @Optional
    @OnClick(R.id.nav_send) void onMaybeMissingClicked() {
        Logger.d("maybe never be called but will not throw exception");
    }

    // an action for view
    static final ButterKnife.Action<View> DISABLE = new ButterKnife.Action<View>() {
        @Override
        public void apply(@NonNull View view, int index) {
            view.setEnabled(false);
        }
    };
    
    // the same thing
    static final ButterKnife.Action<View> GONE = new ButterKnife.Action<View>() {
        @Override
        public void apply(@NonNull View view, int index) {
            view.setVisibility(View.GONE);
        }
    };

    // an action with values
    static final ButterKnife.Setter<View, Boolean> ENABLED = new ButterKnife.Setter<View, Boolean>() {
        @Override
        public void set(@NonNull View view, Boolean value, int index) {
            view.setEnabled(value);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        // just bind!
        ButterKnife.bind(this);
        // no findViewById
        // no set group and I got view list mEditTexts
        // and ...
        // just apply action
        ButterKnife.apply(mEditTexts, ENABLED, false);
        // and action
        ButterKnife.apply(mEditTexts, DISABLE);
        // or actions
        ButterKnife.apply(mEditTexts, DISABLE, GONE);
        // or custom property
        ButterKnife.apply(mEditTexts, View.ALPHA, 0.0f);

    }

    // no setOnClickListener and I can use
    @OnClick({R.id.password, R.id.login})
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.password:
                mUsername.setText(mAppName);
                break;
            case R.id.login:
                Logger.d("Hail Hydra!");
                break;
        }
    }

//    @OnClick(R.id.username)
//    public void onWhateverINamedAMethod(View view) {
//        Logger.d("Hail Avengers!");
//    }

//    @OnClick(R.id.username)
//    public void onOrICanDoItWithoutView() {
//        Logger.d("Hail PedroNeer!");
//    }

    // oh yes ! auto cast
    @OnClick(R.id.username)
    public void onOrNotOnlyView(AppCompatEditText editText) {
        Logger.d("Hail PedroNeer!");
    }

    // also ...
    public void ifStillNeedToFindView(ViewGroup parent) {
        View view = LayoutInflater.from(this).inflate(R.layout.activity_login, parent, false);
        AppCompatEditText firstName = ButterKnife.findById(view, R.id.username);
        AppCompatEditText lastName = ButterKnife.findById(view, R.id.password);
    }

//    @OnItemClick
//    @OnItemLongClick
//    @OnLongClick

    public static class IAmAFragment extends Fragment {

        @BindView(R.id.username)
        AppCompatEditText mUsername;
        @BindView(R.id.password)
        AppCompatEditText mPassword;
        @BindView(R.id.login)
        Button mLogin;
        // Fragments have a different view lifecycle than activities.
        // When binding a fragment in onCreateView, set the views to null in onDestroyView.
        // Butter Knife returns an Unbinder instance when you call bind to do this for you.
        // Call its unbind method in the appropriate lifecycle callback.
        private Unbinder unBinder;

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

        @Override
        public void onDestroyView() {
            super.onDestroyView();
            unBinder.unbind();
        }
    }
}

不使用视图注入:

/**
 * Created by nielongyu on 16/7/18.
 */
public class NoButterKnifeActivity extends BaseActivity implements View.OnClickListener {

    private AppCompatEditText mUsername;
    private AppCompatEditText mPassword;
    private Button mLogin;
    private List<AppCompatEditText> mEditTexts;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        // lots of code needed in block
        {

            // oh I need findViewById
            mUsername = (AppCompatEditText) findViewById(R.id.username);
            mPassword = (AppCompatEditText) findViewById(R.id.password);
            mLogin = (Button) findViewById(R.id.login);
            // oh I need setOnClickListener
            mUsername.setOnClickListener(mEditTextListener);
            mPassword.setOnClickListener(mEditTextListener);
            mLogin.setOnClickListener(this);
            // oh I need group Views
            mEditTexts.add(mUsername);
            mEditTexts.add(mPassword);
            // oh I need foreach
            for (AppCompatEditText editText : mEditTexts) {
                editText.setEnabled(false);
            }
        }

    }

    // I need implements View.OnClickListener
    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.username) {
            mUsername.setText(getString(R.string.app_name));
        } else if (v.getId() == R.id.password || v.getId() == R.id.login) {
            //ugly code 
        }
    }

    // define custom listener
    View.OnClickListener mEditTextListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {

        }
    };
}

三.原理

编译后的class文件如下,BindView等熟悉的注解还是在的,看了眼确实是Class级别的注解。

public class TestButterKnifeActivity extends BaseActivity {
    // 这里可以看到R.id.xx已经更换为int值
    // 注解还是完整的保留在class中
    @BindView(2131492969)
    AppCompatEditText mUsername;
    @BindView(2131492970)
    AppCompatEditText mPassword;
    @BindView(2131492971)
    Button mLogin;
  
    // String 的id同理
    @BindString(2131099669)
    String mAppName;
    @BindViews({2131492969, 2131492970})
    List<AppCompatEditText> mEditTexts;
    @BindView(2131492977)
    RecyclerView recyclerView;
    @Nullable
    @BindView(2131493008)
    View mIDoNotExist;
    static final Action<View> DISABLE = new Action() {
        public void apply(@NonNull View view, int index) {
            view.setEnabled(false);
        }
    };
    static final Action<View> GONE = new Action() {
        public void apply(@NonNull View view, int index) {
            view.setVisibility(8);
        }
    };
    static final Setter<View, Boolean> ENABLED = new Setter() {
        public void set(@NonNull View view, Boolean value, int index) {
            view.setEnabled(value.booleanValue());
        }
    };

    public TestButterKnifeActivity() {
    }

    //注解还在
    @Optional
    @OnClick({2131493008})
    void onMaybeMissingClicked() {
        Logger.d("maybe never be called", new Object[0]);
    }

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(2130968601);
        ButterKnife.bind(this);
        ButterKnife.apply(this.mEditTexts, ENABLED, Boolean.valueOf(false));
        ButterKnife.apply(this.mEditTexts, DISABLE);
        ButterKnife.apply(this.mEditTexts, new Action[]{DISABLE, GONE});
        ButterKnife.apply(this.mEditTexts, View.ALPHA, Float.valueOf(0.0F));
    }

    @OnClick({2131492970, 2131492971})
    public void onClick(View view) {
        switch(view.getId()) {
        case 2131492970:
            this.mUsername.setText(this.mAppName);
            break;
        case 2131492971:
            Logger.d("Hail Hydra!", new Object[0]);
        }

    }

    @OnClick({2131492969})
    public void onOrNotOnlyView(AppCompatEditText editText) {
        Logger.d("Hail PedroNeer!", new Object[0]);
    }

    public void ifStillNeedToFindView(ViewGroup parent) {
        View view = LayoutInflater.from(this).inflate(2130968601, parent, false);
        AppCompatEditText firstName = (AppCompatEditText)ButterKnife.findById(view, 2131492969);
        AppCompatEditText lastName = (AppCompatEditText)ButterKnife.findById(view, 2131492970);
    }

    public static class IAmAFragment extends Fragment {
        @BindView(2131492969)
        AppCompatEditText mUsername;
        @BindView(2131492970)
        AppCompatEditText mPassword;
        @BindView(2131492971)
        Button mLogin;
        private Unbinder unBinder;

        public IAmAFragment() {
        }

        @Nullable
        public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
            View view = inflater.inflate(2130968601, container, false);
            this.unBinder = ButterKnife.bind(this, view);
            return view;
        }

        public void onDestroyView() {
            super.onDestroyView();
            this.unBinder.unbind();
        }
    }
}

再看其他生成的代码:

抓到一只ViewBinder与一只ViewBinding。

TestButterKnifeActivity_ViewBinder.class
TestButterKnifeActivity_ViewBinding.class

ViewBinder:

public final class TestButterKnifeActivity_ViewBinder implements ViewBinder<TestButterKnifeActivity> {
    public TestButterKnifeActivity_ViewBinder() {
    }
    // 可以这么理解 
    // Finder用来找view 
    // target是需要被注入的实例 
    // source是view/activity/fragment
    public Unbinder bind(Finder finder, TestButterKnifeActivity target, Object source) {
        Resources res = finder.getContext(source).getResources();
        return new TestButterKnifeActivity_ViewBinding(target, finder, source, res);
    }
}

ViewBinding:

public class TestButterKnifeActivity_ViewBinding<T extends TestButterKnifeActivity> implements Unbinder {
    protected T target;
    private View view2131492969;
    private View view2131492970;
    private View view2131492971;
    private View view2131493008;

    public TestButterKnifeActivity_ViewBinding(final T target, final Finder finder, Object source, Resources res) {
        this.target = target;
        // 首先 会通过finder去找到这个view 里面有一些异常处理 可以跳转到最下面的finder类分析看
        // 还记得我们写的onClick方法么 对userName这个成员
        // oh yes ! auto cast
        // @OnClick(R.id.username)
        // public void onOrNotOnlyView(AppCompatEditText editText) {
        //         Logger.d("Hail PedroNeer!");
    	// }
        View view = finder.findRequiredView(source, 2131492969, "field \'mUsername\' and method \'onOrNotOnlyView\'");
        target.mUsername = (AppCompatEditText)finder.castView(view, 2131492969, "field \'mUsername\'", AppCompatEditText.class);
        this.view2131492969 = view;
        // 这个listener防止多个button同时被点击了= =
        view.setOnClickListener(new DebouncingOnClickListener() {
            public void doClick(View p0) {
                // 这里直接调用我们之前写了注解的点击方法onOrNotOnlyView
                target.onOrNotOnlyView((AppCompatEditText)finder.castParam(p0, "doClick", 0, "onOrNotOnlyView", 0));
            }
        });
        view = finder.findRequiredView(source, 2131492970, "field \'mPassword\' and method \'onClick\'");
        target.mPassword = (AppCompatEditText)finder.castView(view, 2131492970, "field \'mPassword\'", AppCompatEditText.class);
        this.view2131492970 = view;
        view.setOnClickListener(new DebouncingOnClickListener() {
            public void doClick(View p0) {
                target.onClick(p0);
            }
        });
        view = finder.findRequiredView(source, 2131492971, "field \'mLogin\' and method \'onClick\'");
        target.mLogin = (Button)finder.castView(view, 2131492971, "field \'mLogin\'", Button.class);
        this.view2131492971 = view;
        view.setOnClickListener(new DebouncingOnClickListener() {
            public void doClick(View p0) {
                target.onClick(p0);
            }
        });
        // 找不到就放弃
        target.recyclerView = (RecyclerView)finder.findRequiredViewAsType(source, 2131492977, "field \'recyclerView\'", RecyclerView.class);
        view = finder.findOptionalView(source, 2131493008);
        target.mIDoNotExist = view;
        if(view != null) {
            this.view2131493008 = view;
            view.setOnClickListener(new DebouncingOnClickListener() {
                public void doClick(View p0) {
                    target.onMaybeMissingClicked();
                }
            });
        }
        // 一只list
        target.mEditTexts = Utils.listOf(new AppCompatEditText[]{(AppCompatEditText)finder.findRequiredViewAsType(source, 2131492969, "field \'mEditTexts\'", AppCompatEditText.class), (AppCompatEditText)finder.findRequiredViewAsType(source, 2131492970, "field \'mEditTexts\'", AppCompatEditText.class)});
        target.mAppName = res.getString(2131099669);
    }

    public void unbind() {
        TestButterKnifeActivity target = this.target;
        if(target == null) {
            throw new IllegalStateException("Bindings already cleared.");
        } else {
            target.mUsername = null;
            target.mPassword = null;
            target.mLogin = null;
            target.recyclerView = null;
            target.mIDoNotExist = null;
            target.mEditTexts = null;
            this.view2131492969.setOnClickListener((OnClickListener)null);
            this.view2131492969 = null;
            this.view2131492970.setOnClickListener((OnClickListener)null);
            this.view2131492970 = null;
            this.view2131492971.setOnClickListener((OnClickListener)null);
            this.view2131492971 = null;
            if(this.view2131493008 != null) {
                this.view2131493008.setOnClickListener((OnClickListener)null);
                this.view2131493008 = null;
            }

            this.target = null;
        }

再看ButterKnife.bind方法:

@NonNull @CheckResult @UiThread
private static ViewBinder<Object> findViewBinderForClass(Class<?> cls) {
  ViewBinder<Object> viewBinder = BINDERS.get(cls);
  if (viewBinder != null) {
    if (debug) Log.d(TAG, "HIT: Cached in view binder map.");
    return viewBinder;
  }
  String clsName = cls.getName();
  if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
    if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
    return NOP_VIEW_BINDER;
  }
  //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
  //通过 类名找到viewBinder
  try {
    Class<?> viewBindingClass = Class.forName(clsName + "_ViewBinder");
    //noinspection unchecked
    viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
    if (debug) Log.d(TAG, "HIT: Loaded view binder class.");
  } catch (ClassNotFoundException e) {
    if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
    viewBinder = findViewBinderForClass(cls.getSuperclass());
  } catch (InstantiationException e) {
    throw new RuntimeException("Unable to create view binder for " + clsName, e);
  } catch (IllegalAccessException e) {
    throw new RuntimeException("Unable to create view binder for " + clsName, e);
  }
  BINDERS.put(cls, viewBinder);
  return viewBinder;
}

很容易就理解里面是找到对应生成的ViewBinder最后进行bind。

里面的Finder是一个枚举类型,用了枚举策略的设计模式。

public enum Finder {
  VIEW {
    @Override public View findOptionalView(Object source, @IdRes int id) {
      return ((View) source).findViewById(id);
    }

    @Override public Context getContext(Object source) {
      return ((View) source).getContext();
    }

    @Override protected String getResourceEntryName(Object source, @IdRes int id) {
      final View view = (View) source;
      // In edit mode, getResourceEntryName() is unsupported due to use of BridgeResources
      if (view.isInEditMode()) {
        return "<unavailable while editing>";
      }
      return super.getResourceEntryName(source, id);
    }
  },
  ACTIVITY {
    @Override public View findOptionalView(Object source, @IdRes int id) {
      return ((Activity) source).findViewById(id);
    }

    @Override public Context getContext(Object source) {
      return (Activity) source;
    }
  },
  DIALOG {
    @Override public View findOptionalView(Object source, @IdRes int id) {
      return ((Dialog) source).findViewById(id);
    }

    @Override public Context getContext(Object source) {
      return ((Dialog) source).getContext();
    }
  };

  public abstract View findOptionalView(Object source, @IdRes int id);

  public final <T> T findOptionalViewAsType(Object source, @IdRes int id, String who,
      Class<T> cls) {
    View view = findOptionalView(source, id);
    return castView(view, id, who, cls);
  }

  public final View findRequiredView(Object source, @IdRes int id, String who) {
    View view = findOptionalView(source, id);
    if (view != null) {
      return view;
    }
    // 没错 这个who就是为了抛异常给人看的= =
    String name = getResourceEntryName(source, id);
    throw new IllegalStateException("Required view '"
        + name
        + "' with ID "
        + id
        + " for "
        + who
        + " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
        + " (methods) annotation.");
  }

  public final <T> T findRequiredViewAsType(Object source, @IdRes int id, String who,
      Class<T> cls) {
    View view = findRequiredView(source, id, who);
    return castView(view, id, who, cls);
  }

  public final <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
    try {
      return cls.cast(view);
    } catch (ClassCastException e) {
      String name = getResourceEntryName(view, id);
      throw new IllegalStateException("View '"
          + name
          + "' with ID "
          + id
          + " for "
          + who
          + " was of the wrong type. See cause for more info.", e);
    }
  }

  @SuppressWarnings("unchecked") // That's the point.
  public final <T> T castParam(Object value, String from, int fromPos, String to, int toPos) {
    try {
      return (T) value;
    } catch (ClassCastException e) {
      throw new IllegalStateException("Parameter #"
          + (fromPos + 1)
          + " of method '"
          + from
          + "' was of the wrong type for parameter #"
          + (toPos + 1)
          + " of method '"
          + to
          + "'. See cause for more info.", e);
    }
  }

  protected String getResourceEntryName(Object source, @IdRes int id) {
    return getContext(source).getResources().getResourceEntryName(id);
  }

  public abstract Context getContext(Object source);
}

四.总结

ButterKnife在最近一次更新刚刚支持在Library内部使用视图注入,只要R换成R2就好了,Jake神级打脸。

很久之前在issue里强烈声明:never support!

截图 2016-07-19 00时05分02秒.png

然而...

截图 2016-07-19 00时03分36秒.png

ButterKnife作为视图注入神器,将所有依赖的视图、视图处理等委托给相应的ViewBinding,适当的利用反射机制进行视图绑定,目前方法数才100+,生成的代码量及极少,实数居家必备神器。

posted @ 2016-07-20 20:12  pedro_neer  阅读(5032)  评论(1编辑  收藏  举报