本文章已授权微信公众号郭霖(guolin_blog)转载。
什么是DSL?
DSL是domin specific language的缩写,中文名叫做领域特定语言,指的是专注于某个应用程序领域的计算机语言,比如显示网页的HTML、用于数据库处理的SQL、用于检索或者替换文本的正则表达式,它们都是DSL。与DSL相对的就是GPL,GPL是General Purpose Language的简称,即通用编程语言,就是我们非常熟悉的Java、C、Objective-C等等。
DSL分为外部DSL和内部DSL。外部DSL是一种可以独立解析的语言,就像SQL,它专注于数据库的操作;内部DSL是通用语言暴露的用来执行特定任务的API,它利用语言本身的特性,将API以特殊的形式暴露出去,例如Android的Gradle和iOS的依赖管理组件CocosPods,Gradle是基于Groovy的,Groovy是一种通用语言,但是Gradle基于Groovy的语法,构建了自己一套DSL,所以在配置Gradle的时候,必须遵循Groovy的语法,还要遵循Gradle的DSL标准。
Android的Gradle文件
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'
}
}
iOS的Podfile文件
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target 'GenericFrameworkForiOS' **do**
pod 'SnapKit', '~> 4.0'
**end**
实现原理
这篇文章主要讲的是Kotlin的DSL,在讲之前我们先了解下这几个概念和语法。
扩展函数
声明一个扩展函数,需要用到一个接受者类型(receiver type),也就是被扩展的类型作为前缀,如下代码
fun Activity.getViewModelFactory(): ViewModelFactory =
ViewModelFactory((applicationContext as AndroidGenericFrameworkApplication).userRepository)
声明一个getViewModelFactory函数,它是Activity的扩展函数。
Lambda表达式
Java8以下的版本不支持Lambda表达式,Kotlin解决了与Java的互操作性,Kotlin的Lambda表达式以更加简洁的语法实现功能,使开发者从冗余啰嗦的语法中解放出来。
Lambda表达式分类
普通的Lambda表达式
() -> Unit
不接受任何参数返回Unit的Lambda表达式。
(tab: TabLayout.Tab?) -> Unit
接受一个可空的TabLayout.Tab参数返回Unit的Lambda表达式。
带接收者的Lambda表达式
OnTabSelectedListenerBuilder.() -> Unit
带有OnTabSelectedListenerBuilder接收者对象,不接受任何参数返回Unit的Lambda表达式。
这种带接收者的在Kotlin的标准库函数中很常见,例如如下的作用域函数(Scope Functions):
apply函数
/**
* 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.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
let函数
/**
* 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.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
我们还可以给这些Lambda表达式起别称,叫做类型别名(Type aliases)
private typealias OnTabCallback = (tab: TabLayout.Tab?) -> Unit
我们给这个Lambda表达式起了个别称,叫做OnTabCallback。
Lambda表达式在Kotlin里的这些特性是实现DSL的必备的语法糖。
函数类型实例调用
Kotlin提供invoke函数,我们可以这样写:f.invoke(x),其实它相当于f(x),举个例子:
onTabReselectedCallback?.invoke(tab) ?: Unit
它其实可以写成这样:
onTabReselectedCallback?.let { it(tab) } ?: Unit
这两段代码是等价的。
中缀表示法
标有infix关键字的函数可以使用中缀表示法(忽略该调用的点与圆括号)调用,中缀函数必须满足以下要求:
- 它们必须是成员函数和扩展函数。
- 它们必须只有一个参数。
- 其参数不得接受可变数量的参数,而且不能有默认值。
举个例子:
infix fun Int.plus(x: Int): Int =
this.plus(x)
// 可以中缀表示法调用函数
1 plus 2
// 等同于这样
1.plus(2)
我再举些例子,都是些我们常用的函数:
until函数用法
for (i in 0 until 4) {
tlOrder.addTab(tlOrder.newTab().setText("订单$i"))
}
until函数源码
/**
* 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()
}
to函数用法
mapOf("name" to "TanJiaJun","age" to 25)
to函数源码
/**
* 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.to(that: B): Pair = Pair(this, that)
实践
Kotlin DSL使我们的代码更加简洁,更加优雅,而且还很有想象力,让我们看下这几个例子:
回调处理
Java中的回调实现
我们实现的步骤一般是这样:
- 定义一个接口
- 在接口中定义一些回调方法
- 定义一个设置回调接口的方法,这个方法的参数是回调接口的实例,一般以匿名对象的形式存在。
实现TextWatcher接口
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
}
});
实现TabLayout.OnTabSelectedListener接口
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
}
});
Kotlin中的回调实现
实现TextWatcher接口
findViewById(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
}
})
实现TabLayout.OnTabSelectedListener接口
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
}
})
是不是发现跟Java的写法没什么区别,体验不到Kotlin的优势?
Kotlin DSL
TextWatcherBuilder
package com.tanjiajun.kotlindsldemo
import android.text.Editable
import android.text.TextWatcher
/**
* Created by TanJiaJun on 2019-10-01.
*/
private typealias BeforeTextChangedCallback =
(s: CharSequence?, start: Int, count: Int, after: Int) -> Unit
private typealias 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)
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)
一般步骤:
- 先定义一个类去实现回调接口,并且实现它的回调方法。
- 观察回调方法的参数,提取成一个函数类型(function type),并且按照需要使用类型别名给函数类型起一个别称,并且用私有修饰。
- 在类里面声明一些可空的函数类型的可变(var)私有成员变量,并且在回调函数中拿到对应的变量实现它的invoke函数,传入对应的参数。
- 在类中定义一些跟回调接口一样名字,但是参数是对应的函数类型的函数,并且将函数类型赋值给当前类的对应的成员变量。
- 定义一个成员函数,参数是一个带有我们定好那个类的接受者对象并且返回Unit的Lambda表达式,在函数里创建相应的对象,并且使用also函数把Lambda表达式传进去。
如何使用呢?请看下面代码:
TextWatcher
findViewById(R.id.et_dsl_callback_content).addTextChangedListener(
registerTextWatcher {
afterTextChanged { tvDSLCallbackContent.text = it }
})
TabLayout.OnTabSelectedListener
tlOrder.addOnTabSelectedListener(registerOnTabSelectedListener {
onTabSelected { vpOrder.currentItem = it?.position ?: 0 }
})
我再详细地说下为什么可以这样写呢?其实简化之前是这样写的,代码如下:
findViewById(R.id.et_dsl_callback_content).addTextChangedListener(registerTextWatcher({
this.afterTextChanged({ s: Editable? ->
tvDSLCallbackContent.text = s
})
}))
Kotlin语法规定,如果函数最后一个参数是Lambda表达式的话,可以提到小括号外边,同时小括号也可以省略,然后Kotlin可以自己推导出参数的类型,并且使用默认参数it代替命名参数,然后因为这是个带接收者的Lambda表达式,所以我们可以用this拿到对象,并且调用它的afterTextChanged函数,最后就得到我们简化后的代码了。
object对象表达式回调和DSL回调对比
- DSL写法比object写法会更加符合Kotlin风格。
- object写法要实现所有方法,DSL写法可以按照需要实现想要的方法。
- 从性能上对比,DSL写法对每个回调函数都会去创建Lambda表达式的实例对象,而object写法不管有多少个回调方法,都只生成一个匿名对象实例,所以object写法比DSL写法性能好。
我这里拿TextWatcher举个例子,把它们反编译成Java代码,代码如下:
object对象表达式回调
((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);
}
}));
DSL回调
((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);
}
}));
}
})));
可以看到object写法只生成一个匿名的TextWatcher对象实例,而DSL写法对每个回调函数都会创建Lambda表达式的实例对象(Function1、Function4),符合上述预期。
题外话
Java8引入了default关键字,在接口中可以包含一些默认的方法实现。
interface Handlers{
void onLoginClick(View view);
default void onLogoutClick(View view){
}
}
用Kotlin实现的话,我们可以加上@JvmDefault注解代码如下:
interface Handlers{
fun onLoginClick(view: View)
@JvmDefault
fun onLogoutClick(view: View){
}
}
我们可以反编译成Java代码,代码如下:
@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\b\u0091(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");
}
}
在使用@JvmDefault时我们需要注意以下几点:
因为default关键字是Java8才引入的,所以我们需要做些特别处理,从Kotlin官方文档可以看到
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.
翻译一下主要内容,主要有:
只有使用JVM目标字节码1.8版本或者更高,才可以生成default方法。
-
使用这个注解还要指定一个显式的编译参数:-Xjvm-default=enable或
-Xjvm-default=compatibility,使用-Xjvm-default=enable的话对于每个@JvmDefault方法,仅仅是生成default方法,同时这样做可能会破坏二进制兼容性,因为它从DefaultImpls类中删除该方法;使用-Xjvm-default=compatibility的话,除了生成default方法外,还将在DefaultImpls类中生成兼容性访问器,该访问器通过综合访问器调用default方法,在这种模式下,它是二进制兼容的,但是会导致字节码中有更多的方法。
在build.gradle文件中加上如下代码:
allprojects {
repositories {
google()
jcenter()
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs += '-Xjvm-default=compatibility'
}
}
}
我们试下这两种情况,看下符不符合上述预期,代码如下:
加上-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\b\u0091(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");
}
}
加上-Xjvm-default=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\b\u0091(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);
}
}
}
可以看到加上-Xjvm-default=compatibility比-Xjvm-default=enable多了一个DefaultImpls的静态final类,而且类中也有一个静态的方法,其实-Xjvm-default=compatibility是为了兼容Java8之前的版本在接口中也可以实现方法。
Spek
Spek是一个为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() }
}
}
GitHub:Spek
kxDate
kxDate是一个日期处理的库,我们可以写出类似于英语句子的代码,很有意思,代码如下:
val twoMonthsLater = 2 months fromNow
val yesterday = 1 days ago
GitHub:kxdate
Anko
Anko是一个专门针对Android开发的Kotlin库,我们可以这样写布局,代码如下:
verticalLayout {
val name = editText()
button("Say Hello") {
onClick { toast("Hello, ${name.text}!") }
}
}
GitHub:Anko
Demo:KotlinDSLDemo
我的GitHub:TanJiaJunBeyond
Android通用框架:Android通用框架
我的掘金:谭嘉俊
我的:谭嘉俊
我的CSDN:谭嘉俊