diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyModelGroup.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyModelGroup.java index 6f96b15833..14449cd667 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyModelGroup.java +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyModelGroup.java @@ -12,6 +12,7 @@ import androidx.annotation.CallSuper; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; /** * An {@link EpoxyModel} that contains other models, and allows you to combine those models in @@ -59,9 +60,12 @@ @SuppressWarnings("rawtypes") public class EpoxyModelGroup extends EpoxyModelWithHolder { - protected final List> models; - /** By default we save view state if any of the models need to save state. */ - private final boolean shouldSaveViewState; + protected final List> models; + + private boolean shouldSaveViewStateDefault = false; + + @Nullable + private Boolean shouldSaveViewState = null; /** * @param layoutRes The layout to use with these models. @@ -83,7 +87,7 @@ public EpoxyModelGroup(@LayoutRes int layoutRes, EpoxyModel... models) { * @param layoutRes The layout to use with these models. * @param models The models that will be used to bind the views in the given layout. */ - private EpoxyModelGroup(@LayoutRes int layoutRes, List> models) { + private EpoxyModelGroup(@LayoutRes int layoutRes, List> models) { if (models.isEmpty()) { throw new IllegalArgumentException("Models cannot be empty"); } @@ -99,8 +103,22 @@ private EpoxyModelGroup(@LayoutRes int layoutRes, List> break; } } + // By default we save view state if any of the models need to save state. + shouldSaveViewStateDefault = saveState; + } - shouldSaveViewState = saveState; + /** + * Constructor use for DSL + */ + protected EpoxyModelGroup() { + models = new ArrayList<>(); + shouldSaveViewStateDefault = false; + } + + protected void addModel(@NonNull EpoxyModel model) { + // By default we save view state if any of the models need to save state. + shouldSaveViewStateDefault |= model.shouldSaveViewState(); + models.add(model); } @CallSuper @@ -217,11 +235,22 @@ protected final int getDefaultLayout() { "You should set a layout with layout(...) instead of using this."); } + @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. - return shouldSaveViewState; + if (shouldSaveViewState != null) { + return shouldSaveViewState; + } else { + return shouldSaveViewStateDefault; + } } /** diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/GroupModel.kt b/epoxy-adapter/src/main/java/com/airbnb/epoxy/GroupModel.kt new file mode 100644 index 0000000000..bbb2cb3013 --- /dev/null +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/GroupModel.kt @@ -0,0 +1,28 @@ +package com.airbnb.epoxy + +/** + * An [EpoxyModelGroup] usable in a DSL manner via the [group] extension. + *

+ * Example: + * ``` + * group { + * id("photos") + * layout(R.layout.photo_grid) + * + * // add your models here, example: + * for (photo in photos) { + * imageView { + * id(photo.id) + * url(photo.url) + * } + * } + * } + * ``` + */ +@EpoxyModelClass +abstract class GroupModel : EpoxyModelGroup(), ModelCollector { + + override fun add(model: EpoxyModel<*>) { + super.addModel(model) + } +} diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Memoizer.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Memoizer.kt index f98085fc4e..d29b4944fa 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Memoizer.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/Memoizer.kt @@ -359,6 +359,20 @@ class Memoizer( } } } + + private val implementsModelCollectorMap = mutableMapOf() + fun implementsModelCollector(classElement: TypeElement): Boolean { + return synchronized(typeMap) { + implementsModelCollectorMap.getOrPut(classElement.qualifiedName) { + classElement.interfaces.any { + it.toString() == ClassNames.MODEL_COLLECTOR.toString() + } || classElement.superClassElement(types)?.let { superClassElement -> + // Also check the class hierarchy + implementsModelCollector(superClassElement) + } ?: false + } + } + } } private val viewModelAnnotations = listOf( diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelBuilderInterfaceWriter.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelBuilderInterfaceWriter.kt index 94984aac50..1675d61f46 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelBuilderInterfaceWriter.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/processor/ModelBuilderInterfaceWriter.kt @@ -74,6 +74,11 @@ class ModelBuilderInterfaceWriter( addTypeVariables(modelInfo.typeVariables) addMethods(interfaceMethods) + if (modelInfo.memoizer.implementsModelCollector(modelInfo.superClassElement)) { + // If the model implements "ModelCollector" we want the builder too + addSuperinterface(ClassNames.MODEL_COLLECTOR) + } + addOriginatingElement(modelInfo.superClassElement) } diff --git a/epoxy-processortest/src/test/resources/EpoxyModelGroupWithAnnotations_.java b/epoxy-processortest/src/test/resources/EpoxyModelGroupWithAnnotations_.java index 414b213079..61761150aa 100644 --- a/epoxy-processortest/src/test/resources/EpoxyModelGroupWithAnnotations_.java +++ b/epoxy-processortest/src/test/resources/EpoxyModelGroupWithAnnotations_.java @@ -134,6 +134,12 @@ public int value() { return value; } + @Override + public EpoxyModelGroupWithAnnotations_ shouldSaveViewState(boolean shouldSaveViewState) { + super.shouldSaveViewState(shouldSaveViewState); + return this; + } + @Override public EpoxyModelGroupWithAnnotations_ id(long id) { super.id(id); diff --git a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/MainActivity.kt b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/MainActivity.kt index a268acb5c8..5d61609928 100644 --- a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/MainActivity.kt +++ b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/MainActivity.kt @@ -8,10 +8,12 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import com.airbnb.epoxy.EpoxyRecyclerView import com.airbnb.epoxy.EpoxyVisibilityTracker +import com.airbnb.epoxy.group import com.airbnb.epoxy.kotlinsample.helpers.carouselNoSnapBuilder import com.airbnb.epoxy.kotlinsample.models.ItemDataClass import com.airbnb.epoxy.kotlinsample.models.ItemViewBindingDataClass import com.airbnb.epoxy.kotlinsample.models.carouselItemCustomView +import com.airbnb.epoxy.kotlinsample.models.coloredSquareView import com.airbnb.epoxy.kotlinsample.models.itemCustomView import com.airbnb.epoxy.kotlinsample.models.itemEpoxyHolder import com.airbnb.epoxy.kotlinsample.models.itemViewBindingEpoxyHolder @@ -32,6 +34,26 @@ class MainActivity : AppCompatActivity() { recyclerView.withModels { + 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) + } + } + for (i in 0 until 100) { dataBindingItem { id("data binding $i") diff --git a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/ColoredSquareView.kt b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/ColoredSquareView.kt new file mode 100644 index 0000000000..aaa6aa1f34 --- /dev/null +++ b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/models/ColoredSquareView.kt @@ -0,0 +1,24 @@ +package com.airbnb.epoxy.kotlinsample.models + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.View +import androidx.annotation.ColorInt +import com.airbnb.epoxy.ModelProp +import com.airbnb.epoxy.ModelView +import com.airbnb.epoxy.kotlinsample.R + +@ModelView(defaultLayout = R.layout.colored_square_view) +class ColoredSquareView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + @JvmOverloads + @ModelProp + fun color(@ColorInt color: Int = Color.RED) { + setBackgroundColor(color) + } +} diff --git a/kotlinsample/src/main/res/layout/colored_square_view.xml b/kotlinsample/src/main/res/layout/colored_square_view.xml new file mode 100644 index 0000000000..0b99a645c1 --- /dev/null +++ b/kotlinsample/src/main/res/layout/colored_square_view.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/kotlinsample/src/main/res/layout/vertical_linear_group.xml b/kotlinsample/src/main/res/layout/vertical_linear_group.xml new file mode 100644 index 0000000000..24e7fb9e8f --- /dev/null +++ b/kotlinsample/src/main/res/layout/vertical_linear_group.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file