使用Butterknife
的主要目的是消除关于view
实例化的样板代码,这是一个专为View
类型的依赖注入框架。Dagger2
是一个更加通用的依赖注入框架。
ButterKnife
的使用非常简单,其工作原理是先对添加的注解域@Bind
做编译时解析,生成一个中间类,这个中间类具有将经过注解的域实例化的能力,在运行时使用中间类完成真正的实例化。
1.ButterKnife的使用
1.注入View
在碎片中使用
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fancy_fragment, container, false);
ButterKnife.bind(this, view);
// TODO Use fields...
return view;
}
在适配器中使用
static class ViewHolder {
@Bind(R.id.title) TextView name;
@Bind(R.id.job_title) TextView jobTitle;
public ViewHolder(View view) {
ButterKnife.bind(this, view);
}
}
2.监听器注入
@OnClick(R.id.submit)
public void sayHi(Button button) {
button.setText("Hello!");
}
多个同类型的控件注册一个监听器
@OnClick({ R.id.door1, R.id.door2, R.id.door3 })
public void pickDoor(DoorView door) {
if (door.hasPrizeBehind()) {
Toast.makeText(this, "You win!", LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Try again", LENGTH_SHORT).show();
}
}
3.为控件列表注入动作
// 1.注入控件列表
@BindViews({ R.id.first_name, R.id.middle_name, R.id.last_name })
List nameViews;
//2.绑定控件列表与动作
ButterKnife.apply(nameViews, DISABLE);
ButterKnife.apply(nameViews, ENABLED, false);
//3.定义动作
static final ButterKnife.Action DISABLE = new ButterKnife.Action() {
@Override public void apply(View view, int index) {
view.setEnabled(false);
}
};
static final ButterKnife.Setter ENABLED = new ButterKnife.Setter() {
@Override public void set(View view, Boolean value, int index) {
view.setEnabled(value);
}
};
4.单个控件的类型查询和转型
TextView firstName = ButterKnife.findById(view, R.id.first_name);
ImageView photo = ButterKnife.findById(view, R.id.photo);
5.NavigationView的注入
默认情况下,注入 NavigationView 实例并不意味着其头视图的中的元素也跟随着被注入,例如下例中的 TextView 虽然经过注解,但实际上并没有被实例化。
@Bind(R.id.navigation_view )
View navigationView = navigationView.getHeaderView(0);
@Bind(R.id.title_text_view) TextView titleTv;
必须对头视图单独进行注入
View headerView = navigationView.getHeaderView(0);
TextView textView = ButterKnife.findById(headerView, R.id.tvName);
2.编译时注解解析
public class MainActivity extends AppCompatActivity{
@Bind(R.id.relative_layout)
RelativeLayout mRelativeLayout;
@BindString(R.string.app_name)
String name;
}
编译时Java
会使用类ButterKnifeProcessor
进行编译时注解处理,为每一个注解过的类生成一个中间类,如MainActivity
类将生成中间类MainActivity$$ViewBinder
;运行时将执行该中间类完成注入。ButterKnifeProcessor
处理代码如下
boolean process(Set extends TypeElement> elements, RoundEnvironment env) {
//1.首先找出使用的所有包含注解的类集合Map,其key是注解类,如MainActivity,其值为相应生成的BindingClass类,包含注解集合。
Map targetClassMap = this.findAndParseTargets(env);
//2.依次为每一个注解类创建中间类
Iterator var4 = targetClassMap.entrySet().iterator();
while(var4.hasNext()) {
Entry entry = (Entry)var4.next();
TypeElement typeElement = (TypeElement)entry.getKey();
BindingClass bindingClass = (BindingClass)entry.getValue();
//3.使用BindingClass类创建中间类
JavaFileObject e = this.filer.createSourceFile(bindingClass.getFqcn(), new Element[]{typeElement});
Writer writer = e.openWriter();
writer.write(bindingClass.brewJava());
writer.flush();
writer.close();
}
return true;
}
上述编译时注解解析分为两步,首先搜索所有经过注解要处理的类集合,其key是注解类,如 MainActivity,其值为相应生成的 BindingClass 类;而后遍历集合使用 BindingClass 类提供的信息生成对应的中间类,如 MainActivity$$ViewBinder。
1.BindingClass 类与使用注解的类一一对应,它包含了该类的注解信息,如通过资源机制获取到控件的 id 信息,是生成中间类的信息来源。定义如下
final class BindingClass {
//1.被注解的View信息集合
private final Map viewIdMap;
//2.被注解的View集合信息集合
private final Map collectionBindings;
//3.被注解的资源信息集合
private final List resourceBindings;
//4.包名
private final String classPackage;
//5.类名
private final String className;
private final String targetClass;
private String parentViewBinder;
}
其中字典集合Map
的key
值表示控件的资源 id,而ViewBindings
类表示对控件域的操作。
2.创建中间类
中间类的创建是使用Java流来写入的,其内容由bindingClass.brewJava()
方法提供,可以处理原类中的注解,生成中间类如下
public class MainActivity$$ViewBinder implements ViewBinder {
@Override
public void bind(final Finder finder, final T target, Object source) {
View view;
view = finder.findRequiredView(source, 2131492971, "field 'mRelativeLayout'");
target.mRelativeLayout = finder.castView(view, 2131492971, "field 'mRelativeLayout'");
Resources res = finder.getContext(source).getResources();
target.name = res.getString(2131099669);
}
@Override
public void unbind(T target) {
target.mRelativeLayout = null;
}
}
调用中间类中的bind
方法就完成了资源的实例化。以string
为例 2131099669 即根据@BindString(R.string.app_name)
注解得到的资源 id。
3.对象实例化
中间类虽然具备了实例化注解对象的能力,但在编译时并没有得到执行,必须在运行时进行注解对象实例化。
ButterKnife.bind(this);
该方法的实际执行代码如下
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
return getViewBinder(target).bind(Finder.ACTIVITY, target, target);
}
1.首先根据活动类MainActivity
寻找其所生成的中间类MainActivity$$ViewBinder
,并生成对象。
@NonNull @CheckResult @UiThread
static ViewBinder
这里 targetClass 就是活动 MainActivity,因为中间类的命名是固定的,即本类加$$ViewBinder
后缀,故可以直接使用类名来查找中间类 MainActivity$$ViewBinder ,而后使用反射创建中间类实例。
2.执行中间类对象的bind
方法对所注解对象进行实例化。该类继承于接口ViewBinder
,只有一个bind
方法。
public interface ViewBinder {
Unbinder bind(Finder finder, T target, Object source);
}
其中Finder
是一个枚举类(VIEW,ACTIVITY,DIALOG),之所以要将它们区别开来,是因为它们查找资源的方式有所差别,对于不同的类型有不同的实现。
执行注解对象实例化的代码最终委托给Finder
类,包括绑定View
与向下转型两步,例如对于 ACTIVITY 是这样实现的。
public class MainActivity$$ViewBinder implements ViewBinder {
public void bind(Finder finder, T target, Object source) {
View view = (View)finder.findRequiredView(source, 2131492944, "field \'mTextView\'");
target.mTextView = (TextView)finder.castView(view, 2131492944, "field \'mTextView\'");
}
}