Skip to content

Commit

Permalink
Start entirely reflection-backed module
Browse files Browse the repository at this point in the history
This is for IDE builds where annotation processing can dramatically slow down iteration time. Right now only BindView, BindViews, and BindString work as those are the bindings used by the integration test.
  • Loading branch information
JakeWharton committed Sep 6, 2018
1 parent 36601bd commit 0821b0c
Show file tree
Hide file tree
Showing 13 changed files with 332 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
import java.lang.annotation.Target;

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

/**
* Bind a field to the specified string resource ID.
* <pre><code>
* {@literal @}BindString(R.string.username_error) String usernameErrorText;
* </code></pre>
*/
@Retention(CLASS) @Target(FIELD)
@Retention(RUNTIME) @Target(FIELD)
public @interface BindString {
/** String resource ID to which the field will be bound. */
@StringRes int value();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import java.lang.annotation.Target;

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

/**
* Bind a field to the view for the specified ID. The view will automatically be cast to the field
Expand All @@ -14,7 +14,7 @@
* {@literal @}BindView(R.id.title) TextView title;
* </code></pre>
*/
@Retention(CLASS) @Target(FIELD)
@Retention(RUNTIME) @Target(FIELD)
public @interface BindView {
/** View ID to which the field will be bound. */
@IdRes int value();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import java.lang.annotation.Target;

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

/**
* Bind a field to the view for the specified ID. The view will automatically be cast to the field
Expand All @@ -15,7 +15,7 @@
* List&lt;TextView&gt; titles;
* </code></pre>
*/
@Retention(CLASS) @Target(FIELD)
@Retention(RUNTIME) @Target(FIELD)
public @interface BindViews {
/** View IDs to which the field will be bound. */
@IdRes int[] value();
Expand Down
23 changes: 19 additions & 4 deletions butterknife-integration-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,22 @@ android {
}

buildTypes {
release {
debug {
minifyEnabled true
proguardFile getDefaultProguardFile('proguard-android.txt')
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard.pro'
}
}

productFlavors {
flavorDimensions 'runtime'

reflect {
dimension 'runtime'
applicationIdSuffix '.reflect'
}
codegen {
dimension 'runtime'
applicationIdSuffix '.codegen'
}
}

Expand All @@ -43,8 +56,10 @@ android {
}

dependencies {
implementation project(':butterknife')
annotationProcessor project(':butterknife-compiler')
reflectImplementation project(':butterknife-reflect')

codegenImplementation project(':butterknife')
codegenAnnotationProcessor project(':butterknife-compiler')

testImplementation deps.junit
testImplementation deps.truth
Expand Down
2 changes: 2 additions & 0 deletions butterknife-integration-test/proguard.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-dontoptimize
-dontobfuscate
29 changes: 29 additions & 0 deletions butterknife-reflect/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
apply plugin: 'com.android.library'

android {
compileSdkVersion versions.compileSdk

defaultConfig {
minSdkVersion versions.minSdk

consumerProguardFiles 'proguard-rules.txt'
}

lintOptions {
textReport true
textOutput 'stdout'
// We run a full lint analysis as build part in CI, so skip vital checks for assemble tasks.
checkReleaseBuilds false
}

// TODO replace with https://issuetracker.google.com/issues/72050365 once released.
libraryVariants.all {
it.generateBuildConfig.enabled = false
}
}

dependencies {
api project(':butterknife-runtime')
}

apply from: rootProject.file('gradle/gradle-mvn-push.gradle')
3 changes: 3 additions & 0 deletions butterknife-reflect/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
POM_ARTIFACT_ID=butterknife-reflect
POM_NAME=ButterKnife Reflect
POM_PACKAGING=aar
2 changes: 2 additions & 0 deletions butterknife-reflect/proguard-rules.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-keepclassmembers class * { @butterknife.* <methods>; }
-keepclassmembers class * { @butterknife.* <fields>; }
1 change: 1 addition & 0 deletions butterknife-reflect/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<manifest package="butterknife.reflect"/>
229 changes: 229 additions & 0 deletions butterknife-reflect/src/main/java/butterknife/ButterKnife.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package butterknife;

import android.app.Activity;
import android.app.Dialog;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.util.Log;
import android.view.View;
import butterknife.internal.Utils;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;

public final class ButterKnife {
private ButterKnife() {
throw new AssertionError();
}

private static final String TAG = "ButterKnife";
private static boolean debug = false;

/** Control whether debug logging is enabled. */
public static void setDebug(boolean debug) {
ButterKnife.debug = debug;
}

/**
* BindView annotated fields and methods in the specified {@link Activity}. The current content
* view is used as the view root.
*
* @param target Target activity for view binding.
*/
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return bind(target, sourceView);
}

/**
* BindView annotated fields and methods in the specified {@link View}. The view and its children
* are used as the view root.
*
* @param target Target view for view binding.
*/
@NonNull @UiThread
public static Unbinder bind(@NonNull View target) {
return bind(target, target);
}

/**
* BindView annotated fields and methods in the specified {@link Dialog}. The current content
* view is used as the view root.
*
* @param target Target dialog for view binding.
*/
@NonNull @UiThread
public static Unbinder bind(@NonNull Dialog target) {
View sourceView = target.getWindow().getDecorView();
return bind(target, sourceView);
}

/**
* BindView annotated fields and methods in the specified {@code target} using the {@code source}
* {@link Activity} as the view root.
*
* @param target Target class for view binding.
* @param source Activity on which IDs will be looked up.
*/
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull Activity source) {
View sourceView = source.getWindow().getDecorView();
return bind(target, sourceView);
}

/**
* BindView annotated fields and methods in the specified {@code target} using the {@code source}
* {@link Dialog} as the view root.
*
* @param target Target class for view binding.
* @param source Dialog on which IDs will be looked up.
*/
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull Dialog source) {
View sourceView = source.getWindow().getDecorView();
return bind(target, sourceView);
}

/**
* BindView annotated fields and methods in the specified {@code target} using the {@code source}
* {@link View} as the view root.
*
* @param target Target class for view binding.
* @param source View root on which IDs will be looked up.
*/
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
List<Unbinder> unbinders = new ArrayList<>();
Class<?> targetClass = target.getClass();
while (true) {
String clsName = targetClass.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
break;
}

for (Field field : targetClass.getDeclaredFields()) {
Unbinder unbinder = parseBindView(target, field, source);
if (unbinder == null) unbinder = parseBindViews(target, field, source);
if (unbinder == null) unbinder = parseBindString(target, field, source);

if (unbinder != null) {
unbinders.add(unbinder);
}
}
targetClass = targetClass.getSuperclass();
}

if (unbinders.isEmpty()) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return Unbinder.EMPTY;
}

if (debug) Log.d(TAG, "HIT: Reflectively found " + unbinders.size() + " bindings.");
return new CompositeUnbinder(unbinders);
}

private static @Nullable Unbinder parseBindView(Object target, Field field, View source) {
BindView bindView = field.getAnnotation(BindView.class);
if (bindView == null) {
return null;
}
// TODO check is instance
// TODO check visibility
boolean isRequired = true; // TODO actually figure out

int id = bindView.value();
Class<?> viewClass = field.getType();
String who = "field '" + field.getName() + "'";
Object view;
if (isRequired) {
view = Utils.findRequiredViewAsType(source, id, who, viewClass);
} else {
view = Utils.findOptionalViewAsType(source, id, who, viewClass);
}
uncheckedSet(field, target, view);

return new FieldUnbinder(target, field);
}

private static @Nullable Unbinder parseBindViews(Object target, Field field, View source) {
BindViews bindViews = field.getAnnotation(BindViews.class);
if (bindViews == null) {
return null;
}
// TODO check is instance
// TODO check visibility
boolean isRequired = true; // TODO actually figure out

Class<?> fieldClass = field.getType();
Class<?> viewClass;
boolean isArray = fieldClass.isArray();
if (isArray) {
viewClass = fieldClass.getComponentType();
} else if (fieldClass == List.class) {
Type fieldType = field.getGenericType();
if (fieldType instanceof ParameterizedType) {
Type viewType = ((ParameterizedType) fieldType).getActualTypeArguments()[0];
// TODO real rawType impl!!!!
viewClass = (Class<?>) viewType;
} else {
throw new IllegalStateException(); // TODO
}
} else {
throw new IllegalStateException(); // TODO
}

int[] ids = bindViews.value();
List<Object> views = new ArrayList<>(ids.length);
String who = "field '" + field.getName() + "'";
for (int id : ids) {
Object view;
if (isRequired) {
view = Utils.findRequiredViewAsType(source, id, who, viewClass);
} else {
view = Utils.findOptionalViewAsType(source, id, who, viewClass);
}
if (view != null) {
views.add(view);
}
}

Object value;
if (isArray) {
Object[] viewArray = (Object[]) Array.newInstance(viewClass, views.size());
value = views.toArray(viewArray);
} else {
value = views;
}

uncheckedSet(field, target, value);
return new FieldUnbinder(target, field);
}

private static @Nullable Unbinder parseBindString(Object target, Field field, View source) {
BindString bindString = field.getAnnotation(BindString.class);
if (bindString == null) {
return null;
}
// TODO check is instance
// TODO check visibility

String string = source.getContext().getString(bindString.value());
uncheckedSet(field, target, string);
return Unbinder.EMPTY;
}

static void uncheckedSet(Field field, Object target, @Nullable Object value) {
field.setAccessible(true); // TODO move this to a visibility check and only do for package.

try {
field.set(target, value);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to assign " + value + " to " + field + " on " + target, e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package butterknife;

import java.util.List;

final class CompositeUnbinder implements Unbinder {
private List<Unbinder> unbinders;

CompositeUnbinder(List<Unbinder> unbinders) {
this.unbinders = unbinders;
}

@Override public void unbind() {
if (unbinders == null) {
throw new IllegalStateException("Bindings already cleared.");
}
for (Unbinder unbinder : unbinders) {
unbinder.unbind();
}
unbinders = null;
}
}
Loading

0 comments on commit 0821b0c

Please sign in to comment.