通过实现ViewBinding,学习自定义插件及JavaPoet的使用

1,036 阅读5分钟

一、概述

这篇文章是为了了解viewBinding是如何实现的,我很好奇为什么我们在项目的module的build.gradle中添加了如下代码

viewBinding{
    enable true
}

在make项目后就能自动生成ActivityMainBinding这些类的呢?

而我们为什么通过binding就能找到对应的控件的呢?

网上的博客中大部分都是只解释了,自动生成的ActivityMainBinding类中替我们做了findViewById所以我们可以通过binding找的对应的控件,但是这个类是怎么生成的呢?

了解过JavaPoet的同学可能知道,通过javaPoet可以用代码生成类,学过注解的同学可能会想通过注解+JavaPoet就能实现这个功能了,但是viewBinding并没有使用注解啊。

通过学习我了解到了,viewBinding的实现流程如下

image.png

由图可知,viewBinding是在make项目后才执行生成文件的代码,再加上build.gradle中的enable配置,所以联想到了gradle插件。

总结:所以整体流程就是由gradle插件实现通过viewBinding的enable配置决定是否启用viewBinding的代码生成,启用的话则使用JavaPoet来实现自动生成内部实现了findViewById和控件赋值的Java类。

二、实现viewBinding

1.使用JavaPoet生成Java文件

我们生成的Java类全貌如下:

public class ActivityMainBinding {
  public final ConstraintLayout rootView;

  public final TextView text;

  private ActivityMainBinding(ConstraintLayout rootView, TextView text) {
    this.rootView = rootView;
    this.text = text;
  }

  public ConstraintLayout getRoot() {
    return rootView;
  }

  public static ActivityMainBinding setContentView(Activity activity, int layoutId) {
    View decorView = activity.getWindow().getDecorView();
    ViewGroup contentView = decorView.findViewById(android.R.id.content);
    ConstraintLayout root = (ConstraintLayout) LayoutInflater.from(activity).inflate(layoutId, contentView, false);
    activity.setContentView(root);
    return bind(root);
  }

  public static ActivityMainBinding bind(ConstraintLayout rootView) {
    String missingId;
    TextView text = rootView.findViewById(R.id.text);
    if(text == null) {
     throw new NullPointerException("Missing required view with ID:"+text);
    }
    return new ActivityMainBinding(rootView,text);
  }
}

我个人使用JavaPoet生成Java类的心得就是你先写好需要的Java类,然后根据这个模版再去使用JavaPoet生成,避免你写到一半忘记自己需要什么方法,以及参数,返回值类型的问题。

(1) 遍历module下的res/layout目录,找到所有的xml文件,并且通过xml解析获取xml中定义了id的控件id和类型;

示例:

//拼接layout目录path
String layoutPath = project.getProjectDir().getAbsolutePath()+
        File.separator + "src" + File.separator + "main" +
        File.separator +"res" + File.separator +"layout";
File layoutDirFile = new File(layoutPath);
System.out.println("layoutPath: "+layoutPath);
if (layoutDirFile.exists()) {//layout目录存在

    File[] listFiles = layoutDirFile.listFiles();
    if (listFiles != null && listFiles.length > 0) {//layout目录下有文件

        for (File file : listFiles) {//遍历目录下的文件
            if (file.isFile() && file.getName().endsWith(".xml")) {//找到xml格式的文件
                // 是文件且是xml文件
                String fileName = file.getName().replace(".xml", "");
                String bindingClassName;
                if (fileName.contains("_")) {
                    // 按下划线切割
                    StringBuilder tempClassName = new StringBuilder();
                    for (String s : fileName.split("_")) {
                        tempClassName.append(s.substring(0, 1).toUpperCase().concat(s.substring(1)));
                    }
                    bindingClassName = tempClassName.append("Binding").toString();
                } else {
                    // 不包含 首字母大写
                    bindingClassName = fileName.substring(0, 1).toUpperCase().concat(fileName.substring(1)).concat("Binding");
                }
                //当前文件的绝对路径
                String filePath = file.getAbsolutePath();

                //通过xml解析获得xml的根标签类型
                String rootEleName = XmlUtil.getRootElement(filePath).getName();

                boolean isRootElementEnableViewBinding = XmlUtil.getRootElementViewBindingEnable(filePath);
                if (!isRootElementEnableViewBinding){
                    System.out.printf("%s`s layout rootElement-%s contains view_binding attribute equals false \n", bindingClassName , rootEleName);
                    continue;
                }
                //收集xml中所有设置了id属性的控件id
                List<String> ids = new ArrayList<>();
                XmlUtil.getAllIdElement(XmlUtil.getRootElement(filePath), ids);
                // 收集到了ID,存储起来
                componentIds.put(bindingClassName.concat(JOINT_MARK1).concat(rootEleName).concat(JOINT_MARK2).concat(fileName), ids);
            }
        }

(2) 根据xml文件名称创建Java类的类名,根据获取到的id集合定义全局控件变量;

示例:

//创建类
TypeSpec.Builder viewBindingClassBuilder = TypeSpec.classBuilder(className)
        .addModifiers(Modifier.PUBLIC);

//创建全局对象
viewBindingClassBuilder.addFields(getFieldSpecList(rootElementName, componentsAndIds));

private static List<FieldSpec> getFieldSpecList(String rootComponentName , List<String> componentsAndIds){
        List<FieldSpec> fieldSpecList = new ArrayList<>();

        //创建rootView变量
        FieldSpec rootFieldSpec = FieldSpec.builder(getViewClassName(rootComponentName),
                "rootView")
                .addModifiers(Modifier.PUBLIC , Modifier.FINAL)
                .build();
        fieldSpecList.add(rootFieldSpec);

        //遍历控件集合,根据id和控件类型创建对应类型的控件变量
        for (String componentNameAndId : componentsAndIds) {
            String componentId = componentNameAndId.substring(0, componentNameAndId.lastIndexOf("_"));
            String componentName = componentNameAndId.substring(componentNameAndId.lastIndexOf("_") + 1);
            String finalComponentName;
            if (componentId.contains("_")) {
                // 按下划线切割
                StringBuilder tempComponentName = new StringBuilder();
                String[] componentLowercaseArray = componentId.toLowerCase().split("_");
                for (int i = 0; i < componentLowercaseArray.length; i++) {
                    if (i==0){
                        tempComponentName.append(componentLowercaseArray[0]);
                    }else{
                        tempComponentName.append(componentLowercaseArray[i].substring(0, 1).toUpperCase().concat(componentLowercaseArray[i].substring(1)));
                    }
                }
                finalComponentName = tempComponentName.toString();
            } else {
                // 不包含 首字母大写
                finalComponentName = componentId.substring(0, 1).toLowerCase().concat(componentId.substring(1));
            }

            FieldSpec componentField = FieldSpec.builder(getViewClassName(componentName),
                    finalComponentName)
                    .addModifiers(Modifier.PUBLIC , Modifier.FINAL)
                    .build();
            fieldSpecList.add(componentField);


        }
        return fieldSpecList;
    }

(3) 创建构造方法,给定义的控件赋值;

// 创建构造方法
MethodSpec construct = MethodSpec.constructorBuilder()
        .addModifiers(Modifier.PRIVATE)
        .addParameters(getConstructorParameters(rootElementName , componentsAndIds))
        .addCode(getConstructorStatements(componentsAndIds))
        .build();
//向类中添加方法
viewBindingClassBuilder.addMethod(construct);

(4) 创建setContentView方法,找到decorView将布局设置给activity;

//创建setContentView方法
ClassName activityMainBindingClass = ClassName.get(packageName, className);
//创建参数
ParameterSpec activityParam = ParameterSpec.builder(ClassName.get("android.app", "Activity"),
        "activity").build();
ParameterSpec layoutIdParam = ParameterSpec.builder(int.class, "layoutId").build();

MethodSpec setContentView = MethodSpec.methodBuilder("setContentView")
        .addModifiers(Modifier.STATIC, Modifier.PUBLIC)
        .returns(activityMainBindingClass)//添加返回类型
        .addParameter(activityParam)//添加方法参数
        .addParameter(layoutIdParam)
        .addStatement("$T decorView = activity.getWindow().getDecorView()",
                ClassName.get("android.view","View"))//添加方法体
        .addStatement("$T contentView = decorView.findViewById(android.R.id.content)",
                ClassName.get("android.view", "ViewGroup"))
        .addStatement("$T root = ($T) $T.from(activity).inflate(layoutId, contentView, false)",
                constraintLayoutClass,
                constraintLayoutClass,
                ClassName.get("android.view", "LayoutInflater"))
        .addStatement("activity.setContentView(root)")
        .addStatement("return bind(root)")
        .build();
viewBindingClassBuilder.addMethod(setContentView);

(5) 创建getRoot方法,方便获取layout的根布局;

// 创建getRoot方法
ClassName constraintLayoutClass = getViewClassName(rootElementName);

MethodSpec getRoot = MethodSpec.methodBuilder("getRoot")
        .addModifiers(Modifier.PUBLIC)
        .returns(constraintLayoutClass)
        .addStatement("return rootView")
        .build();
viewBindingClassBuilder.addMethod(getRoot);

(6) 创建bind方法,通过findViewById找到控件后调用构造方法赋值。

//创建bind方法
ParameterSpec rootViewParam = ParameterSpec.builder(constraintLayoutClass, "rootView").build();
MethodSpec bindMethod = MethodSpec.methodBuilder("bind")
        .addModifiers(Modifier.STATIC, Modifier.PUBLIC)
        .returns(activityMainBindingClass)
        .addParameter(rootViewParam)
        .addStatement("$T missingId", String.class)
        .addCode(getBindCode(className, componentsAndIds))
        .build();
viewBindingClassBuilder.addMethod(bindMethod);

(7) 生成文件,在指定目录下生成真正的Java类文件

示例:

//创建类
TypeSpec viewBindingClass = viewBindingClassBuilder.build();

//生成文件
JavaFile javaFile = JavaFile.builder(packageName, viewBindingClass)
        .build();
String filePath = project.getProjectDir().getAbsolutePath()+"/build/generated/ap_generated_sources/debug/out/viewBinding";
try {
    File filer = new File(filePath);
    javaFile.writeTo(filer);
    System.out.println("generated successfully");
} catch (IOException e) {
    System.out.println("generated unsuccessfully");
    e.printStackTrace();
}

2.自定义Gradle插件

1.自定义gradle插件方式介绍

自定义gradle插件可以在以下三个地方创建,分别是:

1.构建脚本内;

2.buildSrc模块内;

3.单独项目

1.构建脚本内建方式

在build.gradle内直接创建Gradle插件

优点:

  • build.gradle 中创建的插件将被自动编译并包含在 classpath 中,使用时无需在构建脚本内指定 classpath

缺点:

  • 此插件仅在当前构建脚本中有效,对外部文件不可见,无法在当前构建脚本以外的其他地方复用此插件
2.buildSrc模块方式

rootProject/buildSrc 文件夹是 Gradle 的预留目录,用来存放当前项目私有 Gradle 插件的源代码与构建脚本

优点:

  • 项目构建时,Gradle 会自动编译项目目录下的 buildSrc 文件夹下的构建脚本和源码,并将其添加到项目构建脚本的 classpath 中,因此在使用 buildSrc 中创建的插件时,无需再手动指定 classpath
  • buildSrc 文件夹中构建脚本和 Gradle 插件同一项目均可见,因此同一项目中的其他模块也可以使用 buildSrc 中创建的插件

缺点:

  • 此处创建的插件对外部项目不可见,无法在其他项目中复用
3.单独项目方式

采用 buildSrc 模块方式时,Gradle 会妥善处理 buildSrc 模块的构建脚本与源码,并将其添加到当前项目的 classpath 中。但 buildSrc 方式的插件只能在项目内共享与复用,若要在其他项目中使用该插件,还需要再进行下列操作

  • 将插件发布到 maven 仓库(任意仓库)
  • 在需要应用该插件的构建脚本中的 repository 部分添加该插件所在的 maven 仓库
  • 在需要应用该插件的构建脚本中的 classpath 部分添加该插件对应的 maven 坐标 (group : id : version)

因为是在其他项目中使用该项目 buildSrc 模块 中的自定义 Gradle 插件,所以 Gradle 的 buildSrc 保留目录优势不再。如果将模块名由 buildSrc 修改为其他名称,则可将其称为独立的 Gradle 插件模块。

2.使用buildSrc模块方式构建插件

由于我对maven仓库的发布不是很熟悉,所以我选用buildSrc方式构建插件。

1.在project内创建buildsrc目录

目录结构如下:

image.png

需要注意的是这里只是创建buildsrc目录就行,不是创建library,也不需要在setting.gradle中添加buildsrc模块。

src/main下建立groovy和java两个目录,整体目录结构如下:

image.png

2.build.gradle内容编写

(1) 因为gradle插件需要用groovy语言来编写,所以需要引用groovy插件:apply plugin: 'groovy'

(2) 由于buildsrc目录引用不到project的build配置,所以这里需要配置仓库的下载地址:

repositories {
    google()
    mavenCentral()
}

(3) 依赖项:

dependencies {
    //依赖libs下的jar包,因为需要进行xml解析所以引入了dom4j的jar包
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //gradle sdk gradle相关sdk引用
    implementation gradleApi()
    //groovy sdk groovy相关sdk引用
    implementation localGroovy()
    //JavaPoet依赖
    implementation 'com.squareup:javapoet:1.13.0'
}

3.用groovy编写插件

因为groovy是基于Java的因此他们的构建都很类似,且groovy文件可以引用的Java文件的类和方法,我们这次也是插件用groovy写,使用JavaPoet自动生成Java类的工作用Java编写。

(1) 为了让我们的groovy类申明为gradle插件,我们新建的groovy文件需要继承org.gradle.api.Plugin接口并实现apply方法;

示例:

class ViewBinding implements Plugin<Project> {

    private static final String CONFIG_NAME = "viewBinding"
    private static final String TASK_NAME = "generated_code"

    @Override
    void apply(Project project) {
        System.out.println("插件开始")

    }
}

(2) 添加viewBinding的enable扩展属性;

示例:

class ViewBindingConfig {
    boolean enable
}

//添加扩展属性
def viewBinding = project.extensions.create(CONFIG_NAME, ViewBindingConfig.class)

(3) 定义插件任务,实现判断enable的值来决定是否需要生成Java类

示例:

project.task(TASK_NAME){

    //manifest的目录
    String manifest = project.getProjectDir().getAbsolutePath() + File.separator + "src" +
            File.separator + "main" + File.separator + "AndroidManifest.xml"
    //通过manifest获取项目包名
    def packageName = XmlUtil.getRootElement(manifest)
            .attribute(QName.get("package"))
            .getValue()
    System.out.println(String.format("featureName is %s,packageName is %s",project.getProjectDir().getAbsolutePath(),packageName))
    //通过JavaPoet生成文件
    GenerateViewBinding.execute(packageName as String,project, viewBinding.enable as boolean)
}

4.定义插件名

在main下创建resources目录,下面创建META-INF/gradle-plugins/com.mins.plugin.properties文件,注意下划线上的地方可以随便命名,不过这里就是插件名字了,后面使用的时候需要与这里保持一致。

com.mins.plugin.properties文件里面配置好你的插件入口

示例:

implementation-class=com.mins.plugin.ViewBinding

5. 使用插件

在你需要使用自定义插件的module的build.gradle的顶部引用插件id,内容中添加viewBinding enable为true就可以了。

示例:

plugins {
    id 'com.android.application'
    id 'com.mins.plugin'
}

viewBinding{
    enable true
}

activity中:

com.mins.annotationtestapplication.app.ActivityMainBinding bing = com.mins.annotationtestapplication.app.ActivityMainBinding.setContentView(this, R.layout.activity_main);
bing.text.setText("哈哈");

三、问题总结:

  1. make完成后并没有在app的/build/generated/ap_generated_sources/debug/out/viewBinding目录下生成对应文件。

    解决办法:

    1)可以从右侧gradle中找到app/Tasks/Other下的对应插件任务名generated_code,双击执行任务。

image.png

image.png

2)使用project.afterEvaluate,这个方法回调是在make执行完成。

示例:

//在工程make完成后执行里面的方法
project.afterEvaluate {

    //manifest的目录
    String manifest = project.getProjectDir().getAbsolutePath() + File.separator + "src" +
            File.separator + "main" + File.separator + "AndroidManifest.xml"
    //通过manifest获取项目包名
    def packageName = XmlUtil.getRootElement(manifest)
            .attribute(QName.get("package"))
            .getValue()
    System.out.println(String.format("featureName is %s,packageName is %s",project.getProjectDir().getAbsolutePath(),packageName))
    //通过JavaPoet生成文件
    GenerateViewBinding.execute(packageName as String,project, viewBinding.enable as boolean)
}

  1. 生成Java类的文件路径

    开始我使用的是build/generated/source/viewbinding,结果在activity中引用不到类,后来发现需要手动设置目录为Generate Sources Root才能引用到。所以后来我把生成路径改为了系统默认文件夹就是Generate Sources Root类型的/build/generated/ap_generated_sources/debug/out目录下。

相关视频推荐:

【Android开发中高级教程】自定义View基础_哔哩哔哩_bilibili

【Android开发教程】UI绘制体系原理_哔哩哔哩_bilibili

【Android开发教程】今日头条UI结构与ViewPager原理分析与实战_哔哩哔哩_bilibili

【Android开发教程】高级UI训练营_哔哩哔哩_bilibili

对标阿里年薪70W+Android开发进阶合集,含项目实战、面试专题、源码解析以及开源框架解析,已更新至133集_哔哩哔哩_bilibili

Android面试合集——字节跳动、阿里、腾讯、百度等一线大厂2021年面试真题解析/HashMap/组件化/插件化/热修复/性能优化/JVM/事件分发_哔哩哔哩_bilibili