Arun Pandian M

Arun Pandian M

Android Dev | Full-Stack & AI Learner

🍕 Building a Pizza Like Jetpack Compose: A Modifier Story

https://storage.googleapis.com/lambdabricks-cd393.firebasestorage.app/compose_pizza.webp?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=firebase-adminsdk-fbsvc%40lambdabricks-cd393.iam.gserviceaccount.com%2F20251013%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20251013T193903Z&X-Goog-Expires=3600&X-Goog-SignedHeaders=host&X-Goog-Signature=9bd95308e767151ecaed027fd1425363ecb1a70539402027688e5ab188b6b0e9ee21dd3aec6cb64543641bb9424889ae822ab71f55022617acd16b71b771bbe0af4fd616c96873c42e867bcf9e16df1d605109c09778e44eb8397ea09abc53a8030a340574e24da64db074cb4c72b2cc252649f4b142841849cf391355b8eecad867e358f030a21f082f017a6146df0adde9474ee14c433bfef83810145f9b34ba986aba0160d508f176c505101f16c0f674be797143f5e17183535f2667a35fbaef60002495d1f1f4e4fe5efaa40fa0e194191004839a8fdb95347b2b928aa3800211623406d6c5dc465a502f26104826099b9ad4a2bc0b64dad6bf72db7a58
Just like the LEGO pieces add toppings and a crust to this pizza, modifiers allow you to customize a Composable with styling, layout, and behavior

I love pizza. And I love Jetpack Compose. Turns out, they have a lot in common. Stick with me, and I’ll show you why building a pizza can teach you how Compose modifiers actually work.

Modifiers: The Secret Sauce

In Compose, every UI element — a Text, a Box, a Column — can be decorated with modifiers. Think of a modifier as a little instruction: “Add padding here,” or “Make this background red.” You can chain these modifiers:

Modifier
    .padding(8.dp)
    .background(Color.Red)

Each modifier is applied in sequence, just like adding ingredients to a pizza.

So I decided: why not make a Pizza DSL using the same concept?

Step 1: Building the Chain

I created a Chain interface — our version of Compose’s Modifier. Each step, like adding dough, sauce, or cheese, is a ChainUnit. And just like Compose, we can chain them with then():

Chain.pizzaDough("Thin Crust")
    .pizzaSauce("Tomato Basil")
    .pizzaTopping("Mushroom")
    .pizzaCheese("Mozzarella")

Here’s the magic:

  • ChainUnit = a single modification (like Modifier.padding)
  • then() = “do this, then that”
  • fold() = actually apply all the steps in order
  • Without fold, we just have a list of instructions. Fold executes them, building the final pizza.

    Step 2: Fold — The Assembly Line

    Think of fold like an assembly line in a pizza shop:

    1. Dough chef lays down the crust

    2.Sauce chef spreads the sauce

    3.Topping chef sprinkles mushrooms

    4.Cheese chef adds mozzarella

    Each chef doesn’t need to know about the others — they just follow the sequence. This is exactly what fold() does

    chain.fold(Pizza()) { pizza, unit ->
        when(unit) {
            is Dough -> pizza.dough = unit
            is Sauce -> pizza.sauce = unit
            is Topping -> pizza.toppings.add(unit)
            is Cheese -> pizza.cheese = unit
        }
        pizza
    }

    Order matters. Fold ensures the pizza is assembled step by step, just like Compose applies modifiers in order.

    Step 3: Scope-Specific Modifiers

    Some pizzas are special. Pizza Hut likes stuffed crust, and Dominos loves double cheese. We can model this using scopes, similar to BoxScope or ColumnScope in Compose:

    PizzaHutScope().run {
        Chain
            .pizzaDough("Thin Crust")
            .pizzaSauce("Marinara")
            .pizzaTopping("Pepperoni")
            .pizzaCheese("Mozzarella")
            .stuffedCrust()  // only valid in PizzaHutScope
            .let { Pizza.compose(it) }
    }
    

    Step 4: Why Functional Chains Beat Builders

    You might wonder, “Why not just use a builder?” Builders mutate objects step by step, which is fine for small things. But functional chains shine when:

  • You want reusable sequences of modifiers
  • You want predictable order without side effects
  • You want a DSL that reads naturally
  • Compare:

    // Builder pattern
    PizzaBuilder().setDough(...).setSauce(...).addTopping(...).build()
    
    // Functional chain
    Chain.pizzaDough(...).pizzaSauce(...).pizzaTopping(...).fold(Pizza()) { ... }
    

    The chain reads like a recipe. It’s declarative, composable, and fits perfectly with scopes.

    Step 5: Putting It All Together

    Here’s a Pizza Hut example:

    val pizzaHut = PizzaHutScope().run {
        Chain
            .pizzaDough("Thin Crust")
            .pizzaSauce("Marinara")
            .pizzaTopping("Pepperoni")
            .pizzaCheese("Mozzarella")
            .stuffedCrust()
            .let { Pizza.compose(it) }
    }
    
    println(pizzaHut.describe())

    See how naturally it reads? You assemble a pizza step by step, respecting scope rules, just like Compose applies modifiers to UI elements.

    🔑 Takeaways

    Modifiers are just small, reusable steps
    Chains and fold ensure order and composability
    Scope-level extensions enforce context rules
    Functional chain > builder for readability and reuse
    DSL = human-readable recipe, like Compose modifiers reading like a story

    In short, building a pizza like this feels like Compose in action. You can chain, scope, and fold ingredients — or modifiers — creating a final product that’s perfectly layered, predictable, and reusable.

    You’ve seen how we can build a pizza step by step using fold-based chains, scope-specific modifiers, and a Compose-inspired DSL. If you want to play with the code yourself, experiment, or tweak it, I’ve put the entire Kotlin sample in a GitHub Gist.

    Go ahead, copy it, run it, and make your own pizza combinations: https://gist.github.com/arunpandian22/3ae30ee154a223a4137cf64f13732659

    Have fun exploring, and may your pizza — and your code — always come out perfectly! 🍕👩‍💻