-
Notifications
You must be signed in to change notification settings - Fork 727
Model group building with Kotlin DSL #1012
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
Conversation
There was a problem hiding this 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; |
There was a problem hiding this comment.
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 )
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
val implementsModelCollector = modelInfo.superClassElement.interfaces.firstOrNull { | |
val implementsModelCollector = modelInfo.superClassElement.interfaces.any { |
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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()) { |
There was a problem hiding this comment.
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.
…lass with the `@EpoxyModelClass` annotation so it can be used in the DSL
@elihart I have
It works in sample app but what is scary is unit tests, theses ones started to fails :
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. |
It seems test failures are related to code gen and are un-detected in pull request check, is the build incremental on CI ? |
There was a problem hiding this 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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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>() |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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)
@eboudrant I pulled your branch but can't reproduce the test failures. it passes for me with |
@elihart,
Weird |
@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 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 |
There was a problem hiding this 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 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? |
@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 |
@elihart is this available in any beta/snapshot build? |
@plnice not yet, it will be in the next release, whenever I have time to do that |
this is in the 4.0.0 release |
I've updated the wiki to mention this dsl https://github.com/airbnb/epoxy/wiki/Grouping-Models#kotlin-dsl |
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 implementsModelCollector
too.It also require a change in `EpoxyModelGroup as we need the model / layout to be lazily set (require an empty constructor).
To do :
Also:
ModelCollector
only if the model is anEpoxyModelGroup
?GroupModel.kt
added toepoxy-adapter
? It is currently inkotlinsample
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 :