From 7c4ac9c6f81c57d254f6fbab0308fe6b162664c5 Mon Sep 17 00:00:00 2001 From: Emmanuel Boudrant Date: Wed, 13 May 2020 09:46:51 -0700 Subject: [PATCH] Carousel building with Kotlin DSL - Step 1 (#967) * apply commit eeff26586ba43ed8c8416930140a34aef33e87fb * fix wrong merge * Move the builder in sample app waiting for a more robust api * Actually let's do it for the carousel no snap * Use new DSL in sample app * ktlint * ktlint * Simplify, BaseEpoxyController class -> ModelCollector interface * ktlint Co-authored-by: Eli Hart Co-authored-by: Emmanuel Boudrant --- .../com/airbnb/epoxy/EpoxyController.java | 12 +++--- .../java/com/airbnb/epoxy/ModelCollector.kt | 10 +++++ .../main/java/com/airbnb/epoxy/ClassNames.kt | 2 + .../KotlinModelBuilderExtensionWriter.kt | 5 ++- .../airbnb/epoxy/kotlinsample/MainActivity.kt | 21 ++++------ .../helpers/EpoxyCarouselNoSnapBuilder.kt | 41 +++++++++++++++++++ 6 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 epoxy-adapter/src/main/java/com/airbnb/epoxy/ModelCollector.kt create mode 100644 kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/helpers/EpoxyCarouselNoSnapBuilder.kt diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyController.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyController.java index 1a728532e7..17edbc893e 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyController.java +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyController.java @@ -44,7 +44,7 @@ * treated as immutable and never modified again. This is necessary for adapter updates to be * accurate. */ -public abstract class EpoxyController implements StickyHeaderCallbacks { +public abstract class EpoxyController implements ModelCollector, StickyHeaderCallbacks { /** * We check that the adapter is not connected to multiple recyclerviews, but when a fragment has @@ -127,8 +127,8 @@ public EpoxyController(Handler modelBuildingHandler, Handler diffingHandler) { @Retention(RetentionPolicy.SOURCE) @IntDef({RequestedModelBuildType.NONE, - RequestedModelBuildType.NEXT_FRAME, - RequestedModelBuildType.DELAYED}) + RequestedModelBuildType.NEXT_FRAME, + RequestedModelBuildType.DELAYED}) private @interface RequestedModelBuildType { int NONE = 0; /** A request has been made to build models immediately. It is posted. */ @@ -463,7 +463,7 @@ private void assertNotBuildingModels() { * Add the model to this controller. Can only be called from inside {@link * EpoxyController#buildModels()}. */ - protected void add(@NonNull EpoxyModel model) { + public void add(@NonNull EpoxyModel model) { model.addTo(this); } @@ -475,7 +475,7 @@ protected void add(@NonNull EpoxyModel... modelsToAdd) { modelsBeingBuilt.ensureCapacity(modelsBeingBuilt.size() + modelsToAdd.length); for (EpoxyModel model : modelsToAdd) { - model.addTo(this); + add(model); } } @@ -487,7 +487,7 @@ protected void add(@NonNull List> modelsToAdd) { modelsBeingBuilt.ensureCapacity(modelsBeingBuilt.size() + modelsToAdd.size()); for (EpoxyModel model : modelsToAdd) { - model.addTo(this); + add(model); } } diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/ModelCollector.kt b/epoxy-adapter/src/main/java/com/airbnb/epoxy/ModelCollector.kt new file mode 100644 index 0000000000..7229dd6b31 --- /dev/null +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/ModelCollector.kt @@ -0,0 +1,10 @@ +package com.airbnb.epoxy + +/** + * Interface used to collect models. Used by [EpoxyController]. It is also convenient to build DSL + * helpers for carousel: @link https://github.com/airbnb/epoxy/issues/847. + */ +interface ModelCollector { + + fun add(model: EpoxyModel<*>) +} diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/ClassNames.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/ClassNames.kt index 547c6613ca..51e8a3e3d3 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/ClassNames.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/ClassNames.kt @@ -55,6 +55,8 @@ object ClassNames { @JvmField val EPOXY_CONTROLLER = get(PKG_EPOXY, "EpoxyController")!! @JvmField + val MODEL_COLLECTOR = get(PKG_EPOXY, "ModelCollector")!! + @JvmField val EPOXY_STYLE_BUILDER_CALLBACK = get(PKG_EPOXY, "StyleBuilderCallback")!! @JvmField val EPOXY_CONTROLLER_HELPER = get(PKG_EPOXY, "ControllerHelper")!! diff --git a/epoxy-processor/src/main/java/com/airbnb/epoxy/KotlinModelBuilderExtensionWriter.kt b/epoxy-processor/src/main/java/com/airbnb/epoxy/KotlinModelBuilderExtensionWriter.kt index 2cad1f212e..2388f337ad 100644 --- a/epoxy-processor/src/main/java/com/airbnb/epoxy/KotlinModelBuilderExtensionWriter.kt +++ b/epoxy-processor/src/main/java/com/airbnb/epoxy/KotlinModelBuilderExtensionWriter.kt @@ -95,7 +95,7 @@ internal class KotlinModelBuilderExtensionWriter( ) FunSpec.builder(getMethodName(model)).run { - receiver(ClassNames.EPOXY_CONTROLLER.toKPoet()) + receiver(ClassNames.MODEL_COLLECTOR.toKPoet()) val params = constructor?.params ?: listOf() addParameters(params.toKParams()) @@ -117,13 +117,14 @@ internal class KotlinModelBuilderExtensionWriter( addModifiers(KModifier.INLINE) if (constructorIsNotPublic) addModifiers(KModifier.INTERNAL) + addStatement("add(") beginControlFlow( "$tick%T$tick(${params.joinToString(", ") { it.name }}).apply", modelClass ) addStatement("modelInitializer()") endControlFlow() - addStatement(".addTo(this)") + addStatement(")") return build() } } 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 c55e23be94..5595718124 100644 --- a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/MainActivity.kt +++ b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/MainActivity.kt @@ -8,13 +8,13 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import com.airbnb.epoxy.EpoxyRecyclerView import com.airbnb.epoxy.EpoxyVisibilityTracker -import com.airbnb.epoxy.kotlinsample.models.CarouselItemCustomViewModel_ +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.itemCustomView import com.airbnb.epoxy.kotlinsample.models.itemEpoxyHolder import com.airbnb.epoxy.kotlinsample.models.itemViewBindingEpoxyHolder -import com.airbnb.epoxy.kotlinsample.views.carouselNoSnap class MainActivity : AppCompatActivity() { private lateinit var recyclerView: EpoxyRecyclerView @@ -71,18 +71,15 @@ class MainActivity : AppCompatActivity() { } } - carouselNoSnap { + carouselNoSnapBuilder { id("carousel $i") - models(mutableListOf().apply { - val lastPage = 10 - for (j in 0 until lastPage) { - add( - CarouselItemCustomViewModel_() - .id("carousel $i-$j") - .title("Page $j / $lastPage") - ) + val lastPage = 10 + for (j in 0 until lastPage) { + carouselItemCustomView { + id("carousel $i-$j") + title("Page $j / $lastPage") } - }) + } } // Since data classes do not use code generation, there's no extension generated here diff --git a/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/helpers/EpoxyCarouselNoSnapBuilder.kt b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/helpers/EpoxyCarouselNoSnapBuilder.kt new file mode 100644 index 0000000000..e761cc362d --- /dev/null +++ b/kotlinsample/src/main/java/com/airbnb/epoxy/kotlinsample/helpers/EpoxyCarouselNoSnapBuilder.kt @@ -0,0 +1,41 @@ +package com.airbnb.epoxy.kotlinsample.helpers + +import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.ModelCollector +import com.airbnb.epoxy.kotlinsample.views.CarouselNoSnapModelBuilder +import com.airbnb.epoxy.kotlinsample.views.CarouselNoSnapModel_ + +/** + * Example that illustrate how to add a builder for nested list (ex: carousel) that allow building + * it using DSL : + * + * carouselBuilder { + * id(...) + * for (...) { + * carouselItemCustomView { + * id(...) + * } + * } + * } + * + * @link https://github.com/airbnb/epoxy/issues/847 + */ +fun ModelCollector.carouselNoSnapBuilder(builder: EpoxyCarouselNoSnapBuilder.() -> Unit): CarouselNoSnapModel_ { + val carouselBuilder = EpoxyCarouselNoSnapBuilder().apply { builder() } + add(carouselBuilder.carouselNoSnapModel) + return carouselBuilder.carouselNoSnapModel +} + +class EpoxyCarouselNoSnapBuilder( + internal val carouselNoSnapModel: CarouselNoSnapModel_ = CarouselNoSnapModel_() +) : ModelCollector, CarouselNoSnapModelBuilder by carouselNoSnapModel { + private val models = mutableListOf>() + + override fun add(model: EpoxyModel<*>) { + models.add(model) + + // Set models list every time a model is added so that it can run debug validations to + // ensure it is still valid to mutate the carousel model. + carouselNoSnapModel.models(models) + } +}