零.前言

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!

然而...

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

ButterKnife 8.2.1 大圣归来的更多相关文章

  1. ViewPage 大圣归来 原生示例

    VP简介 android-support-v4.jar 是谷歌官方给我们提供的一个兼容低版本安卓设备的软件包,里面包囊了只有在安卓3.0以上可以使用的api.而ViewPage就是其中之一,利用它,我 ...

  2. 大圣画廊v0.2(2015.7.17)

    0.2版本号加入的功能 以tag分类图片 美化.添加瀑布流效果 添加tag页和单张图片页 添加公布图片页 以下是具体解释. 每添加一个功能,都要从模型.模板,视图,路由四个方面一一改动. 模型:添加t ...

  3. Drawing in Singapore

    说到画画,其实很多人都会画.只是很多人都把这种潜能给埋起来了,没有特意的去开发出来.且不论画的好与不好,好看与不好看.自己把自己所想的东西方式表达出来,画画是一种方式.我不是科班出身,全凭自己感觉来的 ...

  4. Poco::JSON::Array 中object 设置preserveInsertionOrder 时,stringify出错-->深入解析

    在使用poco version 1.6.0时 Poco::JSON::Array 在object  设置preserveInsertionOrder =true 时 调用 array.stringif ...

  5. Windows10的革命之路-全新UWP开发平台

    众所周知,最近几年,微软一直在操作系统上进行统一化的尝试.第一次尝试的产品——Windows 8/8.1操作系统完全谈不上成功.请看下图: 我个人认为,这并不意味着操作系统统一化的策略是错误的,只能算 ...

  6. R语言——七月

    这两个月没有写什么代码.也没做什么大项目,基本就是对以前写的那个用ggplot2可视化数据的项目做一些增增补补,大部分技术难关都在ggplot2和R语言EXCEL处理这里解决并总结了.然后业余帮人修改 ...

  7. poco json 中文字符,抛异常JSON Exception -->iconv 转换 备忘录。

    起因 最近linux服务器通信需要用到json. jsoncpp比较出名,但poco 1.5版本以后已经带有json库,所以决定使用poco::json(linux 上已经用到了poco这一套框架). ...

  8. 让复杂Json数据和对象自由转换 --- Gson

    Gson是谷歌用于对Json操作的库,里面有着强大而又方便的功能,最常用的就是 fromJson():将json数据转化为对象: toJson():将对象转化为json数据! 对于普通的json数据使 ...

  9. python爬虫抓取豆瓣电影

    抓取电影名称以及评分,并排序(代码丑炸) import urllib import re from bs4 import BeautifulSoup def get(p): t=0 k=1 n=1 b ...

随机推荐

  1. react+redux教程(一)connect、applyMiddleware、thunk、webpackHotMiddleware

    今天,我们通过解读官方示例代码(counter)的方式来学习react+redux. 例子 这个例子是官方的例子,计数器程序.前两个按钮是加减,第三个是如果当前数字是奇数则加一,第四个按钮是异步加一( ...

  2. Opencv VideoCapture实时捕捉摄像头信息

    #include "opencv2/highgui/highgui.hpp" #include <iostream> using namespace cv; using ...

  3. 1Z0-053 争议题目解析486

    1Z0-053 争议题目解析486 考试科目:1Z0-053 题库版本:V13.02 题库中原题为: 486.You execute the following Recovery Manager (R ...

  4. DotNet常用排序算法总结

    数据结构和算法对一个程序来说是至关重要的,现在介绍一下几种算法,在项目中较为常用的算法有:冒泡排序,简单选择排序,直接插入排序,希尔排序,堆排序,归并排序,快速排序等7中算法. 现在介绍选择排序算法, ...

  5. Windows Server 2008 下解析二级域名的方法

    昨天去了客户那里部署网站,用的是客户那边的windows server 2008. 本文主要以总结问题点的形式来说. 问题1:本机的数据库是SQL SERVER 2008R2,客户那边的数据库是SQL ...

  6. Winform在线更新

    引言 2015年第一篇,Winform在线更新,算是重操旧业吧,09年刚到北京时一直做硬件联动编程,所以大多数时间都在搞Winform, 但是从来没做过在线更新这个功能,前几天参与部门另一个项目,上来 ...

  7. 使用Visual Studio 2015 Community 开发windows服务

    昨天研究在.NET下开发Windows服务程序,期间遇到一些小问题,这里仅将自己的开发过程和需要注意的地方写下和广大网友分享……  1.基础   Windows服务是指系统启动时能够自己运行的程序.W ...

  8. 让我们一起用开源数据库和开源框架废弃Access

    一.为什么要废弃Access? 1.客户的机子上需要安装access的驱动 ps:这个比较烦人,大家都知道部署越简单越好,安装这个对用户来说太繁琐了. 2.操作时性能不佳 using System; ...

  9. Java Web 学习路线

    实际上,如果时间安排合理的话,大概需要六个月左右,有些基础好,自学能力强的朋友,甚至在四个月左右就开始找工作了.大三的时候,我萌生了放弃本专业的念头,断断续续学 Java Web 累计一年半左右,总算 ...

  10. js promise chain

    新的标准里增加了原生的Promise. 这里只讨论链式使用的情况,思考一下其中的细节部分. 一,关于 then() 和 catch() 的复习 then() 和 catch() 的参数里可以放置 ca ...