This article has been authorized to be reprinted by guolin_blog, an official wechat account.
What is DSL?
DSL, short for Domin Specific Language, refers to a computer language that focuses on an application domain, such as HTML for displaying web pages, SQL for database processing, and regular expressions for retrieving or replacing text. The opposite of a DSL is the GPL, which stands for General Purpose Language, or general-purpose programming Language, the familiar Java, C, Objective-C, and so on.
DSLS are classified into external DSL and internal DSL. An external DSL is a language that can be parsed independently, like SQL, which focuses on database operations; Internal DSLS are the apis exposed by a common language to perform specific tasks. They take advantage of the language’s own features to expose the APIS in special forms, such as Android Gradle and iOS dependency management component CocosPods. Gradle is based on Groovy, which is a common language. Gradle builds its own DSL based on Groovy’s syntax, so when configuring Gradle, you must follow Groovy’s syntax as well as Gradle’s DSL standard.
Android Gradle file
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.tanjiajun.androidgenericframework"
minSdkVersion rootProject.minSdkVersion
targetSdkVersion rootProject.targetSdkVersion
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
androidExtensions {
experimental = true
}
dataBinding {
enabled = true
}
packagingOptions {
pickFirst 'META-INF/kotlinx-io.kotlin_module'
pickFirst 'META-INF/atomicfu.kotlin_module'
pickFirst 'META-INF/kotlinx-coroutines-io.kotlin_module'}}Copy the code
IOS Podfile
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target 'GenericFrameworkForiOS' **do**
pod 'SnapKit'.'~ > 4.0'
**end**
Copy the code
Realize the principle of
Before this article focuses on Kotlin’s DSL, let’s take a look at the concepts and syntax.
Extension function
To declare an extension function, prefix it with a receiver Type, the type being extended, as shown in the following code
fun Activity.getViewModelFactory(a): ViewModelFactory =
ViewModelFactory((applicationContext as AndroidGenericFrameworkApplication).userRepository)
Copy the code
Declare a getViewModelFactory function, which is an extension of the Activity.
Lambda expressions
Versions of Java below 8 do not support Lambda expressions, and Kotlin addresses interoperability with Java. Kotlin’s Lambda expressions implement functions in a more concise syntax, freeing developers from redundant and verbose syntax.
Classification of Lambda expressions
Ordinary Lambda expressions
() - >Unit
Copy the code
Return a Lambda expression for Unit without taking any arguments.
(tab: TabLayout.Tab?) -> Unit
Copy the code
Taking a nullable tabLayout. Tab argument returns a Lambda expression for Unit.
Lambda expression with receiver
OnTabSelectedListenerBuilder.() -> Unit
Copy the code
With OnTabSelectedListenerBuilder receiver object, Lambda expressions of returns Unit does not accept any parameters.
This type of receiver is common in Kotlin’s library Functions, such as the following Scope Functions:
The apply function
/** * Calls the specified function [block] with `this` value as its receiver and returns `this` value. * * For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#apply). */
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T. () -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
Copy the code
Let the function
/** * Calls the specified function [block] with `this` value as its argument and returns its result. * * For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#let). */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)}Copy the code
We can also call these Lambda expressions Type aliases.
private typealias OnTabCallback = (tab: TabLayout.Tab?) -> Unit
Copy the code
We call this Lambda expression OnTabCallback.
These features of Lambda expressions in Kotlin are necessary syntactic sugar for implementing DSLS.
Function type instance call
Kotlin provides the invoke function, which we can write as f.invoke(x), which is actually equivalent to f(x), for example:
onTabReselectedCallback? .invoke(tab) ? :Unit
Copy the code
It can actually be written like this:
onTabReselectedCallback? .let { it(tab) } ? :Unit
Copy the code
These two pieces of code are equivalent.
Infix notation
Functions marked with the infix keyword can be called using infix notation (ignoring the dot and parentheses of the call). The infix function must satisfy the following requirements:
- They must be member functions and extension functions.
- They must have only one argument.
- Its arguments cannot accept a variable number of arguments and cannot have default values.
Here’s an example:
infix fun Int.plus(x: Int): Int =
this.plus(x)
// The function can be called with infix notation
1 plus 2
// This is equivalent to this
1.plus(2)
Copy the code
Let me give you some more examples of functions that we often use:
Until function usage
for (i in 0 until 4) {
tlOrder.addTab(tlOrder.newTab().setText("The order$i"))}Copy the code
Until function
/** * Returns a range from this value up to but excluding the specified [to] value. * * If the [to] value is less than or equal to `this` value, then the returned range is empty. */
public infix fun Int.until(to: Int): IntRange {
if (to <= Int.MIN_VALUE) return IntRange.EMPTY
return this. (to -1).toInt()
}
Copy the code
To function usage
mapOf<String,Any>("name" to "TanJiaJun"."age" to 25)
Copy the code
To function source
/**
* Creates a tuple of type [Pair] from this and [that].
*
* This can be useful for creating [Map] literals with less noise, for example:
* @sample samples.collections.Maps.Instantiation.mapFromPairs
*/
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
Copy the code
practice
The Kotlin DSL makes our code simpler, more elegant, and more imaginative. Let’s take a look at some examples:
The callback processing
Callback implementation in Java
The general steps we implement are as follows:
- Define an interface
- Define some callback methods in the interface
- Defines a method that sets the callback interface. The method takes an instance of the callback interface, usually in the form of an anonymous object.
Implement the TextWatcher interface
EditText etCommonCallbackContent = findViewById(R.id.et_common_callback_content);
etCommonCallbackContent.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// no implementation
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// no implementation
}
@Override
public void afterTextChanged(Editable s) {
// no implementation}});Copy the code
Implement TabLayout OnTabSelectedListener interface
TabLayout tlOrder = findViewById(R.id.tl_order);
tlOrder.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// no implementation
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
// no implementation
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
// no implementation}});Copy the code
The callback implementation in Kotlin
Implement the TextWatcher interface
findViewById<EditText>(R.id.et_common_callback_content).addTextChangedListener(object :
TextWatcher {
override fun beforeTextChanged(s: CharSequence? , start:Int, count: Int, after: Int) {
// no implementation
}
override fun onTextChanged(s: CharSequence? , start:Int, before: Int, count: Int) {
// no implementation
}
override fun afterTextChanged(s: Editable?). {
tvCommonCallbackContent.text = s
}
})
Copy the code
Implement TabLayout OnTabSelectedListener interface
tlOrder.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener{
override fun onTabReselected(tab: TabLayout.Tab?). {
// no implementation
}
override fun onTabUnselected(tab: TabLayout.Tab?). {
// no implementation
}
override fun onTabSelected(tab: TabLayout.Tab?).{ vpOrder.currentItem = tab? .position ? :0}})Copy the code
Do you find it similar to the Java way of writing, and you don’t experience Kotlin’s advantages?
Kotlin DSL
TextWatcherBuilder
package com.tanjiajun.kotlindsldemo
import android.text.Editable
import android.text.TextWatcher
/** * Created by TanJiaJun on 2019-10-01. */
privatetypealias BeforeTextChangedCallback = (s: CharSequence? , start:Int, count: Int, after: Int) - >Unit
privatetypealias OnTextChangedCallback = (s: CharSequence? , start:Int, before: Int, count: Int) - >Unit
private typealias AfterTextChangedCallback = (s: Editable?) -> Unit
class TextWatcherBuilder : TextWatcher {
private var beforeTextChangedCallback: BeforeTextChangedCallback? = null
private var onTextChangedCallback: OnTextChangedCallback? = null
private var afterTextChangedCallback: AfterTextChangedCallback? = null
override fun beforeTextChanged(s: CharSequence? , start:Int, count: Int, after: Int)= beforeTextChangedCallback? .invoke(s, start, count, after) ? :Unit
override fun onTextChanged(s: CharSequence? , start:Int, before: Int, count: Int)= onTextChangedCallback? .invoke(s, start, before, count) ? :Unit
override fun afterTextChanged(s: Editable?).= afterTextChangedCallback? .invoke(s) ? :Unit
fun beforeTextChanged(callback: BeforeTextChangedCallback) {
beforeTextChangedCallback = callback
}
fun onTextChanged(callback: OnTextChangedCallback) {
onTextChangedCallback = callback
}
fun afterTextChanged(callback: AfterTextChangedCallback) {
afterTextChangedCallback = callback
}
}
fun registerTextWatcher(function: TextWatcherBuilder. () -> Unit) =
TextWatcherBuilder().also(function)
Copy the code
OnTabSelectedListenerBuilder
package com.tanjiajun.androidgenericframework.utils
import com.google.android.material.tabs.TabLayout
/** * Created by TanJiaJun on 2019-09-07. */
private typealias OnTabCallback = (tab: TabLayout.Tab?) -> Unit
class OnTabSelectedListenerBuilder : TabLayout.OnTabSelectedListener {
private var onTabReselectedCallback: OnTabCallback? = null
private var onTabUnselectedCallback: OnTabCallback? = null
private var onTabSelectedCallback: OnTabCallback? = null
override fun onTabReselected(tab: TabLayout.Tab?).= onTabReselectedCallback? .invoke(tab) ? :Unit
override fun onTabUnselected(tab: TabLayout.Tab?).= onTabUnselectedCallback? .invoke(tab) ? :Unit
override fun onTabSelected(tab: TabLayout.Tab?).= onTabSelectedCallback? .invoke(tab) ? :Unit
fun onTabReselected(callback: OnTabCallback) {
onTabReselectedCallback = callback
}
fun onTabUnselected(callback: OnTabCallback) {
onTabUnselectedCallback = callback
}
fun onTabSelected(callback: OnTabCallback) {
onTabSelectedCallback = callback
}
}
fun registerOnTabSelectedListener(function: OnTabSelectedListenerBuilder. () -> Unit) =
OnTabSelectedListenerBuilder().also(function)
Copy the code
General steps:
- Define a class that implements the callback interface and implements its callback methods.
- Observe the argument to the callback method, extract it into a function type, and give the function type an alternate name using the type alias as needed, and use the private modifier.
- Declare some nullable variable (VAR) private member variables of the function type inside the class, and take the corresponding variable from the callback function to implement its invoke function, passing in the corresponding parameters.
- Define functions in a class with the same name as the callback interface, but with arguments of the corresponding function type, and assign the function type to the corresponding member variable of the current class.
- Define a member function that takes a Lambda expression with the recipient object of that class and returns Unit, creates the corresponding object in the function, and passes the Lambda expression in using the also function.
How do you use it? Take a look at the following code:
TextWatcher
findViewById<EditText>(R.id.et_dsl_callback_content).addTextChangedListener(
registerTextWatcher {
afterTextChanged { tvDSLCallbackContent.text = it }
})
Copy the code
TabLayout.OnTabSelectedListener
tlOrder.addOnTabSelectedListener(registerOnTabSelectedListener { onTabSelected { vpOrder.currentItem = it? .position ? :0}})Copy the code
Let me elaborate a little bit more on why I can write this way? In fact, before simplification is written like this, code is as follows:
findViewById<EditText>(R.id.et_dsl_callback_content).addTextChangedListener(registerTextWatcher({
this.afterTextChanged({ s: Editable? ->
tvDSLCallbackContent.text = s
})
}))
Copy the code
Kotlin syntax states that if the last argument to a function is a Lambda expression, it can be raised outside the parentheses and the parentheses can be omitted. Kotlin can then deduce the type of the argument and use the default argument it instead of the named argument. Then, since this is a Lambda expression with a receiver, So we can take the object with this and call its afterTextChanged function, and we end up with our simplified code.
Object object expression callback and DSL callback comparison
- The DSL notation is much more Kotlin than object notation.
- The object notation implements all methods, and the DSL notation implements the desired method as needed.
- In terms of performance, the DSL method creates an instance object of a Lambda expression for each callback function, while the Object method generates only one anonymous object instance no matter how many callback methods there are. Therefore, the Object method performs better than the DSL method.
Here I take TextWatcher as an example and decompile them into Java code like this:
Object Specifies the callback of an object expression
((EditText)this.findViewById(-1000084)).addTextChangedListener((TextWatcher)(new TextWatcher() {
public void beforeTextChanged(@Nullable CharSequence s, int start, int count, int after) {}public void onTextChanged(@Nullable CharSequence s, int start, int before, int count) {}public void afterTextChanged(@Nullable Editable s) {
TextView var10000 = tvCommonCallbackContent;
Intrinsics.checkExpressionValueIsNotNull(var10000, "tvCommonCallbackContent"); var10000.setText((CharSequence)s); }}));Copy the code
DSL callback
((EditText)this.findViewById(-1000121)).addTextChangedListener((TextWatcher)TextWatcherBuilderKt.registerTextWatcher((Function1)(new Function1() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
this.invoke((TextWatcherBuilder)var1);
return Unit.INSTANCE;
}
public final void invoke(@NotNull TextWatcherBuilder $this$registerTextWatcher) {
Intrinsics.checkParameterIsNotNull($this$registerTextWatcher, "$receiver");
$this$registerTextWatcher.beforeTextChanged((Function4)(new Function4() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1, Object var2, Object var3, Object var4) {
this.invoke((CharSequence)var1, ((Number)var2).intValue(), ((Number)var3).intValue(), ((Number)var4).intValue());
return Unit.INSTANCE;
}
public final void invoke(@Nullable CharSequence s, int start, int count, int after) {
TextView var10000 = tvDSLCallbackContent;
Intrinsics.checkExpressionValueIsNotNull(var10000, "tvDSLCallbackContent"); var10000.setText(s); }})); $this$registerTextWatcher.onTextChanged((Function4)(new Function4() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1, Object var2, Object var3, Object var4) {
this.invoke((CharSequence)var1, ((Number)var2).intValue(), ((Number)var3).intValue(), ((Number)var4).intValue());
return Unit.INSTANCE;
}
public final void invoke(@Nullable CharSequence s, int start, int before, int count) {
TextView var10000 = tvDSLCallbackContent;
Intrinsics.checkExpressionValueIsNotNull(var10000, "tvDSLCallbackContent"); var10000.setText(s); }})); $this$registerTextWatcher.afterTextChanged((Function1)(new Function1() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
this.invoke((Editable)var1);
return Unit.INSTANCE;
}
public final void invoke(@Nullable Editable it) {
TextView var10000 = tvDSLCallbackContent;
Intrinsics.checkExpressionValueIsNotNull(var10000, "tvDSLCallbackContent"); var10000.setText((CharSequence)it); }})); }})));Copy the code
You can see that the Object notation only generates an anonymous TextWatcher object instance, whereas the DSL notation creates an instance object of a Lambda expression (Function1, Function4) for each callback function, as expected.
digression
Java8 introduced the default keyword, which can include some default method implementations in the interface.
interface Handlers{
void onLoginClick(View view);
default void onLogoutClick(View view){}}Copy the code
With Kotlin, we can add ** @jvmdefault ** as follows:
interface Handlers{
fun onLoginClick(view: View)
@JvmDefault
fun onLogoutClick(view: View){}}Copy the code
We can decompile into Java code as follows:
@Metadata(
mv = {1.1.15},
bv = {1.0.3},
k = 1,
d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\ u0002\bf\u0018\u00002\u00020\u0001J\u0010\u0010\u0002\u001a\u00020\u00032\u0006\u0010\u0004\u001a\u00020\u0005H&J\u0010\ U0010 \ u0006 \ u001a \ u00020 \ u00032 \ u0006 \ u0010 \ u0004 \ u001a \ u00020 \ u0005H \ u0017 ø \ u0001 \ u0000 \ u0082 \ u0002 \ u0007 \ n \ u0005 \ \ u00 b 91 (0 \ u0001 ¨ \ u0006 \ u0007"},
d2 = {"Lcom/tanjiajun/kotlindsldemo/MainActivity$Handlers;".""."onLoginClick".""."view"."Landroid/view/View;"."onLogoutClick"."app_debug"})public interface Handlers {
void onLoginClick(@NotNull View var1);
@JvmDefault
default void onLogoutClick(@NotNull View view) {
Intrinsics.checkParameterIsNotNull(view, "view"); }}Copy the code
Note the following when using ** @jvmdefault ** :
Since the default keyword was only introduced in Java8, we need to do something special, as you can see from the official Kotlin documentation
Specifies that a JVM default method should be generated for non-abstract Kotlin interface member.
Usages of this annotation require an explicit compilation argument to be specified: either
-Xjvm-default=enable
or-Xjvm-default=compatibility
.
- with
-Xjvm-default=enable
, only default method in interface is generated for each @JvmDefault method. In this mode, annotating an existing method with @JvmDefault can break binary compatibility, because it will effectively remove the method from theDefaultImpls
class.- with
-Xjvm-default=compatibility
, in addition to the default interface method, a compatibility accessor is generated in theDefaultImpls
class, that calls the default interface method via a synthetic accessor. In this mode, annotating an existing method with @JvmDefault is binary compatible, but results in more methods in bytecode.Removing this annotation from an interface member is a binary incompatible change in both modes.
Generation of default methods is only possible with JVM target bytecode version 1.8 (
-jvm-target 1.8
) or higher.@JvmDefault methods are excluded from interface delegation.
Translate the main content, mainly include:
-
The default method can only be generated using JVM target bytecode version 1.8 or higher.
-
Use this annotation to specify an explicit compilation parameter: -xJVM-default =enable or
-xjVM-default =compatibility, compatibility, compatibility, compatibility, compatibility, compatibility, compatibility, compatibility, compatibility Because it removes the method from the DefaultImpls class; With -xJVM-default = Compatibility, in addition to generating the default method, compatibility accessors are generated in the DefaultImpls class, which calls the default** method through the composite accessors. In this mode, it is binary compatible, But it leads to more methods in bytecode.
Add the following code to the build.gradle file:
allprojects {
repositories {
google()
jcenter()
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs += '-Xjvm-default=compatibility'}}}Copy the code
Let’s try these two cases to see if they meet the above expectations. The code is as follows:
Decompiled code with -xJVM-default =enable
@Metadata(
mv = {1.1.15},
bv = {1.0.3},
k = 1,
d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\ u0002\bf\u0018\u00002\u00020\u0001J\u0010\u0010\u0002\u001a\u00020\u00032\u0006\u0010\u0004\u001a\u00020\u0005H&J\u0010\ U0010 \ u0006 \ u001a \ u00020 \ u00032 \ u0006 \ u0010 \ u0004 \ u001a \ u00020 \ u0005H \ u0017 ø \ u0001 \ u0000 \ u0082 \ u0002 \ u0007 \ n \ u0005 \ \ u00 b 91 (0 \ u0001 ¨ \ u0006 \ u0007"},
d2 = {"Lcom/tanjiajun/kotlindsldemo/MainActivity$Handlers;".""."onLoginClick".""."view"."Landroid/view/View;"."onLogoutClick"."app_debug"})public interface Handlers {
void onLoginClick(@NotNull View var1);
@JvmDefault
default void onLogoutClick(@NotNull View view) {
Intrinsics.checkParameterIsNotNull(view, "view"); }}Copy the code
-xJVM-default =compatibility (compatibility
@Metadata(
mv = {1.1.15},
bv = {1.0.3},
k = 1,
d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\ u0002\bf\u0018\u00002\u00020\u0001J\u0010\u0010\u0002\u001a\u00020\u00032\u0006\u0010\u0004\u001a\u00020\u0005H&J\u0010\ U0010 \ u0006 \ u001a \ u00020 \ u00032 \ u0006 \ u0010 \ u0004 \ u001a \ u00020 \ u0005H \ u0017 ø \ u0001 \ u0000 \ u0082 \ u0002 \ u0007 \ n \ u0005 \ \ u00 b 91 (0 \ u0001 ¨ \ u0006 \ u0007"},
d2 = {"Lcom/tanjiajun/kotlindsldemo/MainActivity$Handlers;".""."onLoginClick".""."view"."Landroid/view/View;"."onLogoutClick"."app_debug"})public interface Handlers {
void onLoginClick(@NotNull View var1);
@JvmDefault
default void onLogoutClick(@NotNull View view) {
Intrinsics.checkParameterIsNotNull(view, "view");
}
@Metadata(
mv = {1.1.15},
bv = {1.0.3},
k = 3
)
public static final class DefaultImpls {
@JvmDefault
public static void onLogoutClick(MainActivity.Handlers $this, @NotNull View view) {$this.onLogoutClick(view); }}}Copy the code
** -xjVM-default = Compatibility > -xJVM-default =enable -xJVM-default =compatibility** -xjVm-default =compatibility** -xjVm-default =compatibility** -xjVm-default =compatibility**
Spek
Spek is a testing framework built for Kotlin.
describe("Verify Check Email Valid") {
it("Email Is Null") {
presenter.checkEmailValid("")
verify { viewRenderer.showEmailEmptyError() }
}
it("Email Is Not Null and Is Not Valid") {
presenter.checkEmailValid("ktan")
verify { viewRenderer.showEmailInvalidError() }
}
it("Email Is Not Null and Is Valid") {
presenter.checkEmailValid("[email protected]")
verify { viewRenderer.hideEmailError() }
}
}
Copy the code
Making: Spek
kxDate
KxDate is a date processing library, we can write similar to the English sentence code, very interesting, code as follows:
val twoMonthsLater = 2 months fromNow
val yesterday = 1 days ago
Copy the code
Making: kxdate
Anko
Anko is a Kotlin library developed specifically for Android. We can write the layout as follows:
verticalLayout {
val name = editText()
button("Say Hello") {
onClick { toast("Hello, ${name.text}!")}}}Copy the code
Making: Anko
Demo: KotlinDSLDemo
My GitHub: TanJiaJunBeyond
Common Android Framework: Common Android framework
My nuggets: Tan Jiajun
My simple book: Tan Jiajun
My CSDN: Tan Jiajun