阅读 620

Kotlin系列——DSL

本文章已授权微信公众号郭霖(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> 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, R> 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关键字的函数可以使用中缀表示法(忽略该调用的点与圆括号)调用,中缀函数必须满足以下要求:

  1. 它们必须是成员函数和扩展函数。
  2. 它们必须只有一个参数。
  3. 其参数不得接受可变数量的参数,而且不能有默认值。

举个例子:

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<String,Any>("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, B> A.to(that: B): Pair<A, B> = Pair(this, that)
复制代码

实践

Kotlin DSL使我们的代码更加简洁,更加优雅,而且还很有想象力,让我们看下这几个例子:

回调处理

Java中的回调实现

我们实现的步骤一般是这样:

  1. 定义一个接口
  2. 在接口中定义一些回调方法
  3. 定义一个设置回调接口的方法,这个方法的参数是回调接口的实例,一般以匿名对象的形式存在。

实现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<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
    }
})
复制代码

实现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)
复制代码

一般步骤:

  1. 先定义一个类去实现回调接口,并且实现它的回调方法。
  2. 观察回调方法的参数,提取成一个函数类型(function type),并且按照需要使用类型别名给函数类型起一个别称,并且用私有修饰。
  3. 在类里面声明一些可空的函数类型的可变(var)私有成员变量,并且在回调函数中拿到对应的变量实现它的invoke函数,传入对应的参数。
  4. 在类中定义一些跟回调接口一样名字,但是参数是对应的函数类型的函数,并且将函数类型赋值给当前类的对应的成员变量。
  5. 定义一个成员函数,参数是一个带有我们定好那个类的接受者对象并且返回Unit的Lambda表达式,在函数里创建相应的对象,并且使用also函数把Lambda表达式传进去。

如何使用呢?请看下面代码:

TextWatcher

findViewById<EditText>(R.id.et_dsl_callback_content).addTextChangedListener(
    registerTextWatcher {
        afterTextChanged { tvDSLCallbackContent.text = it }
    })
复制代码

TabLayout.OnTabSelectedListener

tlOrder.addOnTabSelectedListener(registerOnTabSelectedListener {
    onTabSelected { vpOrder.currentItem = it?.position ?: 0 }
})
复制代码

我再详细地说下为什么可以这样写呢?其实简化之前是这样写的,代码如下:

findViewById<EditText>(R.id.et_dsl_callback_content).addTextChangedListener(registerTextWatcher({
    this.afterTextChanged({ s: Editable? ->
        tvDSLCallbackContent.text = s
    })
}))
复制代码

Kotlin语法规定,如果函数最后一个参数是Lambda表达式的话,可以提到小括号外边,同时小括号也可以省略,然后Kotlin可以自己推导出参数的类型,并且使用默认参数it代替命名参数,然后因为这是个带接收者的Lambda表达式,所以我们可以用this拿到对象,并且调用它的afterTextChanged函数,最后就得到我们简化后的代码了。

object对象表达式回调和DSL回调对比

  1. DSL写法object写法会更加符合Kotlin风格。
  2. object写法要实现所有方法,DSL写法可以按照需要实现想要的方法。
  3. 从性能上对比,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 the DefaultImpls class.
  • with -Xjvm-default=compatibility, in addition to the default interface method, a compatibility accessor is generated in the DefaultImpls 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.

翻译一下主要内容,主要有:

  1. 只有使用JVM目标字节码1.8版本或者更高,才可以生成default方法。

  2. 使用这个注解还要指定一个显式的编译参数:-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("ktan@xogrp.com")
        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:谭嘉俊

文章分类
Android
文章标签