Kotlin DSL - let's express code in "mini-language" - Part 4 of 5

Kotlin DSL - let's express code in "mini-language" - Part 4 of 5

In this post, we take a look at building a simpler API to work with Broadcast receiver, which will automatically unregister itself when the Activity pauses, and register again when it resumes.

Broadcast Receiver

To build this , we first create a class which observes  lifecycle events

class BroadcastReceiver<T>(
  context: T,
  constructor: Builder.() -> Unit
) : LifecycleObserver where T : Context, T : LifecycleOwner {

  
  @OnLifecycleEvent(ON_START)
  fun start() {
    appContext.registerReceiver(broadcastReceiver, filter)
  }

  @OnLifecycleEvent(ON_DESTROY)
  fun stop() = appContext.unregisterReceiver(broadcastReceiver)
}

To attach lifecycle events to our custom class BroadcastReceiver , we extend our class with LifecycleObserver and LifecycleOwner

context.lifecycle.addObserver(this)

During initialize, we init our Builder :

init {
    val builder = Builder()
    constructor(builder)
    filter = builder.filter()
    instructions = builder.instructions()

    context.lifecycle.addObserver(this)
  }

Builder is another class

class Builder internal constructor() {

  private val filter = IntentFilter()
  private val instructions = mutableListOf<Instructions>()

  fun onAction(
    action: String,
    execution: Execution
  ) {
    filter.addAction(action)
    instructions.add(OnAction(action, execution))
  }

  fun onDataScheme(
    scheme: String,
    execution: Execution
  ) {
    filter.addDataScheme(scheme)
    instructions.add(OnDataScheme(scheme, execution))
  }

  fun onCategory(
    category: String,
    execution: Execution
  ) {
    filter.addCategory(category)
    instructions.add(OnCategory(category, execution))
  }

  internal fun filter() = filter

  internal fun instructions() = instructions
}

in which we have 3 main functions : onAction , onDataScheme and onCategory where we do operation on the filter for this Broadcast receiver

Instructions are set of data classes where the checks for action , category are handled.

typealias Execution = (Intent) -> Unit

sealed class Instructions {

  abstract fun matches(intent: Intent): Boolean

  abstract fun execution(): Execution

  data class OnAction(
    val action: String,
    val execution: Execution
  ) : Instructions() {

    override fun matches(intent: Intent): Boolean {
      return intent.action == action
    }

    override fun execution() = execution
  }

  data class OnDataScheme(
    val scheme: String,
    val execution: Execution
  ) : Instructions() {
    override fun matches(intent: Intent): Boolean {
      return intent.data?.scheme == scheme
    }

    override fun execution() = execution
  }

  data class OnCategory(
    val category: String,
    val execution: Execution
  ) : Instructions() {
    override fun matches(intent: Intent): Boolean {
      return intent.hasCategory(category)
    }

    override fun execution() = execution
  }
}

Here , typealias is allowing us to refer to type (Intent)-> Unit as Execution

Now to connect it all :

class BroadcastReceiver<T>(
  context: T,
  constructor: Builder.() -> Unit
) : LifecycleObserver where T : Context, T : LifecycleOwner {

  private val appContext = context.applicationContext
  private val filter: IntentFilter
  private val instructions: List<Instructions>

  init {
    val builder = Builder()
    constructor(builder)
    filter = builder.filter()
    instructions = builder.instructions()

    context.lifecycle.addObserver(this)
  }

  private val broadcastReceiver = object : BroadcastReceiver() {
    override fun onReceive(
      context: Context,
      intent: Intent
    ) {
      for (ins in instructions) {
        if (ins.matches(intent)) {
          ins.execution()
              .invoke(intent)
          break
        }
      }
    }
  }

  @OnLifecycleEvent(ON_START)
  fun start() {
    appContext.registerReceiver(broadcastReceiver, filter)
  }

  @OnLifecycleEvent(ON_DESTROY)
  fun stop() = appContext.unregisterReceiver(broadcastReceiver)
}

Our Broadcast receiver DSL can now be called in the following way :

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // Unregister when onPause, and register again when it resumes.
    BroadcastReceiver(this) {
      onAction("app.SOME_ACTION") {
        // Do something
      }
      onCategory("messages") {
        // Do something
      }
      onDataScheme("file://") {
        // Do something
      }
    }

(credits : Aidan Follestad https://goo.gl/Mi7Z9x)

In Part 5 of this series , we will take a look at how to apply DSLs in writing easier to understand tests for android.