From 8acc25c8af512527afbf67d5c507e26292c0943b Mon Sep 17 00:00:00 2001 From: Leonard Chioveanu Date: Fri, 13 Jan 2017 14:28:17 +0200 Subject: [PATCH 1/5] Add ability to create views via a factory function. --- anvil/src/main/java/trikita/anvil/Anvil.java | 34 ++++++++++++++++++- .../src/main/java/trikita/anvil/BaseDSL.java | 11 ++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/anvil/src/main/java/trikita/anvil/Anvil.java b/anvil/src/main/java/trikita/anvil/Anvil.java index 96404fd3..87c34468 100644 --- a/anvil/src/main/java/trikita/anvil/Anvil.java +++ b/anvil/src/main/java/trikita/anvil/Anvil.java @@ -33,6 +33,10 @@ public final class Anvil { private static Handler anvilUIHandler = null; + public interface FactoryFunc { + T apply(Context context); + } + /** Renderable can be mounted and rendered using Anvil library. */ public interface Renderable { /** This method is a place to define the structure of your layout, its view @@ -46,12 +50,16 @@ public interface AttrFunc { } interface ViewFactory { + View fromFactoryFunc(Context c, FactoryFunc factoryFunc); View fromClass(Context c, Class v); View fromXml(Context c, int xmlId); View fromId(View v, int viewId); } final static ViewFactory viewFactory = new ViewFactory() { + public View fromFactoryFunc(Context c, FactoryFunc factoryFunc) { + return factoryFunc.apply(c); + } public View fromClass(Context c, Class viewClass) { try { return viewClass.getConstructor(Context.class).newInstance(c); @@ -231,6 +239,27 @@ Node startNode() { return node; } + void startFromFactory(FactoryFunc viewFactoryFunc) { + Node node = startNode(); + View view = node.view; + if (view == null || node.viewFactoryFunc != viewFactoryFunc) { + node.layoutId = 0; + node.viewClass = null; + node.viewFactoryFunc = viewFactoryFunc; + node.children.clear(); + node.attrs.clear(); + if (view != null) { + node.parentView.removeView(view); + } + View v = viewFactory.fromFactoryFunc(node.parentView.getContext(), viewFactoryFunc); + if (node.viewIndex == -1) { + node.viewIndex = node.parentView.getChildCount(); + } + node.parentView.addView(v, node.viewIndex); + node.view = v; + } + } + // Create/replace view object from the given view class void startFromClass(Class viewClass) { Node node = startNode(); @@ -238,6 +267,7 @@ void startFromClass(Class viewClass) { if (view == null || node.viewClass != viewClass) { node.layoutId = 0; node.viewClass = viewClass; + node.viewFactoryFunc = null; node.children.clear(); node.attrs.clear(); if (view != null) { @@ -258,6 +288,7 @@ void startFromLayout(int layoutId) { if (node.layoutId != layoutId) { node.layoutId = layoutId; node.viewClass = null; + node.viewFactoryFunc = null; node.children.clear(); node.attrs.clear(); if (node.view != null) { @@ -311,7 +342,8 @@ private final static class Node { // Index of the real view inside the parent viewgroup private int viewIndex = -1; - // view class or layout id given when the node was last updated + // view factory func, class or layout id given when the node was last updated + private FactoryFunc viewFactoryFunc; private Class viewClass; private int layoutId; diff --git a/anvil/src/main/java/trikita/anvil/BaseDSL.java b/anvil/src/main/java/trikita/anvil/BaseDSL.java index 8d983ca9..2d553b1d 100644 --- a/anvil/src/main/java/trikita/anvil/BaseDSL.java +++ b/anvil/src/main/java/trikita/anvil/BaseDSL.java @@ -716,6 +716,11 @@ public static ViewClassResult v(Class c) { return null; } + public static ViewClassResult v(Anvil.FactoryFunc viewFactoryFunc) { + Anvil.currentMount().startFromFactory(viewFactoryFunc); + return null; + } + public static ViewClassResult xml(int layoutId) { Anvil.currentMount().startFromLayout(layoutId); return null; @@ -735,6 +740,12 @@ public static Void v(Class c, Anvil.Renderable r) { return end(); } + public static Void v(Anvil.FactoryFunc c, Anvil.Renderable r) { + v(c); + r.view(); + return end(); + } + public static Void xml(int layoutId, Anvil.Renderable r) { xml(layoutId); r.view(); From 24afcaf3f66ee15e1dd2d19796a14686390308dd Mon Sep 17 00:00:00 2001 From: Leonard Chioveanu Date: Fri, 13 Jan 2017 14:47:45 +0200 Subject: [PATCH 2/5] Add / update tests with the new FactoryFunc option. --- .../java/trikita/anvil/CurrentViewTest.java | 26 +++++++++++ .../trikita/anvil/IncrementalRenderTest.java | 44 +++++++++++++++++++ anvil/src/test/java/trikita/anvil/Utils.java | 22 ++++++++++ .../test/java/trikita/anvil/ViewByIdTest.java | 39 ++++++++++++++++ 4 files changed, 131 insertions(+) diff --git a/anvil/src/test/java/trikita/anvil/CurrentViewTest.java b/anvil/src/test/java/trikita/anvil/CurrentViewTest.java index 8cf3487b..fd931676 100644 --- a/anvil/src/test/java/trikita/anvil/CurrentViewTest.java +++ b/anvil/src/test/java/trikita/anvil/CurrentViewTest.java @@ -33,4 +33,30 @@ public void view() { }); assertNull(Anvil.currentView()); } + + @Test + public void testCurrentViewWithFactoryFunc() { + assertNull(Anvil.currentView()); + Anvil.mount(container, new Anvil.Renderable() { + public void view() { + assertTrue(Anvil.currentView() instanceof ViewGroup); + v(MockLayout.FACTORY, new Anvil.Renderable() { + public void view() { + assertTrue(Anvil.currentView() instanceof MockLayout); + v(MockView.FACTORY, new Anvil.Renderable() { + public void view() { + assertTrue(Anvil.currentView() instanceof MockView); + prop("foo", "bar"); + MockView view = Anvil.currentView(); // should cast automatically + assertEquals("bar", view.props.get("foo")); + } + }); + assertTrue(Anvil.currentView() instanceof MockLayout); + } + }); + assertTrue(Anvil.currentView() instanceof ViewGroup); + } + }); + assertNull(Anvil.currentView()); + } } diff --git a/anvil/src/test/java/trikita/anvil/IncrementalRenderTest.java b/anvil/src/test/java/trikita/anvil/IncrementalRenderTest.java index 4685e7c6..d73a5d66 100644 --- a/anvil/src/test/java/trikita/anvil/IncrementalRenderTest.java +++ b/anvil/src/test/java/trikita/anvil/IncrementalRenderTest.java @@ -24,6 +24,20 @@ public void view() { assertEquals(1, (int) changedAttrs.get("foo")); } + @Test + public void testConstantsRenderedOnceWithFactoryFunc() { + Anvil.mount(container, new Anvil.Renderable() { + public void view() { + o(v(MockLayout.FACTORY), prop("foo", "bar")); + } + }); + assertEquals(1, (int) createdViews.get(MockLayout.class)); + assertEquals(1, (int) changedAttrs.get("foo")); + Anvil.render(); + assertEquals(1, (int) createdViews.get(MockLayout.class)); + assertEquals(1, (int) changedAttrs.get("foo")); + } + @Test public void testDynamicAttributeRenderedLazily() { Anvil.mount(container, new Anvil.Renderable() { @@ -71,6 +85,36 @@ public void view() { assertEquals(2, (int) createdViews.get(MockView.class)); } + @Test + public void testDynamicViewRenderedLazilyWithFactoryFunc() { + Anvil.mount(container, new Anvil.Renderable() { + public void view() { + o(v(MockLayout.FACTORY), + o(v(MockLayout.FACTORY)), + showView ? + o(v(MockView.FACTORY)) : + null); + } + }); + MockLayout layout = (MockLayout) container.getChildAt(0); + assertEquals(2, layout.getChildCount()); + assertEquals(1, (int) createdViews.get(MockView.class)); + Anvil.render(); + assertEquals(1, (int) createdViews.get(MockView.class)); + showView = false; + Anvil.render(); + assertEquals(1, layout.getChildCount()); + assertEquals(1, (int) createdViews.get(MockView.class)); + Anvil.render(); + assertEquals(1, (int) createdViews.get(MockView.class)); + showView = true; + Anvil.render(); + assertEquals(2, layout.getChildCount()); + assertEquals(2, (int) createdViews.get(MockView.class)); + Anvil.render(); + assertEquals(2, (int) createdViews.get(MockView.class)); + } + private String firstMountValue = "foo"; private String secondMountValue = "bar"; diff --git a/anvil/src/test/java/trikita/anvil/Utils.java b/anvil/src/test/java/trikita/anvil/Utils.java index ec4c2cdc..041777d6 100644 --- a/anvil/src/test/java/trikita/anvil/Utils.java +++ b/anvil/src/test/java/trikita/anvil/Utils.java @@ -43,6 +43,14 @@ protected void mockViewFactory(Anvil.ViewFactory viewFactory) { } } + @Override + public View fromFactoryFunc(Context c, Anvil.FactoryFunc factoryFunc) { + View v = factoryFunc.apply(c); + Class vClass = v.getClass(); + createdViews.put(vClass, !createdViews.containsKey(vClass) ? 1 : (createdViews.get(vClass) + 1)); + return Mockito.spy(v); + } + public View fromClass(Context c, Class v) { try { createdViews.put(v, !createdViews.containsKey(v) ? 1 : (createdViews.get(v) + 1)); @@ -97,6 +105,13 @@ public Context getContext() { } public static class MockView extends View { + public final static Anvil.FactoryFunc FACTORY = new Anvil.FactoryFunc() { + @Override + public MockView apply(Context context) { + return new MockView(context); + } + }; + public final Map props = new HashMap<>(); public MockView(Context c) { super(c); @@ -110,6 +125,13 @@ public int getId() { } public static class MockLayout extends FrameLayout { + public final static Anvil.FactoryFunc FACTORY = new Anvil.FactoryFunc() { + @Override + public MockLayout apply(Context context) { + return new MockLayout(context); + } + }; + public final Map props = new HashMap<>(); private List children = new ArrayList<>(); diff --git a/anvil/src/test/java/trikita/anvil/ViewByIdTest.java b/anvil/src/test/java/trikita/anvil/ViewByIdTest.java index 93f7f439..7193f5e6 100644 --- a/anvil/src/test/java/trikita/anvil/ViewByIdTest.java +++ b/anvil/src/test/java/trikita/anvil/ViewByIdTest.java @@ -25,6 +25,12 @@ public CustomLayout(Context c) { addView(firstView, 0); addView(secondView, 0); } + public final static Anvil.FactoryFunc FACTORY = new Anvil.FactoryFunc() { + @Override + public CustomLayout apply(Context context) { + return new CustomLayout(context); + } + }; } @Test @@ -59,4 +65,37 @@ public void view() { assertEquals("qux", layout.secondView.props.get("baz")); assertEquals("world", layout.secondView.props.get("hello")); } + + @Test + public void testWithIdWithFactoryFunc() { + Anvil.mount(container, new Anvil.Renderable() { + public void view() { + v(CustomLayout.FACTORY, new Anvil.Renderable() { + public void view() { + // The order doesn't matter + withId(ID_SECOND, new Anvil.Renderable() { + public void view() { + prop("baz", "qux"); + } + }); + withId(ID_FIRST, new Anvil.Renderable() { + public void view() { + prop("foo", "bar"); + } + }); + // Also, one view can be looked up by id many times + withId(ID_SECOND, new Anvil.Renderable() { + public void view() { + prop("hello", "world"); + } + }); + } + }); + } + }); + CustomLayout layout = (CustomLayout) container.getChildAt(0); + assertEquals("bar", layout.firstView.props.get("foo")); + assertEquals("qux", layout.secondView.props.get("baz")); + assertEquals("world", layout.secondView.props.get("hello")); + } } From 150aab7cb5bd05972314e84a82d8b72a6ffcd3a7 Mon Sep 17 00:00:00 2001 From: Leonard Chioveanu Date: Fri, 13 Jan 2017 14:50:56 +0200 Subject: [PATCH 3/5] anvilgen: lazy factory functions instead of reflection for instantiating views. --- .../trikita/anvilgen/DSLGeneratorTask.kt | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/kotlin/trikita/anvilgen/DSLGeneratorTask.kt b/buildSrc/src/main/kotlin/trikita/anvilgen/DSLGeneratorTask.kt index b202a50f..5a7341da 100644 --- a/buildSrc/src/main/kotlin/trikita/anvilgen/DSLGeneratorTask.kt +++ b/buildSrc/src/main/kotlin/trikita/anvilgen/DSLGeneratorTask.kt @@ -172,18 +172,60 @@ open class DSLGeneratorTask : DefaultTask() { name = toCase(name, { c -> Character.toLowerCase(c) }) val baseDsl = ClassName.get("trikita.anvil", "BaseDSL") val result = ClassName.get("trikita.anvil", "BaseDSL", "ViewClassResult") + val factoryName = "${view.simpleName}FactoryFunc" builder.addMethod(MethodSpec.methodBuilder(name) .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(result) - .addStatement("return \$T.v(\$T.class)", baseDsl, view) + .addStatement("return \$T.v($factoryName.getInstance())", baseDsl) .build()) builder.addMethod(MethodSpec.methodBuilder(name) .addParameter(ParameterSpec.builder(ClassName.get("trikita.anvil", "Anvil", "Renderable"), "r").build()) .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(TypeName.VOID.box()) - .addStatement("return \$T.v(\$T.class, r)", baseDsl, view) + .addStatement("return \$T.v($factoryName.getInstance(), r)", baseDsl) .build()) + + generateViewFactory(builder, view, factoryName) + } + + // + // View factory func generator + // + fun generateViewFactory(builder: TypeSpec.Builder, view: Class<*>, factoryName: String) { + val cls = TypeName.get(view) + val factoryFuncType = ClassName.get("trikita.anvil", "Anvil", "FactoryFunc") + + val factoryBuilder = TypeSpec.classBuilder(factoryName) + .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .addSuperinterface(ParameterizedTypeName.get(factoryFuncType, cls)) + + factoryBuilder.addField(FieldSpec + .builder(ClassName.get("", factoryName), "instance") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .initializer("null") + .build()) + + factoryBuilder.addMethod(MethodSpec + .methodBuilder("getInstance") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(ClassName.get("", factoryName)) + .beginControlFlow("if(instance == null)") + .addStatement("instance = new $factoryName()") + .endControlFlow() + .addStatement("return instance") + .build()) + + factoryBuilder.addMethod(MethodSpec + .methodBuilder("apply") + .addModifiers(Modifier.PUBLIC) + .returns(view) + .addParameter(ClassName.get("android.content", "Context"), "c") + .addStatement("return new \$T(c)", view) + .build()) + + builder.addType(factoryBuilder.build()) + builder.build() } // From 6c8a2d976334007cc9b3d5ac41e63c6b53cd2301 Mon Sep 17 00:00:00 2001 From: Leonard Chioveanu Date: Fri, 13 Jan 2017 14:54:27 +0200 Subject: [PATCH 4/5] anvilgen: skip view generation for abstract views. --- .../src/main/kotlin/trikita/anvilgen/DSLGeneratorTask.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/buildSrc/src/main/kotlin/trikita/anvilgen/DSLGeneratorTask.kt b/buildSrc/src/main/kotlin/trikita/anvilgen/DSLGeneratorTask.kt index 5a7341da..47a917ac 100644 --- a/buildSrc/src/main/kotlin/trikita/anvilgen/DSLGeneratorTask.kt +++ b/buildSrc/src/main/kotlin/trikita/anvilgen/DSLGeneratorTask.kt @@ -155,6 +155,11 @@ open class DSLGeneratorTask : DefaultTask() { // class, e.g. FrameLayout.class => frameLayout() { v(FrameLayout.class) } // fun processViews(builder: TypeSpec.Builder, view: Class<*>) { + // Skip abstract views. + // We shortcircuit it here, since we still want to generate attrs for these kinds of views. + if (java.lang.reflect.Modifier.isAbstract(view.modifiers)) { + return + } val className = view.canonicalName var name = view.simpleName val extension = project.extensions.getByName("anvilgen") as AnvilGenPluginExtension From ac6c3213ea91e6183be4c39f7628bab722272dcb Mon Sep 17 00:00:00 2001 From: Leonard Chioveanu Date: Fri, 13 Jan 2017 14:57:04 +0200 Subject: [PATCH 5/5] anvilgen: skip view generation for views without constructor(Context). --- .../src/main/kotlin/trikita/anvilgen/DSLGeneratorTask.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/buildSrc/src/main/kotlin/trikita/anvilgen/DSLGeneratorTask.kt b/buildSrc/src/main/kotlin/trikita/anvilgen/DSLGeneratorTask.kt index 47a917ac..3b2d6095 100644 --- a/buildSrc/src/main/kotlin/trikita/anvilgen/DSLGeneratorTask.kt +++ b/buildSrc/src/main/kotlin/trikita/anvilgen/DSLGeneratorTask.kt @@ -160,6 +160,14 @@ open class DSLGeneratorTask : DefaultTask() { if (java.lang.reflect.Modifier.isAbstract(view.modifiers)) { return } + // Skip classes without single argument Context constructors + if (!view.constructors.isEmpty()) { // No constructors. Valid, since superclass should have the right one. + val contextConstructor = view.constructors.filter { it -> + it.parameterCount == 1 && it.parameters[0].type.canonicalName == "android.content.Context" + }.firstOrNull() + contextConstructor ?: return + } + val className = view.canonicalName var name = view.simpleName val extension = project.extensions.getByName("anvilgen") as AnvilGenPluginExtension