Share
Embed
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
Transcript
64 slides
-
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 -
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 -
Building Blocks Statically typed Syntax <3 DSL IDE + Code
completion JVM / Android / JS / Native Kotlin -
Building Blocks Lambdas with receivers uiLabel.apply { textColor = Color.BLACK
backgroundColor = Color.GREEN text = "" } -
Building Blocks Infix calls infix fun String.concat(other:String) = "$this$other" val
a = "a" val b = "b" val c = a concat b -
Building Blocks Invoke class Family { fun addMember(name: String) {}
operator fun invoke(body: Family.() -> Unit) { body() } } -
Building Blocks Invoke family { addMember("Mom") addMember("Dad") addMember("Kid") } //
neat , nested DSL syntax family.addMember(“Aunt") Or -
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) -
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) -
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)) -
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 -
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 -
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 -
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 -
inline fun <reified T : Any> newIntent(context: Context): Intent =
Intent(context, T::class.java) Intents -
toolbar.search { id = R.id.action_search textSubmitted { presenter reduce Search(it)
true } onClose { presenter reduce Load() true } } Search -
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 } -
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 } -
Search class ToolbarSearchBuilder { fun build(): Toolbar { searchView?.apply {
setOnQueryTextListener(...) setOnActionExpandListener(...) } return toolbar } } -
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 } } -
Search fun Toolbar.search(setup: ToolbarSearchBuilder.() -> Unit) { with(ToolbarSearchBuilder()) { toolbar
= this@search setup() build() } } -
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 -
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 } } -
class Builder internal constructor() { private val filter = IntentFilter()
private val instructions = mutableListOf<Instructions>() } Broadcast -
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)) } -
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 -
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 -
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 -
• 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 -
@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 -
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 -
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 -
object User { infix fun selects(block: SelectsActions.() -> Unit): User
{ block.invoke(SelectsActions) return this } } Tests -
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 -
@Test fun logsInWhenUserSelectsLogin() { } Tests given { user has
{ loggedOut() } } whenever { user launches { app() } selects { login() } } then { user sees { loggedIn() } } -
"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 -
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 -
Gradle // Groovy implementation ‘com.squareup.okhttp3:okhttp:$okhttpVersion' // Kotlin implementation(“com.squareup.okhttp3:okhttp:${extra["okhttpVersion"]}") // Kotlin,
after migrating to buildSrcVersions implementation(Libs.okhttp) -
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 -
Gradle // buildSrc/src/main/kotlin/ProjectVersions.kt object ProjectVersions { const val COMPILE_SDK =
28 } // app/build.gradle android { compileSdkVersion(ProjectVersions.COMPILE_SDK) } -
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 } } -
Gradle // Groovy buildTypes { debug { ... } release
{ ... } anotherbuildtype { ... } } // Kotlin buildTypes { getByName("debug") { ... } getByName("release") { ... } create("anotherbuildtype") { ... } }