kotlin_tips

Project Url: heimashi/kotlin_tips
Introduction: 用 Kotlin 去提高生产力:汇总 Kotlin 相对于 Java 的优势,以及怎么用 Kotlin 去简洁、务实、高效、安全开发的 Tips
More: Author   ReportBugs   
Tags:

汇总 Kotlin 相对于 Java 的优势,以及怎么用 Kotlin 去简洁、务实、高效、安全的开发,每个 tip 都有详细的说明和案例代码,争取把每个 tip 分析得清楚易懂,会不断的更新维护 tips,欢迎 fork 进来加入我们一起来维护,有问题的话欢迎提 Issues。

  • 推荐:Android 模块化通信项目module-service-manager,支持模块间功能服务/View/Fragment 的通信调用等,通过注解标示模块内需要暴露出来的服务和 View,然后 gradle 插件会通过 transform 来 hook 编译过程,扫描出注解信息后再利用 asm 生成代码来向服务管理中心注册对应的服务和 View,之后模块间就可以利用框架这个桥梁来调用和通信了
  • 推荐:Kotlin 的实践项目debug_view_kotlin,用 Kotlin 实现的 Android 浮层调试控制台,实时的显示内存、FPS、App 启动时间、Activity 启动时间、文字 Log
  • 推荐:数据预加载项目and-load-aot,通过提前加载数据来提高页面启动速度,利用编译时注解生成加载方法的路由,在 Activity 启动前就去加载数据

目录


Tip1-更简洁的字符串

回到目录

三个引号

详见案例代码KotlinTip1

Kotlin 中的字符串基本 Java 中的类似,有一点区别是加入了三个引号"""来方便长篇字符的编写。 而在 Java 中,这些都需要转义,先看看 java 中的式例

    public void testString1() {
        String str1 = "abc";
        String str2 = "line1\n" +
                "line2\n" +
                "line3";
        String js = "function myFunction()\n" +
                "{\n" +
                "    document.getElementById(\"demo\").innerHTML=\"My First JavaScript Function\";\n" +
                "}";
        System.out.println(str1);
        System.out.println(str2);
        System.out.println(js);
    }

kotlin 除了有单个双引号的字符串,还对字符串的加强,引入了三个引号,"""中可以包含换行、反斜杠等等特殊字符:

/*
* kotlin 对字符串的加强,三个引号"""中可以包含换行、反斜杠等等特殊字符
* */
fun testString() {
    val str1 = "abc"
    val str2 = """line1\n
        line2
        line3
        """
    val js = """
        function myFunction()
        {
            document.getElementById("demo").innerHTML="My First JavaScript Function";
        }
        """.trimIndent()
    println(str1)
    println(str2)
    println(js)
}

字符串模版

同时,Kotlin 中引入了字符串模版,方便字符串的拼接,可以用$符号拼接变量和表达式

/*
* kotlin 字符串模版,可以用$符号拼接变量和表达式
* */
fun testString2() {
    val strings = arrayListOf("abc", "efd", "gfg")
    println("First content is $strings")
    println("First content is ${strings[0]}")
    println("First content is ${if (strings.size > 0) strings[0] else "null"}")
}

值得注意的是,在 Kotlin 中,美元符号$是特殊字符,在字符串中不能直接显示,必须经过转义,方法 1 是用反斜杠,方法二是${'$'}

/*
* Kotlin 中,美元符号$是特殊字符,在字符串中不能直接显示,必须经过转义,方法 1 是用反斜杠,方法二是${'$'}
* */
fun testString3() {
    println("First content is \$strings")
    println("First content is ${'$'}strings")
}

Tip2-Kotlin 中大多数控制结构都是表达式

回到目录

首先,需要弄清楚一个概念语句和表达式,然后会介绍控制结构表达式的优点:简洁

语句和表达式是什么?

  • 表达式有值,并且能作为另一个表达式的一部分使用
  • 语句总是包围着它的代码块中的顶层元素,并且没有自己的值

    Kotlin 与 Java 的区别

  • Java 中,所有的控制结构都是语句,也就是控制结构都没有值
  • Kotlin 中,除了循环(for、do 和 do/while)以外,大多数控制结构都是表达式(if/when 等)

详见案例代码tip2

Example1:if 语句

java 中,if 是语句,没有值,必须显式的 return

/*
* java 中的 if 语句
* */
public int max(int a, int b) {
    if (a > b) {
        return a;
    } else {
        return b;
    }
}

kotlin 中,if 是表达式,不是语句,因为表达式有值,可以作为值 return 出去

/*
* kotlin 中,if 是表达式,不是语句,类似于 java 中的三目运算符 a > b ? a : b
* */
fun max(a: Int, b: Int): Int {
    return if (a > b) a else b
}

上面的 if 中的分支最后一行语句就是该分支的值,会作为函数的返回值。这其实跟 java 中的三元运算符类似,

/*
* java 的三元运算符
* */
public int max2(int a, int b) {
    return a > b ? a : b;
}

上面是 java 中的三元运算符,kotlin 中 if 是表达式有值,完全可以替代,故 kotlin 中已没有三元运算符了,用 if 来替代。 上面的 max 函数还可以简化成下面的形式

/*
* kotlin 简化版本
* */
fun max2(a: Int, b: Int) = if (a > b) a else b

Example2:when 语句

Kotlin 中的 when 非常强大,完全可以取代 Java 中的 switch 和 if/else,同时,when 也是表达式,when 的每个分支的最后一行为当前分支的值 先看一下 java 中的 switch

    /*
    * java 中的 switch
    * */
    public String getPoint(char grade) {
        switch (grade) {
            case 'A':
                return "GOOD";
            case 'B':
            case 'C':
                return "OK";
            case 'D':
                return "BAD";
            default:
                return "UN_KNOW";
        }
    }

java 中的 switch 有太多限制,我们再看看 Kotlin 怎样去简化的

/*
* kotlin 中,when 是表达式,可以取代 Java 中的 switch,when 的每个分支的最后一行为当前分支的值
* */
fun getPoint(grade: Char) = when (grade) {
    'A' -> "GOOD"
    'B', 'C' -> {
        println("test when")
        "OK"
    }
    'D' -> "BAD"
    else -> "UN_KNOW"
}

同样的,when 语句还可以取代 java 中的 if/else if,其是表达式有值,并且更佳简洁

    /*
    * java 中的 if else
    * */
    public String getPoint2(Integer point) {
        if (point > 100) {
            return "GOOD";
        } else if (point > 60) {
            return "OK";
        } else if (point.hashCode() == 0x100) {
            //...
            return "STH";
        } else {
            return "UN_KNOW";
        }
    }

再看看 kotlin 的版本,使用不带参数的 when,只需要 6 行代码

/*
* kotlin 中,when 是表达式,可以取代 java 的 if/else,when 的每个分支的最后一行为当前分支的值
* */
fun getPoint2(grade: Int) = when {
    grade > 90 -> "GOOD"
    grade > 60 -> "OK"
    grade.hashCode() == 0x100 -> "STH"
    else -> "UN_KNOW"
}

Tip3-更好调用的函数-显式参数名及默认参数值

回到目录

显式参数名

Kotlin 的函数更加好调用,主要是表现在两个方面:1,显式的标示参数名,可以方便代码阅读;2,函数可以有默认参数值,可以大大减少 Java 中的函数重载。 例如现在需要实现一个工具函数,打印列表的内容: 详见案例代码KotlinTip3

/*
* 打印列表的内容
* */
fun <T> joinToString(collection: Collection<T>,
                     separator: String,
                     prefix: String,
                     postfix: String): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
/*
* 测试
* */
fun printList() {
    val list = listOf(2, 4, 0)
    // 不标明参数名
    println(joinToString(list, " - ", "[", "]"))
    // 显式的标明参数名称
    println(joinToString(list, separator = " - ", prefix = "[", postfix = "]"))
}

如上面的代码所示,函数 joinToString 想要打印列表的内容,需要传入四个参数:列表、分隔符、前缀和后缀。 由于参数很多,在后续使用该函数的时候不是很直观的知道每个参数是干什么用的,这时候可以显式的标明参数名称,增加代码可读性。

默认参数值

同时,定义函数的时候还可以给函数默认的参数,如下所示:

/*
* 打印列表的内容,带有默认的参数,可以避免 java 的函数重载
* */
fun <T> joinToString2(collection: Collection<T>,
                      separator: String = ", ",
                      prefix: String = "",
                      postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
/*
* 测试
* */
fun printList3() {
    val list = listOf(2, 4, 0)
    println(joinToString2(list, " - "))
    println(joinToString2(list, " , ", "["))
}

这样有了默认参数后,在使用函数时,如果不传入该参数,默认会使用默认的值,这样可以避免 Java 中大量的函数重载。

@JvmOverloads

在 java 与 kotlin 的混合项目中,会发现用 kotlin 实现的带默认参数的函数,在 java 中去调用的化就不能利用这个特性了,还是需要给所有参数赋值,像下面 java 这样:

List<Integer> arr = new ArrayList<Integer>() {{add(2);add(4);add(0);}};
String res = joinToString2(arr, "-", "", "");
System.out.println(res);

这时候可以在 kotlin 的函数前添加注解@JvmOverloads,添加注解后翻译为 class 的时候 kotlin 会帮你去生成多个函数实现函数重载,kotlin 代码如下:

/*
* 通过注解@JvmOverloads 解决 java 调用 kotlin 时不支持默认参数的问题
* */
@JvmOverloads
fun <T> joinToString2New(collection: Collection<T>,
                         separator: String = ", ",
                         prefix: String = "",
                         postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

这样以后,java 调用 kotlin 的带默认参数的函数就跟 kotlin 一样方便了:

List<Integer> arr = new ArrayList<Integer>() {{add(2);add(4);add(0);}};
String res = joinToString2New(arr, "-");
System.out.println(res);
String res2 = joinToString2New(arr, "-", "=>");
System.out.println(res2);

Tip4-扩展函数和属性

回到目录

扩展函数和扩展属性是 Kotlin 非常方便实用的一个功能,它可以让我们随意的扩展第三方的库,你如果觉得别人给的 SDK 的 Api 不好用,或者不能满足你的需求,这时候你可以用扩展函数完全去自定义。

扩展函数

例如 String 类中,我们想获取最后一个字符,String 中没有这样的直接函数,你可以用.后声明这样一个扩展函数: 详见案例代码KotlinTip4

/*
* 扩展函数
* */
fun String.lastChar(): Char = this.get(this.length - 1)
/*
* 测试
* */
fun testFunExtension() {
    val str = "test extension fun";
    println(str.lastChar())
}

这样定义好 lastChar()函数后,之后只需要 import 进来后,就可以用 String 类直接调用该函数了,跟调用它自己的方法没有区别。这样可以避免重复代码和一些静态工具类,而且代码更加简洁明了。 例如我们可以改造上面 tip3 中的打印列表内容的函数:

/*
* 用扩展函数改造 Tip3 中的列表打印内容函数
* */
fun <T> Collection<T>.joinToString3(separator: String = ", ",
                                    prefix: String = "",
                                    postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, element) in withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

fun printList4() {
    val list = listOf(2, 4, 0)
    println(list.joinToString3("/"))
}

扩展属性

除了扩展函数,还可以扩展属性,例如我想实现 String 和 StringBuilder 通过属性去直接获得最后字符:

/*
* 扩展属性 lastChar 获取 String 的最后一个字符
* */
val String.lastChar: Char
    get() = get(length - 1)
/*
* 扩展属性 lastChar 获取 StringBuilder 的最后一个字符
* */
var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value: Char) {
        setCharAt(length - 1, value)
    }
/*
* 测试
* */
fun testExtension() {
    val s = "abc"
    println(s.lastChar)
    val sb = StringBuilder("abc")
    println(sb.lastChar)
}

定义好扩展属性后,之后只需 import 完了就跟使用自己的属性一样方便了。

Why?Kotlin 为什么能实现扩展函数和属性这样的特性?

在 Kotlin 中要理解一些语法,只要认识到Kotlin 语言最后需要编译为 class 字节码,Java 也是编译为 class 执行,也就是可以大致理解为 Kotlin 需要转成 Java 一样的语法结构, Kotlin 就是一种强大的语法糖而已,Java 不具备的功能 Kotlin 也不能越界的。

  • 那 Kotlin 的扩展函数怎么实现的呢?介绍一种万能的办法去理解 Kotlin 的语法:将 Kotlin 代码转化成 Java 语言去理解,步骤如下:
    • 在 Android Studio 中选择 Tools ---> Kotlin ---> Show Kotlin Bytecode 这样就把 Kotlin 转化为 class 字节码了
    • class 码阅读不太友好,点击左上角的 Decompile 就转化为 Java
  • 再介绍一个小窍门,在前期对 Kotlin 语法不熟悉的时候,可以先用 Java 写好代码,再利用 AndroidStudio 工具将 Java 代码转化为 Kotlin 代码,步骤如下:
    • 在 Android Studio 中选中要转换的 Java 代码 ---> 选择 Code ---> Convert Java File to Kotlin File

我们看看将上面的扩展函数转成 Java 后的代码

/*
* 扩展函数会转化为一个静态的函数,同时这个静态函数的第一个参数就是该类的实例对象
* */
public static final char lastChar(@NotNull String $receiver) {
    Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
    return $receiver.charAt($receiver.length() - 1);
}
/*
* 获取的扩展属性会转化为一个静态的 get 函数,同时这个静态函数的第一个参数就是该类的实例对象
* */
public static final char getLastChar(@NotNull StringBuilder $receiver) {
    Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
    return $receiver.charAt($receiver.length() - 1);
}
/*
* 设置的扩展属性会转化为一个静态的 set 函数,同时这个静态函数的第一个参数就是该类的实例对象
* */
public static final void setLastChar(@NotNull StringBuilder $receiver, char value) {
    Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
    $receiver.setCharAt($receiver.length() - 1, value);
}

查看上面的代码可知:对于扩展函数,转化为 Java 的时候其实就是一个静态的函数,同时这个静态函数的第一个参数就是该类的实例对象,这样把类的实例传入函数以后,函数内部就可以访问到类的公有方法。 对于扩展属性也类似,获取的扩展属性会转化为一个静态的 get 函数,同时这个静态函数的第一个参数就是该类的实例对象,设置的扩展属性会转化为一个静态的 set 函数,同时这个静态函数的第一个参数就是该类的实例对象。 函数内部可以访问公有的方法和属性。顶层的扩展函数是 static 的,不能被 override

  • 从上面转换的源码其实可以看到扩展函数和扩展属性适用的地方和缺陷

    • 扩展函数和扩展属性内只能访问到类的公有方法和属性,私有的和 protected 是访问不了的
    • 扩展函数不是真的修改了原来的类,定义一个扩展函数不是将新成员函数插入到类中,扩展函数的类型是"静态的",不是在运行时决定类型,案例代码StaticllyExtension.kt

        open class C
      
        class D : C()
      
        fun C.foo() = "c"
      
        fun D.foo() = "d"
      
        /*
        * https://kotlinlang.org/docs/reference/extensions.html
        * Extensions do not actually modify classes they extend. By defining an extension, you do not insert new members into a class,
        * but merely make new functions callable with the dot-notation on variables of this type. Extension functions are
        * dispatched statically.
        * */
        fun printFoo(c: C) {
          println(c.foo())
        }
      
        fun testStatically() {
          printFoo(C()) // print c
          printFoo(D()) // also print c
        }
      

      上面的案例中即使调用 printFoo(D())还是打印出 c,而不是 d。转成 java 中会看到下面的代码,D 类型在调用的时候会强制转换为 C 类型:

        public static final String foo(@NotNull C $receiver) {
          Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
          return "c";
        }
      
        public static final String foo(@NotNull D $receiver) {
          Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
          return "d";
        }
      
        public static final void printFoo(@NotNull C c) {
          Intrinsics.checkParameterIsNotNull(c, "c");
          String var1 = foo(c);
          System.out.println(var1);
        }
        public static final void testStatically() {
          printFoo(new C());
          printFoo((C)(new D()));
        }
      
  • 声明扩展函数作为类的成员变量

    • 上面的例子扩展函数是作为顶层函数,如果把扩展函数申明为类的成员变量,即扩展函数的作用域就在类的内部有效,案例代码ExtensionsAsMembers.kt ```kotlin open class D { }

class D1 : D() { }

open class C { open fun D.foo() { println("D.foo in C") }

open fun D1.foo() {
    println("D1.foo in C")
}

fun caller(d: D) {
    d.foo()   // call the extension function
}

}

class C1 : C() { override fun D.foo() { println("D.foo in C1") }

override fun D1.foo() {
    println("D1.foo in C1")
}

}

fun testAsMembers() { C().caller(D()) // prints "D.foo in C" C1().caller(D()) // prints "D.foo in C1" - dispatch receiver is resolved virtually C().caller(D1()) // prints "D.foo in C" - extension receiver is resolved statically C1().caller(D1()) // prints "D.foo in C1" }

函数 caller 的类型是 D,即使调用 C().caller(D1()),打印的结果还是 D.foo in C,而不是 D1.foo in C,不是运行时来动态决定类型,成员扩展函数申明为 open,
一旦在子类中被 override,就调用不到在父类中的扩展函数,在子类中的作用域内的只能访问到 override 后的函数,不能像普通函数 override 那样通过 super 关键字访问了。

- 下面再举几个扩展函数的例子,让大家感受一下扩展函数的方便:
```kotlin
/*
* show toast in activity
* */
fun Activity.toast(msg: String) {
    Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}

val Context.inputMethodManager: InputMethodManager?
    get() = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager

/*
* hide soft input
* */
fun Context.hideSoftInput(view: View) {
    inputMethodManager?.hideSoftInputFromWindow(view.windowToken, 0)
}


/**
 * screen width in pixels
 */
val Context.screenWidth
    get() = resources.displayMetrics.widthPixels

/**
 * screen height in pixels
 */
val Context.screenHeight
    get() = resources.displayMetrics.heightPixels

/**
 * returns dip(dp) dimension value in pixels
 * @param value dp
 */
fun Context.dip2px(value: Int): Int = (value * resources.displayMetrics.density).toInt()

Tip5-懒初始化 bylazy 和延迟初始化 lateinit

回到目录

懒初始化 by lazy

懒初始化是指推迟一个变量的初始化时机,变量在使用的时候才去实例化,这样会更加的高效。因为我们通常会遇到这样的情况,一个变量直到使用时才需要被初始化,或者仅仅是它的初始化依赖于某些无法立即获得的上下文。 详见案例代码KotlinTip5

/*
* 懒初始化 api 实例
* */
val purchasingApi: PurchasingApi by lazy {
    val retrofit: Retrofit = Retrofit.Builder()
            .baseUrl(API_URL)
            .addConverterFactory(MoshiConverterFactory.create())
            .build()
    retrofit.create(PurchasingApi::class.java)
}

像上面的代码,retrofit 生成的 api 实例会在首次使用到的时候才去实例化。需要注意的是 by lazy 一般只能修饰 val 不变的对象,不能修饰 var 可变对象。

class User(var name: String, var age: Int)

/*
* 懒初始化 by lazy
* */
val user1: User by lazy {
    User("jack", 15)
}

延迟初始化 lateinit

另外,对于 var 的变量,如果类型是非空的,是必须初始化的,不然编译不通过,这时候需要用到 lateinit 延迟初始化,使用的时候再去实例化。

/*
* 延迟初始化 lateinit
* */
lateinit var user2: User

fun testLateInit() {
    user2 = User("Lily", 14)
}

by lazy 和 lateinit 的区别

  • by lazy 修饰 val 的变量
  • lateinit 修饰 var 的变量,且变量是非空的类型

Tip6-不用再手写 findViewById

回到目录

在 Activity 中使用

在 Android 的 View 中,会有很多代码是在声明一个 View,然后通过 findViewById 后从 xml 中实例化赋值给对应的 View。在 kotlin 中可以完全解放出来了,利用 kotlin-android-extensions 插件,不用再手写 findViewById。步骤如下: 详见案例代码KotlinTip6

  • 步骤 1,在项目的 gradle 中 apply plugin: 'kotlin-android-extensions'
  • 步骤 2,按照原来的习惯书写布局 xml 文件 ```xml <?xml version="1.0" encoding="utf-8"?>

- 步骤 3,在 java 代码中 import 对应的布局就可以开始使用了,View 不用提前声明,插件会自动根据布局的 id 生成对应的 View 成员(其实没有生成属性,原理见下面)
```kotlin
import com.sw.kotlin.tips.R
/*
* 导入插件生成的 View
* */
import kotlinx.android.synthetic.main.activity_tip6.*


class KotlinTip6 : Activity() {

    /*
    * 自动根据 layout 的 id 生成对应的 view
    * */
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_tip6)
        tip6Tv.text = "Auto find view for TextView"
        tip6Img.setImageBitmap(null)
        tip6Btn.setOnClickListener {
            test()
        }
    }

    private fun test(){
        tip6Tv.text = "update"
    }

}

像上面代码这样,Activity 里的三个 View 自动生成了,不用再去声明,然后 findViewById,然后转型赋值,是不是减少了很多没必要的代码,让代码非常的干净。

Why?原理是什么?插件帮我们做了什么?

要看原理还是将上面的代码转为 java 语言来理解,参照 tips4 提供的方式转换为如下的 java 代码:

public final class KotlinTip6 extends Activity {
   private HashMap _$_findViewCache;

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131296284);
      TextView var10000 = (TextView)this._$_findCachedViewById(id.tip6Tv);
      Intrinsics.checkExpressionValueIsNotNull(var10000, "tip6Tv");
      var10000.setText((CharSequence)"Auto find view for TextView");
      ((ImageView)this._$_findCachedViewById(id.tip6Img)).setImageBitmap((Bitmap)null);
      ((Button)this._$_findCachedViewById(id.tip6Btn)).setOnClickListener((OnClickListener)(new OnClickListener() {
         public final void onClick(View it) {
            KotlinTip6.this.test();
         }
      }));
   }

   private final void test() {
      TextView var10000 = (TextView)this._$_findCachedViewById(id.tip6Tv);
      Intrinsics.checkExpressionValueIsNotNull(var10000, "tip6Tv");
      var10000.setText((CharSequence) "update");
   }

   public View _$_findCachedViewById(int var1) {
      if (this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }

      View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
      if (var2 == null) {
         var2 = this.findViewById(var1);
         this._$_findViewCache.put(Integer.valueOf(var1), var2);
      }

      return var2;
   }

   public void _$_clearFindViewByIdCache() {
      if (this._$_findViewCache != null) {
         this._$_findViewCache.clear();
      }
   }
}

如上面的代码所示,在编译阶段,插件会帮我们生成视图缓存,视图由一个 Hashmap 结构的 _$_findViewCache 变量缓存, 会根据对应的 id 先从缓存里查找,缓存没命中再去真正调用 findViewById 查找出来,再存在 HashMap 中。

子 View 或者 include 标签中 findViewById

子子 View 或者 include 标签中,同样可以省略 findViewById,但需要主要默认的 activity 的布局 import 是不会将这个 include 的 View 引入进来

<include layout="@layout/layout_tip6"/>

//include layout
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/test_inside_id"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/app_name"/>

</FrameLayout>

需要我们引入对应的 View 的 id,像这样 import kotlinx.android.synthetic.main.layout_tip6.*

//导入插件生成的 View
import kotlinx.android.synthetic.main.activity_tip6.*
//include layout 的 View
import kotlinx.android.synthetic.main.layout_tip6.*

test_inside_id.text = "Test include"

在 Fragment 中 findViewById

在 Fragment 中也类似,但有一点需要注意但地方,例子如下:

class Tip6Fragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater?.inflate(R.layout.fragment_tip6, container, false)
        /*
        * 这时候不能在 onCreateView 方法里用 view,需要在 onViewCreate 里,原理是插件用了 getView 来 findViewById
        * */
        // tip6Tv.text = "test2"
        return view
    }

    /*
    * 需要在 onViewCreate 里,原理是插件用了 getView 来 findViewById
    * */
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        tip6Tv.text = "test"
    }
}

如上所示,Fragment 需要注意,不能在 onCreateView 方法里用 view,不然会出现空指针异常,需要在 onViewCreate 里,原理是插件用了 getView 来 findViewById, 我们看看将上面的代码转成 java 后的代码:

public final class Tip6Fragment extends Fragment {
   private HashMap _$_findViewCache;

   @Nullable
   public View onCreateView(@Nullable LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
      View view = inflater != null?inflater.inflate(2131296286, container, false):null;
      return view;
   }

   public void onViewCreated(@Nullable View view, @Nullable Bundle savedInstanceState) {
      super.onViewCreated(view, savedInstanceState);
      TextView var10000 = (TextView)this._$_findCachedViewById(id.tip6Tv);
      Intrinsics.checkExpressionValueIsNotNull(var10000, "tip6Tv");
      var10000.setText((CharSequence)"test");
   }

   public View _$_findCachedViewById(int var1) {
      if (this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }

      View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
      if (var2 == null) {
         View var10000 = this.getView();
         if (var10000 == null) {
            return null;
         }

         var2 = var10000.findViewById(var1);
         this._$_findViewCache.put(Integer.valueOf(var1), var2);
      }

      return var2;
   }

   public void _$_clearFindViewByIdCache() {
      if (this._$_findViewCache != null) {
         this._$_findViewCache.clear();
      }

   }

   // $FF: synthetic method
   public void onDestroyView() {
      super.onDestroyView();
      this._$_clearFindViewByIdCache();
   }
}

跟 Activity 中类似,会有一个 View 的 HashMap,关键不同的地方在$_findCachedViewById 里面,需要 getView 获得当前 Fragment 的 View, 故在 onViewCreated 中 getView 还是空的,原理就好理解了。另外在 onDestroyView 会调用$_clearFindViewByIdCache 方法清掉缓存。

Tip7-利用局部函数抽取重复代码

回到目录

局部函数抽取代码

Kotlin 中提供了函数的嵌套,在函数内部还可以定义新的函数。这样我们可以在函数中嵌套这些提前的函数,来抽取重复代码。如下面的案例所示: 详见案例代码KotlinTip7

class User(val id: Int, val name: String, val address: String, val email: String)

fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}: empty Name")
    }
    if (user.address.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}: empty Address")
    }
    if (user.email.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}: empty Email")
    }
    // save to db ...
}

上面的代码在判断 name、address 等是否为空的处理其实很类似。这时候,我们可以利用在函数内部嵌套的声明一个通用的判空函数将相同的代码抽取到一起:

/*
* 利用局部函数抽取相同的逻辑,去除重复的代码
* */
fun saveUser2(user: User) {
    fun validate(value: String, fildName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user ${user.id}: empty $fildName")
        }
    }

    validate(user.name, "Name")
    validate(user.address, "Address")
    validate(user.email, "Email")
    // save to db ...
}

扩展函数抽取代码

除了利用嵌套函数去抽取,此时,其实也可以用扩展函数来抽取,如下所示:

/*
* 利用扩展函数抽取逻辑
* */
fun User.validateAll() {
    fun validate(value: String, fildName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user $id: empty $fildName")
        }
    }

    validate(name, "Name")
    validate(address, "Address")
    validate(email, "Email")
}

fun saveUser3(user: User) {
    user.validateAll()
    // save to db ...
}

Tip8-使用数据类来快速实现 model 类

回到目录

在 java 中要声明一个 model 类需要实现很多的代码,首先需要将变量声明为 private,然后需要实现 get 和 set 方法,还要实现对应的 hashcode equals toString 方法等,如下所示: 详见案例代码Tip8

    public static class User {

        private String name;
        private int age;
        private int gender;
        private String address;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public int getGender() {
            return gender;
        }

        public void setGender(int gender) {
            this.gender = gender;
        }

        public String getAddress() {
            return address;
        }

        public void setAddress(String address) {
            this.address = address;
        }

        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", gender=" + gender +
                    ", address='" + address + '\'' +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            User user = (User) o;

            if (age != user.age) return false;
            if (gender != user.gender) return false;
            if (name != null ? !name.equals(user.name) : user.name != null) return false;
            return address != null ? address.equals(user.address) : user.address == null;
        }

        @Override
        public int hashCode() {
            int result = name != null ? name.hashCode() : 0;
            result = 31 * result + age;
            result = 31 * result + gender;
            result = 31 * result + (address != null ? address.hashCode() : 0);
            return result;
        }
    }

这段代码 Java 需要 70 行左右,而如果用 kotlin,只需要一行代码就可以做到。

/*
* Kotlin 会为类的参数自动实现 get set 方法
* */
class User(val name: String, val age: Int, val gender: Int, var address: String)

/*
* 用 data 关键词来声明一个数据类,除了会自动实现 get set,还会自动生成 equals hashcode toString
* */
data class User2(val name: String, val age: Int, val gender: Int, var address: String)

对于 Kotlin 中的类,会为它的参数自动实现 get set 方法。而如果加上 data 关键字,还会自动生成 equals hashcode toString。原理其实数据类中的大部分代码都是模版代码,Kotlin 聪明的将这个模版代码的实现放在了编译器处理的阶段。

Tip9-用类委托来快速实现装饰器模式

回到目录

通过继承的实现容易导致脆弱性,例如如果需要修改其他类的一些行为,这时候 Java 中的一种策略是采用装饰器模式:创建一个新类,实现与原始类一样的接口并将原来的类的实例作为一个成员变量。 与原始类拥有相同行为的方法不用修改,只需要直接转发给原始类的实例。如下所示: 详见案例代码KotlinTip9

/*
* 常见的装饰器模式,为了修改部分的函数,却需要实现所有的接口函数
* */
class CountingSet<T>(val innerSet: MutableCollection<T> = HashSet<T>()) : MutableCollection<T> {

    var objectAdded = 0

    override val size: Int
        get() = innerSet.size

    /*
    * 需要修改的方法
    * */
    override fun add(element: T): Boolean {
        objectAdded++
        return innerSet.add(element)
    }

    /*
    * 需要修改的方法
    * */
    override fun addAll(elements: Collection<T>): Boolean {
        objectAdded += elements.size
        return innerSet.addAll(elements)
    }

    override fun contains(element: T): Boolean {
        return innerSet.contains(element)
    }

    override fun containsAll(elements: Collection<T>): Boolean {
        return innerSet.containsAll(elements)
    }

    override fun isEmpty(): Boolean {
        return innerSet.isEmpty()
    }

    override fun clear() {
        innerSet.clear()
    }

    override fun iterator(): MutableIterator<T> {
        return innerSet.iterator()
    }

    override fun remove(element: T): Boolean {
        return innerSet.remove(element)
    }

    override fun removeAll(elements: Collection<T>): Boolean {
        return innerSet.removeAll(elements)
    }

    override fun retainAll(elements: Collection<T>): Boolean {
        return innerSet.retainAll(elements)
    }

}

如上所示,想要修改 HashSet 的某些行为函数 add 和 addAll,需要实现 MutableCollection 接口的所有方法,将这些方法转发给 innerSet 去具体的实现。虽然只需要修改其中的两个方法,其他代码都是模版代码。 只要是重复的模版代码,Kotlin 这种全新的语法糖就会想办法将它放在编译阶段再去生成。 这时候可以用到类委托 by 关键字,如下所示:

/*
* 通过 by 关键字将接口的实现委托给 innerSet 成员变量,需要修改的函数再去 override 就可以了
* */
class CountingSet2<T>(val innerSet: MutableCollection<T> = HashSet<T>()) : MutableCollection<T> by innerSet {

    var objectAdded = 0

    override fun add(element: T): Boolean {
        objectAdded++
        return innerSet.add(element)
    }

    override fun addAll(elements: Collection<T>): Boolean {
        objectAdded += elements.size
        return innerSet.addAll(elements)
    }
}

通过 by 关键字将接口的实现委托给 innerSet 成员变量,需要修改的函数再去 override 就可以了,通过类委托将 10 行代码就可以实现上面接近 100 行的功能,简洁明了,去掉了模版代码。

Tip10-Lambda 表达式简化 OnClickListener

回到目录

详见案例代码KotlinTip10 lambda 表达式可以简化我们的代码。以 Android 中常见的 OnClickListener 来说明,在 Java 中我们一般这样设置:

        TextView textView = new TextView(context);
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // handle click
            }
        });

Java 中需要声明一个匿名内部类去处理,这种情况可以用 lambda 表达式来简化。

  • lambda 表达式一般长这样
    • { x:Int, y:Int -> x+y }
    • 参数 -> 表达式 并且始终在大括号中
    • it 作为默认参数名
    • lambda 捕捉,当捕捉 final 变量时,它的值和 lambda 代码一起存储
    • 非 final 变量,它的值被封装在一个特殊的包装器中,这个包装器的引用会和 lambda 代码一起存储

我们来看看 Kotlin 中的例子:

    val textView = TextView(context)

    /*
    * 传统方式
    * */
    textView.setOnClickListener(object : android.view.View.OnClickListener {
        override fun onClick(v: android.view.View?) {
            // handle click
        }
    })

    /*
    * lambda 的方式
    * */
    textView.setOnClickListener({ v ->
        {
            // handle click
        }
    })

当 lambda 的参数没有使用时可以省略,省略的时候用 it 来替代

    /*
    * lambda 的参数如果没有使用可以省略,省略的时候用 it 来替代
    * */
    textView.setOnClickListener({
        // handle click
    })

lambda 在参数的最后一个的情况可以将之提出去

    /*
    * lambda 在参数的最后一个的情况可以将之提出去
    * */
    textView.setOnClickListener() {
        // handle click
    }

lambda 提出去之后,函数如果没有其他参数括号可以省略

    /*
    * lambda 提出去之后,函数如果没有其他参数括号可以省略
    * */
    textView.setOnClickListener {
        // handle click
    }

我们再看看如果自己去实现一个带 lambda 参数的函数应该怎么去定义:

interface OnClickListener {
    fun onClick()
}

class View {
    var listener: OnClickListener? = null;

    /*
    * 传统方式
    * */
    fun setOnClickListener(listener: OnClickListener) {
        this.listener = listener
    }

    fun doSth() {
        // some case:
        listener?.onClick()
    }

    /*
    * 声明 lambda 方式,listener: () -> Unit
    * */
    fun setOnClickListener(listener: () -> Unit) {

    }
}

在函数参数中需要声明 lambda 的类型后,再调用该函数的时候就可以传入一个 lambda 表达式了。

Tip11-with 函数来简化代码

回到目录

  • with 函数原型:
    inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
    
  • with 函数并不是扩展函数,返回值是最后一行,可以直接调用对象的方法

Kotlin 中可以用 with 语句来省略同一个变量的多次声明,例如下面的函数 详见案例代码KotlinTip11

/*
*打印字母表函数,在函数内 result 变量在好几处有使用到
* */
fun alphabet(): String {
    val result = StringBuilder()
    result.append("START\n")
    for (letter in 'A'..'Z') {
        result.append(letter)
    }
    result.append("\nEND")
    return result.toString()
}

在上面的函数中,result 变量出现了 5 次,如果用 with 语句,可以将这 5 次都不用再出现了,我们来一步一步地看是怎么实现的:

/*
* 通过 with 语句,将 result 作为参数传入,在内部就可以通过 this 来表示 result 变量了
* */
fun alphabet2(): String {
    val result = StringBuilder()
    return with(result) {
        this.append("START\n")
        for (letter in 'A'..'Z') {
            this.append(letter)
        }
        this.append("\nEND")
        this.toString()
    }
}

通过 with 语句,将 result 作为参数传入,在内部就可以通过 this 来表示 result 变量了,而且这个 this 是可以省略的

/*
* 通过 with 语句,将 result 参数作为参数,在内部 this 也可以省略掉
* */
fun alphabet3(): String {
    val result = StringBuilder()
    return with(result) {
        append("START\n")
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\nEND")
        toString()
    }
}

在内部 this 省略掉后,现在只有一个 result 了,这个其实也是没必要的,于是出现了下面的最终版本:

/*
* 通过 with 语句,可以直接将对象传入,省掉对象的声明
* */
fun alphabet4(): String {
    return with(StringBuilder()) {
        append("START\n")
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\nEND")
        toString()
    }
}

像上面这样,我们可以把同一个变量的显式调用从 5 次变为 0 次,发现 Kotlin 的魅力了吧。

Tip12-apply 函数来简化代码

回到目录

  • apply 函数原型:
    inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
    
  • apply 函数,在函数范围内,可以任意调用该对象的任意方法,并返回该对象

除了用上面的 with 可以简化同一个变量的多次声明,还可以用 apply 关键字,我们来改造一下 tip11 中的函数: 详见案例代码KotlinTip12

/*
* 用 apply 语句简化代码,在 apply 的大括号里可以访问类的公有属性和方法
* */
fun alphabet5() = StringBuilder().apply {
    append("START\n")
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nEND")
}.toString()

像上面这样的,通过 apply 后,在 apply 的大括号里可以访问类的公有属性和方法。这在对应类的初始化是非常方便的,例如下面的例子

/*
* 用 apply 语句简化类的初始化,在类实例化的时候,就可以通过 apply 把需要初始化的步骤全部实现,非常的简洁
* */
fun testApply(context: Context) {
    var imgView = ImageView(context).apply {
        setBackgroundColor(0)
        setImageBitmap(null)
    }

    var textView = TextView(context).apply {
        text = "content"
        textSize = 20.0f
        setPadding(10, 0, 0, 0)
    }

    var user = User().apply { 
        age = 15
        name = "Jack"
        val a = address
        address = "bbb"
    }
}

在类实例化的时候,就可以通过 apply 把需要初始化的步骤全部实现,非常的简洁

Tip13-在编译阶段避免掉 NullPointerException

回到目录

可空类型和不可空类型

NullPointerException 是 Java 程序员非常头痛的一个问题,我们知道 Java 中分受检异常和非受检异常,NullPointerException 是非受检异常,也就是说 NullPointerException 不需要显示的去 catch 住, 往往在运行期间,程序就可能报出一个 NullPointerException 然后 crash 掉,Kotlin 作为一门高效安全的语言,它尝试在编译阶段就把空指针问题显式的检测出来,把问题留在了编译阶段,让程序更加健壮。 详见案例代码KotlinTip13

  • Kotlin 中类型分为可空类型和不可空类型,通过?代表可空,不带?代表不可为空

    fun testNullType() {
      val a: String = "aa"
      /*
      * a 是非空类型,下面的给 a 赋值为 null 将会编译不通过
      * */
      // a = null
      a.length
    
      /*
      * ?声明是可空类型,可以赋值为 null
      * */
      var b: String? = "bb"
      b = null
    
      /*
      * b 是可空类型,直接访问可空类型将编译不通过,需要通过?.或者!!.来访问
      * */
      // b.length
      b?.length
      b!!.length
    }
    
  • 对于一个不可为空类型:如果直接给不可为空类型赋值一个可能为空的对象就在编译阶段就不能通过
  • 对于一个可空类型:通过?声明,在访问该类型的时候直接访问不能编译通过,需要通过?.或者!!.

    • ?. 代表着如果该类型为空的话就返回 null 不做后续的操作,如果不为空的话才会去访问对应的方法或者属性
    • !!. 代表着如果该类型为空的话就抛出 NullPointerException,如果不为空就去访问对应的方法或者属性, 所以只有在很少的特定场景才用这种符号,代表着程序不处理这种异常的 case 了,会像 java 代码一样抛出 NullPointerException。 而且代码中一定不用出现下面这种代码,会让代码可读性很差而且如果有空指针异常,我们也不能马上发现是哪空了: ```kotlin /*
    • 不推荐这样的写法:链式的连续用!!.
    • */ val user = User() user!!.name!!.subSequence(0,5)!!.length 对应一个可空类型,每次对它的访问都需要带上?.判断kotlin val user: User? = User()

      /*

    • 每次访问都用用?.判断
    • / user?.name user?.age user?.toString() 但这样多了很多代码,kotlin 做了一些优化,kotlin /
    • 或者提前判断是否为空,如果不为空在这个分支里会自动转化为非空类型就可以直接访问了
    • / if (user != null) { user.name user.age user.toString() } ``` 通过 if 提前判断类型是否为空,如果不为空在这个分支里会*自动转化为非空类型就可以直接访问了。

let 语句简化对可空对象对访问

  • let 函数原型:
    inline fun <T, R> T.let(block: (T) -> R): R = block(this)
    
  • let 函数默认当前这个对象作为闭包的 it 参数,返回值是函数里面最后一行,或者指定 return。

上面的代码还可以用?.let 语句进行,如下所示:

    /*
    * 通过 let 语句,在?.let 之后,如果为空不会有任何操作,只有在非空的时候才会执行 let 之后的操作
    * */
    user?.let {
        it.name
        it.age
        it.toString()
    }

通过 let 语句,在?.let 之后,如果为空不会有任何操作,只有在非空的时候才会执行 let 之后的操作

Elvis 操作符 ?: 简化对空值的处理

如果值可能为空,对空值的处理可能会比较麻烦,像下面这样:

/*
* 对空值的处理
* */
fun testElvis(input: String?, user: User?) {
    val a: Int?
    if (input == null) {
        a = -1
    } else {
        a = input?.length
    }

    if (user == null) {
        var newOne = User()
        newOne.save()
    } else {
        user.save()
    }
}

Elvis 操作符?:能够简化上面的操作,?:符号会在符号左边为空的情况才会进行下面的处理,不为空则不会有任何操作。跟?.let 正好相反,例如我们可以用两行代码来简化上面从操作:

/**
 * Elvis 操作符 ?: 简化对空值的处理
 */
fun testElvis2(input: String?, user: User?) {
    val b = input?.length ?: -1;
    user?.save() ?: User().save()
}

Tip14-运算符重载

回到目录

Kotlin 支持对运算符的重载,这对于对一些对象的操作更加灵活直观。

  • 使用 operator 来修饰 plus\minus 函数
  • 可重载的二元算术符
    • A * B times
    • A / B div
    • A % B mod
    • A + B plus
    • A - B minus

以下面对坐标点 Point 的案例说明怎么去重载运算符: 详见案例代码KotlinTip14

class Point(val x: Int, val y: Int) {

    /*
    * plus 函数重载对 Point 对象的加法运算符
    * */
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }

    /*
    * minus 函数重载对 Point 对象的减法运算符
    * */
    operator fun minus(other: Point): Point {
        return Point(x - other.x, y - other.y)
    }

    override fun toString(): String {
        return "[x:$x, y:$y]"
    }

}

如上所示,通过 plus 函数重载对 Point 对象的加法运算符,通过 minus 函数重载对 Point 对象的减法运算符,然后就可以用+、-号对两个对象进行操作了:

fun testOperator() {
    val point1 = Point(10, 10)
    val point2 = Point(4, 4)
    val point3 = point1 + point2
    println(point3)
    println(point1 - point2)
}

Tip15-高阶函数简化代码

回到目录

  • 高阶函数:以另一个函数作为参数或者返回值的函数
  • 函数类型
    • (Int, String) -> Unit
    • 参数类型->返回类型 Unit 不能省略
    val list = listOf(2, 5, 10)
    /*
    * 传入函数来过滤
    * */
    println(list.filter { it > 4 })

    /*
    * 定义函数类型
    * */
    val sum = { x: Int, y: Int -> x + y }
    val action = { println(42) }

    val sum2: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
    val action2: () -> Unit = { println(42) }

函数作为参数

函数作为参数,即高阶函数中,函数的参数可以是一个函数类型,例如要定义一个函数,该函数根据传入的操作函数来对 2 和 3 做相应的处理。 详见案例代码KotlinTip15

/*
* 定义对 2 和 3 的操作函数
* */
fun twoAndThree(operator: (Int, Int) -> Int) {
    val result = operator(2, 3)
    println("Result:$result")
}

fun test03() {
    twoAndThree { a, b -> a + b }
    twoAndThree { a, b -> a * b }
}

operator 是函数类型,函数的具体类型为(Int, Int) -> Int,即输入两个 Int 返回一个 Int 值。定义完了后就可以像上面这样使用了。 再举一个例子,实现 String 类的字符过滤:

/*
* 函数作为参数,实现 String 类的字符过滤
* */
fun String.filter(predicate: (Char) -> Boolean): String {
    val sb = StringBuilder()
    for (index in 0 until length) {
        val element = get(index)
        if (predicate(element)) sb.append(element)
    }
    return sb.toString()
}

fun test04() {
    println("12eafsfsfdbzzsa".filter { it in 'a'..'f' })
}

像上面这样 predicate 是函数类型,它会根据传入的 char 来判断得到一个 Boolean 值。

函数作为返回值

函数作为返回值也非常实用,例如我们的需求是根据不同的快递类型返回不同计价公式,普通快递和高级快递的计价规则不一样,这时候我们可以将计价规则函数作为返回值:

enum class Delivery {
    STANDARD, EXPEDITED
}

/*
* 根据不同的运输类型返回不同的快递方式
* */
fun getShippingCostCalculator(delivery: Delivery): (Int) -> Double {
    if (delivery == Delivery.EXPEDITED) {
        return { 6 + 2.1 * it }
    }
    return { 1.3 * it }
}

fun test05() {
    val calculator1 = getShippingCostCalculator(Delivery.EXPEDITED)
    val calculator2 = getShippingCostCalculator(Delivery.STANDARD)
    println("Ex costs ${calculator1(5)}")
    println("St costs ${calculator2(5)}")
}

如果是普通快递,采用 1.3 it 的规则计算价格,如果是高级快递按照 6 + 2.1 it 计算价格,根据不同的类型返回不同的计价函数。

Tip16-用 Lambda 来简化策略模式

回到目录

策略模式是常见的模式之一,java 的例子如下。 详见案例代码Tip16

/**
     * 定义策略接口
     */
    public interface Strategy {
        void doSth();
    }

    /**
     * A 策略
     */
    public static class AStrategy implements Strategy {
        @Override
        public void doSth() {
            System.out.println("Do A Strategy");
        }
    }

    /**
     * B 策略
     */
    public static class BStrategy implements Strategy {
        @Override
        public void doSth() {
            System.out.println("Do B Strategy");
        }
    }

    /**
     * 策略实施者
     */
    public static class Worker {

        private Strategy strategy;

        public Worker(Strategy strategy) {
            this.strategy = strategy;
        }

        public void work() {
            System.out.println("START");
            if (strategy != null) {
                strategy.doSth();
            }
            System.out.println("END");
        }
    }

如上面的例子所示,有 A、B 两种策略,Worker 根据不同的策略做不同的工作,使用策略时:

    Worker worker1 = new Worker(new AStrategy());
    Worker worker2 = new Worker(new BStrategy());
    worker1.work();
    worker2.work();

在 java 中实现这种策略模式难免需要先定义好策略的接口,然后根据接口实现不同的策略, 在 Kotlin 中完全可以用用 Lambda 来简化策略模式,上面的例子用 Kotlin 实现:

/**
 * 策略实施者
 * @param strategy lambda 类型的策略
 */
class Worker(private val strategy: () -> Unit) {
    fun work() {
        println("START")
        strategy.invoke()
        println("END")
    }
}

/*
* 测试
* */
fun testStrategy() {
    val worker1 = Worker({
        println("Do A Strategy")
    })
    val bStrategy = {
        println("Do B Strategy")
    }
    val worker2 = Worker(bStrategy)
    worker1.work()
    worker2.work()
}

不需要先定义策略的接口,直接把策略以 lambda 表达式的形式传进来就行了。

参考文档

Apps
About Me
GitHub: Trinea
Facebook: Dev Tools