Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin DSLs -"mini languge "

Kotlin DSLs -"mini languge "

This talk introduces and highlights how we could use Kotlin language to build custom DSL’s in android and add some readability of code by enforcing the use of declarative code with minimum boilerplate, as well as look at why Kotlin is particularly good for DSLs with examples.

Adit Lal

June 22, 2019
Tweet

More Decks by Adit Lal

Other Decks in Programming

Transcript

  1. Adit Lal GoPay $6.3B @aditlal We have 18+ products from

    food tech to fin-tech to hyper local delivery and massage services Ride Sharing 16.5m KM
  2. A domain-specific language (DSL) is a computer language specialised to

    a particular application domain. This is in contrast to a general-purpose language (GPL), which is broadly applicable across domains. DSL
  3. Building Blocks Statically typed Syntax <3 DSL IDE + Code

    completion JVM / Android / JS / Native Kotlin
  4. Building Blocks Invoke class Family { fun addMember(name: String) {}

    operator fun invoke(body: Family.() -> Unit) { body() } }
  5. Spans val spannable1 = SpannableString("some formatted text") spannable1.setSpan(StyleSpan(Typeface.BOLD), 0, 4,

    SPAN_EXCLUSIVE_EXCLUSIVE) spannable1.setSpan(StyleSpan(Typeface.ITALIC), 6, 15, SPAN_EXCLUSIVE_EXCLUSIVE) spannable1.setSpan(ForegroundColorSpan(COLOR.RED),17,21, SPAN_EXCLUSIVE_EXCLUSIVE) val spannable2 = SpannableString("nested text") spannable.setSpan(StyleSpan(Typeface.BOLD), 0, 6, SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(StyleSpan(Typeface.ITALIC), 0, 6, SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(URLSpan(url), 8, 12, SPAN_EXCLUSIVE_EXCLUSIVE) val spannable3 = SpannableString("no wrapping") spannable.setSpan(StyleSpan(Typeface.BOLD), 0, 42, SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(SuperscriptSpan(), 4, 12, SPAN_EXCLUSIVE_EXCLUSIVE)
  6. fun spannable(func: () -> SpannableString) = func() Spans private fun

    span(s: CharSequence, o: Any) = (if (s is String) SpannableString(s) else s as? SpannableString ?: SpannableString("")).apply { setSpan(o, 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) } operator fun SpannableString.plus(s: SpannableString) = SpannableString(this concat s) operator fun SpannableString.plus(s: String) = SpannableString(this concat s)
  7. fun bold(s: CharSequence) = span(s, StyleSpan(android.graphics.Typeface.BOLD)) Spans fun sub(s: CharSequence)

    = span(s, SubscriptSpan()) // baseline is lowered fun size(size: Float, s: CharSequence) = span(s, RelativeSizeSpan(size)) fun color(color: Int, s: CharSequence) = span(s, ForegroundColorSpan(color)) fun url(url: String, s: CharSequence) = span(s, URLSpan(url)) fun italic(s: CharSequence) = span(s, StyleSpan(android.graphics.Typeface.ITALIC))
  8. val spanned = spannable{ bold("some") + italic(" formatted") + color(Color.RED,

    " text") } Spans val nested = spannable{ bold(italic("nested ")) + url("www.google.com", “text") } val noWrapping = bold("no ") + sub(“wrapping") text_view.text = spanned + nested + noWrapping
  9. inline fun <reified T : Any> Activity.launchActivity( requestCode: Int =

    -1, options: Bundle? = null, noinline init: Intent.() -> Unit = {}) { val intent = newIntent<T>(this) intent.init() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { startActivityForResult(intent, requestCode, options) } else { startActivityForResult(intent, requestCode) } } Intents
  10. inline fun <reified T : Any> Context.launchActivity( options: Bundle? =

    null, noinline init: Intent.() -> Unit = {}) { val intent = newIntent<T>(this) intent.init() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { startActivity(intent, options) } else { startActivity(intent) } } Intents
  11. toolbar.search { id = R.id.action_search textSubmitted { presenter reduce Search(it)

    true } onClose { presenter reduce Load() true } } Search
  12. class TextSearchBuilder { lateinit var action: (String) -> Boolean }

    Search class ToolbarSearchBuilder { lateinit var toolbar: Toolbar private var closeAction: (() -> Boolean)? = null private var textChange: TextSearchBuilder? = null private var textSubmitted: TextSearchBuilder? = null }
  13. class TextSearchBuilder { lateinit var action: (String) -> Boolean }

    Search class ToolbarSearchBuilder { lateinit var toolbar: Toolbar private var closeAction: (() -> Boolean)? = null private var textChange: TextSearchBuilder? = null private var textSubmitted: TextSearchBuilder? = null }
  14. Search class ToolbarSearchBuilder { fun build(): Toolbar { searchView?.apply {

    setOnQueryTextListener(...) setOnActionExpandListener(...) } return toolbar } }
  15. Search fun textChange(setup: (String) -> Boolean) { textChange = TextSearchBuilder().apply

    { action = setup } } fun onClose(close: () -> Boolean) { closeAction = close } fun textSubmitted(setup: (String) -> Boolean) { textSubmitted = TextSearchBuilder().apply { action = setup } }
  16. 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 } } Broadcast
  17. typealias Execution = (Intent) -> Unit Broadcast 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 } }
  18. class Builder internal constructor() { private val filter = IntentFilter()

    private val instructions = mutableListOf<Instructions>() } Broadcast
  19. fun onAction(action: String, execution: Execution) { filter.addAction(action) instructions.add(OnAction(action, execution)) }

    Broadcast 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)) }
  20. 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) } Broadcast
  21. class BroadcastReceiver<T>( context: T, constructor: Builder.() -> Unit ) :

    LifecycleObserver where T : Context, T : LifecycleOwner { init { val builder = Builder() constructor(builder) filter = builder.filter() instructions = builder.instructions() context.lifecycle.addObserver(this) } } Broadcast
  22. 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 } } Broadcast
  23. • Update preferences to make sure that the user is

    logged out before the test starts • The user launches the app • The user clicks on “Log In” • We assert that the user sees the logged in text Tests
  24. @Test fun logsInWhenUserSelectsLogin() { ... resetLoginInPref() //sets login pref key

    as false instrumentation.startActivitySync(loginIntent) onView(allOf(withId(R.id.login_button), withText(R.string.login))) .perform(click()) val expectedText = context.getString(R.string.is_logged_in, "true") onView(AllOf.allOf(withId(R.id.label), withText(expectedText))) .perform(ViewActions.click()) } Tests
  25. Setup, actions and assertions. Given the user is logged out

    When the user launches the app When the user clicks “Log In” Then the user sees the logged in text Tests
  26. infix fun Any.given(block: () -> Unit) = block.invoke() infix fun

    Any.whenever(block: () -> Unit) = block.invoke() infix fun Any.then(block: () -> Unit) = block.invoke() Tests
  27. object User { infix fun selects(block: SelectsActions.() -> Unit): User

    { block.invoke(SelectsActions) return this } } Tests
  28. object SelectsActions { fun logout() { onView(allOf(withId(R.id.login_button), withText(R.string.logout))) .perform(click()) }

    fun login() { onView(allOf(withId(R.id.login_button), withText(R.string.login))) .perform(click()) } } Tests
  29. @Test fun logsInWhenUserSelectsLogin() { } Tests given { user has

    { loggedOut() } } whenever { user launches { app() } selects { login() } } then { user sees { loggedIn() } }
  30. "person" : { "id": "dd9e889a-a4a9-4930-9f1d-498e44a9cb96", "isActive": true, "picture": "http://placehold.it/32x32", "age":

    21, "name": “John Doe”, "gender": "male", "email": “[email protected]“, "phone": "+1 (914) 555-2368” } JSON
  31. person { Id = "dd9e889a-a4a9-4930-9f1d-498e44a9cb96" isActive = true picture =

    "http://placehold.it/32x32" age = 21 gender = "male" name { first = "John" last = “Doe" } contact { email = "[email protected]" phone = "+1 (914) 555-2368" } JSON
  32. Gradle // buildSrc/src/main/kotlin/Versions.kt // Before buildSrcVersions task object Versions {

    const val com_squareup_okhttp3: String = "3.12.0" } // After buildSrcVersions task object Versions { const val com_squareup_okhttp3: String = "3.12.0" // available: "3.12.1" } // After manual update object Versions { const val com_squareup_okhttp3: String = "3.12.1" } buildSrcVersions: https://goo.gl/8MTFkY ./gradlew buildSrcVersions
  33. Gradle // buildSrc/src/main/kotlin/ProjectVersions.kt object ProjectVersions { const val COMPILE_SDK =

    28 } // app/build.gradle android { compileSdkVersion(ProjectVersions.COMPILE_SDK) }
  34. Gradle // Groovy buildTypes { debug { ... } release

    { ... } anotherbuildtype { ... } } // Kotlin buildTypes { getByName("debug") { ... } getByName("release") { ... } create("anotherbuildtype") { ... } }