Elegant Event Handling in Kotlin - A Refactoring Walkthrough

Unlock peak performance in Kotlin code with these expert refactoring tips.


9 min read
Elegant Event Handling in Kotlin - A Refactoring Walkthrough

In the world of software development, code refactoring is the hero that rescues us from tangled and inefficient code. In this article, we'll embark on an adventure to revamp Kotlin code handling diverse events. Our mission? To enhance performance and style, making the code sleeker, more maintainable, and a joy to work with.

What We Aim to Achieve

On this journey to transform Kotlin event handling, our goal is to refine our code to be more efficient, readable, and maintainable. We're introducing a variety of improvements, including:

  • Replacing a convoluted when statement with a HashMap for lightning-fast (O(1)) performance.
  • Infusing syntactic sweetness with inline functions and reified type parameters.
  • Employing delegated properties for cleaner dependency injection.
  • Adhering to the Single Responsibility Principle by enabling multiple specialized event handler functions.

Step 1: The Starting Line

sven2244_a_playful_abstract_artwork_that_represents_the_complex_6e9491ff-8203-4ae2-9a5e-52025e4010be.png

Our adventure begins with a glance at the original code. This codebase manages a variety of block events through a function named handleBlockEvent and an event handler function called onEvent. Let's unveil the original code:

open fun onEvent(event: Event) {    
    // ...
    handleBlockEvent(engine, getBlockForEvents(), checkNotNull(assetsRepo.fontFamilies.value).getOrThrow())
}

fun handleBlockEvent(engine: Engine, block: DesignBlock, fontFamilyMap: Map<String, FontFamilyData>, event: BlockEvent) {
    when (event) {
        BlockEvent.OnDelete -> engine.delete(block)
        BlockEvent.OnBackward -> engine.sendBackward(block)
        BlockEvent.OnDuplicate -> engine.duplicate(block)
        BlockEvent.OnForward -> engine.bringForward(block)
        BlockEvent.ToBack -> engine.sendToBack(block)
        BlockEvent.ToFront -> engine.bringToFront(block)
        BlockEvent.OnChangeFinish -> engine.editor.addUndoStep()
        is BlockEvent.OnChangeBlendMode -> onChangeBlendMode(engine, block, event.blendMode)
        is BlockEvent.OnChangeOpacity -> engine.block.setOpacity(block, event.opacity)
        is BlockEvent.OnChangeFillColor -> onChangeFillColor(engine, block, event.color)
        // and so on...
    }
}

sealed class BlockEvent : Event {
    object OnChangeFinish : BlockEvent
    object OnForward : BlockEvent
    object OnBackward : BlockEvent
    object OnDuplicate : BlockEvent
    object OnDelete : BlockEvent
    object ToFront : BlockEvent
    object ToBack : BlockEvent
    data class OnChangeBlendMode(val blendMode: BlendMode) : BlockEvent
    data class OnChangeOpacity(val opacity: Float) : BlockEvent
    data class OnChangeFillColor(val color: Color) : BlockEvent
    // and so on...
}

To use the original code, you'd typically call the onEvent function with a specific event:

onEvent(BlockEvent.OnChangeFillColor(Color.RED))

This would then trigger the handleBlockEvent function to deal with the event at hand. Now, let's embark on our first refactoring adventure.

Step 2: Unveiling HashMaps and Payloads for Peak Performance

sven2244_an_abstract_image_ofa_futuristic_cityscape_with_neon-l_fbda2c5b-b80f-430b-9f00-609ceecc1292.png

In our first act of refactoring, we introduce a trusty HashMap to map each event type to its corresponding action. This heroic move eliminates the need for the convoluted when statement, making our code more efficient. We also unveil a payload mechanism to convey essential data to the event handlers.

Behold the refactored code:

abstract class EventsHandler<Payloads>(
    val fillPayload: (cache: Payloads) -> Unit
) {
    abstract val payloadCache: Payloads
    private val eventMap = mutableMapOf<KClass<out Event>, Payloads.(event: Event) -> Unit>()

    fun handleEvent(event: Event) {
        eventMap[event::class]?.let {
            it.invoke(payloadCache.also { fillPayload(it) }, event)
        }
    }

    operator fun <EventType : Event> set(event: KClass<out EventType>, lambda: Payloads.(event: EventType) -> Unit) {
        eventMap[event] = lambda as Payloads.(event: Event) -> Unit
    }
}

class BlockEventsHandler(fillPayload: (cache: BlockEventsHandler.Payloads) -> Unit) : EventsHandler<BlockEventsHandler.Payloads>(fillPayload) {
    class Payloads {
        lateinit var engine: Engine
        lateinit var block: DesignBlock
        lateinit var fontFamilyMap: Map<String, FontFamilyData>
    }
    override val payloadCache: Payloads = Payloads()

    init {
        it[BlockEvent.OnDelete::class] = { engine.delete(block) }
        it[BlockEvent.OnBackward::class] = { engine.sendBackward(block) }
        it[BlockEvent.OnDuplicate::class] = { engine.duplicate(block) }
        it[BlockEvent.OnForward::class] = { engine.bringForward(block) }
        it[BlockEvent.ToBack::class] = { engine.sendToBack(block) }
        it[BlockEvent.ToFront::class] = { engine.bringToFront(block) }
        it[BlockEvent.OnChangeFinish::class] = { engine.editor.addUndoStep() }
        it[BlockEvent.OnChangeBlendMode::class] = { onChangeBlendMode(engine, block, it.blendMode) }
        it[BlockEvent.OnChangeOpacity::class] = { engine.block.setOpacity(block, it.opacity) }
        it[BlockEvent.OnChangeFillColor::class] = { onChangeFillColor(engine, block, it.color) }
        // and so on...
    }
}

private val blockEventHandler = BlockEventsHandler {
    it.engine = engine
    it.block = getBlockForEvents()
    it.fontFamilyMap = checkNotNull(assetsRepo.fontFamilies.value).getOrThrow()
}

open fun onEvent(event: Event) {    
    // ...
    blockEventHandler.handleEvent(event)
}

A Performance Boost

By harnessing the power of a HashMap, we've turbocharged our event handling. The time complexity for handling an event is now a lightning-fast (O(1)), a monumental improvement over the (O(n)) time complexity of the ponderous when statement. While our payload mechanism adds a dollop of syntactic sugar. It enables us to bundle all the necessary data into a single object, making our code more legible and maintainable.

💡 Note: Using a HashMap instead of a large when() statement provides a significant performance improvement. It can be up to 40 to 150 times faster. However, explaining the details would exceed the scope of this blog post. Therefore, I will cover it, along with other Kotlin performance puzzles, in a future blog post.

While the refactored code remains as simple as before:

onEvent(BlockEvent.OnChangeFillColor(Color.RED))

This still triggers the handleEvent method in BlockEventsHandler, which in turn performs the appropriate action based on the event type. The BlockEvent itself is a data object containing all event details, and it serves as the lambda parameter.

A Note on Payloads

The payload creation is a dynamic lambda function that's executed each time an event is handled. This ensures that all variables not part of the event are consistently up-to-date. Given that we're dealing with a single thread per event handler, caching the payload is entirely secure.

Step 3: Adding Syntactic Sweetness with Infix Functions

sven2244_an_abstract_composition_resembling_a_candy_store_fille_894e8c68-0a37-4ad0-adc8-c313db1c29da.png

In our next act, we elevate our syntax to a new level of expressiveness and readability. We introduce an infix function called to, allowing us to map an event class to its corresponding action elegantly.

Witness the updated code:

abstract class EventsHandler<Payloads>(
    val fillPayload: (cache: Payloads) -> Unit
) {
    infix fun <Payloads, EventType : Event> KClass<out EventType>.to(lambda: Payloads.(event: EventType) -> Unit) {
        eventMap[event] = lambda as Payloads.(event: Event) -> Unit
    }
    // ... (rest of the code remains the same)
}

class BlockEventsHandler(
    manager: EventsManager,
    override val fillPayload: (cache: TextBlockEventsHandler) -> Unit
) : EventsHandler<TextBlockEventsHandler>(manager) {
    lateinit var engine: Engine
    lateinit var block: DesignBlock
    lateinit var fontFamilyMap: Map<String, FontFamilyData>

    init {
        BlockEvent.OnDelete::class to { 
            engine.delete(block) 
        }
        BlockEvent.OnBackward::class to { 
            engine.sendBackward(block) 
        }
        BlockEvent.OnDuplicate::class to { 
            engine.duplicate(block) 
        }
        BlockEvent.OnForward::class to { 
            engine.bringForward(block) 
        }
        BlockEvent.ToBack::class to { 
            engine.sendToBack(block) 
        }
        BlockEvent.ToFront::class to { 
            engine.bringToFront(block) 
        }
        BlockEvent.OnChangeFinish::class to { 
            engine.editor.addUndoStep() 
        }
        BlockEvent.OnChangeBlendMode::class to { 
            onChangeBlendMode(engine, block, it.blendMode) 
        }
        BlockEvent.OnChangeOpacity::class to { 
            engine.block.setOpacity(block, it.opacity) 
        }
        BlockEvent.OnChangeFillColor::class to { 
            onChangeFillColor(engine, block, it.color) 
        }
        // ...
    }
}

Syntactic Sweetness and Performance

The introduction of the to infix function adds a sprinkle of syntactic sweetness that enhances code expressiveness and enables a more natural usage. This makes it crystal clear what each event is all about. And fear not, the performance remains at a blazing-fast (O(1)), thanks to our trusty HashMap.

Flexibility in Syntax

While the to keyword is used here, feel free to substitute it with other terms like handle, trigger, or anything that best suits your context. Flexibility is the name of the game.

Step 4: Embracing Inline Functions for Elegance

sven2244_a_mesmerizing_image_of_a_kaleidoscope_with_vibrant_pat_b53a7e30-820a-4308-a7aa-11222be8a7a1.png

However, this is still not perfect because the ::class breaks smooth reading.

So let's do it differently. Let us try to introduce a more elegant way to register an event. Let us eliminate the need to specify ::class every time we register an event handler will make our code more concise and readable.

This is made possible by an inline function with a verified type parameter that maintains the class reference at runtime.

To do this, we extend the EventsHandler class with this new register function:

class EventsHandler(
    register: EventsHandler.() -> Unit,
) {
    inline fun <reified EventType : BaseEvent> register(noinline lambda: (event: EventType) -> Unit) : Any {
        this[EventType::class] = lambda
        return lambda
    }
   // ... (rest of the code remains the same)
}

The New Syntax

This is what registering an event handler looks like with the new syntax:

register<BlockEvent.OnChangeLineWidth> {
    engine.block.setWidth(block, engine.block.getFrameWidth(block))
    engine.block.setHeight(block, it.width)
}

Much better, right? The new syntax is more concise, eliminates redundancy, and is type-safe because the reified type parameters ensure that the event type is known at compile-time and runtime, eliminating the need for unsafe casting.

Step 5: Elevating register to an Extension Function for Highlighting

highlighting.jpg

To improve code readability, we'll make a subtle but effective step by converting the register function from a EventsHandler class function, into an EventsHandler extension function.

Sounds stupid! So why?

This small change improves code readability by highlighting the register keyword through syntax highlighting from a Kotlin extension function. This will make it much more colorful, which improves readability.

Updated EventsHandler Class

The EventsHandler class remains largely unchanged, but the register function is now outside the class and transformed into an extension function for the EventsHandler class:

class EventsHandler(
    register: EventsHandler.() -> Unit,
) {
    // ... (rest of the code remains the same)
}

inline fun <reified EventType : BaseEvent> EventsHandler.register(noinline lambda: (event: EventType) -> Unit) : Any {
    this[EventType::class] = lambda
    return lambda
}

By simply shifting register out of the class, the EventsHandler class definition now stands out with distinctive syntax highlighting. It's a clever trick that doesn't impact runtime or compile performance, since it's an inline operation anyway.

**register**<BlockEvent.OnChangeLineWidth> {
    engine.block.setWidth(block, engine.block.getFrameWidth(block))
    engine.block.setHeight(block, it.width)
}

Step 6: Eliminating lateinit Variables with Delegated Properties

sven2244_an_abstract_image_of_interconnected_gears_and_cogs_for_3b651ffc-9367-470d-8be3-82c3c19b9ffe.png

Now, it's time to address the enigmatic lateinit variables and the somewhat convoluted fillPayload mechanism. Let us introduce a cleaner approach, using delegated properties and lambda functions to inject dependencies.

Let's add an Inject class to wrap a normal lambda as delegable:

class Inject<Type>(private val inject: () -> Type) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): Type = inject()
}

With this newfound power, our event handler code becomes cleaner and more intuitive. It takes on the style of Jetpack Compose's declarative syntax:

fun EventsHandler.textBlockEvents(
    engine: () -> Engine,
    block: () -> DesignBlock,
    fontFamilyMap: () -> Map<String, FontFamilyData>,
) {
    // Inject the dependencies
    val engine by Inject(engine)
    val block by Inject(block)
    val fontFamilyMap by Inject(fontFamilyMap)

    // Event handling logic here
    // ...
}

Whenever one of the variables is accessed, the lambda is called, and you always get the current variable.

Also, the creation of the "payload" becomes more straightforward, clean, and type-safe. It kinda looks like passing a variable:

private val eventHandler = EventsHandler {
    textBlockEvents (
        engine = ::engine,
        block = ::getBlockForEvents,
        fontFamilyMap = { checkNotNull(assetsRepo.fontFamilies.value).getOrThrow() },
    )
}

Looks and feels like magic! Pretty cool, right?

Step 7: Multiple Event Handlers for Single Responsibility Principle

sven2244_code_as_a_magical_candy-filled_wonderland._Syntax_Suga_a50bcd92-bea2-4fea-aa92-e1cd557277b6.png

In our grand finale, we harness the newfound flexibility from our previous changes to register multiple event handler functions. Each event handler registration function now has a specific topic, aligning perfectly with the Single Responsibility Principle (SRP).

Enhanced Event Handler Registration

We can now register multiple event handler functions within the same EventsHandler instance. Each function can specialize in handling a particular type of event, making the code more modular and manageable. Behold the grand design:

private val eventHandler = EventsHandler {
    cropEvents(
        engine = ::engine,
        block = ::getBlockForEvents,
    )
    blockEvents (
        engine = ::engine,
        block = ::getBlockForEvents,
    )
    textBlockEvents (
        engine = ::engine,
        block = ::getBlockForEvents,
        fontFamilyMap = { checkNotNull(assetsRepo.fontFamilies.value).getOrThrow() },
    )
    // ...
}

fun EventsHandler.blockEvents(
    engine: () -> Engine,
    block: () -> DesignBlock
) {
    val engine: Engine by Inject(engine)
    val block: DesignBlock by Inject(block)

    register<BlockEvent.OnDelete> { engine.delete(block) }

    register<BlockEvent.OnBackward> { engine.sendBackward(block) }

    register<BlockEvent.OnDuplicate> { engine.duplicate(block) }

    register<BlockEvent.OnForward> { engine.bringForward(block) }

    register<BlockEvent.ToBack> { engine.sendToBack(block) }

    register<BlockEvent.ToFront> { engine.bringToFront(block) }

    register<BlockEvent.OnChangeFinish> { engine.editor.addUndoStep() }

    register<BlockEvent.OnChangeBlendMode> {
        if (engine.block.getBlendMode(block) != it.blendMode) {
            engine.block.setBlendMode(block, it.blendMode)
            engine.editor.addUndoStep()
        }
    }

    register<BlockEvent.OnChangeOpacity> { engine.block.setOpacity(block, it.opacity) }
}

fun EventsHandler.cropEvents(
    engine: () -> Engine,
    block: () -> DesignBlock
) {
    val engine: Engine by Inject(engine)
    val block: DesignBlock by Inject(block)
    // ... (event handling logic for cropping events)
}

fun EventsHandler.textBlockEvents(
    engine: () -> Engine,
    block: () -> DesignBlock,
    fontFamilyMap: () -> Map<String, FontFamilyData>,
) {
    val engine by Inject(engine)
    val block by Inject(block)
    val fontFamilyMap by Inject(fontFamilyMap)
    // ... (event handling logic for text block events)
}

While the triggering and its API remain unchanged, and no extra parameters need to be passed:

open fun onEvent(event: Event) {
    eventHandler.handleEvent(event)
}

Final Words

As we conclude our journey through Kotlin code refactoring, we've unlocked the secrets to enhanced performance and style. By embracing techniques such as HashMaps, infix functions, and inline functions with reified type parameters, we've elevated our code to new heights. The benefits are clear: improved efficiency, readability, and adherence to the Single Responsibility Principle. Armed with these tools, you're now ready to embark on your own coding adventures, transforming messy code into elegant masterpieces.

If you’d like to try it out, I’ve created a working example code on the Kotlin Playground.

Thank you for accompanying, and happy coding! Never miss out on updates and subscribe to our newsletter.

Related Articles

CE.SDK v1.20 Release Notes
2 min read
CE.SDK v1.17 Release Notes
2 min read
CE.SDK v1.15 Release Notes
2 min read

GO TOP