使用APT减少MVP的冗余代码

Posted 何以诚

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用APT减少MVP的冗余代码相关的知识,希望对你有一定的参考价值。

前言

不知道从何时起,移动端开发都开始采用MVP。我们在认识到MVP有点的时候,也不妨会察觉到它其实也有很多恼人的地方,比如,我们针对每种状态渲染不同的视图:

  private void renderInit() 
        mViewA.setVisibility(View.VISIBLE);
        mViewB.setVisibility(View.GONE);
        mViewC.setVisibility(View.GONE);
        mViewD.setVisibility(View.GONE);
        mViewE.setVisibility(View.GONE);
    

    private void renderSummary() 
        mViewA.setVisibility(View.GONE);
        mViewB.setVisibility(View.VISIBLE);
        mViewC.setVisibility(View.GONE);
        mViewD.setVisibility(View.GONE);
        mViewE.setVisibility(View.GONE);
    

可以看到在这里,我们渲染Init状态时,把View A设为可见,把其他的View设为不可见,当我们又去渲染Summary状态是,又重复上面的动作,不过这次是吧View B设为可见。这种冗余代码(或者说是模板代码)非常的烦人,因为我们在复制粘贴的时候极有可能设置错误的View为可见了。那么我们有没有什么办法来避免这样的问题呢。其实是有的,我们不妨回忆下ButterKnife怎么做的——对于findViewById这样的冗余代码,ButterKnife是采用注解的方式解决的:

   @Bind(R.id.id_name)
    TextView m_name;

    @Bind(R.id.id_who)
    TextView m_who;

    @Bind(R.id.id_musicBar)
    MusicBar m_musicBar;

    @Bind(R.id.id_playControl)
    ImageView m_bottomPlayControlView;

    @Override
    protected IView createView() 
        return this;
    

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        ButterKnife.bind(this);
        ...
    

在执行

ButterKnife.bind(this);

后,ButterKnife会采用APT自动生成代码执行findViewById操作。
同样的,我们在解决MVP冗余代码时,我们也可以使用APT生成代码执行
setVisibility(View.VISIBLE); 操作。

思路

1:模仿ButterKnife对于要setVisibility的View我们使用注解来标示
2:当知道有哪些View要setVisibility后,我们可以把它们存到容器里
3:当外部要setVisibility某些View时,我们可以提供一个类似
4:为了避免APT生成的代码和现有的代码重复类名,我们可以尝试在APT的类名中出现$符号,但是这样用户用起来很难受,我们可以是APT生成的代码都实现某个接口,当new出对象后以接口类型返回以保障代码整洁性。

void setVisible(View... target)

的接口去遍历容器,如果容器中的View在集合target中,就设为可见,否则不可见。


1:如果你最APT还不是很了解,建议阅读下鸿洋的文章鸿洋APT


实现

0x01:
android Studio里新建一个java工程:

在java工程的build.gradle脚本里添加依赖:

apply plugin: 'java'

sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7

dependencies 
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.google.auto.service:auto-service:1.0-rc2'

0x02:
然后我们定义注解:

/**
 * Created by chan on 16/10/15.
 * jiacheng.li@shanbay.com
 */
@Documented
@Retention(RetentionPolicy.SOURCE)
@Inherited
@Target(ElementType.FIELD)
public @interface JoinView 

只能用于field,它用于标示我们要setVisibility的view,像这样:

  @JoinView
  View mViewC;

0x03:
当注解标示某个field之后,我们就可以拿到field的变量名,我们可以通过activity.mViewC的方式读取里面的值,不过这有个前提——mView最起码应该是protected, 或者public的,但是我们还是选用protected,毕竟这样可以最大化数据的封装程度。如果是这样的话我们生成的类必须得和被注解的类在同一包下面当然这很容易实现。

我们自定义Processor:

@AutoService(Processor.class)
public class YellowPeachProcessor extends AbstractProcessor 
    /**
     * 用于写java文件
     */
    private Filer mFiler;
    /**
     * 可以理解为log
     */
    private Messager mMessager;
    /**
     * 注解检查器,用于判断被注解的field不是private的
     */
    private AnnotationChecker mAnnotationChecker;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) 
        super.init(processingEnv);

        mFiler = processingEnv.getFiler();
        mMessager = processingEnv.getMessager();
        mAnnotationChecker = new AnnotationChecker(mMessager);
    

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 

        //找到被注解的field
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(JoinView.class);

        if (set != null) 

            CodeGenerator codeGenerator = new CodeGenerator(mFiler, mMessager);
            for (Element element : set) 
                //先检查权限
                if (!mAnnotationChecker.checkAnnotation(element)) 
                    return false;
                
                //把备注解的field添加到生成器里,准备用来生成代码
                codeGenerator.add((VariableElement) element);
            
            //开始生成代码
            codeGenerator.generate();
        
        return true;
    

    @Override
    public Set<String> getSupportedAnnotationTypes() 

        //添加支持的注解类型 我们支持JoinView
        Set<String> set = new HashSet<>();
        set.add(JoinView.class.getCanonicalName());
        return set;
    

    @Override
    public SourceVersion getSupportedSourceVersion() 
        return SourceVersion.RELEASE_7;
    

整体代码还是很简单,不过里面有两个类我们依次看下实现方式。

0x04:
检查被注解的field的访问权限

/**
 * Created by chan on 16/10/15.
 * jiacheng.li@shanbay.com
 */
public class AnnotationChecker 

    private Messager mMessager;

    public AnnotationChecker(Messager messager) 
        mMessager = messager;
    

    public boolean checkAnnotation(Element element) 
        VariableElement variableElement = (VariableElement) element;
        if (variableElement.getModifiers().contains(Modifier.PRIVATE)) 
            mMessager.printMessage(Diagnostic.Kind.ERROR, "JoinView不能用于private field: "
                    + variableElement.getEnclosingElement() + " -> " + variableElement.getSimpleName());
            return false;
        

        return true;
    

可以看到如果针对private field,我们是不能通过类似activity.mViewC的方式访问的,所以这里会报错。

0x05:
生成代码,这里比较复杂,我特意建一个Title进行解释。

生成代码

当我们收集到备注注解的field信息之后,我们就可以生成代码,不过怎么处理这些field是个问题。我们首先想到的就是创建一个Map, key为被注解域的class,而值就是它一系列的被注解的field:

public class CodeGenerator  
     private Map<String, List<VariableElement>> mVariableElementMap = new HashMap<>();  

     public void add(VariableElement element) 
        List<VariableElement> variableElements = mVariableElementMap.get(element.getEnclosingElement().toString());
        if (variableElements == null) 
            variableElements = new ArrayList<>();
            //获得被注解的class的名称作为键
            mVariableElementMap.put(element.getEnclosingElement().toString(), variableElements);
        

        //当前class下备注解的field
        variableElements.add(element);
    

这里可能有些人对于

element.getEnclosingElement().toString()

感到困惑,举个例子:

package com.chan.yellowpeach;

import android.support.v7.app.AppCompatActivity;
import android.view.View;


public class MainActivity extends AppCompatActivity 

    @JoinView
    View mViewC;

这里element.getEnclosingElement().toString()返回的就是com.chan.yellowpeach.MainActivity,这必定是唯一的啊,所以作为key再合适不过了,而element就是对应的View mViewC,有了这些生成代码只是分分钟的事。

我们可以尝试看下完整的代码:

package com.chan.apt.core;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.lang.model.element.Element;
import javax.lang.model.element.VariableElement;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

/**
 * Created by chan on 16/10/15.
 * jiacheng.li@shanbay.com
 */
public class CodeGenerator 
    private Map<String, List<VariableElement>> mVariableElementMap = new HashMap<>();
    /**
     * 用于写java文件
     */
    private Filer mFiler;
    /**
     * logger
     */
    private Messager mMessager;

    /**
     * APT生成代码所在的包名
     */
    private String mPackage;

    public CodeGenerator(Filer filer, Messager messager) 
        mFiler = filer;
        mMessager = messager;
    

    public void add(VariableElement element) 
        List<VariableElement> variableElements = mVariableElementMap.get(element.getEnclosingElement().toString());
        if (variableElements == null) 
            variableElements = new ArrayList<>();
            //获得被注解的class的名称作为键
            mVariableElementMap.put(element.getEnclosingElement().toString(), variableElements);
        

        //当前class下备注解的field
        variableElements.add(element);
    

    public void generate() 

        if (mVariableElementMap.isEmpty()) 
            return;
        

        init();

        try 
            for (Map.Entry<String, List<VariableElement>> entry : mVariableElementMap.entrySet()) 
                String clazzName = "YellowPeach$" + entry.getKey().replaceAll("\\\\.", "\\\\$");
                JavaFileObject javaFileObject = mFiler.createSourceFile(mPackage + "." + clazzName);
                mMessager.printMessage(Diagnostic.Kind.NOTE, "在" + mPackage + "." + clazzName + "生成代码");
                Writer writer = javaFileObject.openWriter();
                writer.write(generateSourceCode(entry, mPackage, clazzName));
                writer.flush();
                writer.close();
            
         catch (IOException e) 
            e.printStackTrace();
        
    

    private void init() 

        //先获得包名
        Iterator<Map.Entry<String, List<VariableElement>>> iterator = mVariableElementMap.entrySet().iterator();
        Map.Entry<String, List<VariableElement>> elementEntry = iterator.next();

        VariableElement variableElement = elementEntry.getValue().get(0);

        Element element = variableElement.getEnclosingElement();
        while (element != null && element.getEnclosingElement() != null) 
            mPackage = element.toString();
            element = element.getEnclosingElement();
        

        mPackage = mPackage.substring(0, mPackage.lastIndexOf("."));
    

    private static String generateSourceCode(Map.Entry<String, List<VariableElement>> entry, String packageName, String clazzName) 

        //包
        StringBuilder stringBuilder = new StringBuilder("package ");
        stringBuilder.append(packageName);
        stringBuilder.append(";\\n");

        //import
        stringBuilder.append("import android.view.View;\\n" +
                "\\n" +
                "import com.chan.lib.Peach;\\n" +
                "\\n" +
                "import java.util.ArrayList;\\n" +
                "import java.util.List;");

        stringBuilder.append("public class ");
        stringBuilder.append(clazzName);
        stringBuilder.append(" implements Peach \\n");

        //成员变量
        stringBuilder.append("private List<View> mViews = new ArrayList<>();\\n");

        //构造函数
        stringBuilder.append("public ");
        stringBuilder.append(clazzName);
        stringBuilder.append("(");
        stringBuilder.append(entry.getKey());
        stringBuilder.append(" o)");

        for (VariableElement item : entry.getValue()) 
            stringBuilder.append("mViews.add(");
            stringBuilder.append("o.");
            stringBuilder.append(item.getSimpleName());
            stringBuilder.append(");");
        

        stringBuilder.append("");

        //override的内容
        stringBuilder.append(" @Override\\n" +
                "    public void setVisible(View... target) \\n" +
                "\\n" +
                "        for (View v : mViews) \\n" +
                "            v.setVisibility(View.GONE);\\n" +
                "        \\n" +
                "\\n" +
                "        for (int i = 0; i < target.length; ++i) \\n" +
                "            final int index = mViews.indexOf(target[i]);\\n" +
                "            if (index != -1) \\n" +
                "                mViews.get(index).setVisibility(View.VISIBLE);\\n" +
                "            \\n" +
                "        \\n" +
                "    ");

        //结尾
        stringBuilder.append("");
        return stringBuilder.toString();
    

从之前的例子可以看到在add(xxx)之后就是收集完所有的信息,我们所要做的就是调用codeGenerator.generate()生成代码

在codeGenerator.generate()函数里,我们首先调用init来获取包名:

  private void init() 

        //先获得包名
        Iterator<Map.Entry<String, List<VariableElement>>> iterator = mVariableElementMap.entrySet().iterator();
        Map.Entry<String, List<VariableElement>> elementEntry = iterator.next();

        VariableElement variableElement = elementEntry.getValue().get(0);

        Element element = variableElement.getEnclosingElement();
        while (element != null && element.getEnclosingElement() != null) 
            mPackage = element.toString();
            element = element.getEnclosingElement();
        

        mPackage = mPackage.substring(0, mPackage.lastIndexOf("."));
    

读者可以通过打mMessager打log查看执行的过程,本身也比较简单,讲解却十分烦,光是例子就不少代码。

在获得包名之后就是生成响应的java代码:

 for (Map.Entry<String, List<VariableElement>> entry : mVariableElementMap.entrySet()) 
                //把.都换成$
                String clazzName = "YellowPeach$" + entry.getKey().replaceAll("\\\\.", "\\\\$");
                //指定java文件写入的位置
                JavaFileObject javaFileObject = mFiler.createSourceFile(mPackage + "." + clazzName);
                mMessager.printMessage(Diagnostic.Kind.NOTE, "在" + mPackage + "." + clazzName + "生成代码");

                //开始写文件
                Writer writer = javaFileObject.openWriter();
                writer.write(generateSourceCode(entry, mPackage, clazzName));
                writer.flush();
                writer.close();
            

写文件再上文已经给出,其中没有多少技术难度,只有有一点核心代码需要解释:

    //构造函数 参数为被注解的class
        stringBuilder.append("public ");
        stringBuilder.append(clazzName);
        stringBuilder.append("(");
        stringBuilder.append(entry.getKey());
        stringBuilder.append(" o)");

        for (VariableElement item : entry.getValue()) 
            stringBuilder.append("mViews.add(");
            stringBuilder.append("o.");
            //返回field的名字
            stringBuilder.append(item.getSimpleName());
            stringBuilder.append(");");
        

我们不妨看下APT生成的代码。如果你一切顺利地话,会在这个目录下看到apt代码:

package com.chan.yellowpeach;

import android.view.View;

import com.chan.lib.Peach;

import java.util.ArrayList;
import java.util.List;

public class YellowPeach$com$chan$yellowpeach$Main2Activity implements Peach 
    private List<View> mViews = new ArrayList<>();

    public YellowPeach$com$chan$yellowpeach$Main2Activity(
            com.chan.yellowpeach.Main2Activity o) 
        mViews.add(o.mView);
    

    @Override
    public void setVisible(View... target) 

        for (View v : mViews) 
            v.setVisibility(View.GONE);
        

        for (int i = 0; i < target.length; ++i) 
            final int index = mViews.indexOf(target[i]);
            if (index != -1) 
                mViews.get(index).setVisibility(View.VISIBLE);
            
        
    

还是很简单的,那么下面的问题就只剩下如何new一个apt生成的class的对象

new 一个对象

package com.chan.lib;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * Created by chan on 16/10/15.
 * jiacheng.li@shanbay.com
 */
public class YellowPeach 
    public static Peach bind(Object o) 
        try 
            final String clazzName = o.getClass().getPackage().getName().toString() +
                    ".YellowPeach$" + o.getClass().getCanonicalName().replaceAll("\\\\.", "\\\\$");
            Class<?> clazz = o.getClass().getClassLoader().loadClass(clazzName);
            Constructor<?> constructors[] = clazz.getConstructors();
            return (Peach) constructors[0].newInstance(o);
         catch (ClassNotFoundException e) 
            e.printStackTrace();
         catch (IllegalAccessException e) 
            e.printStackTrace();
         catch (InstantiationException e) 
            e.printStackTrace();
         catch (InvocationTargetException e) 
            e.printStackTrace();
        

        return null;
    

我们使用反射的方式获得APT生成的类,之后直接new出来然后作为Peach接口类型返回。我们看下客户端是如何使用的

使用

package com.chan.yellowpeach;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;

import com.chan.apt.annotations.JoinView;
import com.chan.lib.Peach;
import com.chan.lib.YellowPeach;


public class MainActivity extends AppCompatActivity 

    @JoinView
    View mViewC;

    private Peach mPeach;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mViewC = findViewById(R.id.viewC);

        mPeach = YellowPeach.bind(this);
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View v) 
                ViewHolder holder = new ViewHolder(findViewById(R.id.viewA));
                ViewHolder.ViewHolderA viewHolder = holder.new ViewHolderA(findViewById(R.id.viewB));
                viewHolder.foo();
                holder.foo();
                mPeach.setVisible(mViewC);
            
        );

        findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View v) 
                Intent intent = new Intent(MainActivity.this, Main2Activity.class);
                startActivity(intent);
            
        );
    

    public class ViewHolder 
        @JoinView
        View mView;
        private Peach mPeach;

        public ViewHolder(View view) 
            mView = view;
            mPeach = YellowPeach.bind(this);
        

        public void foo() 
            mPeach.setVisible(mView);
        

        public class ViewHolderA 
            @JoinView
            View mView;
            private Peach mPeach;

            public ViewHolderA(View view) 
                mView = view;
                mPeach = YellowPeach.bind(this);
            

            public void foo() 
                mPeach.setVisible(mView);
            
        
    

运行效果

代码下载

repo

以上是关于使用APT减少MVP的冗余代码的主要内容,如果未能解决你的问题,请参考以下文章

使用APT减少MVP的冗余代码

使用APT减少MVP的冗余代码

使用APT减少MVP的冗余代码

GraphQL总结

MVP模式之与APT技术结合产生的火花

MVP模式之与APT技术结合产生的火花