Kotlin开发Android的奇技淫巧记录

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

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

扩展函数的利用

现象

先来看一段代码:

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

其中 avaliable_time 资源的定义如下

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

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

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

实现原理

新建文件Extends.kt

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

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

拓展

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

Extends.kt文件

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),而且当且仅当变量被第一次调用的时候,委托方法才会执行。

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对象就是延迟初始化的。

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

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

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

lateinit

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

class Demo {
  var value: String
  
  fun printValue() {
    println(value)
  }
}

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

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表达式。

fun test(){
    var animal = "cat"
    run {
        val animal = "dog"
        println(animal)   // dog
    }
    println(animal)       //cat
}

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

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

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

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

let函数

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

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对象(获取调用者本身)

@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 指代该对象。返回值为该对象自己。

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函数将整块逻辑分割开来。

Object.also{
    //do some thing 1
}.also{
    //do some thing 2
}

with函数

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

val a = with("string") {
    println(this)
    3
}
println(a)

运行结果

string
3

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

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关键字

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代码如下

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

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代码如下

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中我写了一个非常好用的工具方法。

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{ }来遍历集合。

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