在 Kotlin 1.4.20-M2 中舍扰,JetBrains 廢棄了 Kotlin Android Extensions 編譯插件。
不要與Data Binding混淆侣诵。View Binding是一種功能痢法,它允許您更容易地編寫與視圖交互的代碼。
一旦在一個模塊中啟用了View Binding窝趣,它就會為該模塊中存在的每個 XML 布局文件生成一個綁定類疯暑。
- 綁定類的實例包含對相應布局中具有ID的所有VIew的直接引用。
- View Binding對于在多個配置中定義的布局來說是Null-safe的哑舒。
- View Binding將檢測視圖是否只存在于某些配置中妇拯,并創(chuàng)建一個@Nullable屬性。
- View Binding適用于Java和Kotlin。
禁用某個布局文件使用 ViewBinding越锈,則需要在布局文件的根節(jié)點中添加
tools:viewBindingIgnore = "true"
仗嗦。
環(huán)境配置
- 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 要對應德撬,否則會不兼容,出現(xiàn)同步失敗的問題躲胳。查看 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 文件如下:
<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
目錄,源碼如下:
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 節(jié)點
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");
// 創(chuàng)建 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
为鳄,描述了原始布局文件的相關信息裳仆。
<?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 類
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,并根據(jù)文件名進行分組排序
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
// 創(chuàng)建 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>
; - 根據(jù)
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