Kotlin开发Android的奇技淫巧记录

参考链接:

《聊一聊Kotlin扩展函数run,with,let,also和apply的使用和区别》

前面几篇我们了解了kotlin的语法和概念的东西,接下来我们在实际使用中看看kotlin带给我们的惊喜。

扩展函数的利用

现象

先来看一段代码:

1
2
3
val avaliableTime: String?
avaliableTime = R.string.avaliable_time.resToString("3年")
println(avaliableTime)

其中 avaliable_time 资源的定义如下

1
<string name="avaliable_time">有效期%s</string>

最终打印结果为:有效期3年

上面这个特点就是Kotlin扩展函数带给我们的福利。

实现原理

新建文件Extends.kt

1
fun Int.resToString(vararg params: Any): String = WangApplication.RESOURCES.getString(this, *params)

实际上这里是对Int对象的方法进行了扩展,扩展了一个resToString方法来方便fromat字符串。

拓展

处理对字符串处理可以添加扩展函数外,我们也可以对dp和px转换、显示toast等也添加扩展函数。

Extends.kt文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
fun Int.resToColor(): Int = ResourcesCompat.getColor(WangApplication.RESOURCES, this, null)

fun Int.resToString(vararg params: Any): String = WangApplication.RESOURCES.getString(this, *params)

@JvmOverloads
fun Int.inflate(viewGroup: ViewGroup? = null, attachView: Boolean = false, context: Context = WangApplication.CONTEXT): View = LayoutInflater.from(context).inflate(this, viewGroup, attachView)

fun Float.dpToPx(): Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, WangApplication.RESOURCES.displayMetrics)

fun Float.spToPx(): Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this, WangApplication.RESOURCES.displayMetrics)

fun Int.resToDrawable(): Drawable = ResourcesCompat.getDrawable(WangApplication.RESOURCES, this, null)!!

fun Activity.toast(message: String) {
this.runOnUiThread {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}

fun Activity.toast(@StringRes message: Int) {
this.runOnUiThread {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}

fun Calendar.formatTo(formatter: SafeSimpleDateFormat): String {
return formatter.format(this.timeInMillis)
}

fun Activity.hideKeyBoard() {
val inputMethodManager = this.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(this.window.decorView.windowToken, 0)
}

infix fun <T> Boolean.then(value: T?) = if (this) value else null

fun <T> Boolean.then(value: T?, default: T) = if (this) value else default

inline fun <T> Boolean.then(function: () -> T, default: T) = if (this) function() else default

inline fun <T> Boolean.then(function: () -> T, default: () -> T) = if (this) function() else default()

inline infix fun <reified T> Boolean.then(function: () -> T) = if (this) function() else null


fun Bundle.put(params: Array<out Pair<String, Any?>>): Bundle {

params.forEach {
val key = it.first
val value = it.second
when (value) {
is Int -> putInt(key, value)
is IntArray -> putIntArray(key, value)
is Long -> putLong(key, value)
is LongArray -> putLongArray(key, value)
is CharSequence -> putCharSequence(key, value)
is String -> putString(key, value)
is Float -> putFloat(key, value)
is FloatArray -> putFloatArray(key, value)
is Double -> putDouble(key, value)
is DoubleArray -> putDoubleArray(key, value)
is Char -> putChar(key, value)
is CharArray -> putCharArray(key, value)
is Short -> putShort(key, value)
is ShortArray -> putShortArray(key, value)
is Boolean -> putBoolean(key, value)
is BooleanArray -> putBooleanArray(key, value)
is SparseSerializableArray<*> -> putSerializable(key, value)

is Serializable -> putSerializable(key, value)
is Parcelable -> putParcelable(key, value)
is Bundle -> putAll(value)
is Array<*> -> when {
value.isArrayOf<Parcelable>() -> putParcelableArray(key, value as Array<out Parcelable>?)
}

}
}
return this
}

延迟创建对象实例

lazy

lazy 应用于单例模式(if-null-then-init-else-return),而且当且仅当变量被第一次调用的时候,委托方法才会执行。

1
2
3
4
5
6
7
8
9
private val adapter by lazy {
object : BaseQuickAdapter<OverageObject, BaseViewHolder>(R.layout.item_qrcode_quick_coupon) {
override fun convert(helper: BaseViewHolder, item: OverageObject) {
helper.setText(R.id.create_time, TimeFormat.formatTotal(item.insertTime.toLong()))
.setText(R.id.available_time, R.string.avaliable_time.resToString(Formats.daySecond(item.scanValidPeriod / 60)))
.setText(R.id.coupon_value, convertCoupon(item))
}
}
}

我们一般在Java的类中初始化成员变量需要根据时机才选择初始化代码的位置,但是在kotlin中完全不用担心这个问题,我们可以使用by lazy来延迟初始化对象,例如上面的adapter对象就是延迟初始化的。

还有一种场景,比如多次使用同一个对象,只获取不赋值。

1
2
3
private val mUserMannager: UserMannager by lazy {
UserMannager.getInstance()
}

上面的object: 类似于Java中的匿名内部类的实现,具体可参考《kotlin中的object关键字》

lateinit

lateinit 则用于只能生命周期流程中进行获取或者初始化的变量,我们在Java中习惯先定义后初始化,例如下面代码。

1
2
3
4
5
6
7
class Demo {
var value: String

fun printValue() {
println(value)
}
}

这样的代码在kotlin中会报错:Property must be initialized or be abstract

1
2
3
4
5
6
7
8
class Demo {

lateinit var value: String //添加lateinit关键字

fun printValue() {
println(value)
}
}

运行后会发现还是报错:Exception in thread “main” kotlin.UninitializedPropertyAccessException: lateinit property value has not been initialized

lateinit 的蛋疼之处在于,萌新以为找到了接近 Java 的写法,但实际上一只脚已经踩在了地雷上;有一定 Kotlin 经验的同学呢,要么觉得 lateinit 给的自由度完全不够,必要的时候直接 “var + 可空类型”浪起,要么处处担惊受怕,生怕碰到没有初始化的属性。这样,lateinit 就变成一个十足的鸡肋了,会用的不想用,不会用的处处掉坑。

对于 Kotlin 新手来说,应该抛开 Java 式的写法,牢记类属性的三种初始化方式:

  • 主构造函数内定义属性,使用传入的参数初始化属性;
  • 类体内定义属性,同时初始化;
  • 类体内定义属性,init 块里初始化。

特殊函数

run函数

调用run函数块。返回值为函数块最后一行,或者指定return表达式。

1
2
3
4
5
6
7
8
fun test(){
var animal = "cat"
run {
val animal = "dog"
println(animal) // dog
}
println(animal) //cat
}

在这个简单的test函数当中我们拥有一个单独的作用域,在run函数中能够重新定义一个animal变量,并且它的作用域只存在于run函数当中。

例如现在有这么一个场景,用户领取app的奖励,如果用户没有登录弹出登录dialog,如果已经登录则弹出领取奖励的dialog。我们可以使用以下代码来处理这个逻辑。

1
2
3
run {
if (islogin) loginDialog else getAwardDialog
}.show()

可以看到上面这段代码会变得更加的简洁,并且可以将show方法一次应用到上面两个dialog当中,而不是去调用两次。

let函数

默认当前这个对象作为闭包的it参数,返回值是函数里面最后一行,或者指定return

1
2
3
4
5
6
7
8
9
RetrofitService.create(ApiService::class.java)
.testRequest(params)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : MessagedAction<JSONResponse>(view) {
override fun onSuccess(response: JSONResponse) {
view.doTestChange()
}
}).let { watchSubscription(it) }

例如上面我们使用Retrofit请求testRequest接口,然后使用let函数,此时it代表的是Subscription对象(获取调用者本身)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public void watchSubscription(Subscription subscription) {
recyclerSubscriptions();
mSubscriptions.add(new WeakReference<>(subscription));
}

@Override
public final void unsubscribeAll() {
for (WeakReference<Subscription> ref : mSubscriptions) {
Subscription subscription = ref.get();
if (subscription != null && !subscription.isUnsubscribed()) {
subscription.unsubscribe();
}
}
mSubscriptions.clear();
}

在当前Activity引用所有Subscription并在onDestroy的时候调用unsubscribeAll()方法来取消和释放所有请求。

apply函数

调用某对象的apply函数,在函数块内可以通过 this 指代该对象。返回值为该对象自己。

1
2
3
4
5
6
fun toOtherPage(activity: Activity, plateObject: PlateObject?): Intent {
return Intent(activity, OtherActivity::class.java).apply {
putExtra(Constant.Key.IS_FILL_CAR_TIME, true)
putExtra(Constant.Key.CAR_IN_TIME, plateObject)
}
}

可以使用apply函数对一些创建的特定对象进行进一步的操作和包装,如上面的例子就是对Intent设置传入参数。所以一般对象初始化可以使用apply函数。

also函数

调用某对象的also函数,则该对象为函数的参数。在函数块内可以通过 it 指代该对象。返回值为该对象自己。

可以使用also函数将整块逻辑分割开来。

1
2
3
4
5
Object.also{
//do some thing 1
}.also{
//do some thing 2
}

with函数

with函数和前面的几个函数使用方式略有不同,因为它不是以扩展的形式存在的。它是将某对象作为函数的参数,在函数块内可以通过 this 指代该对象。返回值为函数块的最后一行或指定return表达式。

1
2
3
4
5
val a = with("string") {
println(this)
3
}
println(a)

运行结果

1
2
string
3

通常我们使用with对一个对象实例调用多个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Turtle {
fun penDown()
fun penUp()
fun turn(degrees: Double)
fun forward(pixels: Double)
}
val myTurtle = Turtle()
with(myTurtle) { // 画一个 100 像素的正方形
penDown()
for(i in 1..4) {
forward(100.0)
turn(90.0)
}
penUp()
}

工具类创建

在kotlin中创建工具类有两种方法,一种是使用object关键字,一种是全局函数。

object关键字

1
2
3
4
5
6
7
8
9
10
object AppUtil {

/**
* 获取meta-data的value值
*/
fun getMetaData(key: String): String{
return CustomeApplication.instance.packageManager.getApplicationInfo(
BuildConfig.APPLICATION_ID, PackageManager.GET_META_DATA).metaData.getString(key)
}
}

我们反编译后decompil之后的Java代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class AppUtil {
public static final AppUtil INSTANCE;

@NotNull
public final String getMetaData(@NotNull String key) {
Intrinsics.checkParameterIsNotNull(key, "key");
String var10000 = CustomeApplication.Companion.getInstance().getPackageManager().getApplicationInfo("com.parkingwang.irain.airporttaxi", 128).metaData.getString(key);
Intrinsics.checkExpressionValueIsNotNull(var10000, "CustomeApplication.insta…).metaData.getString(key)");
return var10000;
}

static {
AppUtil var0 = new AppUtil();
INSTANCE = var0;
}
}

可以看到object关键字实际上对应的是Java中的单例模式。

全局函数

我们新建文件Ext.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.xxx.airporttaxi.util

import android.content.pm.PackageManager
import com.xxx.airporttaxi.BuildConfig
import com.xxx.airporttaxi.CustomeApplication

/**
* 描述:
*
* @author 李小强 (lxq_xsyu@163.com)
* @date 2019/3/6
*/

/**
* 获取meta-data的value值
*/
fun getMetaData(key: String): String{
return CustomeApplication.instance.packageManager.getApplicationInfo(
BuildConfig.APPLICATION_ID, PackageManager.GET_META_DATA).metaData.getString(key)
}

我们反编译后decompil之后的Java代码如下

1
2
3
4
5
6
7
8
9
public final class ExtKt {
@NotNull
public static final String getMetaData(@NotNull String key) {
Intrinsics.checkParameterIsNotNull(key, "key");
String var10000 = CustomeApplication.Companion.getInstance().getPackageManager().getApplicationInfo("com.xxx.airporttaxi", 128).metaData.getString(key);
Intrinsics.checkExpressionValueIsNotNull(var10000, "CustomeApplication.insta…).metaData.getString(key)");
return var10000;
}
}

可以看出来实际上全局函数最后会自动生成该kt文件命名的一个类,函数会成为静态成员函数。

单例VS静态方法

单例和静态方法都可以方便我们全局的使用方法,这两个有什么区别呢?

单例的特点:

  1. 可以延迟初始化
  2. 可以继承、实现接口
  3. 方法可以被覆写
  4. 可以维持状态(成员变量)
  5. 一直存在于内存,不会被GC
  6. 相对于静态方法更加面向对象

静态方法的特点:

  1. 静态方法中的对象会在方法执行完后释放
  2. 不需要实例化静态方法所在的类
  3. 静态方法更加适用于一些工具类

根据EditText内容来改变Button透明度

有的时候我们需要在用户全部输入后将按钮的透明度改为100%不透明,在未输入完成的时候透明度为50%。在kotlin中我写了一个非常好用的工具方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fun addButtonEmptyAlpha(alphaButton: Button, vararg texts: TextView){
alphaButton.alpha = 0.5f
texts.forEach {
it.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) { }

override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
var hasEmpty = false
texts.forEach { if(it.text.isEmpty()) hasEmpty = true }
if(hasEmpty){
alphaButton.alpha = 0.5f
}else{
alphaButton.alpha = 1f
}
}
})
}
}

该方法的第一个参数就是需要改变透明度的按钮对象,第二个参数是Kotlin中的可变参数(vararg来声明)可以传入多个TextView对象。
连续使用了两次forEach{ }来遍历集合。

1
AppUtil.addButtonEmptyAlpha(registSubmit, registPhoneEdit, registCheckCodeEdit, registPwdEdit, registPwdReEdit)