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 enables an object to use another “helper” object to provide data or perform a task rather than do the task 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 pattern like Visitor, Observer, Strategy, and Event Listener.

Composition

This pattern is mainly composed of three different components:

Delegating Object: A delegating object retains the delegate. A delegate is usually held as a weak property to avoid a retain cycle.

Delegate Interface: An interface that defines the methods a delegate should implement.

Delegation: A helper object that implements the delegate interface.

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

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

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Prokash Sarkar

Prokash Sarkar

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