问题描述
当我们通过include 引入aar中的布局时,在其生成的对应viewbinding/DataBinding类中类型为view,而不是具体的viewbinding 类型,例如以下布局
其中@layout/test 为aar中的布局
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
android:id="@+id/local"
layout="@layout/local_test" />
<include
android:id="@+id/test"
layout="@layout/test" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
生成的viewbinding类为
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final ConstraintLayout rootView;
@NonNull
public final LocalTestBinding local;
@NonNull
public final ConstraintLayout main;
//其对应类型为View,
@NonNull
public final View test;
事实上我们期待其生成的类型为具体的viewBinding,在这里为TestBinding,即public final TestBinding test;
分析源码
从build输出的任务观察到可能是task: dataBindingGenBaseClassesDebug
因此,我们从TaskManager 进入找到改task的具体实现类DataBindingGenBaseClassesTask
protected fun createDataBindingTasksIfNecessary(creationConfig: ComponentCreationConfig) {
val dataBindingEnabled = creationConfig.buildFeatures.dataBinding
val viewBindingEnabled = creationConfig.buildFeatures.viewBinding
if (!dataBindingEnabled && !viewBindingEnabled) {
return
}
taskFactory.register(
DataBindingMergeDependencyArtifactsTask.CreationAction(creationConfig))
DataBindingBuilder.setDebugLogEnabled(logger.isDebugEnabled)
//重点
taskFactory.register(DataBindingGenBaseClassesTask.CreationAction(creationConfig))
// DATA_BINDING_TRIGGER artifact is created for data binding only (not view binding)
if (dataBindingEnabled) {
if (creationConfig.services.projectOptions.get(BooleanOption.NON_TRANSITIVE_R_CLASS)
&& isKotlinKaptPluginApplied(project)) {
val kotlinVersion = getProjectKotlinPluginKotlinVersion(project)
if (kotlinVersion != null && kotlinVersion < KAPT_FIX_KOTLIN_VERSION) {
// Before Kotlin version 1.5.20 there was an issue with KAPT resolving files
// at configuration time. We only need this task as a workaround for it, if the
// version is newer than 1.5.20 or KAPT isn't applied, we can skip it.
taskFactory.register(
MergeRFilesForDataBindingTask.CreationAction(creationConfig))
}
}
taskFactory.register(DataBindingTriggerTask.CreationAction(creationConfig))
creationConfig.sources.java {
it.addSource(
TaskProviderBasedDirectoryEntryImpl(
name = "databinding_generated",
directoryProvider = creationConfig.artifacts.get(
InternalArtifactType.DATA_BINDING_TRIGGER
),
)
)
}
setDataBindingAnnotationProcessorParams(creationConfig)
}
}
@TaskAction
fun writeBaseClasses(inputChanges: InputChanges) {
// TODO extend NewIncrementalTask when moved to new API so that we can remove the manual call to recordTaskAction
recordTaskAction(analyticsService.get()) {
// TODO figure out why worker execution makes the task flake.
// Some files cannot be accessed even though they show up when directory listing is
// invoked.
// b/69652332
val args = buildInputArgs(inputChanges)
CodeGenerator(
args,
sourceOutFolder.get().asFile,
Logger.getLogger(DataBindingGenBaseClassesTask::class.java),
encodeErrors,
getRPackageProvider()).run()
}
}
再看CodeGenerator.run()
override fun run() {
try {
initLogger()
BaseDataBinder(
LayoutInfoInput(args),
getRPackage)
.generateAll(DataBindingBuilder.GradleFileWriter(sourceOutFolder.absolutePath))
} finally {
clearLogger()
}
}
再看BaseDataBinder.generateAll()
fun generateAll(writer : JavaFileWriter) {
input.invalidatedClasses.forEach {
writer.deleteFile(it)
}
val myLog = LayoutInfoLog()
myLog.addAll(input.unchangedLog)
val useAndroidX = input.args.useAndroidX
val libTypes = LibTypes(useAndroidX = useAndroidX)
// Sort the layout bindings to ensure deterministic order
val layoutBindings = resourceBundle.allLayoutFileBundlesInSource
.groupBy(LayoutFileBundle::getFileName).toSortedMap()
//for循环将layoutBindings 转化为对应的viewBinding或者dataBinding
layoutBindings.forEach { layoutName, variations ->
val layoutModel = BaseLayoutModel(variations, getRPackage)
val javaFile: JavaFile
val classInfo: GenClassInfoLog.GenClass
//如果是dataBinding
if (variations.first().isBindingData) {
check(input.args.enableDataBinding) {
"Data binding is not enabled but found data binding layouts: $variations"
}
val binderWriter = BaseLayoutBinderWriter(layoutModel, libTypes)
javaFile = binderWriter.write()
classInfo = binderWriter.generateClassInfo()
} else {////如果是viewBinding
check(input.args.enableViewBinding) {
"View binding is not enabled but found non-data binding layouts: $variations"
}
//重点,生成viewbinger
val viewBinder = layoutModel.toViewBinder()
//将viewbinder 转为为javaclassFile
javaFile = viewBinder.toJavaFile(useLegacyAnnotations = !useAndroidX)
classInfo = viewBinder.generatedClassInfo()
}
writer.writeToFile(javaFile)
myLog.classInfoLog.addMapping(layoutName, classInfo)
variations.forEach {
it.bindingTargetBundles.forEach { bundle ->
if (bundle.isBinder) {
myLog.addDependency(layoutName, bundle.includedLayout)
}
}
}
}
input.saveLog(myLog)
// data binding will eat some errors to be able to report them later on. This is a good
// time to report them after the processing is done.
Scope.assertNoError()
}
再看layoutModel.toViewBinder()
fun BaseLayoutModel.toViewBinder(): ViewBinder {
//获取生成类中R文件的包名
val rClassName = ClassName.get(modulePackage, "R")
fun BindingTargetBundle.toBinding(): ViewBinding {
val idReference = id.parseXmlResourceReference().toResourceReference(rClassName, getRPackage)
val (present, absent) = layoutConfigurationMembership(this)
return ViewBinding(
name = fieldName(this),
type = parseLayoutClassName(fieldType, baseFileName),
//在这里判断生成类型是viewBinding还是view
//isBinder 是BaseLayoutModel 的内部属性,所以我们需要回头看layoutModel的生成
form = if (isBinder) ViewBinding.Form.Binder else ViewBinding.Form.View,
id = idReference,
presentConfigurations = present,
absentConfigurations = absent
)
}
validateExplicitViewBindingTypes()
val bindings = sortedTargets
.filter { it.id != null }
.filter { it.viewName != "merge" } // <merge> can have ID but it's ignored at runtime.
.map { it.toBinding() }
val rootNode = parseRootNode(rClassName, bindings)
return ViewBinder(
generatedTypeName = ClassName.get(bindingClassPackage, bindingClassName),
layoutReference = ResourceReference(rClassName, "layout", baseFileName),
bindings = bindings,
rootNode = rootNode
)
}
回头看layoutModel 的生成
val layoutBindings = resourceBundle.allLayoutFileBundlesInSource
.groupBy(LayoutFileBundle::getFileName).toSortedMap()
layoutBindings.forEach { layoutName, variations ->
val layoutModel = BaseLayoutModel(variations, getRPackage)
其来源于 resourceBundle ,resourceBundle 是BaseDataBinder 的内部属性
是在初始化时为其进行了赋值
class BaseDataBinder(val input : LayoutInfoInput, val getRPackage: ((String, String) -> (String))?) {
private val resourceBundle : ResourceBundle = ResourceBundle(
input.packageName, input.args.useAndroidX)
init {
//读取固定目录下的xml文件,将其反序列化为LayoutFileBundle
input.filesToConsider
.forEach {
it.inputStream().use {
val bundle = LayoutFileBundle.fromXML(it)
resourceBundle.addLayoutBundle(bundle, true)
}
}
//添加日志信息
resourceBundle.addDependencyLayouts(input.existingBindingClasses)
//重点,进行有效性判断,同时在这里也判断了生成类中的属性是view还是Viewbinding
resourceBundle.validateAndRegisterErrors()
}
resourceBundle.validateAndRegisterErrors() 的相关逻辑这里不再列出,感兴趣自己具体看下,这里列举出最后的关键代码
可以看到,这里指定了具体的viewBinder还是view
if (target.isBinder()) {
List<LayoutFileBundle> boundTo =
mLayoutBundles.get(target.getIncludedLayout());
String targetBinding = null;
String targetBindingPackage = null;
if (boundTo != null && !boundTo.isEmpty()) {
targetBinding = boundTo.get(0).getFullBindingClass();
targetBindingPackage = boundTo.get(0).getModulePackage();
} else {
IncludedLayout included = mDependencyBinders.getOrDefault(
target.getIncludedLayout(), null);
if (included != null) {
targetBinding = included.interfaceQName;
targetBindingPackage = included.modulePackage;
}
}
if (targetBinding == null) {
L.d("There is no binding for %s, reverting to plain layout",
target.getIncludedLayout());
if (target.getId() == null) {
unboundIncludes.add(target);
} else {
target.setInterfaceType("android.view.View");
target.mViewName = "android.view.View";
}
} else {
target.setInterfaceType(targetBinding, targetBindingPackage);
}
}
}
编写gradle 插件修复该问题
从上面分析的源码就可以看出,我们只要在执行dataBindingGenBaseClassesDebug任务之前,在初始化resourceBundle的固定目录下插入我们需要生成布局对应的LayoutFileBundle 的xml文件,就可以继续生成
正好生成类是公有方法
public final class LayoutFileParser {
...
@Nullable
public static ResourceBundle.LayoutFileBundle parseXml(@NonNull final RelativizableFile input,
@NonNull final File outputFile, @NonNull final String pkg,
@NonNull final LayoutXmlProcessor.OriginalFileLookup originalFileLookup,
boolean isViewBindingEnabled, boolean isDataBindingEnabled)
...
}
我们直接调用
//inputfile 对应aar中的layout布局
//dirFile
//outFile
//com.ligh.customplugin 包名
val bulder = LayoutFileParser.parseXml(RelativizableFile.fromAbsoluteFile(input,dirFile),outFile,"com.ligh.customplugin",CustomLoomUp(),true,false)
//转化为string
val content = bulder.toXML()
//..按照固定目录自定义插入
运行自定义的插件,发现我们已经生成了 对应aar布局中的viewbinding文件,但是存在编译不通过,因为生成类中的R文件时我们指定包名中的R文件
但是并没有提供一个hook点来实现自定义R文件包名,此时我们需要自定义实现类转化viewBinder来指定R文件为aar中的R文件
即重写BaseLayoutModel.toViewBinder
fun BaseLayoutModel.toViewBinder(rPackage:String): ViewBinder {
val rClassName = ClassName.get(rPackage, "R")
fun ResourceBundle.BindingTargetBundle.toBinding(): ViewBinding {
val idReference = id.parseXmlResourceReference().toResourceReference(rClassName, getRPackage)
val (present, absent) = layoutConfigurationMembership(this)
return ViewBinding(
name = fieldName(this),
type = parseLayoutClassName(fieldType, baseFileName),
form = if (isBinder) ViewBinding.Form.Binder else ViewBinding.Form.View,
id = idReference,
presentConfigurations = present,
absentConfigurations = absent
)
}
validateExplicitViewBindingTypes()
val bindings = sortedTargets
.filter { it.id != null }
.filter { it.viewName != "merge" } // <merge> can have ID but it's ignored at runtime.
.map { it.toBinding() }
val rootNode = parseRootNode(rClassName, bindings)
return ViewBinder(
generatedTypeName = ClassName.get(bindingClassPackage, bindingClassName),
layoutReference = ResourceReference(rClassName, "layout", baseFileName),
bindings = bindings,
rootNode = rootNode
)
}
自此,大功告成。aar的layout文件不再受viewBinding的约束
即使时include aar中的layout,也会生成其对应的布局
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final ConstraintLayout rootView;
@NonNull
public final LocalTestBinding local;
@NonNull
public final ConstraintLayout main;
//其类型为具体的viewBinding
@NonNull
public final TestBinding test;