Skip to content

Commit

Permalink
Add view injection and injection using activity support.
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeWharton committed Mar 7, 2013
1 parent 037d9b2 commit 05fee10
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 46 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Change Log
==========

Version 1.2.0 *(In Development)*
--------------------------------

* Support injection on any object using an Activity as the view root.
* Support injection on views for their children.


Version 1.1.1 *(2013-05-06)*
----------------------------

Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,24 @@ public class MyAdapter extends BaseAdapter {

You can see this implementation in action in the provided sample.

Other provided injection APIs:

* Inject arbitrary objects using an activity as the view root. If you use a
pattern like MVC you can inject the controller using its activity with
`Views.inject(this, activity)`.
* Inject a view's children into fields using `Views.inject(this)`. If you use
`<merge>` tags in a layout and inflate in a custom view constructor you can
call this immediately after. Alternatively, custom view types inflated from
XML can use it in the `onLayoutInflated()` callback.



Bonus
-----

Also included is a helper method for simplifying code which still has to call
`findViewById` on either a `View` or `Activity`:
Also included are two `findById` methods which simplify code that still has to
find views on a `View` or `Activity`. It uses generics to infer the return type
and automatically performs the cast.

```java
View view = LayoutInflater.from(context).inflate(R.layout.thing, null);
Expand Down
115 changes: 71 additions & 44 deletions butterknife/src/main/java/butterknife/Views.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
Expand All @@ -19,6 +20,8 @@
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.JavaFileObject;

import static javax.lang.model.element.ElementKind.CLASS;
Expand All @@ -27,45 +30,87 @@
import static javax.lang.model.element.Modifier.STATIC;
import static javax.tools.Diagnostic.Kind.ERROR;

/** View injection utilities. */
public class Views {
private Views() {
// No instances.
}

public enum Finder {
VIEW() {
@SuppressWarnings("unchecked") @Override
public <T extends View> T findById(Object source, int id) {
return (T) ((View) source).findViewById(id);
}
},
ACTIVITY() {
@SuppressWarnings("unchecked") @Override
public <T extends View> T findById(Object source, int id) {
return (T) ((Activity) source).findViewById(id);
}
};

public abstract <T extends View> T findById(Object source, int id);
}

private static final Map<Class<?>, Method> INJECTORS = new LinkedHashMap<Class<?>, Method>();

/**
* Inject fields annotated with {@link InjectView} in the specified {@link Activity}.
* Inject fields annotated with {@link InjectView} in the specified {@link Activity}. The current
* content view is used as the view root.
*
* @param target Target activity for field injection.
* @throws UnableToInjectException if injection could not be performed.
*/
public static void inject(Activity target) {
inject(target, Activity.class, target);
inject(target, target, Finder.ACTIVITY);
}

/**
* Inject fields annotated with {@link InjectView} in the specified {@code source} using {@code
* target} as the view root.
* Inject fields annotated with {@link InjectView} in the specified {@link View}. The view and
* its children are used as the view root.
*
* @param target Target view for field injection.
* @throws UnableToInjectException if injection could not be performed.
*/
public static void inject(View target) {
inject(target, target, Finder.VIEW);
}

/**
* Inject fields annotated with {@link InjectView} in the specified {@code source} using the
* {@code target} {@link View} as the view root.
*
* @param target Target class for field injection.
* @param source View tree root on which IDs will be looked up.
* @param source View root on which IDs will be looked up.
* @throws UnableToInjectException if injection could not be performed.
*/
public static void inject(Object target, View source) {
inject(target, View.class, source);
inject(target, source, Finder.VIEW);
}

private static void inject(Object target, Class<?> sourceType, Object source) {
/**
* Inject fields annotated with {@link InjectView} in the specified {@code source} using the
* {@code target} {@link Activity} as the view root.
*
* @param target Target class for field injection.
* @param source Activity on which IDs will be looked up.
* @throws UnableToInjectException if injection could not be performed.
*/
public static void inject(Object target, Activity source) {
inject(target, source, Finder.ACTIVITY);
}

private static void inject(Object target, Object source, Finder finder) {
try {
Class<?> targetClass = target.getClass();
Method inject = INJECTORS.get(targetClass);
if (inject == null) {
Class<?> injector = Class.forName(targetClass.getName() + AnnotationProcessor.SUFFIX);
inject = injector.getMethod("inject", targetClass, sourceType);
Class<?> injector = Class.forName(targetClass.getName() + InjectViewProcessor.SUFFIX);
inject = injector.getMethod("inject", Finder.class, targetClass, Object.class);
INJECTORS.put(targetClass, inject);
}
inject.invoke(null, target, source);
inject.invoke(null, finder, target, source);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
Expand Down Expand Up @@ -104,7 +149,11 @@ private void error(String message, Object... args) {
}

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
TypeMirror viewType = processingEnv.getElementUtils().getTypeElement(TYPE_VIEW).asType();
Elements elementUtils = processingEnv.getElementUtils();
Types typeUtils = processingEnv.getTypeUtils();
Filer filer = processingEnv.getFiler();

TypeMirror viewType = elementUtils.getTypeElement("android.view.View").asType();

Map<TypeElement, Set<InjectionPoint>> injectionsByClass =
new LinkedHashMap<TypeElement, Set<InjectionPoint>>();
Expand All @@ -114,7 +163,7 @@ private void error(String message, Object... args) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

// Verify that the target type extends from View.
if (!processingEnv.getTypeUtils().isSubtype(element.asType(), viewType)) {
if (!typeUtils.isSubtype(element.asType(), viewType)) {
error("@InjectView fields must extend from View (%s.%s).",
enclosingElement.getQualifiedName(), element);
continue;
Expand Down Expand Up @@ -144,9 +193,8 @@ private void error(String message, Object... args) {

// Assemble information on the injection point.
String variableName = element.getSimpleName().toString();
String type = element.asType().toString();
int value = element.getAnnotation(InjectView.class).value();
injections.add(new InjectionPoint(variableName, type, value));
injections.add(new InjectionPoint(variableName, value));

// Add to the valid injection targets set.
injectionTargets.add(enclosingElement.asType());
Expand All @@ -157,9 +205,7 @@ private void error(String message, Object... args) {
Set<InjectionPoint> injectionPoints = injection.getValue();

String targetType = type.getQualifiedName().toString();
String sourceType = resolveSourceType(type);
String packageName =
processingEnv.getElementUtils().getPackageOf(type).getQualifiedName().toString();
String packageName = elementUtils.getPackageOf(type).getQualifiedName().toString();
String className =
type.getQualifiedName().toString().substring(packageName.length() + 1).replace('.', '$')
+ SUFFIX;
Expand All @@ -177,11 +223,10 @@ private void error(String message, Object... args) {

// Write the view injector class.
try {
JavaFileObject jfo =
processingEnv.getFiler().createSourceFile(packageName + "." + className, type);
JavaFileObject jfo = filer.createSourceFile(packageName + "." + className, type);
Writer writer = jfo.openWriter();
writer.write(String.format(INJECTOR, packageName, className, targetType, sourceType,
injections.toString()));
writer.write(
String.format(INJECTOR, packageName, className, targetType, injections.toString()));
writer.flush();
writer.close();
} catch (IOException e) {
Expand All @@ -192,21 +237,6 @@ private void error(String message, Object... args) {
return true;
}

/** Returns {@link #TYPE_ACTIVITY} or {@link #TYPE_VIEW} as the injection target type. */
private String resolveSourceType(TypeElement typeElement) {
TypeMirror type;
while (true) {
type = typeElement.getSuperclass();
if (type.getKind() == TypeKind.NONE) {
return TYPE_VIEW;
}
if (type.toString().equals(TYPE_ACTIVITY)) {
return TYPE_ACTIVITY;
}
typeElement = (TypeElement) ((DeclaredType) type).asElement();
}
}

/** Finds the parent injector type in the supplied set, if any. */
private String resolveParentType(TypeElement typeElement, Set<TypeMirror> parents) {
TypeMirror type;
Expand All @@ -224,28 +254,25 @@ private String resolveParentType(TypeElement typeElement, Set<TypeMirror> parent

private static class InjectionPoint {
private final String variableName;
private final String type;
private final int value;

InjectionPoint(String variableName, String type, int value) {
InjectionPoint(String variableName, int value) {
this.variableName = variableName;
this.type = type;
this.value = value;
}

@Override public String toString() {
return String.format(INJECTION, variableName, type, value);
return String.format(INJECTION, variableName, value);
}
}

private static final String TYPE_ACTIVITY = "android.app.Activity";
private static final String TYPE_VIEW = "android.view.View";
private static final String INJECTION = " target.%s = (%s) source.findViewById(%s);";
private static final String INJECTION = " target.%s = finder.findById(source, %s);";
private static final String INJECTOR = ""
+ "// Generated code from Butter Knife. Do not modify!\n"
+ "package %s;\n\n"
+ "import butterknife.Views.Finder;\n\n"
+ "public class %s {\n"
+ " public static void inject(%s target, %s source) {\n"
+ " public static void inject(Finder finder, %s target, Object source) {\n"
+ "%s"
+ " }\n"
+ "}\n";
Expand Down

0 comments on commit 05fee10

Please sign in to comment.