大名鼎鼎的 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<Any> 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<Any> 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<TestViewActivity> { @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 上还有一些问题需要解决。这个库在未来还有很多可以优化的地方。