转载

动态 Android 编程:避免无聊的代码

动态 Android 编程:避免无聊的代码

作者:刘聪

注意:本文章有些例子需要对Java或Android有一定编程基础。

与Python相比,Java是一门比较严肃的语言。作为一个先学Python的程序员,做起Android难免会觉得不舒服,有些死板,非常怀念decorator等方便的方法。为了实现一个简单的逻辑,你可能需要写很多额外的代码。

举个例子,做数据库查询时,怎么从一个 Cursor 里取出类型为 User 的实例到 List

假设 User 是这样的

class User {       int id;     String name; }

1. 找出 User 对应所有的列和每列在 Cursor 对应的索引。

2. 如果索引存在,根据类型取出正确的值。

3. 对于每个属性,不断重复上述步骤取出对应的值。

{   int columnIndex = cursor.getColumnIndex("id");   if (columnIndex >= 0) {     instance.id = cursor.getInt(columnIndex);   } }  {   int columnIndex = cursor.getColumnIndex("name");   if (columnIndex >= 0) {     instance.name = cursor.getString(columnIndex);   } }

这么做的问题在哪?

* 重复代码

* 重复代码

* 无聊

* 容易出错,不好维护

反射

我就是不想写那么多无聊的代码,怎么办?要不试试范型/反射。1. 取出所有的属性。

2. 循环属性队列。

3. 把属性设置成accessible。

4. 找到索引。

5. 取出属性的类型,根据类型从 Cursor 里取出正确的值。

public static  T fromCursor(Cursor cursor, Class cls) {       T instance = cls.newInstance();     List fields = Arrays.asList(cls.getDeclaredFields())     for (Field field : fields) {       field.setAccessible(true);       String fieldName = field.getName();       int columnIndex = cursor.getColumnIndex(fieldName);       if (columnIndex < 0) {         continue;       }       Class fieldType = field.getType();       if (fieldType.equals(int.class)) {         field.setInt(instance, cursor.getInt(columnIndex));       } else {         // 处理更多类型 String/float...    }    return instance; }

这样我们就不用很无聊的把同样的逻辑对于每种类型写一遍又一遍。

Processor

用了反射后,也会有一些其他问题,这样的代码可读性不是太好,不是很容易调试。

既然我们可以通过反射实现这些逻辑,为什么不干脆通过反射把这部分代码直接生成出来呢?1. 定义你要处理的annotation。

@Retention(RetentionPolicy.CLASS) @Target(ElementType.TYPE) public @interface MyAnnotation {     String value(); }

2. 定义你的 Processor 类,继承 AbstractProcessor

// Helper to define the Processor @AutoService(Processor.class) // Define the supported Java source code version @SupportedSourceVersion(SourceVersion.RELEASE_7) // Define which annotation you want to process @SupportedAnnotationTypes("com.glow.android.MyAnnotation") public class MyProcessor extends AbstractProcessor {

3. 重载 process 方法,实现生成代码的逻辑。

 @Override   public boolean process(Set annotations, RoundEnvironment roundEnv) {     Set elements = roundEnv.getElementsAnnotatedWith(MyAnnotation.class);     Set typeElements = ElementFilter.typesIn(elements);     for (TypeElement element : typeElements) {       ClassName currentType = ClassName.get(element);       MethodSpec.Builder builder = MethodSpec.methodBuilder("fromCursor")        .returns(currentType)        .addModifiers(Modifier.STATIC)        .addModifiers(Modifier.PUBLIC)        .addParameter(ClassName.get("android.database", "Cursor"), "cursor");        ... // 像在反射那节中,取出所有的fields,并循环取出每个元素,即每列       CodeBlock.Builder blockBuilder = CodeBlock.builder();       blockBuilder.beginControlFlow("");       blockBuilder.addStatement("int columnIndex = cursor.getColumnIndex($S)", column);       blockBuilder.beginControlFlow("if (columnIndex >= 0)");       ColumnType columnType = columnTypeMap.get(column);       String cursorType = null;       if (columnType == ColumnType.INT) {         cursorType = "Int";       } else if (columnType == ColumnType.LONG) {         cursorType = "Long";       } else if (columnType == ColumnType.FLOAT) {         cursorType = "Float";       } else if (columnType == ColumnType.STRING) {         cursorType = "String";       } else {         abort("Unsupported type", element);       }        blockBuilder.addStatement("instance.$L = cursor.get$L(columnIndex)",             fieldName,             cursorType);       blockBuilder.endControlFlow();       blockBuilder.endControlFlow();       builder.addCode(blockBuilder.build());       // 结束循环        // 生成代码到文件里       String className = ... // 设置你要生成的代码class名字       JavaFileObject sourceFile =   processingEnv.getFiler().createSourceFile(className, element);       Writer writer = sourceFile.openWriter();       javaFile.writeTo(writer);       writer.close();     }      return false;   }

4. 修改Userclass加上annotation

 @MyAnnotationclass User {       int id;     String name; }

5. 用 apt 工具把我们上面写的库加到编译过程去。

Tips:

* 用 AutoService 可以方便的生成 Processor 方法

* 强推 Javapoet ,用来生成漂亮的代码

AOP

AOP的做法和Processor类似,这里就不详述。你可能用 AspectJ 。

Gradle plugin

最后我还是没有完全采用上面的方法,因为:

* 在编译时生成的代码在打开编译器时找不到

* 有时候有些特殊需求,比如很多属性要在多个地方共享使用,能配置化会更好些

于是我们就用了Gradle Plugin来通过可配置文件生成代码

以下是简单的例子:

1. 定义配置文件,这里选用比较简单的 toml 文件

srcDir = "src/main/java"   pkg = "com.glow.android.storage.db"   [[tables]] name = "user"     [[tables.columns]]   name = "id"   type = "int"   isKey = true

2. 在 buildSrc 项目里创建Plugin

public class DbPlugin implements Plugin {    @Override   void apply(Project project) {     project.task('initDb') << {       def dir = project.getProjectDir()       def file = new File(dir, "table.toml")       generateCode(dir, new Toml().parse(file).to(DB.class))     }   }    static void generateCode(File dir, DB db) {     def outputDir = new File(dir, db.srcDir)     outputDir.mkdirs()     for (Table table : db.tables) {       //  Process it     }   } }

3. 像在上节讲的那样生成代码,把数据源从annotation换成toml里的定义

4. 在项目里把Plugin引用进去,并执行

5. 这样就可以得到漂亮的已经生成好的代码

正文到此结束
Loading...