Decorator pattern in Android Development

Denis Rebrov
5 min readOct 13, 2021

--

Decorator is great pattern that in many cases may in best way separate some accompanying or wrapping logic from core system parts.

Decorator pattern class diagram

Decorator of AbstractType at the same time:

  • IS AbstractType (implements its interface)
  • HAS AbstractType (has reference to some other AbstractType object)

So, decorator can wrap AbstractType behaviour and add some logic before and after its target methods.

You can read more about decorator here

🌲 Decorator in Kotlin

Kotlin allows us to use Decorators in convenient and concise way by delegation — with the use of the by keyword.

abstract class AbstractType {
abstract fun
foo()
abstract fun bar()
}
class Decorator(target: AbstractType): AbstractType by target {
//no errors regardless of AbstractType unimplemented members
}

Using delegation allows us to omit overriding all abstract members, that makes our code cleaner.

💉 Decorator with DI

One of Decorator usage benefits is that you can define via Dependency Injection where to use original class object, or decorator, or decorators stack.

DI makes controlling this behaviour much easier, and keeps decorator-related code separated from core logic, connected only in DI modules.

Let's look how Dagger allows you to maintain at the same time original provider, and decorated variance in project!

You can define separate providers, and decorated provider must be annotated with @Named annotation with some String qualifier.

In you never used @Named before, you can read about it here.

@Module
abstract class FooModule {
@Binds
abstract fun provideFoo(foo: FooDefaultImpl): IFoo
}

@Module
class FooDecoratorModule {
@Provides
@Named(NAMED_KEY)
fun provideFoo(foo: IFoo): IFoo {
return FooDecorator(original)
}

companion object {
const val NAMED_KEY = "Foo Decorator"
}
}

Then, if you have to use decorated variance in some class, you can just add @Named(NAMED_KEY) qualifier to injected variable.

class Bar @Inject constructor(
@Named(NAMED_KEY) private val foo: IFoo
) {
//use decorated foo below
}

Further we can add or remove @Named annotation to use different decorated objects in different contexts.

🐦 Example 1: Fake Network Data

Suppose you are developing a mobile game that has in-game currency available for purchase with real money.

One day, the managers consulted and decided that it would be great to add a discount mechanic, which would be managed on the backend. You, as a mobile developer, just have to display the status of the current discount.

data class SaleData(
val description: String
val startDate: Date,
val endDate: Date
//some other event data
)
interface ISaleServerDataSource {
fun getSaleData(): Flowable<SaleData>
}

Having written the domain and presentation logic, you are faced with a problem: how do you test whether the discount is displayed correctly in different situations: when it hasn’t started yet, when it’s already running, and when the discount is over.

Now you have 3 options:

  • Ask backend developer to temporarily change discount period
    — about 3–5 times just to test your code!
  • Add your debug data directly into repository/data source
    — about 30% chance that you forget remove it before review, and 0.5% chance that reviewer won’t notice it and this code will get into the release
  • Use Decorator!
class SaleServerDataSourceDebugDecorator @Inject constructor(
private val dataSource: ISaleServerDataSource
) : ISaleServerDataSource {
override fun getSaleDataFlow() = dataSource.getSaleData().map {
return@map it.copy(
startDate = getYesterday(),
endDate = getTomorrow()
)
}
}
private fun getYesterday(): Date {
val cal = Calendar.getInstance()
cal.add(Calendar.DATE, -1)
return cal.time
}
private fun getTomorrow(): Date {
val cal = Calendar.getInstance()
cal.add(Calendar.DATE, 1)
return cal.time
}
}

The code written above substitutes the real start and end dates for the dates we want to test. As in all the other examples, it’s easy to add/ remove with DI.

✈️ Example 2: Ads Navigation

Task: show interstital ads when user tries to open specific screen.

Time to use Decorator not only for debugging!

Let’s assume, that we already have existing activity / app screen — that displays some video stream. Navigation logic encapsulated into “navigator” entity, that interface has for instance exactly one method — openVideoStream with streamId argument.

interface IOpenStreamNavigator {
fun
openVideoStream(streamId: Long)
}

Now you have to integrate optional interstital ads when user tries to open stream. Interstitial is not always nesessary — we can check, should we show it or not via AdsUseCases.getShowAdState.

interface AdsUseCases{
fun
getShowAd(place: AdPlace): Boolean
}
enum class AdPlace{
ON_START_STREAM
}

Instead of writing ads-related code directly into IOpenStreamNavigator implementation, let’s define separate component AdsNavigator with single method showAd, that returns Maybe<ShowAdResult>.

interface AdsNavigator{
fun
showAd(): Maybe<ShowAdResult>
}
enum class ShowAdResult {
AD_OPEN,
AD_DISMISSED,
ERROR,
UNDEFINED
}

Then, we can create OpenStreamNavigatorAdsDecorator, that decorates IOpenStreamNavigator with ad apperance behaviour.

class OpenStreamNavigatorAdsDecorator @Inject constructor(
private val adsUseCases: AdsUseCases,
private val adsNavigator: AdsNavigator,
private val targetNavigator: IOpenStreamNavigator
) : IOpenStreamNavigator by targetNavigator, IDestroyable {

private val composite = CompositeDisposable()

override fun openVideoStream(streamId: Long) = tryShowAd {
targetNavigator.openVideoStream(streamId)
}

override fun destroy() = composite.clear()

private fun tryShowAd(onAdShown: (ShowAdResult) -> Unit) {
if(!adsUseCases.getShowAd(AdPlace.ON_START_STREAM))
return onAdShown(ShowAdResult.UNDEFINED)

composite.clear()
adsNavigator
.showAd()
.subscribe(onAdShown)
.let(composite::add)
}
}

📚 Example 3: Ads Statistics

Task: add stats logging to an existing ad behaviour in android app.

For instance, assume that our ads SDK has following callback interface:

//Ads SDK - provided callback interface
interface
AdEventListener {
fun onAdLoaded()
fun onAdShown()
fun onAdDismissed()
}

We already have AdEventListener implementation that provides ads-related navigation: launches ad when it has been loaded, shows toast on loading error, etc…

//Base implementation knows nothing about stats
class
AdEventListenerBaseImpl : AdEventListener {
override fun onAdLoaded() {
//Show ad
}

override fun onAdShown() {
//Do some navigation logic
}

override fun onAdDismissed() {
//Do another navigation logic
}
}

Instead of putting stats-related code here or inherit base implementation class, we can put this code into separate wrapping class:

//All stats logic is encapsulated into decorator
class
AdEventListenerLoggingDecorator(
private val target: AdEventListener,
private val statistics: SomeStatistics
) : AdEventListener by target {
//We can omit onAdLoaded() override fun onAdShown() {
target.onAdShown()
statistics.sendEvent(“ad_shown”)
}
override fun onAdDismissed() {
target.onAdShown()
statistics.sendEvent(“ad_dismissed”)
}
}

Conclusion

Thank you for reading this article to the end!

Decorator is a great pattern for adding behavior to objects on top of their underlying logic. Examples taken from a real commercial app project and simplified/renamed for better understanding.

Don’t forget to clap this post and follow my account 😊

--

--