大名鼎鼎的 ButterKnife 库相信很多 android 开发者都听过,在 Github 上star的数目已经快15k了,而且很多知名的app都在使用。
ButterKnife 可以简化像 findViewById、setOnClickListener 这种代码,让开发者摆脱一些繁琐的细节,更加关注于业务代码的开发。
既然 ButterKnife 已经足够强大了,为何还要再造一个轮子呢?你说好代码相见恨晚,我说造轮子你不够勇敢。_ 其实这个库更加轻量级只做了几个最常用的注解,并且它是完全基于Kotlin进行开发的。
下载安装:
在根目录下的build.gradle中添加
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
在app 模块目录下的build.gradle中添加
apply plugin: 'com.neenbedankt.android-apt'
...
dependencies {
compile 'com.safframework.injectview:saf-injectview:1.0.0'
apt 'com.safframework.injectview:saf-injectview-compiler:1.0.0'
...
}
demo的演示
demo和库的地址:
https://github.com/fengzhizi715/SAF-Kotlin-InjectView
整个库的设计:
整个库包括三个模块:
- injectview:android library,包括Injector类和ViewBinder接口。用于在Activity、Fragment、View、Dialog中进入注入。
- injectview-annotations:java library,用于存放各个注解,比如@InjectView。
- injectview-compiler:java library,使用apt来生成代码
- injectview module
import android.app.Activity
import android.app.Dialog
import android.support.v4.app.Fragment
import android.view.View
import java.lang.reflect.Field
/**
* Created by Tony Shen on 2017/1/24.
*/
object Injector {
enum class Finder {
DIALOG {
override fun findById(source: Any, id: Int): View {
return (source as Dialog).findViewById(id)
}
},
ACTIVITY {
override fun findById(source: Any, id: Int): View {
return (source as Activity).findViewById(id)
}
override fun getExtra(source: Any, key: String, fieldName: String): Any? {
val intent = (source as Activity).intent
if (intent != null) {
val extras = intent.extras
var value: Any? = extras?.get(key)
var field: Field? = null
try {
field = source.javaClass.getDeclaredField(fieldName)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
}
if (field == null) return null;
if (value == null) {
when {
field.type.name == Int::class.java.name || field.type.name == "int" -> value = 0
field.type.name == Boolean::class.java.name || field.type.name == "boolean" -> value = false
field.type.name == java.lang.String::class.java.name -> value = ""
field.type.name == Long::class.java.name || field.type.name == "long" -> value = 0L
field.type.name == Double::class.java.name || field.type.name == "double" -> value = 0.0
}
}
if (value != null) {
try {
field.isAccessible = true
field.set(source, value)
return field.get(source)
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
}
return null
}
},
FRAGMENT {
override fun findById(source: Any, id: Int): View {
return (source as View).findViewById(id)
}
},
VIEW {
override fun findById(source: Any, id: Int): View {
return (source as View).findViewById(id)
}
};
abstract fun findById(source: Any, id: Int): View
open fun getExtra(source: Any, key: String, fieldName: String): Any? {
return null
}
}
/**
* 在Activity中使用注解
* @param activity
*/
@JvmStatic fun injectInto(activity: Activity) {
inject(activity, activity, Finder.ACTIVITY)
}
/**
* 在fragment中使用注解
* @param fragment
* @param v
*
* @return
*/
@JvmStatic fun injectInto(fragment: Fragment, v: View) {
inject(fragment, v, Finder.FRAGMENT)
}
/**
* 在dialog中使用注解
* @param dialog
*
* @return
*/
@JvmStatic fun injectInto(dialog: Dialog) {
inject(dialog, dialog, Finder.DIALOG)
}
/**
* 在view中使用注解
* @param obj
* @param v
*
* @return
*/
@JvmStatic fun injectInto(obj: Object, v: View) {
inject(obj, v, Finder.VIEW)
}
private fun inject(host: Any, source: Any,finder: Finder) {
val className = host.javaClass.name
try {
val finderClass = Class.forName(className+"\$\$ViewBinder")
val viewBinder = finderClass.newInstance() as ViewBinder
viewBinder.inject(host, source, finder)
} catch (e: Exception) {
// throw new RuntimeException("Unable to inject for " + className, e);
println("Unable to inject for " + className)
}
}
}
枚举Finder中的方法getExtra()默认是final的,需要标记成open,Kotlin 要求使用open显式标注成员可被覆写。
由于ViewBinder接口里包含泛型,所以在inject方法中需要写成Any
val viewBinder = finderClass.newInstance() as ViewBinder
viewBinder.inject(host, source, finder)
否则会无法编译通过的。
- injectview-annotations module
Kotlin 可以简化annotation类,例如@InjectView
在Java版本是这样的
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by Tony Shen on 2016/12/6.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface InjectView {
int value() default 0;
}
Kotlin版本是这样的
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
/**
* Created by Tony Shen on 2017/1/24.
*/
@Target(AnnotationTarget.FIELD)
@Retention(RetentionPolicy.CLASS)
annotation class InjectView(val value: Int = 0)
看上去经过 Kotlin 改写会更加直观和简单。
- injectview-compiler module
所有的注解都是编译时的注解类型,比如Activity中在使用时,会生成一个相同的类名+$$ViewBinder的类。
基于apt生成的TestViewActivity$$ViewBinder类
import com.safframework.app.ui.TitleView;
import com.safframework.injectview.Injector.Finder;
import com.safframework.injectview.ViewBinder;
import java.lang.Object;
import java.lang.Override;
public class TestViewActivity$$ViewBinder implements ViewBinder {
@Override
public void inject(final TestViewActivity host, Object source, Finder finder) {
host.titleView = (TitleView)(finder.findById(source, 2131427422));
}
}
整个库的使用方法:
- @InjectView
@InjectView可以简化组件的查找注册,包括android自带的组件和自定义组件。在使用@InjectView之前,我们会这样写代码
public class MainActivity extends Activity {
ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = (ImageView) findViewById(R.id.imageview);
}
}
在使用@InjectView之后,会这样写代码
public class MainActivity extends Activity {
@InjectView(R.id.imageView)
ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Injector.injectInto(this);
}
}
目前,@InjectView可用于Activity、Dialog、Fragment中。在Activity和Dialog用法相似,在Fragment中用法有一点区别。
public class DemoFragment extends Fragment {
@InjectView(R.id.title)
TextView titleView;
@InjectView(R.id.imageview)
ImageView imageView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_demo, container, false);
Injector.injectInto(this,v); // 和Activity使用的区别之处在这里
initViews();
initData();
return v;
}
......
}
- @InjectViews
public class MainActivity extends Activity {
@InjectViews(ids={R.id.imageView1,R.id.imageView2})
ImageView[] imageviews;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Injector.injectInto(this);
}
}
- @InjectExtra
/**
* MainActivity传递数据给SecondActivity
* Intent i = new Intent(MainActivity.this,SecondActivity.class);
* i.putExtra("test", "saf");
* i.putExtra("test_object", hello);
* startActivity(i);
* 在SecondActivity可以使用@InjectExtra注解
*
* @author Tony Shen
*
*/
public class SecondActivity extends Activity{
@InjectExtra(key="test")
String testStr;
@InjectExtra(key="test_object")
Hello hello;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Injector.injectInto(this);
Log.i("++++++++++++","testStr="+testStr);
Log.i("++++++++++++","hello="+SAFUtil.printObject(hello)); // 该方法用于打印对象
}
}
- @OnClick
@OnClick 可以在Activity、Fragment、Dialog、View中使用,也支持多个组件绑定同一个方法。
public class AddCommentFragment extends BaseFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_add_comment, container, false);
Injector.injectInto(this, v);
initView();
return v;
}
@OnClick(id={R.id.left_menu,R.id.btn_comment_cancel})
void clickLeftMenu() {
popBackStack();
}
@OnClick(id=R.id.btn_comment_send)
void clickCommentSend() {
if (StringHelper.isBlank(commentEdit.getText().toString())) {
ToastUtil.showShort(mContext, R.string.the_comment_need_more_character);
} else {
AsyncTaskExecutor.executeAsyncTask(new AddCommentTask(showDialog(mContext)));
}
}
....
}
总结:
它只实现了 ButterKnife 的几个注解功能,不过都是一些最常用的注解。有一点遗憾是,目前在 ListView 和 RecyclerView 上还有一些问题需要解决。这个库在未来还有很多可以优化的地方。