IOC全称Inverse Of Control,中文释义为控制反转,常见的方式叫作依赖注入(Dependency Injection),IOC核心的思想和代理模式一样,使用者不必关心资源的具体获取,资源通过第三方来管理
之前有提到过注解是设计框架时常用的工具,利用注解可以在编译期(通过APT)或运行期生成代码,今天通过运行期使用注解来实现ButterKnife的布局和事件绑定功能
一、布局注入
我们希望在类上通过注解的方式,指定Activity的布局
1.新建注解
该注解需要一个布局id的参数
/**
* 布局注解
* Created by aruba on 2021/10/27.
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class ContentView(val value: Int)
2.定义注入工具类
通过反射获取ContentView注解,并最终调用Activity的setContentView方法
/**
* 注入工具
* Created by aruba on 2021/10/27.
*/
object InjectUtils {
fun inject(activity: BaseActivity) {
injectContentView(activity)
}
/**
* 注入布局文件
*/
private fun injectContentView(activity: BaseActivity) {
activity.javaClass.getAnnotation(ContentView::class.java)?.apply {
activity.setContentView(value)
}
}
}
3.Activity创建时,调用注入工具
写一个基类,在onCreate中调用注入工具的方法
/**
* Created by aruba on 2021/10/27.
*/
open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//运行时注入
InjectUtils.inject(this)
}
}
4.Activity类上使用注解
我们继承BaseActivity基类,并使用ContentView注解指定布局id
@ContentView(R.layout.activity_main)
class MainActivity : BaseActivity() {
}
布局就一个默认的TextView,内容如下:
效果:
二、控件id绑定
有了上面的基础,控件id绑定也是依葫芦画瓢
1.控件绑定注解
/**
* 绑定id注解
* Created by aruba on 2021/10/27.
*/
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class BindID(val id: Int)
2.注入工具实现
/**
* 注入工具
* Created by aruba on 2021/10/27.
*/
object InjectUtils {
fun inject(activity: BaseActivity) {
injectContentView(activity)
injectViewId(activity)
}
/**
* 注入布局文件
*/
private fun injectContentView(activity: BaseActivity) {
activity.javaClass.getAnnotation(ContentView::class.java)?.apply {
activity.setContentView(value)
}
}
/**
* 注入控件id
*/
private fun injectViewId(activity: BaseActivity) {
activity.javaClass.declaredFields.filter { field ->
field.getAnnotation(BindID::class.java) != null
}.forEach{ field ->
field.apply {
isAccessible = true
set(activity, activity.findViewById(getAnnotation(BindID::class.java)!!.id))
}
}
}
}
3.Activity中使用注解
@ContentView(R.layout.activity_main)
class MainActivity : BaseActivity() {
@BindID(R.id.tv_hello)
val tvHello: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
tvHello?.apply {
text = "hello inject"
}
}
}
效果:
三、事件注入
事件注入需要使用动态代理,我们需要生成View对应的事件回调(点击、长按等)匿名类对象
1.定义事件元注解
为了方便扩展,我们定义一个元注解,来表示事件注解需要代理的设置监听方法、监听事件接口、接口方法,如:setOnClickListener,View.OnClickListener::class,onClick
/**
* 事件元注解
* Created by aruba on 2021/10/27.
*/
@Target(AnnotationTarget.ANNOTATION_CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Event(
val setter: String,
val listenerClz: KClass,
val listenerCallbackMethodName: String
)
2.定义事件注解
事件注解需要使用元注解,注明代理控件的设置监听方法、监听方法传入的参数类型、监听类的回调函数名。
还需要一个集合属性,用来获取需要绑定的控件id集合
/**
* 点击事件注解
* Created by aruba on 2021/10/27.
*/
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@Event(
setter = "setOnClickListener",
listenerClz = View.OnClickListener::class,
listenerCallbackMethodName = "onClick"
)
annotation class OnClick(
vararg val ids: Int
)
3.注入工具实现
我们需要获取Activity中使用OnClick注解的方法,并获取OnClick注解的元注解Event,通过元注解,获取控件的setOnClickListener方法,并通过动态代理生成View.OnClickListener的代理对象,最后通过反射调用setOnClickListener方法为view绑定代理对象
/**
* 注入工具
* Created by aruba on 2021/10/27.
*/
object InjectUtils {
fun inject(activity: BaseActivity) {
injectContentView(activity)
injectViewId(activity)
injectClick(activity)
}
/**
* 注入布局文件
*/
private fun injectContentView(activity: BaseActivity) {
activity.javaClass.getAnnotation(ContentView::class.java)?.apply {
activity.setContentView(value)
}
}
/**
* 注入控件id
*/
private fun injectViewId(activity: BaseActivity) {
activity.javaClass.declaredFields.filter { field ->
field.getAnnotation(BindID::class.java) != null
}.forEach { field ->
field.apply {
isAccessible = true
set(activity, activity.findViewById(getAnnotation(BindID::class.java)!!.id))
}
}
}
/**
* 注入点击事件
*/
private fun injectClick(activity: BaseActivity) {
//获取方法
activity.javaClass.declaredMethods.filter { method ->
//过滤非OnClick注解
method.getAnnotation(OnClick::class.java) != null
}.forEach { method -> //method 为 Activity中的clickView方法
//获取OnClick注解
val onClick = method.getAnnotation(OnClick::class.java)
//获取控件id数组
val ids = onClick!!.ids
//获取OnClick注解的 元注解:Event
method.annotations.forEach {
//强转成Java Annotation对象,因为kotlin无法获取元注解(注解的注解)
(it as java.lang.annotation.Annotation).annotationType()
.getAnnotation(Event::class.java)
?.apply {
//给每个View绑定事件
ids.forEach { id ->
val view: View = activity.findViewById(id)
//获取setOnClickListener方法,入参为View.OnClickListener
val setOnClickListenerMethod =
view.javaClass.getMethod(setter, listenerClz.java)
//绑定 点击事件回调函数名onClick 与 Activity中的clickView方法(被OnClick注解的方法) 的关系
val map = mapOf(listenerCallbackMethodName to method)
//动态代理
val handler = ClickInvocationHandler(WeakReference(activity), map)
//动态代理生成的对象:点击事件匿名内部类
val proxy = Proxy.newProxyInstance(
listenerClz.java.classLoader,
arrayOf(listenerClz.java),
handler
)
//为view设置setOnClickListener
setOnClickListenerMethod.invoke(view, proxy)
}
}
}
}
}
}
4.动态代理
InvocationHandler中,我们需要代理View.OnClickListener的onClick方法,改为调用被OnClick注解的方法,通过外部传入的Map,可以通过方法名快速获取到被OnClick注解的方法
/**
* 动态代理事件
* Created by aruba on 2021/10/27.
*/
class ClickInvocationHandler(
private val activity: WeakReference,
private val map: Map
) : InvocationHandler {
override fun invoke(proxy: Any?, method: Method?, args: Array?): Any? {
//发现Method是onClick方法,执行被OnClick注解的clickView方法
method?.apply {
activity.get()?.run {
//onClick回调函数的入参为view: View?,将它强转成View后再传给clickView方法
return map[name]?.invoke(this, args!![0] as View)
}
}
return method?.invoke(proxy, args)
}
}
5.封装
我们将注入事件的方法优化,使它更具扩展性,将注解类型作为参数传入,并将控件id集合通过lambda获取
/**
* 注入工具
* Created by aruba on 2021/10/27.
*/
object InjectUtils {
fun inject(activity: BaseActivity) {
injectContentView(activity)
injectViewId(activity)
injectClick(activity, OnClick::class.java) {
it.ids
}
}
/**
* 注入布局文件
*/
private fun injectContentView(activity: BaseActivity) {
activity.javaClass.getAnnotation(ContentView::class.java)?.apply {
activity.setContentView(value)
}
}
/**
* 注入控件id
*/
private fun injectViewId(activity: BaseActivity) {
activity.javaClass.declaredFields.filter { field ->
field.getAnnotation(BindID::class.java) != null
}.forEach { field ->
field.apply {
isAccessible = true
set(activity, activity.findViewById(getAnnotation(BindID::class.java)!!.id))
}
}
}
/**
* 注入点击事件
*/
private inline fun injectClick(
activity: BaseActivity,
clz: Class,
getIds: (annotation: T) -> IntArray
) {
//获取方法
activity.javaClass.declaredMethods.filter { method ->
//过滤非OnClick注解
method.getAnnotation(clz) != null
}.forEach { method -> //method 为 Activity中的clickView方法
//获取OnClick注解
val onClick = method.getAnnotation(clz)
//获取控件id数组
val ids = getIds(onClick)
//获取OnClick注解的 元注解:Event
method.annotations.forEach {
//强转成Java Annotation对象,因为kotlin无法获取元注解(注解的注解)
(it as java.lang.annotation.Annotation).annotationType()
.getAnnotation(Event::class.java)
?.apply {
//给每个View绑定事件
ids.forEach { id ->
val view: View = activity.findViewById(id)
//获取setOnClickListener方法,入参为View.OnClickListener
val setOnClickListenerMethod =
view.javaClass.getMethod(setter, listenerClz.java)
//绑定 点击事件回调函数名onClick 与 Activity中的clickView方法(被OnClick注解的方法) 的关系
val map = mapOf(listenerCallbackMethodName to method)
//动态代理
val handler = ClickInvocationHandler(WeakReference(activity), map)
//动态代理生成的对象:点击事件匿名内部类
val proxy = Proxy.newProxyInstance(
listenerClz.java.classLoader,
arrayOf(listenerClz.java),
handler
)
//为view设置setOnClickListener
setOnClickListenerMethod.invoke(view, proxy)
}
}
}
}
}
}
这样如果我们想要新增长按事件功能,只需要新增长按事件的注解,并再次调用注入事件方法即可
6.Activity中使用注解
@ContentView(R.layout.activity_main)
class MainActivity : BaseActivity() {
@BindID(R.id.tv_hello)
val tvHello: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
tvHello?.apply {
text = "hello inject"
}
}
@OnClick(R.id.tv_hello, R.id.tv_hello2)
fun clickView(view: View) {
Toast.makeText(this, (view as TextView).text, Toast.LENGTH_SHORT).show()
}
}
效果: