Skip to content

Commit

Permalink
Databinding naming pattern support (#319)
Browse files Browse the repository at this point in the history
* Easier way to add databinding layouts

* Layout pattern option
  • Loading branch information
elihart authored Oct 16, 2017
1 parent 80f3bd4 commit 12e27f9
Show file tree
Hide file tree
Showing 19 changed files with 333 additions and 256 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
* <p>
* The layouts must not specify a custom databinding class name or package via the
* class="com.example.CustomClassName" override in the layout xml.
* <p>
* Alternatively you can use {@link EpoxyDataBindingPattern} to avoid explicitly declaring each
* layout.
*/
@Target(ElementType.PACKAGE)
@Retention(RetentionPolicy.CLASS)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.airbnb.epoxy;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Used to specify a naming pattern for the databinding layouts that you want models generated for.
* Use this instead of {@link EpoxyDataBindingLayouts} to avoid having to explicitly list every
* databinding layout.
* <p>
* The layouts must not specify a custom databinding class name or package via the
* class="com.example.CustomClassName" override in the layout xml.
*/
@Target(ElementType.PACKAGE)
@Retention(RetentionPolicy.CLASS)
public @interface EpoxyDataBindingPattern {
/**
* The R class used in this module (eg "com.example.app.R.class"). This is needed so Epoxy can
* look up layout files.
*/
Class<?> rClass();
/**
* A string prefix that your databinding layouts start with. Epoxy will generate a model for each
* databinding layout whose name starts with this.
* <p>
* For example, if you set this prefix to "view_holder" and you have a "view_holder_header.xml"
* databinding layout, Epoxy will generate a HeaderBindingModel_ class for that layout.
*/
String layoutPrefix();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
@EpoxyDataBindingPattern(rClass = R.class, layoutPrefix = "view_holder")
@EpoxyDataBindingLayouts({R.layout.model_with_data_binding})
package com.airbnb.epoxy.integrationtest;

import com.airbnb.epoxy.EpoxyDataBindingLayouts;
import com.airbnb.epoxy.EpoxyDataBindingPattern;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>

<variable
name="stringValue"
type="String" />

<variable
name="clickListener"
type="android.view.View.OnClickListener" />
</data>

<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="40dp"
android:text="@{stringValue}" />

</layout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<!-- This layout does nothing, but it tests that the databinding processor skips none databinding layouts that match the naming pattern. -->

</LinearLayout>
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@

import com.airbnb.epoxy.DataBindingEpoxyModel.DataBindingHolder;
import com.airbnb.epoxy.integrationtest.BuildConfig;
import com.airbnb.epoxy.integrationtest.DatabindingTestBindingModel_;
import com.airbnb.epoxy.integrationtest.ModelWithDataBindingBindingModel_;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;


import java.util.Collections;
import java.util.List;

Expand Down Expand Up @@ -144,4 +144,10 @@ public void typesWithHashCodeAreDiffed() {
verify(observerMock).onItemRangeChanged(eq(0), eq(1), any());
verifyNoMoreInteractions(observerMock);
}

@Test
public void generatesBindingModelFromNamingPattern() {
// Make sure that the model was generated from the annotation naming pattern
new DatabindingTestBindingModel_();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ private ClassNames() {
static final ClassName EPOXY_STRING_ATTRIBUTE_DATA = get(PKG_EPOXY, "StringAttributeData");
static final ClassName EPOXY_CONTROLLER = get(PKG_EPOXY, "EpoxyController");
static final ClassName EPOXY_STYLE_BUILDER_CALLBACK = get(PKG_EPOXY, "StyleBuilderCallback");
static final ClassName EPOXY_CONTROLLER_HELPER = get(PKG_EPOXY, "ControllerHelper");

static final ClassName PARIS_STYLE_UTILS = get(PKG_PARIS, "StyleApplierUtils", "Companion");
static final ClassName PARIS_STYLE = get(PKG_PARIS, "Style");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,17 +109,16 @@ List<Exception> processConfigurations(RoundEnvironment roundEnv) {
continue;
}

TypeMirror rClassType =
getClassParamFromAnnotation(element, PackageModelViewConfig.class, "rClass");
if (rClassType == null) {
ClassName rClassName =
getClassParamFromAnnotation(element, PackageModelViewConfig.class, "rClass", typeUtils);

if (rClassName == null) {
errors.add(buildEpoxyException(
"Unable to get R class details from annotation %s (package: %s)",
PackageModelViewConfig.class.getSimpleName(), packageName));
continue;
}

ClassName rClassName =
ClassName.get((TypeElement) typeUtils.asElement(rClassType));

String rLayoutClassString = rClassName.reflectionName();
if (!rLayoutClassString.endsWith(".R")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.airbnb.epoxy;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
Expand Down Expand Up @@ -39,7 +38,6 @@
import static com.airbnb.epoxy.Utils.validateFieldAccessibleViaGeneratedCode;

class ControllerProcessor {
private static final String CONTROLLER_HELPER_INTERFACE = "com.airbnb.epoxy.ControllerHelper";
private Filer filer;
private Elements elementUtils;
private Types typeUtils;
Expand Down Expand Up @@ -237,9 +235,9 @@ private void generateJava(Map<TypeElement, ControllerClassInfo> controllerClassM

private void generateHelperClassForController(ControllerClassInfo controllerInfo)
throws IOException {
ClassName superclass = ClassName.get(elementUtils.getTypeElement(CONTROLLER_HELPER_INTERFACE));
ParameterizedTypeName parameterizeSuperClass =
ParameterizedTypeName.get(superclass, controllerInfo.getControllerClassType());
ParameterizedTypeName
.get(ClassNames.EPOXY_CONTROLLER_HELPER, controllerInfo.getControllerClassType());

TypeSpec.Builder builder = TypeSpec.classBuilder(controllerInfo.getGeneratedClassName())
.addJavadoc("Generated file. Do not modify!")
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.airbnb.epoxy

import com.airbnb.epoxy.ClassNames.*
import com.airbnb.epoxy.Utils.*
import com.squareup.javapoet.*
import javax.lang.model.element.*
import javax.lang.model.util.*

internal class DataBindingModelInfo(
private val typeUtils: Types,
private val elementUtils: Elements,
val layoutResource: ResourceValue,
private val moduleName: String,
private val layoutPrefix: String = ""
) : GeneratedModelInfo() {
private val dataBindingClassName: ClassName

val dataBindingClassElement: Element?
get() = getElementByName(dataBindingClassName, elementUtils, typeUtils)

init {

dataBindingClassName = getDataBindingClassNameForResource(layoutResource, moduleName)

superClassElement = Utils.getElementByName(EPOXY_DATA_BINDING_MODEL,
elementUtils, typeUtils) as TypeElement
superClassName = EPOXY_DATA_BINDING_MODEL
generatedClassName = buildGeneratedModelName()
parametrizedClassName = generatedClassName
boundObjectTypeName = EPOXY_DATA_BINDING_HOLDER
shouldGenerateModel = true

collectMethodsReturningClassType(superClassElement, typeUtils)
}

/**
* Look up the DataBinding class generated for this model's layout file and parse the attributes
* for it.
*/
fun parseDataBindingClass() {
// This databinding class won't exist until the second round of annotation processing since
// it is generated in the first round.

val hashCodeValidator = HashCodeValidator(typeUtils, elementUtils)
dataBindingClassElement!!.enclosedElements
.filter { Utils.isSetterMethod(it) }
.forEach {
addAttribute(
DataBindingAttributeInfo(this, it as ExecutableElement,
hashCodeValidator))
}
}

private fun getDataBindingClassNameForResource(
layoutResource: ResourceValue,
moduleName: String
): ClassName {
val modelName = layoutResource.resourceName!!.toUpperCamelCase().plus(BINDING_SUFFIX)

return ClassName.get(moduleName + ".databinding", modelName)
}

private fun buildGeneratedModelName(): ClassName {
val modelName = layoutResource.resourceName!!
.removePrefix(layoutPrefix)
.toUpperCamelCase()
.plus(BINDING_SUFFIX)
.plus(GeneratedModelInfo.GENERATED_MODEL_SUFFIX)

return ClassName.get(moduleName, modelName)
}

companion object {

val BINDING_SUFFIX = "Binding"
}
}
Loading

0 comments on commit 12e27f9

Please sign in to comment.