ViewBinding makes it easier to write code that interacts with layout files. After view binding is enabled in a module, AGP generates a binding class for each XML layout file in that module. The controls with resource ids declared in the layout are directly referenced in instances of the binding class. This reduces the number of findViewById operations and also protects the security of the control.
The core point of the article:
- VB integration and common use methods, including: Activity, Fragment, Adapter, include, Merge, ViewStub
- KT property proxy with generic implement type parameters
reified
The introduction of - The KT property broker simplifies the VB creation process and encapsulates a library, VBHelper
- LayoutInflater principle and parameter analysis
- The binding process of the XXXBinding class
- The generation of the XXXBinding class
VBHelper: I write this article extraction of a library, through the property proxy simplifies the use of VB, there are want to know can be put forward comments
- Create the ViewBinding binding class in the Activity
// Bind the inflate method of your class via a custom property proxy + reflection
private val binding: ActivityMainBinding by vb()
// Pass the inflate method reference via a custom property proxy +
private val binding: ActivityMainBinding by vb(ActivityMainBinding::inflate)
Copy the code
- Create the ViewBinding binding class in the Fragment
// Bind the inflate method of your class via a custom property proxy + reflection
private val binding: FragmentMainBinding by vb()
// Pass the inflate method reference via a custom property proxy +
private val binding: FragmentMainBinding by vb(FragmentMainBinding::inflate)
Copy the code
- Create the ViewBinding binding class in the View
// Bind the class's three-parameter inflate method via a custom property proxy + reflection
private val binding: MyViewBinding by vb()
// Pass the inflate three-argument method reference through a custom property proxy
private val binding: MyViewBinding by vb(MyViewBinding::inflate)
Copy the code
- Create a BindingViewHolder in the Adapter that contains the binding class
// Bind the class's three-parameter inflate method via a custom property proxy + reflection
val holder: BindingViewHolder<LayoutItemTextBinding> by vh(parent)
// Pass a reference to the three-argument inflate method of the bind class via a custom property proxy +
val holder: BindingViewHolder<LayoutItemTextBinding> by vh(parent, LayoutItemTextBinding::inflate)
Copy the code
1. Summary of VB
-
View binding is available in Android Studio 3.6 Canary 11 and later.
-
Enable automatic generation of binding classes: module build.gradle file in the Android closure, two ways
viewBinding {enabled = true}
The default value is false and is available in Android Studio 3.6 Canary 11 and later.buildFeatures {viewBinding = true}
The default value is false and is available in Android Studio 4.0 and later
-
Ignore automatically generated binding classes: Add tools:viewBindingIgnore=”true” to the root view of the appropriate layout file
-
Generate the name of the Binding class: Convert the name of the XML file to camel case and add the word “Binding” to the end. LayoutInflater.Factory
- result_profile.xml ====>ResultProfileBinding
- Each binding class also contains one
getRoot()
Method to provide a direct reference to the root view of the corresponding layout file.
-
Compare that to using findViewById
- Null-safe: The bound class is created by parsing the layout file at compile time. The reference is generated only for the control whose ID is added to the layout file. Therefore, no control exists in the bound class but does not exist in the layout.
- Type-safe: Controls declared in a layout are type-deterministic. This means that there is no risk of class conversion exceptions.
-
Compare with using DataBinding
-
Both view binding and data binding generate binding classes that can be used to directly reference the view. However, view bindings are designed to handle simpler use cases.
-
Faster compile times: View bindings do not need to process annotation information, resulting in shorter compile times.
-
Easy to use: View binding does not need to be marked in an XML layout file, as long as it is enabled in a module, it is automatically applied to all the layouts of that module.
-
If data binding is used in your project, it is best to use both view binding and data binding in your project. This allows you to use data binding in layouts that require advanced functionality and view binding in layouts that do not. If you’re just replacing the findViewById() call, consider view binding instead.
-
2. VB is generally used
2.1 the Activity
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
}
Copy the code
2.2 fragments
private var _binding: FragmentFirstBinding? = null
// This property is only valid between onCreateView and onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View {
firstViewModel = ViewModelProvider(this).get(FirstViewModel::class.java)
_binding = FragmentFirstBinding.inflate(inflater, container, false)
binding.rvList.layoutManager = LinearLayoutManager(requireContext())
return binding.root
}
Copy the code
2.3 Adapter
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextHolder {
val itemBinding = LayoutItemTextBinding.inflate(LayoutInflater.from(parent.context), parent, false)
// Give the binding class to the Holder
return TextHolder(itemBinding)
}
override fun onBindViewHolder(holder: TextHolder, position: Int) {
val item: String = list[position]
// Pass the data to Holder
holder.bind(item)
}
class TextHolder(val itemBinding: LayoutItemTextBinding) : RecyclerView.ViewHolder(itemBinding.root) {
fun bind(name: String) {
itemBinding.tvName.text = name
}
}
Copy the code
2.4 include
binding.includeLayout.tvInfoInclude.text = "tvInfoInclude:$item"
// The todo include method can sometimes not recognize the actual bound class type, but only recognize it as a View type but compile without error. In this case, it may be good to clear the cache, or you can force a cast or bind yourself
val tvInfoInclude: LayoutInfoBinding = binding.includeLayout as LayoutInfoBinding
val tvInfoInclude = LayoutInfoBinding.bind(binding.root)
tvInfoInclude.tvInfoInclude.text = "tvInfoInclude:$item"
Copy the code
2.5 the merge
//include+merge you can only call the bind method of the bind class manually
val layoutInfoMergeBinding = LayoutInfoMergeBinding.bind(binding.root)
val tvInfoMerge = layoutInfoMergeBinding.tvInfoMerge
tvInfoMerge.text = "tvInfoMerge:$item"
Copy the code
2.6 ViewStub
//ViewStub can only call the bind method of the binding class manually
binding.layoutViewStub.setOnInflateListener { _, inflateId ->
val layoutInfoViewStubBinding = LayoutInfoViewStubBinding.bind(inflateId)
val tvInfoViewStub = layoutInfoViewStubBinding.tvInfoViewStub
tvInfoViewStub.text = "tvInfoViewStub:$item"
}
binding.layoutViewStub.inflate()
Copy the code
Detailed test code reference: making | VBHelper
3. VB and Kotlin by
Using the Kotlin property broker simplifies the tripartite library used by VB
- ViewBindingPropertyDelegate
- ViewBindingKTX
- VBHelper: This is a library that I extracted for this article, borrowing from the above two implementations and simplifying some of the code
3.1 KT Attribute Proxy:by
lazy
-
The by keyword is essentially a symbol for overloading the property proxy operator. Any class that has property proxy rules can use the BY keyword to proxy properties.
-
The by keyword is followed by a proxy object, which does not have to implement a specific interface, but requires the signature of the following two methods (val only requires getValue) to be used as a proxy property.
-
// This is the implementation of the extension, lazy is used this way operator fun MyDelegate.getValue(thisRef: Any? , property:KProperty< * >): String = this.value class MyDelegate { var value: String = "YYY" // The todo proxy class must provide a getValue method, or extend it operator fun getValue(thisRef: Any, property: KProperty< * >): String { return value } operator fun setValue(thisRef: Any, property: KProperty<*>, s: String) { value = s } } Copy the code
-
Lazy is a Kotlin internal best practice for property proxies. Lazy returns a proxy class that implements the lazy interface, which is SynchronizedLazyImpl by default.
-
Lazy has an extension method that conforms to the property broker rules
-
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any? , property:KProperty< * >): T = value Copy the code
3.2 KT inline functioninline
Implements type parameters with genericsreified
The official documentation
reified-type-parameters
Kotlin and Java also have problems with generic type erasure, but Kotlin uses inline inline functions to preserve the type arguments of a generic class at runtime. Kotlin calls this an implementation and uses the reified keyword.
-
Satisfy the necessary conditions to implement type parameter functions
- Must be inline functions, decorated with inline keywords
- A generic class must use the reified keyword when defining a generic parameter
-
Basic definition of a function with an argument of the implemented type
// The type parameter T is the implementation type parameter of the generic function isInstanceOf inline fun <reified T> isInstanceOf(value: Any): Boolean = value is T Copy the code
3.3 Referenced through the lazy attribute proxy + inflate method
// referenced through the lazy attribute + inflate method
fun <VB : ViewBinding> ComponentActivity.binding1(inflate: (LayoutInflater) - >VB) =
lazy {
inflate(layoutInflater).also {
setContentView(it.root)
}
}
Copy the code
3.4 Proxy + reflection via the lazy attribute
// Proxy + reflection via the lazy attribute
//reified the type parameter, which is used to replace a generic type with a real type for reflection, etc
inline fun <reified VB : ViewBinding> ComponentActivity.binding3(a) =
lazy {
// VB knows exactly what type it is after inlining, so it can reflect the specific ViewBinding
val viewBinding: VB = VB::class.java.getMethod("inflate", LayoutInflater::class.java)
.invoke(null, layoutInflater) as VB
viewBinding.also {
setContentView(it.root)
}
}
Copy the code
3.5 Referenced through the custom property proxy + inflate method
// referenced through the custom property proxy + inflate method
fun <VB : ViewBinding> ComponentActivity.binding2(inflate: (LayoutInflater) - >VB) =
ReadOnlyProperty<ComponentActivity, VB> { thisRef, property ->
inflate(layoutInflater).also {
setContentView(it.root)
}
}
Copy the code
3.6 Proxy + Reflection through custom Attributes
// Proxy + reflection via custom properties
//reified the type parameter, which is used to replace a generic type with a real type for reflection, etc
inline fun <reified VB : ViewBinding> ComponentActivity.binding4(a) =
ReadOnlyProperty<ComponentActivity, VB> { thisRef, property ->
// VB knows exactly what type it is after inlining, so it can reflect the specific ViewBinding
val viewBinding: VB = VB::class.java.getMethod("inflate", LayoutInflater::class.java)
.invoke(null, layoutInflater) as VB
viewBinding.also {
setContentView(it.root)
}
}
Copy the code
Four ways to use
// referenced through the lazy attribute + inflate method
private val binding1 by binding1(ActivityMainBinding::inflate)
// referenced through the custom property proxy + inflate method
private val binding2 by binding2(ActivityMainBinding::inflate)
// Proxy + reflection via the lazy attribute
private val binding3: ActivityMainBinding by binding3()
// Proxy + reflection via custom properties
private val binding4: ActivityMainBinding by binding4()
Copy the code
Other fragments, and the View, Adapter and other binding class generation can be adjusted according to the above way, can also be reference: making | VBHelper
Points to note:
- The reflection is done by binding the inflate method of the class, or by reflecting the bind method, which can be adjusted flexibly depending on the input parameter.
- The merge tag is the root view of the bound class whose synchronized method has only one and two parameters. Other cases where the synchronized method is generated with one and three parameters need to be compatible with reflection. VBHelper does not have compatible methods, which can be handled separately by try-cache.
@NonNull
public static LayoutInfoMergeBinding inflate(@NonNull LayoutInflater inflater,
@NonNull ViewGroup parent) {
if (parent == null) {
throw new NullPointerException("parent");
}
inflater.inflate(R.layout.layout_info_merge, parent);
return bind(parent);
}
Copy the code
4. VB principle analysis
4.1 LayoutInflater principle and parameter analysis
Reference: reflection | Android LayoutInflater mechanism design and implementation
There are three ways to obtain LayoutInflaters
/ / get LayoutInflater
// The static method from which LayoutInflater is inflater
val layoutInflater1: LayoutInflater = LayoutInflater.from(this)
//2. Obtain the value from system service getSystemService
val layoutInflater2: LayoutInflater =
getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
// If you are in an Activity or Fragment, you can get the instance directly
val layoutInflater3: LayoutInflater = layoutInflater //相当于调用 getLayoutInflater()
// These three methods are singletons within the scope of the Activity
Log.d("Jay"."layoutInflater1:${layoutInflater1.hashCode()}")
Log.d("Jay"."layoutInflater2:${layoutInflater2.hashCode()}")
Log.d("Jay"."layoutInflater3:${layoutInflater3.hashCode()}")
//2021-09-06 23:41:52.925 6353-6353/com.jay. Vbhelper D/Jay: layoutInflater1:31503528
//2021-09-06 23:41:52.925 6353-6353/com.jay. Vbhelper D/Jay: layoutInflater2:31503528
//2021-09-06 23:41:52.925 6353-6353/com.jay. Vbhelper D/Jay: layoutInflater3:31503528
Copy the code
Either way, it ends up in the ContextThemeWrapper class getSystemService
PhoneLayoutInflater Creation process
The three ways to get LayoutInflater eventually call ContextThemeWrapper#getSystemService
//class ContextThemeWrapper extends ContextWrapper
@Override
public Object getSystemService(String name) {
if (LAYOUT_INFLATER_SERVICE.equals(name)) {
if (mInflater == null) {
mInflater = LayoutInflater.from(getBaseContext()).cloneInContext(this);
}
return mInflater;
}
return getBaseContext().getSystemService(name);
}
Copy the code
CloneInContext is a method of the LayoutInflater interface. The only class that implements LayoutInflater is PhoneLayoutInflater
//class PhoneLayoutInflater extends LayoutInflater
public LayoutInflater cloneInContext(Context newContext) {
return new PhoneLayoutInflater(this, newContext);
}
Copy the code
Layout filling process
The method signature
1.public View inflate(XmlPullParser parser, @Nullable ViewGroup root)
2.public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
3.public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
4.public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
Copy the code
The overloaded methods of the four constructors all end up calling the fourth one, and here’s how they work
// Call the four method overloads of layoutinflater.inflate
// If root is passed in as null, the root View object generated by the Xml layout will be returned directly
val view1_1 = layoutInflater3.inflate(R.layout.layout_view, null)
/ / this way the layout of the loading does not need again addView (), otherwise: under Caused by: Java. Lang. An IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
// If the root passed in is not null and attachToRoot is true, the root View generated by the Xml layout is added to root using the addView method with layout parameters
// Same as view2_1 if root is not empty
val view1_2 = layoutInflater3.inflate(R.layout.layout_view, binding.clContainer)
// The first argument represents the layout to be loaded. The second argument is ViewGroup, which needs to be used in conjunction with the third argument. AttachToRoot if true adds the layout to the ViewGroup; If false, only the LayoutParams of the ViewGroup are used as the measurement basis but not directly added to the ViewGroup.
val view2_1 = layoutInflater3.inflate(R.layout.layout_view, binding.clContainer, true)
// If root is not null and attachToRoot is false, the layout parameters are set to the root View generated by the Xml layout
val view2_2 = layoutInflater3.inflate(R.layout.layout_view, binding.clContainer, false)
val parser: XmlResourceParser = resources.getLayout(R.layout.layout_view)
// These two overloaded methods are not commonly used
val view3 = layoutInflater3.inflate(parser, binding.clContainer)
val view4 = layoutInflater3.inflate(parser, binding.clContainer, false)
binding.clContainer.addView(view1_1)
Copy the code
Both the setContentView in the Activity loads content and the DecorView loads the screen root view via LayoutInflater.
The inflate method, the detailed loading process is organized in a separate article
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
advanceToRootNode(parser);
final String name = parser.getName();
if (TAG_MERGE.equals(name)) {
if (root == null| |! attachToRoot) {throw new InflateException("<merge /> can be used only with a valid "+ "ViewGroup root and attachToRoot=true");
}
//merge root view processing separately
rInflate(parser, root, inflaterContext, attrs, false);
} else {
//Temp is the root view found in XML
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if(root ! =null) {
// Create layout parameters that match the root (if provided)
params = root.generateLayoutParams(attrs);
if(! attachToRoot) {// If we do not attach, set the layout parameters of the root layout for Temptemp.setLayoutParams(params); }}// All children in the temporary state are layout populated according to their context.
rInflateChildren(parser, temp, attrs, true);
// Add all views to root
if(root ! =null && attachToRoot) {
root.addView(temp, params);
}
// Return the top view found in XML as the root passed in.
if (root == null| |! attachToRoot) { result = temp; }}}returnresult; }}Copy the code
LayoutInflater Parameter Description
LayoutResID: Represents the layout resource ID to load,
Root: is of ViewGroup type. This parameter needs to be used in conjunction with the third parameter.
AttachToRoot: If true add the layout to root; If false, only the LayoutParams of the ViewGroup are used as the measurement basis but not directly added to the ViewGroup.
Parser: XML DOM nodes containing descriptions of layout hierarchies.
LayoutInflater.Factory interface extension
LayoutInflater uses a LayoutInflater.Factory interface that can be configured to intercept View creation during XML parsing: New controls are created to avoid heavy use of reflection, and the Factory interface is configured to intercept View creation during XML parsing
LayoutInflater summary
Getting LayoutInflater instances eventually goes to ContextThemeWrapper class getSystemService to build a local singleton of PhoneLayoutInflater instances.
LayoutInflater layout padding has four overloaded methods, all of which end up calling the same method and loading it differently depending on the parameters passed
4.2 Binding process of the ActivityMainBinding class
Inflate the process
The View class retrieves the binding class by calling the inflate or bind method of the binding class that apt automatically generates
//CustomView
val layoutInflater: LayoutInflater = LayoutInflater.from(context)
val binding = LayoutViewBinding.inflate(layoutInflater, this.true)
//SecondFragment
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup? , savedIS:Bundle?).: View {
_binding = FragmentSecondBinding.inflate(inflater, container, false)
return binding.root
}
//include+merge you can only call the bind method of the bind class manually
val layoutInfoMergeBinding = LayoutInfoMergeBinding.bind(binding.root)
Copy the code
Bind the Inflate method of the class, which populates a Layout as a View through the LayoutInflater passed in
//class FragmentSecondBinding implements ViewBinding
@NonNull
public static FragmentSecondBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.fragment_second, parent, false);
// When attachToParent is true, why don't we pass the LayoutInflater to addView instead
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
Copy the code
The bind process
Instantiate all controls from the View that the Inflate process populates (or is passed in from the outside) and build the bound classes
@NonNull
public static FragmentSecondBinding bind(@NonNull View rootView) {
// The body of this method is generated in a way that you wouldn't write. This is done to optimize the size and performance of the compiled bytecode.
int id;
missingId: {
// A normal control in the root layout
id = R.id.button_second;
Button buttonSecond = ViewBindings.findChildViewById(rootView, id);
if (buttonSecond == null) {
break missingId;
}
// Include tags in the root layout
id = R.id.include_layout;
View includeLayout = ViewBindings.findChildViewById(rootView, id);
if (includeLayout == null) {
break missingId;
}
LayoutInfoBinding binding_includeLayout = LayoutInfoBinding.bind(includeLayout);
/ / ViewStub label
id = R.id.layout_view_stub;
ViewStub layoutViewStub = ViewBindings.findChildViewById(rootView, id);
if (layoutViewStub == null) {
break missingId;
}
// Customize the View
id = R.id.name;
CustomView name = ViewBindings.findChildViewById(rootView, id);
if (name == null) {
break missingId;
}
// Include +merge does not generate the corresponding type, you must manually call the bind method of the bound class
// Build the binding class and assign all controls to the class properties
return new FragmentSecondBinding((ConstraintLayout) rootView, buttonSecond, flSecond,
binding_includeLayout, layoutViewStub, llInfo, name, textviewSecond);
}
// An NPE exception is thrown if any controls are not found in the findChildViewById procedure
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId)); }}Copy the code
Iterate over the id in the root View matching layout file and return the View instance through the findViewById method
//ViewBindings
/** Like `findViewById` but skips the view itself. */
@Nullable
public static <T extends View> T findChildViewById(View rootView, @IdRes int id) {
if(! (rootView instanceof ViewGroup)) {return null;
}
final ViewGroup rootViewGroup = (ViewGroup) rootView;
final int childCount = rootViewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
final T view = rootViewGroup.getChildAt(i).findViewById(id);
if(view ! =null) {
returnview; }}return null;
}
Copy the code
Summary of binding Process
DataBinding automatically generates bound classes for all layout files with AGP
The Inflate method of the binding class loads the root layout rootView through the LayoutInflater passed in and the automatically collected root layout ID and passes it to the Bind method to instantiate the controller
The bind method of the bound class instantiates all controls and builds the bound class from the passed root layout along with the control ID that is automatically collected
4.3 The generation process of ActivityMainBinding class
Reference: The nature of ViewBinding
DataBinding Compiler Common
Rely on the source code for easy viewing
// ToDO relies on databinding-Compiler to make it easy to see how the ViewBinding class is generated
// https://mvnrepository.com/artifact/androidx.databinding/databinding-compiler-common
implementation group: 'androidx.databinding'.name: 'databinding-compiler-common'.version: '7.0.1
// https://mvnrepository.com/artifact/com.android.tools.build/gradle
implementation group: 'com.android.tools.build'.name: 'gradle'.version: '7.0.1
Copy the code
ViewBinding is a small feature in the dataBinding library that is common to the logic used to parse layout files to generate binding classes,
Phase one: Parsing the XML layout file
LayoutXmlProcessor: Processes the layout XML, strips the binding attributes and elements, and writes the information to an annotated class file for use by the annotation processor
ProcessResources: Pretend that this method is the entry method that was called after the layout file was changed.
android.databinding.tool.LayoutXmlProcessor
public boolean processResources(ResourceInput input, boolean isViewBindingEnabled, boolean isDataBindingEnabled)
throws ParserConfigurationException, SAXException, XPathExpressionException,
IOException {
ProcessFileCallback callback = new ProcessFileCallback() {
// omit the callback code
}
// The layout file's change input source supports incremental build
if (input.isIncremental()) {
processIncrementalInputFiles(input, callback);
} else {
processAllInputFiles(input, callback);
}
return true;
}
Copy the code
ProcessIncrementalInputFiles incremental processing input (Added, Removed, Changed)
ProcessAllInputFiles processes all input
// Walk through the file
for (File firstLevel : input.getRootInputFolder().listFiles())
// Process the xxx.xml file under the layout_xx directory
if (LAYOUT_FOLDER_FILTER.accept(firstLevel, firstLevel.getName())) {
callback.processLayoutFolder(firstLevel);
//noinspection ConstantConditions
for(File xmlFile : firstLevel.listFiles(XML_FILE_FILTER)) { callback.processLayoutFile(xmlFile); }}Copy the code
ProcessFileCallback Callback after scanning files
public void processLayoutFile(File file)
throws ParserConfigurationException, SAXException, XPathExpressionException,
IOException {
// Process a single file,
processSingleFile(RelativizableFile.fromAbsoluteFile(file, null),
convertToOutFile(file), isViewBindingEnabled, isDataBindingEnabled);
}
Copy the code
processSingleFile
public boolean processSingleFile(@NonNull RelativizableFile input, @NonNull File output,
boolean isViewBindingEnabled, boolean isDataBindingEnabled)
throws ParserConfigurationException, SAXException, XPathExpressionException,
IOException {
// Parse XML file ledger layout file scan class
final ResourceBundle.LayoutFileBundle bindingLayout = LayoutFileParser
.parseXml(input, output, mResourceBundle.getAppPackage(), mOriginalFileLookup,
isViewBindingEnabled, isDataBindingEnabled);
if (bindingLayout == null
|| (bindingLayout.isBindingData() && bindingLayout.isEmpty())) {
return false;
}
// Add it to the map cache
mResourceBundle.addLayoutBundle(bindingLayout, true);
return true;
}
Copy the code
LayoutFileParser: Gets a list of XML files and creates a list of Resourcebundles that can be persisted or converted to LayoutBinder
android.databinding.tool.store public final class LayoutFileParser
ParseXml: path, code, verification, etc
ParseOriginalXml: Parses the layout file into a description class
private static ResourceBundle.LayoutFileBundle parseOriginalXml(
@NonNull final RelativizableFile originalFile, @NonNull final String pkg,
@NonNull final String encoding, boolean isViewBindingEnabled,
boolean isDataBindingEnabled)
throws IOException {}
// Layout tags determine databinding
XMLParser.ElementContext root = expr.element();
boolean isBindingData = "layout".equals(root.elmName.getText());
//dataBinding
if (isBindingData) {
if(! isDataBindingEnabled) { L.e(ErrorMessages.FOUND_LAYOUT_BUT_NOT_ENABLED);return null;
}
data = getDataNode(root);
rootView = getViewNode(original, root);
} else if (isViewBindingEnabled) {
//viewBindingIgnore Adds this property to true in the root layout to skip generating the binding class
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;
}
//dataBinding
elements are not supported as direct children of
elements
boolean isMerge = "merge".equals(rootView.elmName.getText());
if(isBindingData && isMerge && ! filter(rootView,"include").isEmpty()) {
//public static final String INCLUDE_INSIDE_MERGE = "<include> elements are not supported as direct children of <merge> elements";
L.e(ErrorMessages.INCLUDE_INSIDE_MERGE);
return null;
}
String rootViewType = getViewName(rootView);
String rootViewId = attributeMap(rootView).get("android:id");
// Build the wrapper class for the layout description
ResourceBundle.LayoutFileBundle bundle =
new ResourceBundle.LayoutFileBundle(
originalFile, xmlNoExtension, original.getParentFile().getName(), pkg,
isMerge, isBindingData, rootViewType, rootViewId);
final String newTag = original.getParentFile().getName() + '/' + xmlNoExtension;
// Data is a databinding element. ViewBinding is not parsed
parseData(original, data, bundle);
// Parse expressions, which loop through elements, parse xmL-related elements such as view ids, tags, include, fragments, and databinding @={expressions, and then cache the results
parseExpressions(newTag, rootView, isMerge, bundle);
Copy the code
Phase 2: Output the description file
LayoutXmlProcessor
WriteLayoutInfoFiles: this method of execution points can be found in the AGP, task is: com. Android. Build. Gradle. Tasks. MergeResources
MergeResources
@Override
public void doTaskAction(@NonNull InputChanges changedInputs) {... SingleFileProcessor dataBindingLayoutProcessor = maybeCreateLayoutProcessor();if(dataBindingLayoutProcessor ! =null) { dataBindingLayoutProcessor.end(); }... }Copy the code
//maybeCreateLayoutProcessor
return new SingleFileProcessor() {
private LayoutXmlProcessor getProcessor(a) {
return processor;
}
@Override
public boolean processSingleFile(
@NonNull File inputFile,
@NonNull File outputFile,
@Nullable Boolean inputFileIsFromDependency)
throws Exception {
return getProcessor()
.processSingleFile(
normalizedInputFile,
outputFile,
getViewBindingEnabled().get(),
getDataBindingEnabled().get());
}
@Override
public void end(a) throws JAXBException { getProcessor().writeLayoutInfoFiles(getDataBindingLayoutInfoOutFolder().get().getAsFile()); }};// The output path can be viewed from here
artifacts.setInitialProvider(taskProvider, MergeResources::getDataBindingLayoutInfoOutFolder)
.withName("out")
.on( mergeType == MERGE? DATA_BINDING_LAYOUT_INFO_TYPE_MERGE.INSTANCE
: DATA_BINDING_LAYOUT_INFO_TYPE_PACKAGE.INSTANCE);
Copy the code
writeLayoutInfoFiles
public void writeLayoutInfoFiles(File xmlOutDir, JavaFileWriter writer) throws JAXBException {
// Iterate over all the layoutFileBundles collected previously and write to the xmlOutDir path
for(ResourceBundle.LayoutFileBundle layout : mResourceBundle .getAllLayoutFileBundlesInSource()) { writeXmlFile(writer, xmlOutDir, layout); }}Copy the code
writeXmlFile
private void writeXmlFile(JavaFileWriter writer, File xmlOutDir,
ResourceBundle.LayoutFileBundle layout)
throws JAXBException {
String filename = generateExportFileName(layout);// fileName + '-' + dirName + ".xml";
// Iterate over all the layoutFileBundles collected previously and write to the xmlOutDir path
writer.writeToFile(new File(xmlOutDir, filename), layout.toXML());
}
Copy the code
Description file generated path is: app/build/intermediates/data_binding_layout_info_type_merge/debug/out
//fragment_second-layout.xml
<Layout directory="layout" filePath="app/src/main/res/layout/fragment_second.xml"
isBindingData="false" isMerge="false" layout="fragment_second"
modulePackage="com.jay.vbhelper" rootNodeType="androidx.constraintlayout.widget.ConstraintLayout">
<Targets>
<Target tag="layout/fragment_second_0"
view="androidx.constraintlayout.widget.ConstraintLayout">
<Expressions />
<location endLine="78" endOffset="51" startLine="1" startOffset="0" />
</Target>
<Target id="@+id/ll_info" tag="binding_1"
view="androidx.appcompat.widget.LinearLayoutCompat">
<Expressions />
<location endLine="51" endOffset="50" startLine="9" startOffset="4" />
</Target>
<Target id="@+id/include_layout" include="layout_info" tag="binding_1">
<Expressions />
<location endLine="31" endOffset="42" startLine="29" startOffset="8" />
</Target>
<Target include="layout_info_merge" tag="binding_1">
<Expressions />
<location endLine="35" endOffset="53" startLine="35" startOffset="8" />
</Target>
</Targets>
</Layout>
Copy the code
Phase 3: Output the binding class
AGP Task DataBindingGenBaseClassesTask trigger
com.android.build.gradle.internal.tasks.databinding.DataBindingGenBaseClassesTask
DataBindingGenBaseClassesTask
@TaskAction
fun writeBaseClasses(inputs: IncrementalTaskInputs) {
// 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(inputs)
CodeGenerator(
args,
sourceOutFolder.get().asFile,
Logger.getLogger(DataBindingGenBaseClassesTask::class.java),
encodeErrors,
collectResources()).run()// Trigger the build process}}// Bind the class generator
class CodeGenerator @Inject constructor(
val args: LayoutInfoInput.Args,
private val sourceOutFolder: File,
private val logger: Logger,
private val encodeErrors: Boolean.private val symbolTables: List<SymbolTable>? = null
) : Runnable, Serializable {
override fun run(a) {
try {
initLogger()
BaseDataBinder(LayoutInfoInput(args), if(symbolTables ! =null) this::getRPackage else null)
// Generate logic
.generateAll(DataBindingBuilder.GradleFileWriter(sourceOutFolder.absolutePath))
} finally {
clearLogger()
}
}
...
}
//sourceOutFolder Path information
creationConfig.artifacts.setInitialProvider(
taskProvider,
DataBindingGenBaseClassesTask::sourceOutFolder
).withName("out").on(InternalArtifactType.DATA_BINDING_BASE_CLASS_SOURCE_OUT)
Copy the code
BaseDataBinder
@Suppress("unused")// used by tools
class BaseDataBinder(val input : LayoutInfoInput.val getRPackage: ((String.String) - > (String)))?{
private val resourceBundle : ResourceBundle = ResourceBundle(
input.packageName, input.args.useAndroidX)
//
init {
input.filesToConsider .forEach {
it.inputStream().use {
// Convert the collected layout XML to the LayoutFileBundle
val bundle = LayoutFileBundle.fromXML(it)
resourceBundle.addLayoutBundle(bundle, true)
}
}
resourceBundle.addDependencyLayouts(input.existingBindingClasses)
resourceBundle.validateAndRegisterErrors()
}
@Suppress("unused")// used by android gradle plugin
fun generateAll(writer : JavaFileWriter) {
// Get all the LayoutFileBundles and sort them by filename
val layoutBindings = resourceBundle.allLayoutFileBundlesInSource
.groupBy(LayoutFileBundle::getFileName).toSortedMap()
layoutBindings.forEach { layoutName, variations ->
// Wrap the LayoutFileBundle information as BaseLayoutModel
val layoutModel = BaseLayoutModel(variations, getRPackage)
val javaFile: JavaFile
val classInfo: GenClassInfoLog.GenClass
if (variations.first().isBindingData) {
val binderWriter = BaseLayoutBinderWriter(layoutModel, libTypes)
javaFile = binderWriter.write()
classInfo = binderWriter.generateClassInfo()
} else {
// Instead of DataBinding, use ViewBinding
//toViewBinder is an extension of BaseLayoutModel that returns LayoutFileBundle as ViewBinder
val viewBinder = layoutModel.toViewBinder()
//toJavaFile is an extension of ViewBinder that generates Java files from JavapoetjavaFile = 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()
}
}
Copy the code
Generate the bound classes through Javapoet
fun ViewBinder.toJavaFile(useLegacyAnnotations: Boolean = false) =
JavaFileGenerator(this, useLegacyAnnotations).create()
fun create(a) = javaFile(binder.generatedTypeName.packageName(), typeSpec()) {
addFileComment("Generated by view binder compiler. Do not edit!")}private fun typeSpec(a) = classSpec(binder.generatedTypeName) {
addModifiers(PUBLIC, FINAL)
addSuperinterface(ClassName.get(viewBindingPackage, "ViewBinding"))
// TODO elide the separate root field if the root tag has an ID (and isn't a binder)
addField(rootViewField())
addFields(bindingFields())
addMethod(constructor())
addMethod(rootViewGetter())
// Merge is the generated two-parameter infate parameter
if (binder.rootNode is RootNode.Merge) {
addMethod(mergeInflate())
} else {
// In all other cases, the inflate method generates both one and three parameters
addMethod(oneParamInflate())
addMethod(threeParamInflate())
}
addMethod(bind())
}
Copy the code
Summary of generation process
Real-time update generation: AS or AGP or immediately update binding classes after layout file changes (new additions/updates/deletes), this process has not found the corresponding source code
Compile update generation: AGP different tasks trigger
- Parses the XML layout file: LayoutXmlProcessor#processResources should be used to change the input port of the layout file, the corresponding Task is not found, the collection process supports incremental updates. Process the xxx. XML file under the layout_xx directory. Parsing the XML file distinguishes between DataBinding and ViewBinding. The final product is a ResourceBundle LayoutFileBundle and HashMap < String, List > mLayoutBundles
- Output description file: Triggered by MergeResourcesTask in AGP, all layoutFileBundles collected before are traversed and written to the xmlOutDir path. This XML file describes the layout file path, package name, layout name, control ID, control line number, and other information
- Output binding classes: AGP DataBindingGenBaseClassesTask trigger, described in a process to generate the layout of the XML file and then parsed into LayoutFileBundle class information, then pack the information again, finally through Javapoet generated binding classes
TODO
-
The layout file update triggers the scan and processing of the layout file, which is where the processResources method is called
- Both AGP and AS are suspected to be involved
-
Why clicking ActivityMainBinding jumps to the corresponding layout file
- This guess should be related to compilation, generating lexical and parser code
-
Why is it that a new layout file is added and the binding class is not compiled, but there is no binding class in the data_binding_base_class_source_out path that is only seen by compilation
-
There should be some AS AS well
-
Generate Lexer and Parser Code Generate Lexer and Parser code
-