Delegation Pattern: An effective way of replacing Android’s Base Activity with native Kotlin support

Prokash Sarkar
6 min readAug 11, 2022
Delegation Pattern: An effective way of replacing Android’s Base Activity with native Kotlin support

In software engineering, a design pattern is a reusable solution to a frequently occurring problem in a specific context. Those design patterns can solve common problems when designing an application or system. This article explicitly covers the Delegation Design Pattern in Object-Oriented Programming and some of its usage. We will also explore how Kotlin supports it natively requiring zero boilerplate code.

Definition

In the Introduction to Gamma et al. 1994, Grady Booch defined delegation as:

Delegation is a way to make composition as powerful for reuse as inheritance [Lie86, JZ91]. In delegation, two objects are involved in handling a request: a receiving object delegates operations to its delegate.

In short, The delegation pattern allows one object to utilize a different “helper” object to conduct a job or supply data rather than carrying out the operation itself.

In delegation, an object shows a certain behavior to the outside, but in reality, it delegates the responsibility to an associated object. Delegation is commonly used by many other patterns like Visitor, Observer, Strategy, and Event Listener.

Composition

Three elements make up the majority of this pattern:

  • A delegating object to keep the reference of the delegate.
  • An interface to declare how the methods will be implemented inside the delegate.
  • A helper object that will implement the methods defined inside the delegate interface.

Reference: https://www.kodeco.com/books/design-patterns-by-tutorials/v3.0/chapters/4-delegation-pattern

A simple example of the Delegation with a UML Class Diagram could be like this:

Example of a Delegation pattern

Reference: http://best-practice-software-engineering.ifs.tuwien.ac.at/patterns/delegation.html

Here, The ISoundBehavior interface holds the core structure to define the desired behavior. MeowSound class implements the interface and describes how the methods should be created. The Cat class holds an instance of MeowSound class and delegates the request.

Implementing the Delegation Pattern in Kotlin

First, let’s create an interface for our Delegate:

interface ISoundBehavior {
fun makeSound()
}
class MeowSound : ISoundBehavior {
override fun makeSound() {
println("Meow!")
}
}

Now let’s create a Cat class that uses the ISoundBehavior interface in the constructor:

class Cat(private val soundBehavior: ISoundBehavior) {
fun makeSound() {
soundBehavior.makeSound()
}
}

Finally, our Main class to test the delegation:

fun main() {
val cat = Cat(MeowSound())
// Delegation
cat.makeSound()
}

Output:

Meow!

Everything should be working as expected. Keep in mind that, when the number of methods increases inside the ISoundBehavior interface, we have to create an implementation for them inside the Cat class. This will create a lot of boilerplate code. Fortunately, We can take this a step further by using Kotlin’s native feature. Refactor the Cat class as follows:

class Cat(soundBehavior: ISoundBehavior) : ISoundBehavior by soundBehavior

The byclause in the supertype list for Derived indicates that soundBehavior will be stored internally in objects of Derived and the compiler will generate all the methods of ISoundBehavior that forward to soundBehavior.

You might have seen the lazy keyword before. Kotlin has other built in Delegation properties too e.g. by lazy, by Delegates.observable(). Check this link for more: https://kotlinlang.org/docs/delegated-properties.html#observable-properties

Run the Main class again, and the result should be the same.

Output:

Meow!

Overriding a member of an interface implemented by delegation

Override for an interface method works as expected. The compiler will use your override implementations instead of the delegate object. If want to get a custom behavior for the makeSoundmethod, use theoverridekeyword. The program will execute your custom implementation instead of the default one.

class Cat(soundBehavior: ISoundBehavior) : ISoundBehavior by soundBehavior {    override fun makeSound() {
println("Modified Meow!")
}
}

Now the Main class will output “Modified Meow!” instead of “Meow”

Output:

Modified Meow!

While this seems just abstracts away some of the functionality into another class, the real power of this pattern comes when there are multiple delegates. The delegator typically has a method for each delegate that will convert the delegator to use that delegate.

Using Delegation Pattern to replace Android’s Base Activity

Using a Base Activity is very common in Android development. This reduces the number of boilerplate codes and shares the logic with all child Activities. But there are some major limitations:

  1. Inheritance in Java/Kotlin doesn’t support extending multiple classes.
  2. Since you can’t extend multiple Base Activity, All logic needs to be inside a single class. This breaks the Single Responsibility Principle.
  3. You are forced to keep logics that might not be relevant to other Activities, and your code becomes difficult to manage.

Let’s explain this with a typical example:

Suppose our app monitors network and battery status in all Activities. Instead of implementing the same logic everywhere, we can put them inside a BaseActivity and use it from all Activities.

BaseActivity

open class BaseActivity : AppCompatActivity() {

fun observeBatteryChanges() {
TODO("Not yet implemented")
}

fun observeNetworkChanges() {
TODO("Not yet implemented")
}

}

LoginActivity

class LoginActivity : BaseActivity() {override fun onCreate(savedInstanceState: Bundle?) {
observeBatteryChanges()
observeNetworkChanges()
}
}

MainActivity

class MainActivity : BaseActivity() {override fun onCreate(savedInstanceState: Bundle?) {
observeBatteryChanges()
observeNetworkChanges()
}
}

Now, let’s say we want to track user actions in all Activities after login. For this, we can add our new methods related to tracking inside the BaseActivity.

open class BaseActivity : AppCompatActivity() {

fun observeBatteryChanges() {
TODO("Not yet implemented")
}

fun observeNetworkChanges() {
TODO("Not yet implemented")
}

fun trackUserAction(action: String) {
TODO("Not yet implemented")
}

}

This looks very simple, but there are some major drawbacks.

  1. We are only interested in tracking user actions after login, but since we have put this in the BaseActivtity, all Activities that extend BaseActivity will have access to this method.
  2. The codebase inside our BaseActivity will no longer have a single responsibility and quickly become cumbersome.
  3. Someone new to your code will have no idea of the BaseAcitivitie’s functionality at a glance.

As discussed earlier, we can mitigate the issue by using the Delegation pattern. Let’s see how we can refactor the previous code to adopt the new pattern.

For hardware monitor:

interface IHardwareMonitor {

fun observeBatteryChanges()

fun observeNetworkChanges()

}
class HardwareMonitorImpl : IHardwareMonitor {

override fun observeBatteryChanges() {
TODO("Not yet implemented")
}

override fun observeNetworkChanges() {
TODO("Not yet implemented")
}

}

For user action tracking:

interface ITrackingService {

fun trackUserAction(action: String)

}
class UserTrackingImpl : ITrackingService {

override fun trackUserAction(action: String) {
TODO("Not yet implemented")
}

}

Now if our Activity uses both hardware monitor and tracking service we can implement the IHardwareMonitor and ITrackingService class:

class MainActivity : ComponentActivity(),
IHardwareMonitor by HardwareMonitorImpl(), // delegation
ITrackingService by UserTrackingImpl() { // delegation
override fun onCreate(savedInstanceState: Bundle?) {
observeBatteryChanges()
observeNetworkChanges()
}
override fun onPause() {
super.onPause()
trackUserAction("Activity paused")
}
}

Or if our Activity uses only IHardwareMonitor class, we can just plug it into it:

class LoginActivity : ComponentActivity(),
IHardwareMonitor by HardwareMonitorImpl() { // delegation
override fun onCreate(savedInstanceState: Bundle?) {
observeBatteryChanges()
observeNetworkChanges()
}
}

The beauty of the pattern is you can add an unlimited number of features to your Activity and use them like plug and play. This will not only keep the Activity clean but will also improve the long-term scalability.

Delegation can be used as an alternative to inheritance. Inheritance is a good strategy when a close relationship exists between parent and child object, however, inheritance couples object very closely. Often, delegation is the more flexible way to express a relationship between classes.

When to use:

  • To reduce the coupling of methods to their class
  • Components that behave identically but realize that this situation can change in the future

Benefits

  • Separates the different sets of functionality
  • The flexibility of Run-time modification

Drawbacks

  • Not as straightforward as implementing an inheritance

--

--

Prokash Sarkar

An Audiophile and Android enthusiast. Currently pursuing a perfect blend of style and function for a wide range of Android Applications.