Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate model for abstract class if class annotation is present #105

Merged
merged 8 commits into from
Feb 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 40 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,13 @@ public class ButtonModel extends EpoxyModelWithHolder<ButtonHolder> {
}
```

A good pattern is to create a base class that all view holders in your app can extend. Your base class can use [ButterKnife](https://github.com/JakeWharton/butterknife) to bind its view so that subclasses don't explicitly need to.
Alternatively you can allow Epoxy's annotation processor to generate the `createNewHolder` method for you, reducing the boilerplate in your model.

It terms of our example from above this might look like:
Just leave your model class abstract, annotate it with `@EpoxyClassModel` (see [Generating Helper Classes For Models](#annotations)), and don't implement `createNewHolder`. A subclass will be generated that implements the method for you. This implementation will create a new instance of your Holder class by calling the no argument constructor, which is the same as what is implemented manually in the example above.

Another helpful pattern is to create a base Holder class that all view holders in your app can extend. Your base class can use [ButterKnife](https://github.com/JakeWharton/butterknife) to bind its view so that subclasses don't explicitly need to.

For example your base class may look like this:

```java
public abstract class BaseEpoxyHolder extends EpoxyHolder {
Expand All @@ -308,9 +312,30 @@ public abstract class BaseEpoxyHolder extends EpoxyHolder {
ButterKnife.bind(this, itemView);
}
}
```

Applying these two patterns helps shorten our example model to just:

```java
@EpoxyModelClass
public abstract class ButtonModel extends EpoxyModelWithHolder<ButtonHolder> {
@EpoxyAttribute @StringRes int text;
@EpoxyAttribute OnClickListener clickListener;

@Override
protected int getDefaultLayout() {
return R.layout.model_button;
}

@Override
public void bind(ButtonHolder holder) {
holder.button.setText(text);
holder.button.setOnClickListener(clickListener);
}

static class ButtonHolder extends BaseEpoxyHolder {
static class ButtonHolder extends BaseEpoxyHolder {
@BindView(R.id.button) Button button;
}
}
```

Expand Down Expand Up @@ -369,7 +394,7 @@ layoutManager.setSpanSizeLookup(epoxyAdapter.getSpanSizeLookup());

## <a name="annotations"/>Generating helper classes with `@EpoxyAttribute`

You can reduce boilerplate in you model classes by using the EpoxyAttribute annotation to generate a subclass of your model with setters, getters, equals, and hashcode.
You can reduce boilerplate in your model classes by using the EpoxyAttribute annotation to generate a subclass of your model with setters, getters, equals, hashCode, reset, and toString.

For example, you may set up a model like this:

Expand Down Expand Up @@ -405,11 +430,19 @@ models.add(new HeaderModel_()
```

The setters return the model so that they can be used in a builder style. The generated class includes a `hashCode()` implementation for all of the annotated attributes so that the model can be used in [automatic diffing](#diffing).
Sometimes, you may not want certain fields to be included in your hash code and equals such as a click listener that gets recreated in every bind call. To tell Epoxy to skip that annotation, add `hash=false` to the annotation.
Sometimes, you may not want certain fields, such as a click listener, to be included in your hashCode and equals methods since that field may change on every bind call. Add `hash=false` to the annotation to tell Epoxy to skip that field.

The generated class will always be the name of the original class with an underscore appended at the end. If a model class is subclassed from other models that also have EpoxyAttributes, the generated class will include all of the super classes' attributes. The generated class will duplicate any constructors on the original model class. The generated class will also duplicate any methods that have a return type of the model class. The goal of that is to help with chaining calls to methods that exist on the original class. `super` will be called in all of these generated methods.

If the original class is abstract then a class will not be generated for it by default. However, an `@EpoxyModelClass` annotation may be added on the class to force a subclass to be generated. There are several cases where this may be helful.

1. Having your model class be abstract signals to other developers that the class should not be instantiated directly, and that the generated model should be used instead. This may prevent accidentally instantiating the base class instead of the generated class. For larger projects this can be a good pattern to establish for all models.

2. If a class does not have any `@EpoxyAttribute` annotations itself, but one of its super classes does, it would not normally have a class generated for it. Using `@EpoxyModelClass` on the subclass is the only way to generate a model in that case.

The generated class will always be the name of the original class with an underscore appended at the end. If the original class is abstract then a class will not be generated for it. If a model class is subclassed from other models that also have EpoxyAttributes, the generated class will include all of the super class's attributes. The generated class will duplicate any constructors on the original model class. If the original model class has any method names that match generated setters then the generated method will call super.
3. If you are using `EpoxyModelWithHolder` (see [Using View Holders](#view-holders)) you can leave the `createNewHolder` method unimplemented and the generated class will contain a default implementation that creates a new holder by calling a no argument constructor of the holder class.

This is an optional aspect of Epoxy that you may choose not to use, but it can be helpful in reducing the boilerplate in your models.
These annotations are an optional aspect of Epoxy that you may choose not to use, but they can be helpful in reducing the boilerplate in your models.

## License

Expand Down
11 changes: 6 additions & 5 deletions epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,11 @@ public void onViewDetachedFromWindow(T view) {

@Override
public String toString() {
return "{" + getClass().getSimpleName()
+ "id=" + id
+ ", layout=" + getLayout()
+ ", shown=" + shown
+ '}';
return getClass().getSimpleName() + "{" +
"id=" + id +
", layout=" + getLayout() +
", shown=" + shown +
", addedToAdapter=" + addedToAdapter +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import java.lang.annotation.Target;

/**
* Used to annotate {@link EpoxyModel} classes in order to generate a
* Used to annotate EpoxyModel classes in order to generate a
* subclass of that model with getters, setters, equals, and hashcode for the annotated fields.
*/
@Target(ElementType.TYPE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,40 @@
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

public class ClassToGenerateInfo {

private static final String GENERATED_CLASS_NAME_SUFFIX = "_";
private static final String RESET_METHOD = "reset";

private final Elements elementUtils;
private final TypeName originalClassName;
private final TypeName originalClassNameWithoutType;
private final TypeElement originalClassElement;
private final TypeName parameterizedClassName;
private final ClassName generatedClassName;
private final boolean isOriginalClassAbstract;
private final boolean shouldGenerateSubClass;
private final Set<AttributeInfo> attributeInfo = new HashSet<>();
private final List<TypeVariableName> typeVariableNames = new ArrayList<>();
private final List<ConstructorInfo> constructors = new ArrayList<>();
private final Set<MethodInfo> methodsReturningClassType = new LinkedHashSet<>();
private final Types typeUtils;

public ClassToGenerateInfo(Types typeUtils, TypeElement originalClassName,
ClassName generatedClassName, boolean isOriginalClassAbstract) {
ClassToGenerateInfo(Types typeUtils, Elements elementUtils, TypeElement originalClassElement) {
this.typeUtils = typeUtils;
this.originalClassName = ParameterizedTypeName.get(originalClassName.asType());
this.originalClassNameWithoutType = ClassName.get(originalClassName);
this.elementUtils = elementUtils;
this.originalClassName = ParameterizedTypeName.get(originalClassElement.asType());
this.originalClassNameWithoutType = ClassName.get(originalClassElement);
this.originalClassElement = originalClassElement;
ClassName generatedClassName = getGeneratedClassName(originalClassElement);

for (TypeParameterElement typeParameterElement : originalClassName.getTypeParameters()) {
for (TypeParameterElement typeParameterElement : originalClassElement.getTypeParameters()) {
typeVariableNames.add(TypeVariableName.get(typeParameterElement));
}

collectOriginalClassConstructors(originalClassName);
collectMethodsReturningClassType(originalClassName);
collectOriginalClassConstructors(originalClassElement);
collectMethodsReturningClassType(originalClassElement);

if (!typeVariableNames.isEmpty()) {
TypeVariableName[] typeArguments =
Expand All @@ -68,12 +73,32 @@ public ClassToGenerateInfo(Types typeUtils, TypeElement originalClassName,
}

this.generatedClassName = generatedClassName;
this.isOriginalClassAbstract = isOriginalClassAbstract;
this.shouldGenerateSubClass = shouldGenerateSubclass(originalClassElement);
}

private ClassName getGeneratedClassName(TypeElement classElement) {
String packageName = elementUtils.getPackageOf(classElement).getQualifiedName().toString();

int packageLen = packageName.length() + 1;
String className =
classElement.getQualifiedName().toString().substring(packageLen).replace('.', '$');

return ClassName.get(packageName, className + GENERATED_CLASS_NAME_SUFFIX);
}

private boolean shouldGenerateSubclass(TypeElement classElement) {
boolean hasEpoxyClassAnnotation = classElement.getAnnotation(EpoxyModelClass.class) != null;
boolean isAbstract = classElement.getModifiers().contains(Modifier.ABSTRACT);

// By default we don't extend classes that are abstract; if they don't contain all required
// methods then our generated class won't compile. If there is a EpoxyModelClass annotation
// though we will always generate the subclass
return !isAbstract || hasEpoxyClassAnnotation;
}

/**
* Get information about constructors of the original class so we can duplicate
them in the generated class and call through to super with the proper parameters
* Get information about constructors of the original class so we can duplicate them in the
* generated class and call through to super with the proper parameters
*/
private void collectOriginalClassConstructors(TypeElement originalClass) {
for (Element subElement : originalClass.getEnclosedElements()) {
Expand All @@ -89,8 +114,8 @@ private void collectOriginalClassConstructors(TypeElement originalClass) {
}

/**
* Get information about methods returning class type of the original class so we can
* duplicate them in the generated class for chaining purposes
* Get information about methods returning class type of the original class so we can duplicate
* them in the generated class for chaining purposes
*/
private void collectMethodsReturningClassType(TypeElement originalClass) {
TypeElement clazz = originalClass;
Expand Down Expand Up @@ -143,6 +168,10 @@ public void addAttributes(Collection<AttributeInfo> attributeInfo) {
this.attributeInfo.addAll(attributeInfo);
}

public TypeElement getOriginalClassElement() {
return originalClassElement;
}

public TypeName getOriginalClassName() {
return originalClassName;
}
Expand All @@ -167,8 +196,8 @@ public Set<AttributeInfo> getAttributeInfo() {
return attributeInfo;
}

public boolean isOriginalClassAbstract() {
return isOriginalClassAbstract;
public boolean shouldGenerateSubClass() {
return shouldGenerateSubClass;
}

public Iterable<TypeVariableName> getTypeVariables() {
Expand Down
Loading