preface

Control binding is an age-old topic in Android development.

You start with findViewById, and you have all kinds of Finds all over the screen;

Butterknife, which uses annotations for control binding, made the UI layer code much cleaner, even though there were lots of bloated controls with global variables.

And then kotlin came up with the Kotlin-Android-Extensions extension, which is a really nice way to get control objects in XML and use them directly with ids, and it uses bytecode petting to automatically generate something like findViewById, Here you can refer to Guo Shen’s blog:

Is the Kotlin-Android-Extensions plugin deprecated? Lifted me up
Blog.csdn.net/guolin_blog…

The specific reasons for the abandonment, I guess:

1. Incompatible with Java. While all of Google’s new technologies are java-based, such as coroutines and Compose, they are platform-independent, and control bindings are platform-specific and require consideration of the Java user base.

2. Although the kotlin-Android-extensions are very comfortable to use, there are some problems with the implementation principle of the kotlin-Android-extensions, which can reduce the efficiency of the application.

use

The simple use of ViewBinding is very simple.

Build. Gradle for our moudle:

buildFeatures {
        viewBinding true
}
Copy the code

The viewBinding configuration is moudle independent.

After the configuration, the corresponding Binding class will be generated. We can call it directly and bind it.

lateinit var binding: MainActivityBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = MainActivityBinding.inflate(layoutInflater) setContentView(binding.root) Binding.message. text = "Android development stuff "}Copy the code

In use, we can retrieve the controls defined in our XML layout directly by binding, which is very convenient.

The principle of

The generated binding file is in the build/generated/data_binding_base_class_source_out/debug/out directory

Here is the XML layout I defined:

<? The XML version = "1.0" encoding = "utf-8"? > <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/message" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="MainFragment" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>Copy the code

Here is the generated Binding class:

public final class MainActivityBinding implements ViewBinding { private final ConstraintLayout rootView; public final ConstraintLayout main; public final TextView message; private MainActivityBinding( ConstraintLayout rootView, ConstraintLayout main, TextView message) { this.rootView = rootView; this.main = main; this.message = message; } @Override @NonNull public ConstraintLayout getRoot() { return rootView; } @NonNull public static MainActivityBinding inflate(@NonNull LayoutInflater inflater) { return inflate(inflater, null, false); } @NonNull public static MainActivityBinding inflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, boolean attachToParent) { View root = inflater.inflate(R.layout.main_activity, parent, false); if (attachToParent) { parent.addView(root); } return bind(root); } @NonNull public static MainActivityBinding bind(@NonNull View rootView) { int id; missingId: { ConstraintLayout main = (ConstraintLayout) rootView; id = R.id.message; TextView message = ViewBindings.findChildViewById(rootView, id); if (message == null) { break missingId; } return new MainActivityBinding((ConstraintLayout) rootView, main, message); } String missingId = rootView.getResources().getResourceName(id); throw new NullPointerException("Missing required view with ID: ".concat(missingId)); }}Copy the code

In this case, viewBinding parses the XML layout file for me and automatically binds the control whose Id is set. Finally, we get the root layout through ViewBinding and call setContent method to add the layout.

    @Override
    public void setContentView(View v) {
        ensureSubDecor();
        ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        contentParent.addView(v);
        mAppCompatWindowCallback.getWrapped().onContentChanged();
    }
Copy the code

Whereas Actiivty used to do layout parsing and control binding and use, ViewBinding now does both.

So how are these Binding classes generated? That’s what we’re going to explore.

When I changed the layout file, I found that the Binding class file did not change in real time. The binding class file was changed only after compilation. Therefore, THE binding class file should be generated by APG when compiling the project.

Let’s run it and look at Task:

image-20220405142934287

Only three dataBinding tasks were captured in the compile process. We are not sure which task generated the binding class.

So let’s do it one by one. Let’s try it one by one.

In the Gradle taskbar to the right of AS, you find the Task for Databinding

image-20220405143309503

We’ll perform dataBindingMergeDependencyArtifactsDebug respectively, dataBindingMergeGenClassesDebug and dataBindingGenBaseClassesDebug.

After executing the first two tasks, only two empty folders are generated, and no content is generated:

image-20220405143551157

After performing the dataBindingMergeGenClassesDebug, generate the binding class of what we need,

image-20220405143733504

The dataBindingMergeGenClassesDebug the focus of this task is we need to explore.

AGP source code and Databinding source code are introduced to analyze:

Implementation 'com. Android. Tools. Build: gradle: 7.0.2' implementation 'androidx. Databinding: databinding compiler - common: 7.0.2' implementation 'androidx. Databinding: databinding - common: 7.0.2' Implementation 'com. Android. Databinding: baseLibrary: 7.0.2'Copy the code

Build. Gradle in your app. Check it out in External Libraries.

We find there are two main entrance, a AGP TaskManager class, the class management Task was created, there is also another entrance, is from/com/android/build/gradle/internal/tasks path to look for, This is a shortcut that practice makes perfect.

See TaskManager above all, through the method refer to, we will find about creating DataBinding createDataBindingTasksIfNecessary as long as a method of Task,

protected fun createDataBindingTasksIfNecessary(creationConfig: ComponentCreationConfig) { val dataBindingEnabled = creationConfig.buildFeatures.dataBinding val viewBindingEnabled = creationConfig.buildFeatures.viewBinding if (! dataBindingEnabled && ! viewBindingEnabled) { return } taskFactory.register(DataBindingMergeBaseClassLogTask.CreationAction(creationConfig)) 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 (projectOptions[BooleanOption.NON_TRANSITIVE_R_CLASS] && isKotlinKaptPluginApplied(project)) { // TODO(183423660): Undo this workaround for KAPT resolving files at compile time taskFactory.register(MergeRFilesForDataBindingTask.CreationAction(creationConfig)) } taskFactory.register(DataBindingTriggerTask.CreationAction(creationConfig)) setDataBindingAnnotationProcessorParams(creationConfig) } }Copy the code

Get the configuration information first and see the status of ViewBinding and DataBinding on and off. If both are off, return. Otherwise, the Task is registered. Two of the tasks registered here are the ones we saw during the compilation process, and then when databinding is enabled, additional tasks are registered. From this we can see that viewBinding is only part of the DataBinding functionality. Viewbinding only does control binding, and DataBinding has bidirectional DataBinding in addition to basic control binding.

Next we see DataBindingGenBaseClassesTask.

This Task class path is com. Android. Build. Gradle. Internal. The tasks. The databinding, told me to mention the second entry, so after analysis AGP source code, can from this path to find the corresponding Task, this is a tricky way.

Anyone who has written a custom plugin knows that a custom Task requires the @taskAction annotation to identify the entry point of the Task.

    @TaskAction
    fun writeBaseClasses(inputs: IncrementalTaskInputs) {
          recordTaskAction(analyticsService.get()) {
            val args = buildInputArgs(inputs)
            CodeGenerator(
                args,
                sourceOutFolder.get().asFile,
                Logger.getLogger(DataBindingGenBaseClassesTask::class.java),
                encodeErrors,
                collectResources()).run()
        }
    }
Copy the code

You can see that the writeBaseClasses method is marked by the @taskAction annotation, so this is the entry point for our analysis.

Basically, you create the CodeGenerator class and then execute the Run () method.

override fun run() { try { initLogger() BaseDataBinder( LayoutInfoInput(args), if (symbolTables ! = null) this::getRPackage else null) .generateAll(DataBindingBuilder.GradleFileWriter(sourceOutFolder.absolutePath)) } finally { clearLogger() } }Copy the code

In CodeGenerator:: Run (), we see that the BaseDataBinder class is created again and the generateAll method is run.

fun generateAll(writer : JavaFileWriter) { .............. layoutBindings.forEach { layoutName, variations -> ........... 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 { check(input.args.enableViewBinding) { "View binding is not enabled but found non-data binding layouts: $variations" } val viewBinder = layoutModel.toViewBinder() javaFile = viewBinder.toJavaFile(useLegacyAnnotations = ! useAndroidX) classInfo = viewBinder.generatedClassInfo() } ................. }Copy the code

So this is a little bit of a simplification of the code, so we’re doing a little bit of a check here to see if it’s DataBinding, and obviously what we need to analyze is in else.

In the else, viewBinding is enabled, BaseLayoutModel is converted to ViewBinder, and ViewBinder is executed

The extension method toJavaFile, the name of which is obvious, is to convert toJava files.

fun ViewBinder.toJavaFile(useLegacyAnnotations: Boolean = false) =
    JavaFileGenerator(this, useLegacyAnnotations).create()
Copy the code

Here you create the JavaFileGenerator class and execute the create() method.

fun create() = javaFile(binder.generatedTypeName.packageName(), typeSpec()) {
    addFileComment("Generated by view binder compiler. Do not edit!")
}
Copy the code

Here’s how to create a binding class. Let’s look at the typeSpec() method:

Private fun typeSpec() = classSpec(binder. GeneratedTypeName) {add public final addModifiers(public, AddSuperinterface (classname. get(viewBindingPackage, "ViewBinding")) add rootView variable addField(rootViewField()) add control variable addFields(bindingFields()) create a no-argument constructor AddMethod (rootViewGetter()) if (binder. RootNode is rootNode.merge) {addMethod(constructor()) getMethod (rootViewGetter()) AddMethod (mergeInflate())} else {create the inflate method of a single parameter. AddMethod (oneParamInflate()) creates the inflate method of three parameters AddMethod (threeParamInflate())} add bind method addMethod(bind())}Copy the code

Those of you who have used Javapoet can see that this is using Javapoet to create Java files.

After comparing the creation process with the ViewBinding class file we generated, you can see a perfect match.

So this is where our ViewBinding class file is generated via Javapoet.

conclusion

Let’s conclude by saying:

We through the observation of the compilation process, it is concluded that dataBindingGenBaseClassesDebug is to generate the binding class task, and then find the corresponding through TaskManager

DataBindingGenBaseClassesTask, through @ TaskAction annotations found the entrance of task execution, the last call to DataBinding BaseDataBinder class, in the process, ViewBinder calls JavaFileGenerator’s Create () method, where the Viewbinding class we used is generated by Javapoet.

The overall call process:

TaskManager

->writeBaseClasses

->CodeGenerator :: run()

->BaseDataBinder::generateAll()

->ViewBinder::toJavaFile()

->JavaFileGenerator:: create()

->typeSpec()

->javapoet
Copy the code

Write in the last

The overall process is not calculated complex, we read after the best or their own to follow the source code, this personally with a time, their understanding is thorough.

Only by combining theory with practice can we truly learn.