diff options
author | Yigit Boyar <yboyar@google.com> | 2015-04-29 19:49:41 -0700 |
---|---|---|
committer | Yigit Boyar <yboyar@google.com> | 2015-04-30 15:32:34 -0700 |
commit | e9b33bac04bb1ce1444d7f1744fcec1ecd3a57da (patch) | |
tree | dac2454bf8fae14152c532d36300407dfbf330f1 | |
parent | cffffe30fe53455856d3d41724b9d5dd21aebf9a (diff) | |
download | data-binding-e9b33bac04bb1ce1444d7f1744fcec1ecd3a57da.tar.gz |
Support multi-param adapters in code generation
Bug: 19800022
Change-Id: I40c4ac72f24f965db12fd1c7dec6591184160ae5
17 files changed, 774 insertions, 115 deletions
diff --git a/compiler/src/main/java/android/databinding/tool/Binding.java b/compiler/src/main/java/android/databinding/tool/Binding.java index f2bc96fe..657cd7bc 100644 --- a/compiler/src/main/java/android/databinding/tool/Binding.java +++ b/compiler/src/main/java/android/databinding/tool/Binding.java @@ -21,6 +21,7 @@ import android.databinding.tool.reflection.ModelAnalyzer; import android.databinding.tool.reflection.ModelClass; import android.databinding.tool.store.SetterStore; import android.databinding.tool.store.SetterStore.SetterCall; +import android.databinding.tool.writer.CodeGenUtil; public class Binding { @@ -35,7 +36,7 @@ public class Binding { mExpr = expr; } - private SetterStore.SetterCall getSetterCall() { + private SetterStore.BindingSetterCall getSetterCall() { if (mSetterCall == null) { ModelClass viewType = mTarget.getResolvedType(); if (viewType != null && viewType.extendsViewStub()) { @@ -56,8 +57,9 @@ public class Binding { return mTarget; } - public String toJavaCode(String targetViewName, String expressionCode) { - return getSetterCall().toJava(targetViewName, expressionCode); + public String toJavaCode(String targetViewName) { + String argCode = CodeGenUtil.Companion.toCode(getExpr(), false).generate(); + return getSetterCall().toJava(targetViewName, argCode); } /** @@ -70,15 +72,6 @@ public class Binding { return getSetterCall().getMinApi(); } -// private String resolveJavaCode(ModelAnalyzer modelAnalyzer) { -// -// } -//// return modelAnalyzer.findMethod(mTarget.getResolvedType(), mName, -//// Arrays.asList(mExpr.getResolvedType())); -// //} -// - - public String getName() { return mName; } diff --git a/compiler/src/main/java/android/databinding/tool/BindingTarget.java b/compiler/src/main/java/android/databinding/tool/BindingTarget.java index bc52a2a1..da2c5036 100644 --- a/compiler/src/main/java/android/databinding/tool/BindingTarget.java +++ b/compiler/src/main/java/android/databinding/tool/BindingTarget.java @@ -16,15 +16,24 @@ package android.databinding.tool; +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; + import android.databinding.tool.expr.Expr; import android.databinding.tool.expr.ExprModel; import android.databinding.tool.reflection.ModelAnalyzer; import android.databinding.tool.reflection.ModelClass; import android.databinding.tool.store.ResourceBundle; import android.databinding.tool.store.SetterStore; +import android.databinding.tool.util.L; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class BindingTarget { List<Binding> mBindings = new ArrayList<Binding>(); @@ -99,4 +108,66 @@ public class BindingTarget { public void setModel(ExprModel model) { mModel = model; } + + /** + * Called after BindingTarget is finalized. + * <p> + * We traverse all bindings and ask SetterStore to figure out if any can be combined. + * When N bindings are combined, they are demoted from being a binding expression and a new + * ArgList expression is added as the new binding expression that depends on others. + */ + public void resolveMultiSetters() { + L.d("resolving multi setters for %s", getId()); + final SetterStore setterStore = SetterStore.get(ModelAnalyzer.getInstance()); + final String[] attributes = new String[mBindings.size()]; + final ModelClass[] types = new ModelClass[mBindings.size()]; + for (int i = 0; i < mBindings.size(); i ++) { + Binding binding = mBindings.get(i); + attributes[i] = binding.getName(); + types[i] = binding.getExpr().getResolvedType(); + } + final List<SetterStore.MultiAttributeSetter> multiAttributeSetterCalls = setterStore + .getMultiAttributeSetterCalls(attributes, getResolvedType(), types); + if (multiAttributeSetterCalls.isEmpty()) { + return; + } + final Map<String, Binding> lookup = new HashMap<String, Binding>(); + for (Binding binding : mBindings) { + String name = binding.getName(); + if (name.startsWith("android:")) { + lookup.put(name, binding); + } else { + int ind = name.indexOf(":"); + if (ind == -1) { + lookup.put(name, binding); + } else { + lookup.put(name.substring(ind + 1), binding); + } + } + } + List<MergedBinding> mergeBindings = new ArrayList<MergedBinding>(); + for (final SetterStore.MultiAttributeSetter setter : multiAttributeSetterCalls) { + L.d("resolved %s", setter); + final Binding[] mergedBindings = Iterables.toArray( + Iterables.transform(Arrays.asList(setter.attributes), + new Function<String, Binding>() { + @Override + public Binding apply(final String attribute) { + L.d("looking for binding for attribute %s", attribute); + return lookup.get(attribute); + } + }), Binding.class) ; + Preconditions.checkArgument(mergedBindings.length == setter.attributes.length); + for (Binding binding : mergedBindings) { + binding.getExpr().setBindingExpression(false); + mBindings.remove(binding); + } + MergedBinding mergedBinding = new MergedBinding(getModel(), setter, this, + Arrays.asList(mergedBindings)); + mergeBindings.add(mergedBinding); + } + for (MergedBinding binding : mergeBindings) { + mBindings.add(binding); + } + } } diff --git a/compiler/src/main/java/android/databinding/tool/LayoutBinder.java b/compiler/src/main/java/android/databinding/tool/LayoutBinder.java index d8669271..669ed96e 100644 --- a/compiler/src/main/java/android/databinding/tool/LayoutBinder.java +++ b/compiler/src/main/java/android/databinding/tool/LayoutBinder.java @@ -22,8 +22,10 @@ import android.databinding.tool.expr.Dependency; import android.databinding.tool.expr.Expr; import android.databinding.tool.expr.ExprModel; import android.databinding.tool.expr.IdentifierExpr; +import android.databinding.tool.reflection.ModelAnalyzer; import android.databinding.tool.store.ResourceBundle; import android.databinding.tool.store.ResourceBundle.BindingTargetBundle; +import android.databinding.tool.store.SetterStore; import android.databinding.tool.util.ParserHelper; import android.databinding.tool.writer.LayoutBinderWriter; import android.databinding.tool.writer.WriterPackage; @@ -81,6 +83,7 @@ public class LayoutBinder { .getBindingBundleList()) { bindingTarget.addBinding(bindingBundle.getName(), parse(bindingBundle.getExpr())); } + bindingTarget.resolveMultiSetters(); } mSortedBindingTargets = new ArrayList<BindingTarget>(mBindingTargets); Collections.sort(mSortedBindingTargets, COMPARE_FIELD_NAME); diff --git a/compiler/src/main/java/android/databinding/tool/MergedBinding.java b/compiler/src/main/java/android/databinding/tool/MergedBinding.java new file mode 100644 index 00000000..174f89da --- /dev/null +++ b/compiler/src/main/java/android/databinding/tool/MergedBinding.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.databinding.tool; + +import com.google.common.base.Function; +import com.google.common.collect.Iterables; + +import org.apache.commons.lang3.StringUtils; + +import android.databinding.tool.expr.ArgListExpr; +import android.databinding.tool.expr.Expr; +import android.databinding.tool.expr.ExprModel; +import android.databinding.tool.reflection.ModelAnalyzer; +import android.databinding.tool.store.SetterStore; +import android.databinding.tool.util.L; +import android.databinding.tool.writer.CodeGenUtil; + +import java.util.List; + +/** + * Multiple binding expressions can be evaluated using a single adapter. In those cases, + * we replace the Binding with a MergedBinding. + */ +public class MergedBinding extends Binding { + private final SetterStore.MultiAttributeSetter mMultiAttributeSetter; + public MergedBinding(ExprModel model, SetterStore.MultiAttributeSetter multiAttributeSetter, + BindingTarget target, Iterable<Binding> bindings) { + super(target, createMergedName(bindings), createArgListExpr(model, bindings)); + mMultiAttributeSetter = multiAttributeSetter; + } + + private static Expr createArgListExpr(ExprModel model, final Iterable<Binding> bindings) { + Expr expr = model.argListExpr(Iterables.transform(bindings, new Function<Binding, Expr>() { + @Override + public Expr apply(Binding input) { + return input.getExpr(); + } + })); + expr.setBindingExpression(true); + return expr; + } + + private static String createMergedName(Iterable<Binding> bindings) { + return Iterables.toString(Iterables.transform(bindings, new Function<Binding, String>() { + @Override + public String apply(Binding input) { + return input.getName(); + } + })); + } + + @Override + public int getMinApi() { + return 1; + } + + @Override + public String toJavaCode(String targetViewName) { + ArgListExpr args = (ArgListExpr) getExpr(); + final String[] expressions = Iterables + .toArray(Iterables.transform(args.getChildren(), new Function<Expr, String>() { + @Override + public String apply(Expr input) { + return CodeGenUtil.Companion.toCode(input, false).generate(); + } + }), String.class); + L.d("merged binding arg: %s", args.getUniqueKey()); + return mMultiAttributeSetter.toJava(targetViewName, expressions); + } +} diff --git a/compiler/src/main/java/android/databinding/tool/expr/ArgListExpr.java b/compiler/src/main/java/android/databinding/tool/expr/ArgListExpr.java new file mode 100644 index 00000000..91ae6495 --- /dev/null +++ b/compiler/src/main/java/android/databinding/tool/expr/ArgListExpr.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.databinding.tool.expr; + +import android.databinding.tool.reflection.ModelAnalyzer; +import android.databinding.tool.reflection.ModelClass; + +import java.util.List; + +/** + * This is a special expression that is created when we have an adapter that has multiple + * parameters. + * <p> + * When it is detected, we create a new binding with this argument list expression and merge N + * bindings into a new one so that rest of the code generation logic works as expected. + */ +public class ArgListExpr extends Expr { + private int mId; + public ArgListExpr(int id, Iterable<Expr> children) { + super(children); + mId = id; + } + + @Override + protected String computeUniqueKey() { + return "ArgList[" + mId + "]" + super.computeUniqueKey(); + } + + @Override + protected ModelClass resolveType(ModelAnalyzer modelAnalyzer) { + return modelAnalyzer.findClass(Void.class); + } + + @Override + protected List<Dependency> constructDependencies() { + return super.constructDynamicChildrenDependencies(); + } + + @Override + public boolean canBeEvaluatedToAVariable() { + return false; + } +} diff --git a/compiler/src/main/java/android/databinding/tool/expr/Expr.java b/compiler/src/main/java/android/databinding/tool/expr/Expr.java index 84f16404..7e3849c0 100644 --- a/compiler/src/main/java/android/databinding/tool/expr/Expr.java +++ b/compiler/src/main/java/android/databinding/tool/expr/Expr.java @@ -25,6 +25,7 @@ import com.google.common.collect.Lists; import android.databinding.tool.reflection.ModelAnalyzer; import android.databinding.tool.reflection.ModelClass; +import android.databinding.tool.writer.KCode; import java.util.ArrayList; import java.util.BitSet; @@ -144,6 +145,10 @@ abstract public class Expr { return mIsBindingExpression; } + public boolean canBeEvaluatedToAVariable() { + return true; // anything except arg expr can be evaluated to a variable + } + public boolean isObservable() { return getResolvedType().isObservable(); } @@ -199,7 +204,7 @@ abstract public class Expr { for (Dependency dependency : getDependants()) { final boolean isElevated = unreadElevatedCheck.apply(dependency); if (dependency.isConditional()) { - continue; // TODO + continue; // will be resolved later when conditional is elevated } if (isElevated) { // if i already have all flags that will require my dependant's predicate to diff --git a/compiler/src/main/java/android/databinding/tool/expr/ExprModel.java b/compiler/src/main/java/android/databinding/tool/expr/ExprModel.java index 37455452..0847e287 100644 --- a/compiler/src/main/java/android/databinding/tool/expr/ExprModel.java +++ b/compiler/src/main/java/android/databinding/tool/expr/ExprModel.java @@ -42,6 +42,9 @@ public class ExprModel { private int mRequirementIdCount = 0; + // each arg list receives a unique id even if it is the same arguments and method. + private int mArgListIdCounter = 0; + private static final String TRUE_KEY_SUFFIX = "== true"; private static final String FALSE_KEY_SUFFIX = "== false"; @@ -67,6 +70,8 @@ public class ExprModel { private List<Expr> mObservables; + private boolean mSealed = false; + private Map<String, String> mImports = new HashMap<String, String>(); /** @@ -77,6 +82,7 @@ public class ExprModel { * @return The expression itself or another one if the same thing was parsed before */ public <T extends Expr> T register(T expr) { + Preconditions.checkState(!mSealed, "Cannot add expressions to a model after it is sealed"); T existing = (T) mExprMap.get(expr.getUniqueKey()); if (existing != null) { Preconditions.checkState(expr.getParents().isEmpty(), @@ -371,7 +377,7 @@ public class ExprModel { expr.getResolvedType(); } - + mSealed = true; } public int getFlagBucketCount() { @@ -529,4 +535,8 @@ public class ExprModel { public BitSet getInvalidateAnyBitSet() { return mInvalidateAnyFlags; } + + public Expr argListExpr(Iterable<Expr> expressions) { + return register(new ArgListExpr(mArgListIdCounter ++, expressions)); + } } diff --git a/compiler/src/main/java/android/databinding/tool/expr/FieldAccessExpr.java b/compiler/src/main/java/android/databinding/tool/expr/FieldAccessExpr.java index ad0d948a..24e62323 100644 --- a/compiler/src/main/java/android/databinding/tool/expr/FieldAccessExpr.java +++ b/compiler/src/main/java/android/databinding/tool/expr/FieldAccessExpr.java @@ -105,7 +105,7 @@ public class FieldAccessExpr extends Expr { child.resolveType(modelAnalyzer); boolean isStatic = child instanceof StaticIdentifierExpr; ModelClass resolvedType = child.getResolvedType(); - L.d("resolving %s. Resolved type: %s", this, resolvedType); + L.d("resolving %s. Resolved class type: %s", this, resolvedType); mGetter = resolvedType.findGetterOrField(mName, isStatic); if (mGetter.resolvedType.isObservableField()) { diff --git a/compiler/src/main/java/android/databinding/tool/store/SetterStore.java b/compiler/src/main/java/android/databinding/tool/store/SetterStore.java index 0066ca15..2b2442e5 100644 --- a/compiler/src/main/java/android/databinding/tool/store/SetterStore.java +++ b/compiler/src/main/java/android/databinding/tool/store/SetterStore.java @@ -857,7 +857,13 @@ public class SetterStore { } } - public static abstract class SetterCall { + public static interface BindingSetterCall { + String toJava(String viewExpression, String... valueExpressions); + + int getMinApi(); + } + + public static abstract class SetterCall implements BindingSetterCall { private MethodDescription mConverter; protected String mCastString = ""; @@ -870,8 +876,10 @@ public class SetterStore { protected abstract String toJavaInternal(String viewExpression, String converted); - public final String toJava(String viewExpression, String valueExpression) { - return toJavaInternal(viewExpression, convertValue(valueExpression)); + @Override + public final String toJava(String viewExpression, String... valueExpression) { + Preconditions.checkArgument(valueExpression.length == 1); + return toJavaInternal(viewExpression, convertValue(valueExpression[0])); } protected String convertValue(String valueExpression) { @@ -886,7 +894,7 @@ public class SetterStore { } } - public static class MultiAttributeSetter { + public static class MultiAttributeSetter implements BindingSetterCall { public final String[] attributes; private final MethodDescription mAdapter; private final MethodDescription[] mConverters; @@ -905,8 +913,11 @@ public class SetterStore { this.mKey = key; } - public String toJava(String viewExpression, String[] valueExpressions) { - Preconditions.checkArgument(valueExpressions.length == attributes.length); + @Override + public final String toJava(String viewExpression, String[] valueExpressions) { + Preconditions.checkArgument(valueExpressions.length == attributes.length, + "MultiAttributeSetter needs %s items, received %s", + Arrays.toString(attributes), Arrays.toString(valueExpressions)); StringBuilder sb = new StringBuilder(); sb.append(mAdapter.type) .append('.') @@ -935,5 +946,21 @@ public class SetterStore { sb.append(')'); return sb.toString(); } + + @Override + public int getMinApi() { + return 1; + } + + @Override + public String toString() { + return "MultiAttributeSetter{" + + "attributes=" + Arrays.toString(attributes) + + ", mAdapter=" + mAdapter + + ", mConverters=" + Arrays.toString(mConverters) + + ", mCasts=" + Arrays.toString(mCasts) + + ", mKey=" + mKey + + '}'; + } } } diff --git a/compiler/src/main/kotlin/android/databinding/tool/writer/CodeGenUtil.kt b/compiler/src/main/kotlin/android/databinding/tool/writer/CodeGenUtil.kt new file mode 100644 index 00000000..4ddfe88a --- /dev/null +++ b/compiler/src/main/kotlin/android/databinding/tool/writer/CodeGenUtil.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.databinding.tool.writer + +import android.databinding.tool.expr.* +import android.databinding.tool.reflection.Callable +import android.databinding.tool.writer.KCode +import android.databinding.tool.writer.kcode + +class CodeGenUtil { + companion object { + fun toCode(it : Expr, full : Boolean) : KCode { + if (it.isDynamic() && !full) { + return kcode(it.executePendingLocalName) + } + return when (it) { + is ComparisonExpr -> kcode("") { + app("", it.getLeft().toCode()) + app(it.getOp()) + app("", it.getRight().toCode()) + } + is InstanceOfExpr -> kcode("") { + app("", it.getExpr().toCode()) + app(" instanceof ") + app("", it.getType().toJavaCode()) + } + is FieldAccessExpr -> kcode("") { + app("", it.getChild().toCode()) + if (it.getGetter().type == Callable.Type.FIELD) { + app(".", it.getGetter().name) + } else { + app(".", it.getGetter().name).app("()") + } + } + is GroupExpr -> kcode("(").app("", it.getWrapped().toCode()).app(")") + is StaticIdentifierExpr -> kcode(it.getResolvedType().toJavaCode()) + is IdentifierExpr -> kcode(it.executePendingLocalName) + is MathExpr -> kcode("") { + app("", it.getLeft().toCode()) + app(it.getOp()) + app("", it.getRight().toCode()) + } + is UnaryExpr -> kcode("") { + app(it.getOp(), it.getExpr().toCode()) + } + is BitShiftExpr -> kcode("") { + app("", it.getLeft().toCode()) + app(it.getOp()) + app("", it.getRight().toCode()) + } + is MethodCallExpr -> kcode("") { + app("", it.getTarget().toCode()) + app(".", it.getGetter().name) + app("(") + var first = true + it.getArgs().forEach { + apps(if (first) "" else ",", it.toCode()) + first = false + } + app(")") + } + is SymbolExpr -> kcode(it.getText()) // TODO + is TernaryExpr -> kcode("") { + app("", it.getPred().toCode()) + app("?", it.getIfTrue().toCode()) + app(":", it.getIfFalse().toCode()) + } + is ResourceExpr -> kcode("") { + app("", it.toJava()) + } + is BracketExpr -> kcode("") { + app("", it.getTarget().toCode()) + val bracketType = it.getAccessor(); + when (bracketType) { + BracketExpr.BracketAccessor.ARRAY -> { + app("[", it.getArg().toCode()) + app("]") + } + BracketExpr.BracketAccessor.LIST -> { + app(".get(") + if (it.argCastsInteger()) { + app("(Integer)") + } + app("", it.getArg().toCode()) + app(")") + } + BracketExpr.BracketAccessor.MAP -> { + app(".get(", it.getArg().toCode()) + app(")") + } + } + } + is CastExpr -> kcode("") { + app("(", it.getCastType()) + app(") ", it.getCastExpr().toCode()) + } + is ArgListExpr -> throw IllegalStateException("should never try to convert an argument expressions into code"); + else -> kcode("//NOT IMPLEMENTED YET") + } + } + } +}
\ No newline at end of file diff --git a/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt b/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt index 77605c3f..01d5f277 100644 --- a/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt +++ b/compiler/src/main/kotlin/android/databinding/tool/writer/LayoutBinderWriter.kt @@ -14,6 +14,8 @@ package android.databinding.tool.writer import android.databinding.tool.LayoutBinder +import kotlin.properties.Delegates +import android.databinding.tool.BindingTarget import android.databinding.tool.expr.Expr import kotlin.properties.Delegates import android.databinding.tool.ext.joinToCamelCaseAsVar @@ -39,8 +41,11 @@ import android.databinding.tool.ext.lazy import android.databinding.tool.ext.br import android.databinding.tool.expr.ResourceExpr import android.databinding.tool.expr.BracketExpr +import android.databinding.tool.ext; +import android.databinding.tool.util.Log +import java.util.BitSet +import java.util.Arrays import android.databinding.tool.reflection.Callable -import android.databinding.tool.expr.CastExpr import android.databinding.tool.reflection.ModelAnalyzer import java.util.ArrayList import java.util.HashMap @@ -167,96 +172,7 @@ val Expr.dirtyFlagName by Delegates.lazy { expr : Expr -> } -fun Expr.toCode(full : Boolean = false) : KCode { - val it = this - if (isDynamic() && !full) { - return kcode(executePendingLocalName) - } - return when (it) { - is ComparisonExpr -> kcode("") { - app("", it.getLeft().toCode()) - app(it.getOp()) - app("", it.getRight().toCode()) - } - is InstanceOfExpr -> kcode("") { - app("", it.getExpr().toCode()) - app(" instanceof ") - app("", it.getType().toJavaCode()) - } - is FieldAccessExpr -> kcode("") { - app("", it.getChild().toCode()) - if (it.getGetter().type == Callable.Type.FIELD) { - app(".", it.getGetter().name) - } else { - app(".", it.getGetter().name).app("()") - } - } - is GroupExpr -> kcode("(").app("", it.getWrapped().toCode()).app(")") - is StaticIdentifierExpr -> kcode(it.getResolvedType().toJavaCode()) - is IdentifierExpr -> kcode(it.executePendingLocalName) - is MathExpr -> kcode("") { - app("", it.getLeft().toCode()) - app(it.getOp()) - app("", it.getRight().toCode()) - } - is UnaryExpr -> kcode("") { - app(it.getOp(), it.getExpr().toCode()) - } - is BitShiftExpr -> kcode("") { - app("", it.getLeft().toCode()) - app(it.getOp()) - app("", it.getRight().toCode()) - } - is MethodCallExpr -> kcode("") { - app("", it.getTarget().toCode()) - app(".", it.getGetter().name) - app("(") - var first = true - it.getArgs().forEach { - apps(if (first) "" else ",", it.toCode()) - first = false - } - app(")") - } - is SymbolExpr -> kcode(it.getText()) // TODO - is TernaryExpr -> kcode("") { - app("", it.getPred().toCode()) - app("?", it.getIfTrue().toCode()) - app(":", it.getIfFalse().toCode()) - } - is ResourceExpr -> kcode("") { - app("", it.toJava()) - } - is BracketExpr -> kcode("") { - app("", it.getTarget().toCode()) - val bracketType = it.getAccessor(); - when (bracketType) { - BracketExpr.BracketAccessor.ARRAY -> { - app("[", it.getArg().toCode()) - app("]") - } - BracketExpr.BracketAccessor.LIST -> { - app(".get(") - if (it.argCastsInteger()) { - app("(Integer)") - } - app("", it.getArg().toCode()) - app(")") - } - BracketExpr.BracketAccessor.MAP -> { - app(".get(", it.getArg().toCode()) - app(")") - } - } - } - is CastExpr -> kcode("") { - app("(", it.getCastType()) - app(") ", it.getCastExpr().toCode()) - } - else -> kcode("//NOT IMPLEMENTED YET") - } - -} +fun Expr.toCode(full : Boolean = false) : KCode = CodeGenUtil.toCode(this, full) fun Expr.isVariable() = this is IdentifierExpr && this.isDynamic() @@ -730,7 +646,7 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) { tab("${mDirtyFlags.localValue(i)} = 0;") } } tab("}") - model.getPendingExpressions().filterNot {it.isVariable()}.forEach { + model.getPendingExpressions().filterNot {!it.canBeEvaluatedToAVariable() || it.isVariable()}.forEach { tab("${it.getResolvedType().toJavaCode()} ${it.executePendingLocalName} = ${it.getDefaultValue()};") } Log.d {"writing executePendingBindings for $className"} @@ -772,7 +688,7 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) { } else { fieldName = "((${binding.getTarget().getViewClass()}) this.${binding.getTarget().fieldName})" } - val bindingCode = binding.toJavaCode(fieldName, binding.getExpr().toCode().generate()) + val bindingCode = binding.toJavaCode(fieldName) if (binding.getMinApi() > 1) { tab("if(getBuildSdkInt() >= ${binding.getMinApi()}) {") { tab("$bindingCode;") @@ -813,7 +729,7 @@ class LayoutBinderWriter(val layoutBinder : LayoutBinder) { })" val readCode = kcode("") { - if (!expr.isVariable()) { + if (expr.canBeEvaluatedToAVariable() && !expr.isVariable()) { // it is not a variable read it. tab("// read ${expr.getUniqueKey()}") // create an if case for all dependencies that might be null diff --git a/integration-tests/TestApp/app/src/androidTest/java/android/databinding/testapp/MultiArgAdapterEvaluationTest.java b/integration-tests/TestApp/app/src/androidTest/java/android/databinding/testapp/MultiArgAdapterEvaluationTest.java new file mode 100644 index 00000000..ac0a8ffe --- /dev/null +++ b/integration-tests/TestApp/app/src/androidTest/java/android/databinding/testapp/MultiArgAdapterEvaluationTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.databinding.testapp; + +import android.databinding.testapp.BR; +import android.databinding.testapp.databinding.MultiArgAdapterEvaluationTestBinding; +import android.databinding.testapp.databinding.MultiArgAdapterTestBinding; +import android.test.UiThreadTest; + +import static android.databinding.testapp.adapter.MultiArgTestAdapter.MultiBindingClass1; +import static android.databinding.testapp.adapter.MultiArgTestAdapter.MultiBindingClass2; +import static android.databinding.testapp.adapter.MultiArgTestAdapter.join; + +public class MultiArgAdapterEvaluationTest extends BaseDataBinderTest<MultiArgAdapterEvaluationTestBinding> { + + public MultiArgAdapterEvaluationTest() { + super(MultiArgAdapterEvaluationTestBinding.class); + } + + @UiThreadTest + public void testMultiArgIsCalled() { + MultiBindingClass1 obj1 = new MultiBindingClass1(); + MultiBindingClass2 obj2 = new MultiBindingClass2(); + obj1.setValue("a", false); + obj2.setValue("b", false); + mBinder.setObj1(obj1); + mBinder.setObj2(obj2); + mBinder.executePendingBindings(); + + assertEquals(mBinder.merged.getText().toString(), join(obj1.getValue(), obj2.getValue())); + assertEquals(mBinder.view2.getText().toString(), join(obj2.getValue())); + assertEquals(mBinder.view2text.getText().toString(), obj2.getValue()); + + String prev2 = mBinder.view2.getText().toString(); + String prevValue = mBinder.merged.getText().toString(); + obj1.setValue("o", false); + mBinder.executePendingBindings(); + assertEquals(prevValue, mBinder.merged.getText().toString()); + obj2.setValue("p", false); + mBinder.executePendingBindings(); + assertEquals(prevValue, mBinder.merged.getText().toString()); + // now invalidate obj1 only, obj2 should be evaluated as well + obj1.setValue("o2", true); + mBinder.executePendingBindings(); + assertEquals(join(obj1, obj2), mBinder.merged.getText().toString()); + assertEquals("obj2 should not be re-evaluated", prev2, mBinder.view2.getText().toString()); + assertEquals("obj2 should not be re-evaluated", prev2, + mBinder.view2text.getText().toString()); + } +} diff --git a/integration-tests/TestApp/app/src/androidTest/java/android/databinding/testapp/MultiArgAdapterTest.java b/integration-tests/TestApp/app/src/androidTest/java/android/databinding/testapp/MultiArgAdapterTest.java new file mode 100644 index 00000000..d117b9a4 --- /dev/null +++ b/integration-tests/TestApp/app/src/androidTest/java/android/databinding/testapp/MultiArgAdapterTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.databinding.testapp; + +import android.databinding.testapp.adapter.MultiArgTestAdapter; +import android.databinding.testapp.databinding.MultiArgAdapterTestBinding; +import android.test.UiThreadTest; +import android.view.View; +import android.databinding.testapp.BR; +import static android.databinding.testapp.adapter.MultiArgTestAdapter.*; + +public class MultiArgAdapterTest extends BaseDataBinderTest<MultiArgAdapterTestBinding> { + + public MultiArgAdapterTest() { + super(MultiArgAdapterTestBinding.class); + } + + @UiThreadTest + public void testMultiArgIsCalled() { + MultiBindingClass1 obj1 = new MultiBindingClass1(); + MultiBindingClass2 obj2 = new MultiBindingClass2(); + MultiBindingClass1 obj3 = new MultiBindingClass1(); + MultiBindingClass2 obj4 = new MultiBindingClass2(); + obj1.setValue("a", false); + obj2.setValue("b", false); + obj3.setValue("c", false); + obj4.setValue("d", false); + mBinder.setObj1(obj1); + mBinder.setObj2(obj2); + mBinder.setObj3(obj3); + mBinder.setObj4(obj4); + mBinder.executePendingBindings(); + + assertEquals(mBinder.merged.getText().toString(), join(obj1, obj2)); + assertEquals(mBinder.view2.getText().toString(), join(obj2)); + assertEquals(mBinder.view3.getText().toString(), join(obj3)); + assertEquals(mBinder.view4.getText().toString(), join(obj4)); + String prev2 = mBinder.view2.getText().toString(); + String prevValue = mBinder.merged.getText().toString(); + obj1.setValue("o", false); + mBinder.executePendingBindings(); + assertEquals(prevValue, mBinder.merged.getText().toString()); + obj2.setValue("p", false); + mBinder.executePendingBindings(); + assertEquals(prevValue, mBinder.merged.getText().toString()); + // now invalidate obj1 only, obj2 should be evaluated as well + obj1.notifyPropertyChanged(BR._all); + String prev3 = mBinder.view3.getText().toString(); + String prev4 = mBinder.view4.getText().toString(); + obj3.setValue("q", false); + obj4.setValue("r", false); + mBinder.executePendingBindings(); + assertEquals(join(obj1, obj2), mBinder.merged.getText().toString()); + assertEquals("obj2 should not be re-evaluated", prev2, mBinder.view2.getText().toString()); + // make sure 3 and 4 are not invalidated + assertEquals("obj3 should not be re-evaluated", prev3, mBinder.view3.getText().toString()); + assertEquals("obj4 should not be re-evaluated", prev4, mBinder.view4.getText().toString()); + + + } + + +} diff --git a/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/adapter/MultiArgTestAdapter.java b/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/adapter/MultiArgTestAdapter.java new file mode 100644 index 00000000..a2702798 --- /dev/null +++ b/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/adapter/MultiArgTestAdapter.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.databinding.testapp.adapter; + + +import android.databinding.BaseObservable; +import android.databinding.Bindable; +import android.databinding.BindingAdapter; +import android.view.View; +import android.databinding.testapp.BR; +import android.widget.TextView; + +import org.apache.commons.lang3.StringUtils; + +public class MultiArgTestAdapter { + + public static String join(BaseMultiBindingClass... classes) { + StringBuilder sb = new StringBuilder(); + for(BaseMultiBindingClass instance : classes) { + sb.append(instance == null ? "??" : instance.getValue()); + } + return sb.toString(); + } + + public static String join(String... strings) { + StringBuilder sb = new StringBuilder(); + for(String str : strings) { + sb.append(str == null ? "??" : str); + } + return sb.toString(); + + } + + @BindingAdapter(attributes={"android:class1", "android:class2"}) + public static void setBoth(TextView view, MultiBindingClass1 class1, + MultiBindingClass2 class2) { + view.setText(join(class1, class2)); + } + + @BindingAdapter(attributes={"android:class1str", "android:class2str"}) + public static void setBoth(TextView view, String str1, + String str2) { + view.setText(join(str1, str2)); + } + + @BindingAdapter(attributes={"android:class1"}) + public static void setClass1(TextView view, MultiBindingClass1 class1) { + view.setText(class1.getValue()); + } + + @BindingAdapter(attributes={"android:classStr"}) + public static void setClassStr(TextView view, String str) { + view.setText(str); + } + + @BindingAdapter(attributes={"android:class2"}) + public static void setClass2(TextView view, MultiBindingClass2 class2) { + view.setText(class2.getValue()); + } + + public static class MultiBindingClass1 extends BaseMultiBindingClass { + + } + + public static class MultiBindingClass2 extends BaseMultiBindingClass { + + } + + public static class BaseMultiBindingClass extends BaseObservable { + View mSetOn; + @Bindable + String mValue; + public View getSetOn() { + return mSetOn; + } + + public String getValue() { + return mValue; + } + + public void setValue(String value, boolean notify) { + mValue = value; + if (notify) { + notifyPropertyChanged(BR.value); + } + } + + public void clear() { + mSetOn = null; + } + } +} diff --git a/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/vo/MultiAdapterSetterObj.java b/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/vo/MultiAdapterSetterObj.java new file mode 100644 index 00000000..c23a41a8 --- /dev/null +++ b/integration-tests/TestApp/app/src/main/java/android/databinding/testapp/vo/MultiAdapterSetterObj.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.databinding.testapp.vo; + +import android.view.View; + +public class MultiAdapterSetterObj { + public boolean isClickable; + public View.OnClickListener onClickListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + + } + }; +} diff --git a/integration-tests/TestApp/app/src/main/res/layout/multi_arg_adapter_evaluation_test.xml b/integration-tests/TestApp/app/src/main/res/layout/multi_arg_adapter_evaluation_test.xml new file mode 100644 index 00000000..2fbe6a73 --- /dev/null +++ b/integration-tests/TestApp/app/src/main/res/layout/multi_arg_adapter_evaluation_test.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<layout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:bind="http://schemas.android.com/apk/res-auto"> + + <data> + + <variable + name="obj1" + type="android.databinding.testapp.adapter.MultiArgTestAdapter.MultiBindingClass1" /> + + <variable + name="obj2" + type="android.databinding.testapp.adapter.MultiArgTestAdapter.MultiBindingClass2" /> + </data> + + <LinearLayout + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/merged" + android:class1str="@{obj1.value}" + android:class2str="@{obj2.value}" /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/view2" + android:text="@{obj2.value}" /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/view2text" + android:classStr="@{obj2.value}" /> + </LinearLayout> +</layout>
\ No newline at end of file diff --git a/integration-tests/TestApp/app/src/main/res/layout/multi_arg_adapter_test.xml b/integration-tests/TestApp/app/src/main/res/layout/multi_arg_adapter_test.xml new file mode 100644 index 00000000..fd8d4411 --- /dev/null +++ b/integration-tests/TestApp/app/src/main/res/layout/multi_arg_adapter_test.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<layout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:bind="http://schemas.android.com/apk/res-auto"> + + <data> + + <variable + name="obj1" + type="android.databinding.testapp.adapter.MultiArgTestAdapter.MultiBindingClass1" /> + + <variable + name="obj2" + type="android.databinding.testapp.adapter.MultiArgTestAdapter.MultiBindingClass2" /> + + <variable + name="obj3" + type="android.databinding.testapp.adapter.MultiArgTestAdapter.MultiBindingClass1" /> + + <variable + name="obj4" + type="android.databinding.testapp.adapter.MultiArgTestAdapter.MultiBindingClass2" /> + </data> + + <LinearLayout + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/merged" + android:class1="@{obj1}" + android:class2="@{obj2}" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/view3" + android:class1="@{obj3}" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/view4" + android:class2="@{obj4}" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/view2" + android:class2="@{obj2}" /> + </LinearLayout> +</layout>
\ No newline at end of file |