Skip to content

Commit

Permalink
Merge pull request #301 from tomxor/bind-arrays
Browse files Browse the repository at this point in the history
Support binding array resources (String, int, text and TypedArray).
  • Loading branch information
JakeWharton committed Jul 7, 2015
2 parents d05254d + 4627780 commit d6a9a66
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 101 deletions.
37 changes: 37 additions & 0 deletions butterknife/src/main/java/butterknife/BindArray.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package butterknife;

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

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.CLASS;

/**
* Bind a field to the specified array resource ID. The type of array will be inferred from the
* annotated element.
*
* String array:
* <pre><code>
* {@literal @}BindArray(R.array.countries) String[] countries;
* </code></pre>
*
* Int array:
* <pre><code>
* {@literal @}BindArray(R.array.phones) int[] phones;
* </code></pre>
*
* Text array:
* <pre><code>
* {@literal @}BindArray(R.array.options) CharSequence[] options;
* </code></pre>
*
* {@link android.content.res.TypedArray}:
* <pre><code>
* {@literal @}BindArray(R.array.icons) TypedArray icons;
* </code></pre>
*/
@Retention(CLASS) @Target(FIELD)
public @interface BindArray {
/** Array resource ID to which the field will be bound. */
int value();
}
19 changes: 0 additions & 19 deletions butterknife/src/main/java/butterknife/BindStringArray.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

import android.view.View;
import butterknife.Bind;
import butterknife.BindArray;
import butterknife.BindBool;
import butterknife.BindColor;
import butterknife.BindDimen;
import butterknife.BindDrawable;
import butterknife.BindInt;
import butterknife.BindString;
import butterknife.BindStringArray;
import butterknife.OnCheckedChanged;
import butterknife.OnClick;
import butterknife.OnEditorAction;
Expand Down Expand Up @@ -69,6 +69,7 @@ public final class ButterKnifeProcessor extends AbstractProcessor {
static final String VIEW_TYPE = "android.view.View";
private static final String COLOR_STATE_LIST_TYPE = "android.content.res.ColorStateList";
private static final String DRAWABLE_TYPE = "android.graphics.drawable.Drawable";
private static final String TYPED_ARRAY_TYPE = "android.content.res.TypedArray";
private static final String NULLABLE_ANNOTATION_NAME = "Nullable";
private static final String ITERABLE_TYPE = "java.lang.Iterable<?>";
private static final String LIST_TYPE = List.class.getCanonicalName();
Expand Down Expand Up @@ -107,13 +108,13 @@ public final class ButterKnifeProcessor extends AbstractProcessor {
types.add(listener.getCanonicalName());
}

types.add(BindArray.class.getCanonicalName());
types.add(BindBool.class.getCanonicalName());
types.add(BindColor.class.getCanonicalName());
types.add(BindDimen.class.getCanonicalName());
types.add(BindDrawable.class.getCanonicalName());
types.add(BindInt.class.getCanonicalName());
types.add(BindString.class.getCanonicalName());
types.add(BindStringArray.class.getCanonicalName());

return types;
}
Expand Down Expand Up @@ -158,6 +159,15 @@ private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env)
findAndParseListener(env, listener, targetClassMap, erasedTargetNames);
}

// Process each @BindArray element.
for (Element element : env.getElementsAnnotatedWith(BindArray.class)) {
try {
parseResourceArray(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindArray.class, e);
}
}

// Process each @BindBool element.
for (Element element : env.getElementsAnnotatedWith(BindBool.class)) {
try {
Expand Down Expand Up @@ -212,15 +222,6 @@ private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env)
}
}

// Process each @BindStringArray element.
for (Element element : env.getElementsAnnotatedWith(BindStringArray.class)) {
try {
parseResourceStringArray(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindStringArray.class, e);
}
}

// Try to find a parent binder for each.
for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
String parentClassFqcn = findParentFqcn(entry.getKey(), erasedTargetNames);
Expand Down Expand Up @@ -644,38 +645,63 @@ private void parseResourceString(Element element, Map<TypeElement, BindingClass>
erasedTargetNames.add(enclosingElement.toString());
}

private void parseResourceStringArray(Element element,
Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames) {
private void parseResourceArray(Element element, Map<TypeElement, BindingClass> targetClassMap,
Set<String> erasedTargetNames) {
boolean hasError = false;
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

// Verify that the target type is String[].
if (!"java.lang.String[]".equals(element.asType().toString())) {
error(element, "@%s field type must be 'String[]'. (%s.%s)",
BindStringArray.class.getSimpleName(), enclosingElement.getQualifiedName(),
// Verify that the target type is supported.
String methodName = getArrayResourceMethodName(element);
if (methodName == null) {
error(element,
"@%s field type must be one of: String[], int[], CharSequence[], %s. (%s.%s)",
BindArray.class.getSimpleName(), TYPED_ARRAY_TYPE, enclosingElement.getQualifiedName(),
element.getSimpleName());
hasError = true;
}

// Verify common generated code restrictions.
hasError |= isInaccessibleViaGeneratedCode(BindStringArray.class, "fields", element);
hasError |= isBindingInWrongPackage(BindStringArray.class, element);
hasError |= isInaccessibleViaGeneratedCode(BindArray.class, "fields", element);
hasError |= isBindingInWrongPackage(BindArray.class, element);

if (hasError) {
return;
}

// Assemble information on the field.
String name = element.getSimpleName().toString();
int id = element.getAnnotation(BindStringArray.class).value();
int id = element.getAnnotation(BindArray.class).value();

BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
FieldResourceBinding binding = new FieldResourceBinding(id, name, "getStringArray");
FieldResourceBinding binding = new FieldResourceBinding(id, name, methodName);
bindingClass.addResource(binding);

erasedTargetNames.add(enclosingElement.toString());
}

/**
* Returns a method name from the {@link android.content.res.Resources} class for array resource
* binding, null if the element type is not supported.
*/
private static String getArrayResourceMethodName(Element element) {
TypeMirror typeMirror = element.asType();
if (TYPED_ARRAY_TYPE.equals(typeMirror.toString())) {
return "obtainTypedArray";
}
if (TypeKind.ARRAY.equals(typeMirror.getKind())) {
ArrayType arrayType = (ArrayType) typeMirror;
String componentType = arrayType.getComponentType().toString();
if ("java.lang.String".equals(componentType)) {
return "getStringArray";
} else if ("int".equals(componentType)) {
return "getIntArray";
} else if ("java.lang.CharSequence".equals(componentType)) {
return "getTextArray";
}
}
return null;
}

/** Returns the first duplicate element inside an array, null if there are no duplicates. */
private static Integer findDuplicate(int[] array) {
Set<Integer> seenElements = new LinkedHashSet<Integer>();
Expand Down
166 changes: 166 additions & 0 deletions butterknife/src/test/java/butterknife/internal/BindArrayTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package butterknife.internal;

import com.google.common.base.Joiner;
import com.google.testing.compile.JavaFileObjects;
import javax.tools.JavaFileObject;
import org.junit.Test;

import static com.google.common.truth.Truth.ASSERT;
import static com.google.testing.compile.JavaSourceSubjectFactory.javaSource;

public class BindArrayTest {
@Test public void stringArray() throws Exception {
JavaFileObject source = JavaFileObjects.forSourceString("test.Test", Joiner.on('\n').join(
"package test;",
"import android.app.Activity;",
"import butterknife.BindArray;",
"import android.content.res.TypedArray;",
"public class Test extends Activity {",
" @BindArray(1) String[] one;",
"}"
));

JavaFileObject expectedSource = JavaFileObjects.forSourceString("test/Test$$ViewBinder",
Joiner.on('\n').join(
"package test;",
"import android.content.res.Resources;",
"import butterknife.ButterKnife.Finder;",
"import butterknife.ButterKnife.ViewBinder;",
"public class Test$$ViewBinder<T extends test.Test> implements ViewBinder<T> {",
" @Override public void bind(final Finder finder, final T target, Object source) {",
" Resources res = finder.getContext(source).getResources();",
" target.one = res.getStringArray(1);",
" }",
" @Override public void unbind(T target) {",
" }",
"}"
));

ASSERT.about(javaSource()).that(source)
.processedWith(new ButterKnifeProcessor())
.compilesWithoutError()
.and()
.generatesSources(expectedSource);
}

@Test public void intArray() throws Exception {
JavaFileObject source = JavaFileObjects.forSourceString("test.Test", Joiner.on('\n').join(
"package test;",
"import android.app.Activity;",
"import butterknife.BindArray;",
"import android.content.res.TypedArray;",
"public class Test extends Activity {",
" @BindArray(1) int[] one;",
"}"
));

JavaFileObject expectedSource = JavaFileObjects.forSourceString("test/Test$$ViewBinder",
Joiner.on('\n').join(
"package test;",
"import android.content.res.Resources;",
"import butterknife.ButterKnife.Finder;",
"import butterknife.ButterKnife.ViewBinder;",
"public class Test$$ViewBinder<T extends test.Test> implements ViewBinder<T> {",
" @Override public void bind(final Finder finder, final T target, Object source) {",
" Resources res = finder.getContext(source).getResources();",
" target.one = res.getIntArray(1);",
" }",
" @Override public void unbind(T target) {",
" }",
"}"
));

ASSERT.about(javaSource()).that(source)
.processedWith(new ButterKnifeProcessor())
.compilesWithoutError()
.and()
.generatesSources(expectedSource);
}

@Test public void textArray() throws Exception {
JavaFileObject source = JavaFileObjects.forSourceString("test.Test", Joiner.on('\n').join(
"package test;",
"import android.app.Activity;",
"import butterknife.BindArray;",
"import android.content.res.TypedArray;",
"public class Test extends Activity {",
" @BindArray(1) CharSequence[] one;",
"}"
));

JavaFileObject expectedSource = JavaFileObjects.forSourceString("test/Test$$ViewBinder",
Joiner.on('\n').join(
"package test;",
"import android.content.res.Resources;",
"import butterknife.ButterKnife.Finder;",
"import butterknife.ButterKnife.ViewBinder;",
"public class Test$$ViewBinder<T extends test.Test> implements ViewBinder<T> {",
" @Override public void bind(final Finder finder, final T target, Object source) {",
" Resources res = finder.getContext(source).getResources();",
" target.one = res.getTextArray(1);",
" }",
" @Override public void unbind(T target) {",
" }",
"}"
));

ASSERT.about(javaSource()).that(source)
.processedWith(new ButterKnifeProcessor())
.compilesWithoutError()
.and()
.generatesSources(expectedSource);
}

@Test public void typedArray() throws Exception {
JavaFileObject source = JavaFileObjects.forSourceString("test.Test", Joiner.on('\n').join(
"package test;",
"import android.app.Activity;",
"import butterknife.BindArray;",
"import android.content.res.TypedArray;",
"public class Test extends Activity {",
" @BindArray(1) TypedArray one;",
"}"
));

JavaFileObject expectedSource = JavaFileObjects.forSourceString("test/Test$$ViewBinder",
Joiner.on('\n').join(
"package test;",
"import android.content.res.Resources;",
"import butterknife.ButterKnife.Finder;",
"import butterknife.ButterKnife.ViewBinder;",
"public class Test$$ViewBinder<T extends test.Test> implements ViewBinder<T> {",
" @Override public void bind(final Finder finder, final T target, Object source) {",
" Resources res = finder.getContext(source).getResources();",
" target.one = res.obtainTypedArray(1);",
" }",
" @Override public void unbind(T target) {",
" }",
"}"
));

ASSERT.about(javaSource()).that(source)
.processedWith(new ButterKnifeProcessor())
.compilesWithoutError()
.and()
.generatesSources(expectedSource);
}

@Test public void typeMustBeSupported() {
JavaFileObject source = JavaFileObjects.forSourceString("test.Test", Joiner.on('\n').join(
"package test;",
"import android.app.Activity;",
"import butterknife.BindArray;",
"public class Test extends Activity {",
" @BindArray(1) String one;",
"}"
));

ASSERT.about(javaSource()).that(source)
.processedWith(new ButterKnifeProcessor())
.failsToCompile()
.withErrorContaining(
"@BindArray field type must be one of: String[], int[], CharSequence[], "
+ "android.content.res.TypedArray. (test.Test.one)")
.in(source).onLine(5);
}
}
Loading

0 comments on commit d6a9a66

Please sign in to comment.