| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.databinding.tool.writer |
| |
| import android.databinding.tool.ext.L |
| import android.databinding.tool.ext.N |
| import android.databinding.tool.ext.S |
| import android.databinding.tool.ext.T |
| import android.databinding.tool.ext.W |
| import android.databinding.tool.ext.classSpec |
| import android.databinding.tool.ext.constructorSpec |
| import android.databinding.tool.ext.fieldSpec |
| import android.databinding.tool.ext.javaFile |
| import android.databinding.tool.ext.methodSpec |
| import android.databinding.tool.ext.parameterSpec |
| import android.databinding.tool.store.GenClassInfoLog |
| import android.databinding.tool.writer.ViewBinder.RootNode |
| import com.squareup.javapoet.ClassName |
| import com.squareup.javapoet.CodeBlock |
| import com.squareup.javapoet.NameAllocator |
| import com.squareup.javapoet.ParameterSpec |
| import com.squareup.javapoet.TypeName.BOOLEAN |
| import javax.lang.model.element.Modifier.FINAL |
| import javax.lang.model.element.Modifier.PRIVATE |
| import javax.lang.model.element.Modifier.PUBLIC |
| import javax.lang.model.element.Modifier.STATIC |
| |
| fun ViewBinder.toJavaFile(useLegacyAnnotations: Boolean = false) = |
| JavaFileGenerator(this, useLegacyAnnotations).create() |
| |
| fun ViewBinder.generatedClassInfo() = GenClassInfoLog.GenClass( |
| qName = generatedTypeName.toString(), |
| modulePackage = generatedTypeName.packageName(), |
| variables = emptyMap(), |
| implementations = emptySet() |
| ) |
| |
| private class JavaFileGenerator( |
| private val binder: ViewBinder, |
| private val useLegacyAnnotations: Boolean |
| ) { |
| private val annotationPackage = |
| if (useLegacyAnnotations) "android.support.annotation" else "androidx.annotation" |
| private val nonNull = ClassName.get(annotationPackage, "NonNull") |
| private val nullable = ClassName.get(annotationPackage, "Nullable") |
| |
| private val fieldNames = NameAllocator().apply { |
| // Since the binding names are used in public fields, allocate those first. |
| binder.bindings.forEach { binding -> |
| newName(binding.name, binding) |
| } |
| } |
| private val rootFieldName = fieldNames.newName("rootView") |
| |
| 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")) |
| |
| // 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()) |
| |
| if (binder.rootNode is RootNode.Merge) { |
| addMethod(mergeInflate()) |
| } else { |
| addMethod(oneParamInflate()) |
| addMethod(threeParamInflate()) |
| } |
| |
| addMethod(bind()) |
| } |
| |
| private fun rootViewField() = fieldSpec(rootFieldName, binder.rootNode.type) { |
| addModifiers(PRIVATE, FINAL) |
| addAnnotation(nonNull) |
| } |
| |
| private fun bindingFields() = binder.bindings.map { binding -> |
| fieldSpec(binding.name, binding.type) { |
| addModifiers(PUBLIC, FINAL) |
| |
| // TODO addJavadoc when types were normalized to View due to different declarations. |
| |
| if (binding.isRequired) { |
| addAnnotation(nonNull) |
| } else { |
| addJavadoc( |
| renderConfigurationJavadoc( |
| binding.presentConfigurations, |
| binding.absentConfigurations |
| ) |
| ) |
| addAnnotation(nullable) |
| } |
| } |
| } |
| |
| private fun constructor() = constructorSpec { |
| addModifiers(PRIVATE) |
| |
| addParameter(parameterSpec(binder.rootNode.type, rootFieldName) { |
| addAnnotation(nonNull) |
| }) |
| addStatement("this.$rootFieldName = $rootFieldName") |
| |
| binder.bindings.forEach { binding -> |
| val name = fieldNames.get(binding) |
| addParameter(parameterSpec(binding.type, name) { |
| addAnnotation(if (binding.isRequired) nonNull else nullable) |
| }) |
| addStatement("this.$1N = $1N", name) |
| } |
| } |
| |
| private fun rootViewGetter() = methodSpec("getRoot") { |
| // TODO addJavadoc about this being the parent if the root tag was <merge> ...right? |
| |
| addAnnotation(Override::class.java) |
| addAnnotation(nonNull) |
| addModifiers(PUBLIC) |
| |
| returns(binder.rootNode.type) |
| addStatement("return $rootFieldName") |
| } |
| |
| private fun oneParamInflate() = methodSpec("inflate") { |
| // TODO addJavadoc |
| |
| addModifiers(PUBLIC, STATIC) |
| addAnnotation(nonNull) |
| returns(binder.generatedTypeName) |
| |
| val inflaterParam = parameterSpec(ANDROID_LAYOUT_INFLATER, "inflater") { |
| addAnnotation(nonNull) |
| } |
| addParameter(inflaterParam) |
| |
| addStatement("return inflate($N, null, false)", inflaterParam) |
| } |
| |
| private fun threeParamInflate() = methodSpec("inflate") { |
| // TODO addJavadoc |
| |
| addModifiers(PUBLIC, STATIC) |
| addAnnotation(nonNull) |
| returns(binder.generatedTypeName) |
| |
| val inflaterParam = parameterSpec(ANDROID_LAYOUT_INFLATER, "inflater") { |
| addAnnotation(nonNull) |
| } |
| val parentParam = parameterSpec(ANDROID_VIEW_GROUP, "parent") { |
| addAnnotation(nullable) |
| } |
| val attachToParentParam = parameterSpec(BOOLEAN, "attachToParent") |
| |
| addParameter(inflaterParam) |
| addParameter(parentParam) |
| addParameter(attachToParentParam) |
| |
| addStatement("$T root = $N.inflate($L, $N, false)", |
| ANDROID_VIEW, inflaterParam, binder.layoutReference.asCode(), parentParam) |
| beginControlFlow("if ($N)", attachToParentParam) |
| addStatement("$N.addView(root)", parentParam) |
| endControlFlow() |
| addStatement("return bind(root)") |
| } |
| |
| private fun mergeInflate() = methodSpec("inflate") { |
| // TODO addJavadoc |
| |
| addModifiers(PUBLIC, STATIC) |
| addAnnotation(nonNull) |
| returns(binder.generatedTypeName) |
| |
| val inflaterParam = parameterSpec(ANDROID_LAYOUT_INFLATER, "inflater") { |
| addAnnotation(nonNull) |
| } |
| val parentParam = parameterSpec(ANDROID_VIEW_GROUP, "parent") { |
| addAnnotation(nonNull) |
| } |
| |
| addParameter(inflaterParam) |
| addParameter(parentParam) |
| |
| beginControlFlow("if ($N == null)", parentParam) |
| addStatement("throw new $T($S)", NullPointerException::class.java, parentParam.name) |
| endControlFlow() |
| |
| addStatement("$N.inflate($L, $N)", |
| inflaterParam, binder.layoutReference.asCode(), parentParam) |
| addStatement("return bind($N)", parentParam) |
| } |
| |
| private fun bind() = methodSpec("bind") { |
| // TODO addJavadoc |
| |
| addModifiers(PUBLIC, STATIC) |
| addAnnotation(nonNull) |
| returns(binder.generatedTypeName) |
| |
| // We use a dedicated name allocator here because we want the public parameter name to take |
| // precedence over any view with a matching ID which is only used as a local. |
| val localNames = NameAllocator() |
| |
| val rootParam = parameterSpec(ANDROID_VIEW, localNames.newName("rootView")) { |
| addAnnotation(nonNull) |
| } |
| addParameter(rootParam) |
| |
| val rootBinding = (binder.rootNode as? RootNode.Binding)?.binding |
| val nonRootBindings = binder.bindings.filter { it !== rootBinding } |
| |
| if (nonRootBindings.isEmpty()) { |
| // Without any bindings that invoke findViewById, an erroneously-null rootView can be |
| // propagated into the binding instance and its non-null getRoot() method. Synthesize |
| // an explicit null check to prevent this. |
| beginControlFlow("if ($N == null)", rootParam) |
| addStatement("throw new $T($S)", NullPointerException::class.java, rootParam.name) |
| endControlFlow() |
| addCode("\n") |
| } |
| |
| /** Non-null when error-handling is being generated. */ |
| val id: String? |
| |
| if (nonRootBindings.any { it.isRequired }) { |
| addComment("The body of this method is generated in a way you would not otherwise write.") |
| addComment("This is done to optimize the compiled bytecode for size and performance.") |
| |
| id = localNames.newName("id") |
| addStatement("int $id") |
| |
| // By using a named block and break statements, the generated code compiles to bytecode |
| // which optimizes for the common case of all required views being present. It also allows |
| // de-duplicating the exception handling code to save bytecode size. |
| beginControlFlow("missingId:") |
| } else { |
| id = null |
| } |
| |
| val constructorParams = mutableListOf<CodeBlock>() |
| constructorParams += rootParam.asViewReference(binder.rootNode.type) |
| |
| binder.bindings.forEach { binding -> |
| val viewName = localNames.newName(binding.name) |
| |
| val viewType = when (binding.form) { |
| ViewBinding.Form.View -> binding.type |
| ViewBinding.Form.Binder -> ANDROID_VIEW |
| } |
| val viewInitializer = if (binding === rootBinding) { |
| // If this corresponds to the root binding, we can re-use the input View argument. |
| rootParam.asViewReference(viewType) |
| } else if (binding.isRequired) { |
| // Place the id value first into the local in case it's needed for error handling. |
| addStatement("$id = $L", binding.id.asCode()) |
| CodeBlock.of("$N.findViewById($id)", rootParam) |
| } else { |
| CodeBlock.of("$N.findViewById($L)", rootParam, binding.id.asCode()) |
| } |
| addStatement("$T $viewName = $L", viewType, viewInitializer) |
| |
| if (binding.isRequired && binding !== rootBinding) { |
| beginControlFlow("if ($viewName == null)") |
| addStatement("break missingId") |
| endControlFlow() |
| } |
| |
| val constructorParam = when (binding.form) { |
| ViewBinding.Form.View -> viewName |
| ViewBinding.Form.Binder -> { |
| val binderName = localNames.newName("binding_${binding.name}") |
| if (binding.isRequired) { |
| addStatement("$1T $binderName = $1T.bind($viewName)", binding.type) |
| } else { |
| addStatement(""" |
| $1T $binderName = $viewName != null |
| ? $1T.bind($viewName) |
| : null |
| """.trimIndent(), binding.type) |
| } |
| binderName |
| } |
| } |
| constructorParams += CodeBlock.of(L, constructorParam) |
| |
| addCode("\n") |
| } |
| |
| addStatement("return new $T($L)", binder.generatedTypeName, |
| CodeBlock.join(constructorParams, ",$W")) |
| |
| if (id != null) { |
| endControlFlow() |
| |
| val missingId = localNames.newName("missingId") |
| addStatement( |
| "$T $missingId = $N.getResources().getResourceName($id)", |
| String::class.java, |
| rootParam |
| ) |
| |
| // String.concat(String) produces less bytecode than '+' (StringBuilder). |
| addStatement( |
| "throw new $T($S.concat($missingId))", |
| NullPointerException::class.java, |
| "Missing required view with ID: " |
| ) |
| } |
| } |
| |
| /** Return a [CodeBlock] reference to [this] as [viewType], emitting a cast if needed. */ |
| private fun ParameterSpec.asViewReference(viewType: ClassName): CodeBlock { |
| return if (viewType != ANDROID_VIEW) { |
| CodeBlock.of("($T) $N", viewType, this) |
| } else { |
| CodeBlock.of(N, this) |
| } |
| } |
| |
| /** Return the storage type for the view backing a [RootNode]. */ |
| private val RootNode.type get() = when (this) { |
| is RootNode.Merge -> ANDROID_VIEW |
| is RootNode.View -> type |
| is RootNode.Binding -> binding.type |
| } |
| } |