在 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 插件版本说明。
使用 1 2 3 4 5 6 7 8 9 10 11 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 文件如下:
1 2 3 4 5 6 7 8 9 10 11 <LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:layout_width ="match_parent" android:layout_height ="match_parent" android:orientation ="vertical" > <TextView android:id ="@+id/textView" android:layout_width ="wrap_content" android:layout_height ="wrap_content" /> </LinearLayout >
生成的 ActivityMainBinding 在 module/build/generated/data_binding_base_class_source_out/${buildTypes}/out/${包名}/databinding
目录,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 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) { 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 文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 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()) { if (LAYOUT_FOLDER_FILTER.accept(firstLevel, firstLevel.getName())) { callback.processLayoutFolder(firstLevel); for (File xmlFile : firstLevel.listFiles(XML_FILE_FILTER)) { callback.processLayoutFile(xmlFile); } } } } } public boolean processSingleFile (RelativizableFile input, File output, boolean isViewBindingEnabled, boolean isDataBindingEnabled) { final ResourceBundle.LayoutFileBundle bindingLayout = LayoutFileParser.parseXml(input, output, ...); mResourceBundle.addLayoutBundle(bindingLayout, true ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 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(); if (isBindingData) { data = getDataNode(root); rootView = getViewNode(original, root); } else if (isViewBindingEnabled) { 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 ; } 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); String rootViewId = attributeMap(rootView).get("android:id" ); ResourceBundle.LayoutFileBundle bundle = new ResourceBundle .LayoutFileBundle(originalFile, ...); 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 文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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) { 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
,描述了原始布局文件的相关信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <?xml version="1.0" encoding="utf-8" standalone="yes"?> <Layout directory ="layout" filePath ="app\src\main\res\layout\activity_main.xml" isBindingData ="false" isMerge ="false" layout ="activity_main" modulePackage ="com.king.app.workhelper" rootNodeType ="android.widget.LinearLayout" > <Targets > <Target tag ="layout/activity_main_0" view ="LinearLayout" > <Expressions /> <location endLine ="11" endOffset ="14" startLine ="1" startOffset ="0" /> </Target > <Target id ="@+id/textView" view ="TextView" > <Expressions /> <location endLine ="9" endOffset ="46" startLine ="6" startOffset ="4" /> </Target > </Targets > </Layout >
生成 Binding 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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) val layoutBindings = resourceBundle.allLayoutFileBundlesInSource .groupBy(LayoutFileBundle::getFileName) layoutBindings.forEach { layoutName, variations -> val layoutModel = BaseLayoutModel(variations) val javaFile: JavaFile val classInfo: GenClassInfoLog.GenClass if (variations.first().isBindingData) { } else { val viewBinder = layoutModel.toViewBinder() javaFile = viewBinder.toJavaFile(useLegacyAnnotations = !useAndroidX) classInfo = viewBinder.generatedClassInfo() } writer.writeToFile(javaFile) } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 fun ViewBinder.toJavaFile (useLegacyAnnotations: Boolean = false ) = JavaFileGenerator(this , useLegacyAnnotations).create() private class JavaFileGenerator ( private val binder: ViewBinder, private val useLegacyAnnotations: Boolean ) { 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" )) addField(rootViewField()) addFields(bindingFields()) addMethod(constructor ()) addMethod(rootViewGetter()) if (binder.rootNode is RootNode.Merge) { addMethod(mergeInflate()) } else { addMethod(oneParamInflate()) addMethod(threeParamInflate()) } addMethod(bind()) } }
遍历收集的 Set<LayoutFileBundle>
;
根据 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