Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

View factories #93

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion anvil/src/main/java/trikita/anvil/Anvil.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ public final class Anvil {

private static Handler anvilUIHandler = null;

public interface FactoryFunc<T extends View> {
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
Expand All @@ -46,12 +50,16 @@ public interface AttrFunc<T> {
}

interface ViewFactory {
View fromFactoryFunc(Context c, FactoryFunc<? extends View> factoryFunc);
View fromClass(Context c, Class<? extends View> 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<? extends View> factoryFunc) {
return factoryFunc.apply(c);
}
public View fromClass(Context c, Class<? extends View> viewClass) {
try {
return viewClass.getConstructor(Context.class).newInstance(c);
Expand Down Expand Up @@ -231,13 +239,35 @@ Node startNode() {
return node;
}

void startFromFactory(FactoryFunc<? extends View> 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<? extends View> viewClass) {
Node node = startNode();
View view = node.view;
if (view == null || node.viewClass != viewClass) {
node.layoutId = 0;
node.viewClass = viewClass;
node.viewFactoryFunc = null;
node.children.clear();
node.attrs.clear();
if (view != null) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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<? extends View> viewFactoryFunc;
private Class<? extends View> viewClass;
private int layoutId;

Expand Down
11 changes: 11 additions & 0 deletions anvil/src/main/java/trikita/anvil/BaseDSL.java
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,11 @@ public static ViewClassResult v(Class<? extends View> c) {
return null;
}

public static ViewClassResult v(Anvil.FactoryFunc<? extends View> viewFactoryFunc) {
Anvil.currentMount().startFromFactory(viewFactoryFunc);
return null;
}

public static ViewClassResult xml(int layoutId) {
Anvil.currentMount().startFromLayout(layoutId);
return null;
Expand All @@ -735,6 +740,12 @@ public static Void v(Class<? extends View> c, Anvil.Renderable r) {
return end();
}

public static Void v(Anvil.FactoryFunc<? extends View> c, Anvil.Renderable r) {
v(c);
r.view();
return end();
}

public static Void xml(int layoutId, Anvil.Renderable r) {
xml(layoutId);
r.view();
Expand Down
26 changes: 26 additions & 0 deletions anvil/src/test/java/trikita/anvil/CurrentViewTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
44 changes: 44 additions & 0 deletions anvil/src/test/java/trikita/anvil/IncrementalRenderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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";

Expand Down
22 changes: 22 additions & 0 deletions anvil/src/test/java/trikita/anvil/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ protected void mockViewFactory(Anvil.ViewFactory viewFactory) {
}
}

@Override
public View fromFactoryFunc(Context c, Anvil.FactoryFunc<? extends View> 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<? extends View> v) {
try {
createdViews.put(v, !createdViews.containsKey(v) ? 1 : (createdViews.get(v) + 1));
Expand Down Expand Up @@ -97,6 +105,13 @@ public Context getContext() {
}

public static class MockView extends View {
public final static Anvil.FactoryFunc<MockView> FACTORY = new Anvil.FactoryFunc<MockView>() {
@Override
public MockView apply(Context context) {
return new MockView(context);
}
};

public final Map<String, Object> props = new HashMap<>();
public MockView(Context c) {
super(c);
Expand All @@ -110,6 +125,13 @@ public int getId() {
}

public static class MockLayout extends FrameLayout {
public final static Anvil.FactoryFunc<MockLayout> FACTORY = new Anvil.FactoryFunc<MockLayout>() {
@Override
public MockLayout apply(Context context) {
return new MockLayout(context);
}
};

public final Map<String, Object> props = new HashMap<>();
private List<View> children = new ArrayList<>();

Expand Down
39 changes: 39 additions & 0 deletions anvil/src/test/java/trikita/anvil/ViewByIdTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public CustomLayout(Context c) {
addView(firstView, 0);
addView(secondView, 0);
}
public final static Anvil.FactoryFunc<CustomLayout> FACTORY = new Anvil.FactoryFunc<CustomLayout>() {
@Override
public CustomLayout apply(Context context) {
return new CustomLayout(context);
}
};
}

@Test
Expand Down Expand Up @@ -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"));
}
}
59 changes: 57 additions & 2 deletions buildSrc/src/main/kotlin/trikita/anvilgen/DSLGeneratorTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,19 @@ 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
}
// 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
Expand All @@ -172,18 +185,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()
}

//
Expand Down