Skip to content

Conversation

eboudrant
Copy link
Contributor

@eboudrant eboudrant commented Jun 30, 2020

Proposal for DSL usage in with EpoxyModelGroup (Issue #979), the change is based on the `ModelCollector.

It require modification in code generation. Any epoxy model class that implements ModelCollector will have an associated builder that implements ModelCollector too.

It also require a change in `EpoxyModelGroup as we need the model / layout to be lazily set (require an empty constructor).

To do :

  • Add a unit test for code generation

Also:

  • Should we make the builder implements ModelCollector only if the model is an EpoxyModelGroup ?
  • Should GroupModel.kt added to epoxy-adapter ? It is currently in kotlinsample app.

I like how it is now as it give some flexibility (any model can be a collector). Maybe we could also improve carousel DSL so the helper extensions are not needed (which could happen in a subsequent pull request).

So here the usage in sample app :

group {
    id("epoxyModelGroupDsl")
    layout(R.layout.vertical_linear_group)

    coloredSquareView {
        id("coloredSquareView 1")
        color(Color.DKGRAY)
    }

    coloredSquareView {
        id("coloredSquareView 2")
        color(Color.GRAY)
    }

    coloredSquareView {
        id("coloredSquareView 3")
        color(Color.LTGRAY)
    }
}

image

Copy link
Contributor

@elihart elihart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eboudrant thanks for taking a shot at this, I like the look of the DSL you've come up with.

What do you think of adding ModelCollector to the original EpoxyModelGroup, and shipping a modelGroup builder function with the core library?

If there are any other changes we want to make to the model group for the DSL this would be the best time to make this, but I can't think of anything in particular

private final boolean shouldSaveViewState;
protected final List<EpoxyModel<?>> models;

private boolean shouldSaveViewState = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

before the DSL, the recommended approach to customize this was to subclass the EpoxyModelGroup and override shouldSaveViewState. Since that isn't possible with a DSL what do you think of just making this not private?

We will want to make sure that the default handling of setting this based on added models does not override a custom setting, so we can do something like

private boolean defaultShouldSaveViewState = false;
public Boolean shouldSaveViewState = null;

and then the value we use is shouldSaveViewState ?: defaultShouldSaveViewState (i wish this file was in kotlin :P )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I missed it, how about this :

group {
  id(...)
  layout(...)
  shouldSaveViewState(true)
  ...
}

It should be possible with :

  private boolean shouldSaveViewStateDefault = false;

  @Nullable
  public Boolean shouldSaveViewState = null;

  @NonNull
  public EpoxyModelGroup shouldSaveViewState(boolean shouldSaveViewState) {
    onMutation();
    this.shouldSaveViewState = shouldSaveViewState;
    return this;
  }

  @Override
  public boolean shouldSaveViewState() {
    // By default state is saved if any of the models have saved state enabled.
    // Override this if you need custom behavior.
    if (shouldSaveViewState != null) {
      return shouldSaveViewState;
    } else {
      return shouldSaveViewStateDefault;
    }
  }

ps: I was really close to migrate the class to Kotlin :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds great

@@ -74,6 +74,14 @@ class ModelBuilderInterfaceWriter(
addTypeVariables(modelInfo.typeVariables)
addMethods(interfaceMethods)

for (implements in modelInfo.superClassElement.interfaces) {
if (implements.toString() == ClassNames.MODEL_COLLECTOR.toString()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be nice to memoize ClassNames.MODEL_COLLECTOR.toString(), or better yet, memoize on the class level via memoizer.implementsModelCollector(modelInfo.superClassElement)

The memoizer is a shared instance of Memoizer in the processor module that can be passed into this class

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Are you worried about performance in general? Theses was simple .toString() and iteration on a likely small collections.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eboudrant thanks! I am a bit worried about performance - I recently reworked a lot of code to memoize hot spots and it improved performance several times (we dropped processing in one module from 4 seconds to half a second).

The toString functions here are a bit deceiving, one is in the ClassName class, which calls emit to a code writer to format the code for display which isn't trivial. It is internally cached, which I forgot about, but that isn't synchronized, which we have to worry about now since I added parallel processing support. The interface toString function is on the javac class ClassType which potentially loads the class by reading it from file which is not thread safe, so the memoizer helps us to synchronize.

private val implementsModelCollectorMap = mutableMapOf<GeneratedModelInfo, Boolean>()
fun implementsModelCollector(modelInfo: GeneratedModelInfo): Boolean {
return synchronized(typeMap) {
val implementsModelCollector = modelInfo.superClassElement.interfaces.firstOrNull {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val implementsModelCollector = modelInfo.superClassElement.interfaces.firstOrNull {
val implementsModelCollector = modelInfo.superClassElement.interfaces.any {

Copy link
Contributor Author

@eboudrant eboudrant Jul 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also need to remove != null :

.firstOrNull { ... } != null -> .any { ... }

val implementsModelCollector = modelInfo.superClassElement.interfaces.firstOrNull {
it.toString() == ClassNames.MODEL_COLLECTOR.toString()
} != null
implementsModelCollectorMap.getOrPut(modelInfo) { implementsModelCollector }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implementsModelCollector is always calculated, even when the map already contains the value.

Could you remove the implementsModelCollector intermediate property and just put the whole calculation in the getOrPut lambda?

private final boolean shouldSaveViewState;
protected final List<EpoxyModel<?>> models;

private boolean shouldSaveViewState = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds great

@@ -74,6 +74,14 @@ class ModelBuilderInterfaceWriter(
addTypeVariables(modelInfo.typeVariables)
addMethods(interfaceMethods)

for (implements in modelInfo.superClassElement.interfaces) {
if (implements.toString() == ClassNames.MODEL_COLLECTOR.toString()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eboudrant thanks! I am a bit worried about performance - I recently reworked a lot of code to memoize hot spots and it improved performance several times (we dropped processing in one module from 4 seconds to half a second).

The toString functions here are a bit deceiving, one is in the ClassName class, which calls emit to a code writer to format the code for display which isn't trivial. It is internally cached, which I forgot about, but that isn't synchronized, which we have to worry about now since I added parallel processing support. The interface toString function is on the javac class ClassType which potentially loads the class by reading it from file which is not thread safe, so the memoizer helps us to synchronize.

@eboudrant
Copy link
Contributor Author

eboudrant commented Jul 21, 2020

What do you think of adding ModelCollector to the original EpoxyModelGroup, and shipping a modelGroup builder function with the core library?

@elihart I have EpoxyModelGroup implements ModelCollector now and I also added GroupModel.kt in the core library so group { ... } can be used as DSL out of the box :

@EpoxyModelClass
abstract class GroupModel : EpoxyModelGroup()

It works in sample app but what is scary is unit tests, theses ones started to fails :

> Task :epoxy-integrationtest:testDebugUnitTest

com.airbnb.epoxy.AutoModelIntegrationTest > implicitlyAddingAutoModels FAILED
    java.lang.AssertionError at AutoModelIntegrationTest.java:61

com.airbnb.epoxy.AutoModelIntegrationTest > implicitlyAddingAutoModels2 FAILED
    java.lang.AssertionError at AutoModelIntegrationTest.java:69

com.airbnb.epoxy.AutoModelIntegrationTest > implicitlyAddingAutoModels3 FAILED
    java.lang.AssertionError at AutoModelIntegrationTest.java:77
Expected :[Model_{value=1}Model_{id=-1, viewType=2131427371, shown=true, addedToAdapter=false}, Model_{value=0}Model_{id=1, viewType=2131427371, shown=true, addedToAdapter=false}, Model_{value=2}Model_{id=2, viewType=2131427371, shown=true, addedToAdapter=false}, Model_{value=2}Model_{id=-2, viewType=2131427371, shown=true, addedToAdapter=false}, Model_{value=3}Model_{id=3, viewType=2131427371, shown=true, addedToAdapter=false}, Model_{value=0}Model_{id=-4, viewType=2131427371, shown=true, addedToAdapter=false}]
Actual   :[Model_{value=0}Model_{id=1, viewType=2131427371, shown=true, addedToAdapter=false}, Model_{value=2}Model_{id=2, viewType=2131427371, shown=true, addedToAdapter=false}, Model_{value=3}Model_{id=3, viewType=2131427371, shown=true, addedToAdapter=false}, Model_{value=0}Model_{id=-4, viewType=2131427371, shown=true, addedToAdapter=false}]

Do you have any idea? Theses tests don't seems to use model group.

edit, tests seems to pass on pull request check but locally it fails for me.

@eboudrant
Copy link
Contributor Author

It works in sample app but what is scary is unit tests, theses ones started to fails :

It seems test failures are related to code gen and are un-detected in pull request check, is the build incremental on CI ?
Somehow getConfigurationForElement(controller.controllerClassElement).implicitlyAddAutoModels always return false and break auto-model tests by not adding the statement "setControllerToStageTo(controller.\$L, controller)".

Copy link
Contributor

@elihart elihart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eboudrant updates looks great! I just have a few small comments.

Sorry about the test trouble, I can pull your branch later today and dig around. The build should not be incremental on CI, it just builds from clean.

private boolean shouldSaveViewStateDefault = false;

@Nullable
public Boolean shouldSaveViewState = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this not be public? do we want to force usage of shouldSaveViewState so the mutation check is always done?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I can't remember why I changed it to public

package com.airbnb.epoxy

/**
* An [EpoxyModelGroup] usable in DSL.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be nice to give a small example of usage (or we can link to the wiki docs here later)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, also wdyt to add the ModelCollector interface / add method in this small class instead of in EpoxyModelGroup class, like :

@EpoxyModelClass
abstract class GroupModel : EpoxyModelGroup(), ModelCollector {

    override fun add(model: EpoxyModel<*>) {
        super.addModel(model)
    }
}

Because in any case EpoxyModelGroup can't be used directly in the DSL. It allow to separate responsibilities.

@@ -359,6 +359,17 @@ class Memoizer(
}
}
}

private val implementsModelCollectorMap = mutableMapOf<GeneratedModelInfo, Boolean>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to actually get good cache hits on this the parameter of the function should be classElement: TypeElement
then the map can be mutableMapOf<Name, Boolean>(), with getOrPut(classElement.qualifiedName) as the key to make sure it is consistent across type instances

fun implementsModelCollector(modelInfo: GeneratedModelInfo): Boolean {
return synchronized(typeMap) {
implementsModelCollectorMap.getOrPut(modelInfo) {
modelInfo.superClassElement.interfaces.any {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this check the super class recursively? If there are multiple subclasses a superclass up the chain could have the interface instead.

maybe interfaces.any {...} || implementsModelCollector(superClassElement.superclass) (psuedocode)

@elihart
Copy link
Contributor

elihart commented Jul 22, 2020

@eboudrant I pulled your branch but can't reproduce the test failures. it passes for me with ./gradlew clean testDebug

@eboudrant
Copy link
Contributor Author

eboudrant commented Jul 22, 2020

@eboudrant I pulled your branch but can't reproduce the test failures. it passes for me with ./gradlew clean testDebug

@elihart,./gradlew clean testDebug pass for me too, then when I make a modification to ControllerWithImplicitlyAddedModels3 and run ./gradlew testDebug, it fails. Ex, here the modification I do :

image

> Task :epoxy-integrationtest:testDebugUnitTest

com.airbnb.epoxy.AutoModelIntegrationTest > implicitlyAddingAutoModels FAILED
    java.lang.AssertionError at AutoModelIntegrationTest.java:61

com.airbnb.epoxy.AutoModelIntegrationTest > implicitlyAddingAutoModels2 FAILED
    java.lang.AssertionError at AutoModelIntegrationTest.java:69

com.airbnb.epoxy.AutoModelIntegrationTest > implicitlyAddingAutoModels3 FAILED
    java.lang.AssertionError at AutoModelIntegrationTest.java:77

100 tests completed, 3 failed

> Task :epoxy-integrationtest:testDebugUnitTest FAILED

Weird

@elihart
Copy link
Contributor

elihart commented Jul 23, 2020

@eboudrant this is very likely a bug with incremental processing, probably because of the kapt issue where package-info.java files are not properly included in incremental kapt processing. This leads to the configuration info from @PackageEpoxyConfig to not be included, which then causes the failure you see.

This is unrelated to your change, so let's just ignore it for now. The workaround I will have to apply l later is similar to #1020

Copy link
Contributor

@elihart elihart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latest code looks great, thanks for all the iteration!

really happy how this turned out, great addition!

I'll get another beta release out soon to include this

@elihart elihart merged commit 1f14d64 into airbnb:master Jul 23, 2020
@eboudrant
Copy link
Contributor Author

@elihart how close are we from a 4.0.0 out of beta? Is there a list of things to do apart from the package info issue?

@elihart
Copy link
Contributor

elihart commented Jul 25, 2020

@eboudrant I would say it is very close. We have been using it in production for a long time with no issues. The package-info issue is the only known problem, and that is only going to be a sporadic incremental issue that requires a clean (or sometimes deleting local build cache), and only if you happen to be using that config annotation.

My current plan is to fix the package-info problem, and then I can make a normal release

@plnice
Copy link
Contributor

plnice commented Aug 5, 2020

@elihart is this available in any beta/snapshot build?

@elihart
Copy link
Contributor

elihart commented Aug 6, 2020

@plnice not yet, it will be in the next release, whenever I have time to do that

@elihart
Copy link
Contributor

elihart commented Sep 9, 2020

this is in the 4.0.0 release

@elihart
Copy link
Contributor

elihart commented Sep 9, 2020

I've updated the wiki to mention this dsl https://github.com/airbnb/epoxy/wiki/Grouping-Models#kotlin-dsl

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants