在 Kotlin 1.4.20-M2 中,JetBrains 废弃了 Kotlin Android Extensions 编译插件。
不要与Data Binding混淆。View Binding是一种功能,它允许您更容易地编写与视图交互的代码。
一旦在一个模块中启用了View Binding,它就会为该模块中存在的每个 XML 布局文件生成一个绑定类。
- 绑定类的实例包含对相应布局中具有ID的所有VIew的直接引用。
- View Binding对于在多个配置中定义的布局来说是Null-safe的。
- View Binding将检测视图是否只存在于某些配置中,并创建一个@Nullable属性。
- View Binding适用于Java和Kotlin。
禁用某个布局文件使用 ViewBinding,则需要在布局文件的根节点中添加
tools:viewBindingIgnore = "true"
。
环境配置
- Android Studio 3.6 及以上;
- Android Gradle 插件 3.6.0 及以上;
- Gradle 版本 5.6.4 及以上;
- 在模块的 build.gradle 文件中添加
android { buildFeatures { viewBinding = true } }
,android { viewBinding.enabled = true }
方式已废弃。
DSL element 'android.viewBinding.enabled' is obsolete and has been replaced with 'android.buildFeatures.viewBinding'.
GradlePlugin 和 Gradle 要对应,否则会不兼容,出现同步失败的问题。查看 Android Gradle 插件版本说明。
使用
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.textView.text = "text"
}
}
Activity 对应的 activity_main.xml 文件如下:
生成的 ActivityMainBinding 在 module/build/generated/data_binding_base_class_source_out/${buildTypes}/out/${包名}/databinding
目录,源码如下:
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final LinearLayout rootView;
@NonNull
public final TextView textView;
private ActivityMainBinding(@NonNull LinearLayout rootView, @NonNull TextView textView) {
this.rootView = rootView;
this.textView = textView;
}
@Override
@NonNull
public LinearLayout getRoot() {
return rootView;
}
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent,
boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_main, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
@NonNull
public static ActivityMainBinding bind(@NonNull View rootView) {
// The body of this method is generated in a way you would not otherwise write.
// This is done to optimize the compiled bytecode for size and performance.
int id;
missingId: {
id = R.id.textView;
TextView textView = rootView.findViewById(id);
if (textView == null) {
break missingId;
}
return new ActivityMainBinding((LinearLayout) rootView, textView);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
Binding 类生成原理
由于 ViewBinding 类是通过 GradlePlugin 支持的,编译时包含有 DataBinding 相关的 task Task :app:dataBindingGenBaseClassesDebug
。
com.android.tools.build:gradle:4.1.3
依赖 androidx.databinding:databinding-compiler-common:4.1.3
,下载 databinding-compiler-common
jar 包,解压到文件夹后使用 AS 打开查看源码。
解析 xml 文件
/**
* Processes the layout XML, stripping the binding attributes and elements
* and writes the information into an annotated class file for the annotation
* processor to work with.
*/
public class LayoutXmlProcessor {
public boolean processResources(ResourceInput input, boolean isViewBindingEnabled,
boolean isDataBindingEnabled) {
ProcessFileCallback callback = new ProcessFileCallback() {
@Override
public void processLayoutFile(File file) {
processSingleFile(...);
}
// 其他回调方法
};
if (input.isIncremental()) {
processIncrementalInputFiles(input, callback);
} else {
processAllInputFiles(input, callback);
}
}
private static void processAllInputFiles(ResourceInput input, ProcessFileCallback callback) {
for (File firstLevel : input.getRootInputFolder().listFiles()) {
if (firstLevel.isDirectory()) {
// 判断文件名是否已 layout 开头
if (LAYOUT_FOLDER_FILTER.accept(firstLevel, firstLevel.getName())) {
callback.processLayoutFolder(firstLevel);
// 过滤以 “.xml” 结尾的文件
for (File xmlFile : firstLevel.listFiles(XML_FILE_FILTER)) {
callback.processLayoutFile(xmlFile);
}
}
}
}
}
public boolean processSingleFile(RelativizableFile input, File output,
boolean isViewBindingEnabled,
boolean isDataBindingEnabled) {
// 解析 xml
final ResourceBundle.LayoutFileBundle bindingLayout = LayoutFileParser.parseXml(input, output, ...);
// 缓存解析的 xml
mResourceBundle.addLayoutBundle(bindingLayout, true);
}
}
public final class LayoutFileParser {
public static ResourceBundle.LayoutFileBundle parseXml(final RelativizableFile input,
final File outputFile, ...) {
return parseOriginalXml(
RelativizableFile.fromAbsoluteFile(originalFile, input.getBaseDir()),...);
}
private static ResourceBundle.LayoutFileBundle parseOriginalXml(
final RelativizableFile originalFile, ...) {
File original = originalFile.getAbsoluteFile();
// 是否是 DataBinding
if (isBindingData) {
data = getDataNode(root);
rootView = getViewNode(original, root);
} else if (isViewBindingEnabled) {
// 排除不生成 Binding 类的 xml
if ("true".equalsIgnoreCase(attributeMap(root).get("tools:viewBindingIgnore"))) {
L.d("Ignoring %s for view binding", originalFile);
return null;
}
data = null;
rootView = root;
} else {
return null;
}
// 判断是否是 merge 节点
boolean isMerge = "merge".equals(rootView.elmName.getText());
if (isBindingData && isMerge && !filter(rootView, "include").isEmpty()) {
L.e(ErrorMessages.INCLUDE_INSIDE_MERGE);
return null;
}
String rootViewType = getViewName(rootView);
// 获取 View ID
String rootViewId = attributeMap(rootView).get("android:id");
// 创建 LayoutFileBundle 对象
ResourceBundle.LayoutFileBundle bundle =
new ResourceBundle.LayoutFileBundle(originalFile, ...);
// ViewBinding data == null,不会解析
parseData(original, data, bundle);
// 解析表达式
parseExpressions(newTag, rootView, isMerge, bundle);
return bundle;
}
private static String getViewName(XMLParser.ElementContext elm) {
String viewName = elm.elmName.getText();
if ("view".equals(viewName)) {
String classNode = attributeMap(elm).get("class");
if (Strings.isNullOrEmpty(classNode)) {
L.e("No class attribute for 'view' node");
}
return classNode;
}
if ("include".equals(viewName) && !XmlEditor.hasExpressionAttributes(elm)) {
return "android.view.View";
}
if ("fragment".equals(viewName)) {
return "android.view.View";
}
return viewName;
}
}
生成 xml 文件
public class LayoutXmlProcessor {
public void writeLayoutInfoFiles(File xmlOutDir, JavaFileWriter writer) {
for (ResourceBundle.LayoutFileBundle layout : mResourceBundle
.getAllLayoutFileBundlesInSource()) {
writeXmlFile(writer, xmlOutDir, layout);
}
}
private void writeXmlFile(JavaFileWriter writer, File xmlOutDir,
ResourceBundle.LayoutFileBundle layout) {
// layout.getFileName() + '-' + layout.getDirectory() + ".xml
// 如 activity_main.xml -> activity_main-layout.xml
String filename = generateExportFileName(layout);
writer.writeToFile(new File(xmlOutDir, filename), layout.toXML());
}
}
生成的 xml 文件路径为 module/build/intermediates/data_binding_layout_info_type_merge/${buildTypes}/out/activity_main-layout.xml
,描述了原始布局文件的相关信息。
生成 Binding 类
class BaseDataBinder(val input : LayoutInfoInput) {
fun generateAll(writer : JavaFileWriter) {
input.invalidatedClasses.forEach {
writer.deleteFile(it)
}
val useAndroidX = input.args.useAndroidX
val libTypes = LibTypes(useAndroidX = useAndroidX)
// 获取所有的 LayoutFileBundle,并根据文件名进行分组排序
val layoutBindings = resourceBundle.allLayoutFileBundlesInSource
.groupBy(LayoutFileBundle::getFileName)
// 遍历 layoutBindings
layoutBindings.forEach { layoutName, variations ->
// 将 LayoutFileBundle 信息包装成 BaseLayoutModel
val layoutModel = BaseLayoutModel(variations)
val javaFile: JavaFile
val classInfo: GenClassInfoLog.GenClass
// 处理 DataBinding
if (variations.first().isBindingData) {
//...
} else {
// 处理 ViewBinding
// 创建 ViewBinder 对象
val viewBinder = layoutModel.toViewBinder()
// 通过 ViewBinder 扩展方法,调用到 ViewBinderGenerateJava#create() 方法
javaFile = viewBinder.toJavaFile(useLegacyAnnotations = !useAndroidX)
classInfo = viewBinder.generatedClassInfo()
}
writer.writeToFile(javaFile)
}
}
}
// ViewBinderGenerateJava.kt
fun ViewBinder.toJavaFile(useLegacyAnnotations: Boolean = false) =
JavaFileGenerator(this, useLegacyAnnotations).create()
private class JavaFileGenerator(
private val binder: ViewBinder,
private val useLegacyAnnotations: Boolean) {
// 使用了 com.squareup.javapoet 库
fun create() = javaFile(binder.generatedTypeName.packageName(), typeSpec()) {
addFileComment("Generated by view binder compiler. Do not edit!")
}
private fun typeSpec() = classSpec(binder.generatedTypeName) {
addModifiers(PUBLIC, FINAL)
val viewBindingPackage = if (useLegacyAnnotations) "android" else "androidx"
addSuperinterface(ClassName.get("$viewBindingPackage.viewbinding", "ViewBinding"))
// 生成 rootView 字段
addField(rootViewField())
// 生成 View 字段
addFields(bindingFields())
// 生成构造方法
addMethod(constructor())
// 生成获取 rootView 的方法
addMethod(rootViewGetter())
if (binder.rootNode is RootNode.Merge) {
addMethod(mergeInflate())
} else {
// 生成 inflate(LayoutInflater inflater) 方法
addMethod(oneParamInflate())
// 生成 inflate(LayoutInflater inflater, ViewGroup parent, boolean attachToParent) 方法
addMethod(threeParamInflate())
}
addMethod(bind())
}
}
- 遍历收集的
Set
; - 根据
LayoutFileBundle
生成ViewBinder
对象; - ViewBinder 传参给
JavaFileGenerator
,通过JavaPoet(com.squareup.javapoet)
库生成 Binding Class 文件。
参考
[1] ViewBinding - Developer
[2] 终于有人写:「新技术ViewBinding」 的本质了
[3] [译]深入研究ViewBinding 在 include, merge, adapter, fragment, activity 中使用
[4] hoc081098/ViewBindingDelegate