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. Let’s Express Code “mini language” Kotlin DSL

  2. 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
  3. DSL Building blocks of DSL Custom DSL Use cases ?

  4. 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
  5. None
  6. Easy to read, Express, Understand DSL

  7. Configuration Templates Logic UI ..more DSL

  8. Building Blocks Kotlin language features that help build DSL’s

  9. Building Blocks Statically typed Syntax <3 DSL IDE + Code

    completion JVM / Android / JS / Native Kotlin
  10. Building Blocks Lambdas with receivers Infix call Invoke Extension functions

    Kotlin
  11. Building Blocks Lambdas with receivers uiLabel.apply { textColor = Color.BLACK

    backgroundColor = Color.GREEN text = "" }
  12. Building Blocks Infix calls infix fun String.concat(other:String) = "$this$other" val

    a = "a" val b = "b" val c = a concat b
  13. Building Blocks Invoke class Family { fun addMember(name: String) {}

    operator fun invoke(body: Family.() -> Unit) { body() } }
  14. Building Blocks Invoke family { addMember("Mom") addMember("Dad") addMember("Kid") } //

    neat , nested DSL syntax family.addMember(“Aunt") Or
  15. Spans Custom spans builders for Android Text

  16. Spans

  17. 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)
  18. 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)
  19. 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))
  20. 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
  21. Intents Simplifying Android Intents

  22. var intent = Intent(myActivity, TargetActivity::class) intent.putExtra(“user_id”, user.id) intent.putExtra("some_param_1", user.name) intent.putExtra("some_param_2",

    false) myActivity.startActivity(intent) Intents
  23. myActivity.launchActivity<TargetActivity> { putExtra(INTENT_USER_ID, user.id) putExtra(INTENT_USER_PARAM_1, user.name) putExtra(INTENT_USER_PARAM_2, false) } Intents

  24. launchActivity<TargetActivity> { putExtra(INTENT_USER_ID, user.id) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } Intents

  25. 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
  26. 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
  27. inline fun <reified T : Any> newIntent(context: Context): Intent =

    Intent(context, T::class.java) Intents
  28. Search fun Use Case

  29. toolbar.search { id = R.id.action_search textSubmitted { presenter reduce Search(it)

    true } onClose { presenter reduce Load() true } } Search
  30. 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 }
  31. 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 }
  32. Search class ToolbarSearchBuilder { fun build(): Toolbar { searchView?.apply {

    setOnQueryTextListener(...) setOnActionExpandListener(...) } return toolbar } }
  33. 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 } }
  34. Search fun Toolbar.search(setup: ToolbarSearchBuilder.() -> Unit) { with(ToolbarSearchBuilder()) { toolbar

    = [email protected] setup() build() } }
  35. Broadcast DSL with actions

  36. 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
  37. 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 } }
  38. class Builder internal constructor() { private val filter = IntentFilter()

    private val instructions = mutableListOf<Instructions>() } Broadcast
  39. 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)) }
  40. internal fun filter() = filter internal fun instructions() = instructions

    Broadcast
  41. 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
  42. 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
  43. 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
  44. Tests Make tests more readable

  45. • 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
  46. @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
  47. 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
  48. @Test fun logsInWhenUserSelectsLogin() { } Tests given(user).has().loggedOut(); when(user).launches().app(); when(user).selects().login(); then(user).sees().loggedIn();

  49. 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
  50. object User { infix fun selects(block: SelectsActions.() -> Unit): User

    { block.invoke(SelectsActions) return this } } Tests
  51. 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
  52. @Test fun logsInWhenUserSelectsLogin() { } Tests given { user has

    { loggedOut() } } whenever { user launches { app() } selects { login() } } then { user sees { loggedIn() } }
  53. JSON fun Use Case

  54. "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
  55. 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
  56. Gradle Type safe build logic

  57. Gradle

  58. Gradle // Groovy implementation ‘com.squareup.okhttp3:okhttp:$okhttpVersion' // Kotlin implementation(“com.squareup.okhttp3:okhttp:${extra["okhttpVersion"]}") // Kotlin,

    after migrating to buildSrcVersions implementation(Libs.okhttp)
  59. 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
  60. Gradle // buildSrc/src/main/kotlin/ProjectVersions.kt object ProjectVersions { const val COMPILE_SDK =

    28 } // app/build.gradle android { compileSdkVersion(ProjectVersions.COMPILE_SDK) }
  61. Gradle // buildSrc/src/main/kotlin/Extensions.kt fun Project.getCiBuildNumber() = if (project.hasProperty("ciBuildNumber") property("ciBuildNumber") else

    null // app/build.gradle android { defaultConfig { versionCode = getCiBuildNumber() ?: 1 } }
  62. Gradle // Groovy buildTypes { debug { ... } release

    { ... } anotherbuildtype { ... } } // Kotlin buildTypes { getByName("debug") { ... } getByName("release") { ... } create("anotherbuildtype") { ... } }
  63. https://superapp.is https://www.gojek.io

  64. Adit Lal @aditlal Thats all folks! superapp.is https://adit.dev/tag/dsl/