Epoxy: EpoxyModelGroup DSL improvement

Created on 24 May 2020  路  2Comments  路  Source: airbnb/epoxy

Hello everyone 馃憢 ,

I came up with a working solution for EpoxyModelGroup DSL.

I checked https://github.com/airbnb/epoxy/issues/800 but the solution involved some workaround on building them first and then getting them. Not sure if there are shortcomings to this approach so it would be great to get some 馃憖 on it.

I have a simple EpoxyModelGroup, just a cardview with a linear layout like shown in the grouping models example where I fill that layout up:

image

My Kotlin class looks like:

class CardViewContainer(vararg models: EpoxyModel<*>) :
    EpoxyModelGroup(R.layout.container_card_view, *models)

fun EpoxyController.cardViewContainer(vararg models: EpoxyModel<*>) =
    CardViewContainer(*models).addTo(this)

Usage in current version is something like this (non dsl way)

override fun controller() = simpleController(viewModel) { state ->
        CardViewEpoxyModelGroup(
            TextRowModel_()
                .id("text")
                .body("Hello")
                .style(TextRow.TextStyle.BODY)
        // Other models...
        ).addTo(this)
    }

I was looking into the generated EpoxyModelKotlinExtensions.kt and for all the models it automatically adds itself

inline fun EpoxyController.textRow(modelInitializer: TextRowModelBuilder.() -> Unit) {
    TextRowModel_().apply { modelInitializer() }.addTo(this)
}

Just to try it out I created manually an equivalent, appending Model at the end and the only difference is that it returns the Model_ instead of adding it

inline fun EpoxyController.textRowModel(modelInitializer: TextRowModelBuilder.() -> Unit): TextRowModel_ =
 TextRowModel_().apply { modelInitializer() }

So the new usage becomes something like this:

    override fun controller() = simpleController(viewModel) { state ->
        cardViewContainer(
            textRowModel {
                id("text")
                body("Hello")
                style(TextRow.TextStyle.BODY)
            }
        )
    }

It still requires to have commas between models (so it is not a real DSL?) and you have to remember to use the Model naming at the end of what before was textRow but I think it is an improvement.

Are there any disadvantages to this (apart from having to write the model function manually)?
Would it be a good idea to autogenerate these methods as well?
Any suggestions?

I was testing it with different components, static and also ones where the model changed a lot (text input layout) and I didn't see any issues

The last few questions are:
Could grouping models be used as a replacement for Paris? Having something like a Padding Container for specific cases? (mimicking what flutter does)
From the description of grouping models it says: This is NOT a replacement for creating a custom view but it also mentions add functionality to an existing model via composition... Is this the correct approach if I want to have a simple set of epoxy views (loading, text, buttons, image rows) and let the other epoxy views of the app just be a different EpoxyModelGroups?

Thank you

Most helpful comment

The ModelCollector is very helpful. One constraint I noticed is that EpoxyModelGroup requires its items to be provided during instantiation. So if you use the builder generated by EpoxyModelClass/EpoxyAttribute, you have to create a dummy instance to delegate the builder methods into, then copy those attributes into a new instance once we have all the models. Here鈥檚 what I did:

@EpoxyModelClass
abstract class SampleEpoxyModel(
    modelList: List<EpoxyModel<*>>
) : EpoxyModelGroup(R.layout.sample, modelList) {

    @EpoxyAttribute var size: Int = 0
    @EpoxyAttribute var backgroundImageUrl: String? = null
    @EpoxyAttribute @ColorInt var backgroundColor: Int? = null

    override fun bind(holder: ModelGroupHolder) {
        super.bind(holder)
        ...
    }
}

fun ModelCollector.sample(builder: EpoxySampleViewBuilder.() -> Unit): SampleEpoxyModel {
    val modelGroup = EpoxySampleViewBuilder().apply { builder() }.build()
    add(modelGroup)
    return modelGroup
}

class EpoxySampleViewBuilder(
    private val sampleEpoxyModel: SampleEpoxyModel_ = SampleEpoxyModel_(listOf(DummyEpoxyModel())) // list with one model required
) : ModelCollector, SampleEpoxyModelBuilder by sampleEpoxyModel {
    private val models = mutableListOf<EpoxyModel<*>>()

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

    internal fun build() = SampleEpoxyModel_(models)
        .size(sampleEpoxyModel.size())
        .backgroundImageUrl(sampleEpoxyModel.backgroundImageUrl())
        .backgroundColor(sampleEpoxyModel.backgroundColor())
}

All 2 comments

Hi, I would instead recommend using the new DSL enabled by ModelCollector in https://github.com/airbnb/epoxy/pull/967

you can play with that to see how you can improve the model group creation

The ModelCollector is very helpful. One constraint I noticed is that EpoxyModelGroup requires its items to be provided during instantiation. So if you use the builder generated by EpoxyModelClass/EpoxyAttribute, you have to create a dummy instance to delegate the builder methods into, then copy those attributes into a new instance once we have all the models. Here鈥檚 what I did:

@EpoxyModelClass
abstract class SampleEpoxyModel(
    modelList: List<EpoxyModel<*>>
) : EpoxyModelGroup(R.layout.sample, modelList) {

    @EpoxyAttribute var size: Int = 0
    @EpoxyAttribute var backgroundImageUrl: String? = null
    @EpoxyAttribute @ColorInt var backgroundColor: Int? = null

    override fun bind(holder: ModelGroupHolder) {
        super.bind(holder)
        ...
    }
}

fun ModelCollector.sample(builder: EpoxySampleViewBuilder.() -> Unit): SampleEpoxyModel {
    val modelGroup = EpoxySampleViewBuilder().apply { builder() }.build()
    add(modelGroup)
    return modelGroup
}

class EpoxySampleViewBuilder(
    private val sampleEpoxyModel: SampleEpoxyModel_ = SampleEpoxyModel_(listOf(DummyEpoxyModel())) // list with one model required
) : ModelCollector, SampleEpoxyModelBuilder by sampleEpoxyModel {
    private val models = mutableListOf<EpoxyModel<*>>()

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

    internal fun build() = SampleEpoxyModel_(models)
        .size(sampleEpoxyModel.size())
        .backgroundImageUrl(sampleEpoxyModel.backgroundImageUrl())
        .backgroundColor(sampleEpoxyModel.backgroundColor())
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

chrisbanes picture chrisbanes  路  3Comments

ailuoyi picture ailuoyi  路  5Comments

Sergiyss picture Sergiyss  路  6Comments

aliab picture aliab  路  3Comments

thanapongp picture thanapongp  路  6Comments