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

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.

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.

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.

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

?.let {
it.invoke(payloadCache.also { fillPayload(it) }, event)
}
}

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

= 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)
}

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.

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.

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 = 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)
}
// ...
}
}

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.

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.

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)
}

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.

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.

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)
}

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?

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).

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)
}

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 my work at IMG.LY, and our updates. Subscribe to our newsletter.

Source link