用 kotlin 打造简化版本的 ButterKnife

1,287 阅读4分钟

大名鼎鼎的 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的演示


injectview demo演示.gif

demo和库的地址:
github.com/fengzhizi71…

整个库的设计:

整个库包括三个模块:

  • injectview:android library,包括Injector类和ViewBinder接口。用于在Activity、Fragment、View、Dialog中进入注入。
  • injectview-annotations:java library,用于存放各个注解,比如@InjectView。
  • injectview-compiler:java library,使用apt来生成代码

项目结构图.png

整个项目的module图.png

1. 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)

否则会无法编译通过的。

2. 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 改写会更加直观和简单。

3. injectview-compiler module

所有的注解都是编译时的注解类型,比如Activity中在使用时,会生成一个相同的类名+?ViewBinder的类。


apt生成的类.png

基于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));
  }
}

整个库的使用方法:

1. @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;
                   }

                  ......
           }

2. @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);
                }
          }

3. @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)); // 该方法用于打印对象
              }
          }

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