使用Kotlin高效地開發Android App(三)

Tony沈哲發表於2018-05-16

漂亮的氣球.jpg

一. ?、!! 、lateinit 以及 let

Kotlin 的型別系統旨在從我們的程式碼中消除 NullPointerException。

1.1 ?

Kotlin基於Java的空指標提出了一個空安全的概念,即每個屬性預設不可為null。

例如:

var a: String = "test kotlin"
a = null //編譯錯誤
複製程式碼

如果要允許為空,我們需要手動宣告一個變數為可空字串型別,寫為String?

var a: String? = "test kotlin"
a = null //編譯成功
複製程式碼

1.2 !!

!!是非空斷言運算子。將任何值轉換為非空型別,若該值為空則丟擲異常。

object Test {

    var s:String?=null

    @JvmStatic
    fun main(args: Array<String>) {

        println(s!!.length)
    }
}
複製程式碼

執行上述程式碼會丟擲如下異常。

Exception in thread "main" kotlin.KotlinNullPointerException
複製程式碼

在App快要釋出時,我們會進行檢查儘量避免使用“!!”,轉而考慮使用lateinit或者let函式來代替它。

1.3 lateinit

在某個類中,如果某些成員變數沒辦法在一開始就初始化,並且又不想使用可空型別(也就是帶?的型別)。那麼,可以使用lateinit來修飾它。

被lateinit修飾的變數,並不是不初始化,它需要在生命週期流程中進行獲取或者初始化。

如果訪問未初始化的 lateinit 變數會導致 UninitializedPropertyAccessException。

1.4 let函式

let函式把當前物件作為閉包的it引數,返回值是函式裡面最後一行,或者指定return。它看起來有點類似於run函式。

let函式跟run函式的區別是:let函式在函式內可以通過 it 指代該物件。

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}
複製程式碼

跟?結合使用, let函式可以在物件不為 null 的時候執行函式內的程式碼,從而避免了空指標異常的出現。

一般是這樣使用:

?.let {
       ....
}
複製程式碼

使用Kotlin高效地開發Android App(二)中,曾經介紹過結合run和apply函式一起使用的方式。其實,裡面使用了“!!”是有隱患的。

        viewModel.email.run {

            if (value!!.isEmpty()) {
                toast(resources.getString(R.string.you_have_not_completed_the_email_address)).show()
                return@onClickRight
            }
            if (!Util.checkEmail(value!!)) {
                toast(resources.getString(R.string.the_email_format_you_have_filled_is_incorrect)).show()
                return@onClickRight
            }

            viewModel
        }.subject.run {

            if (value!!.isEmpty()) {
                toast(resources.getString(R.string.you_have_not_completed_the_feedback_subject)).show()
                return@onClickRight
            }

            viewModel
        }.content.apply {

            if (value!!.isEmpty()) {
                toast(resources.getString(R.string.you_have_not_completed_the_details)).show()
                return@onClickRight
            }
        }
複製程式碼

可以使用let函式進行優化,避免出現空指標的情況。

        viewModel.email.run {
            value?.let {
                if (it.isEmpty()) {
                    toast(string(R.string.you_have_not_completed_the_email_address)).show()
                    return@onClickRight
                }
                if (!Util.checkEmail(it)) {
                    toast(string(R.string.the_email_format_you_have_filled_is_incorrect)).show()
                    return@onClickRight
                }
            }

            viewModel
        }.subject.run {
            value?.let {
                if (it.isEmpty()) {
                    toast(string(R.string.you_have_not_completed_the_feedback_subject)).show()
                    return@onClickRight
                }
            }

            viewModel
        }.content.apply {
            value?.let {
                if (it.isEmpty()) {
                    toast(string(R.string.you_have_not_completed_the_details)).show()
                    return@onClickRight
                }
            }
        }
複製程式碼

二.函式的預設引數

在Kotlin中,函式可以擁有預設引數,這樣一來就不再需要像Java那樣為了預設引數而寫一大長串過載函式了。

例如,我們使用RxBinding時,可能會考慮到防止UI控制元件被重複點選,於是寫下了這樣的Transformer

    /**
     * 防止重複點選的Transformer
     */
    @JvmStatic
    fun <T> preventDuplicateClicksTransformer(windowDuration:Long=1000,timeUnit: TimeUnit=TimeUnit.MILLISECONDS): ObservableTransformer<T, T> {
        return ObservableTransformer { upstream ->
            upstream.throttleFirst(windowDuration, timeUnit)
        }
    }
複製程式碼

在1秒內不能重複點選某個UI控制元件,可以這樣寫,因為使用了預設引數。

        RxView.clicks(textview)
                .compose(RxJavaUtils.preventDuplicateClicksTransformer())
                .subscribe({
                         ......
                })
複製程式碼

三.DSL的使用

去年的時候,我曾經寫過一篇關於kotlin dsl的文章——用kotlin來實現dsl風格的程式設計,使用dsl的方式編寫程式碼個人感覺更加簡潔和直觀。

在專案中,我對toast以及glide框架嘗試使用dsl的方式來封裝。之前的用法是使用Kotlin的擴充套件函式,由於團隊的其他成員更偏好鏈式呼叫,目前暫時保留了兩種寫法。

3.1 對glide的封裝

glide的擴充套件函式,可以滿足專案中的使用。

/**
 * 佔位符矩形
 */
fun ImageView.load(url: String?) {
    get(url).placeholder(R.drawable.shape_default_rec_bg)
            .error(R.drawable.shape_default_rec_bg)
            .into(this)
}

/**
 * 佔位符圓角矩形
 */
fun ImageView.loadRound(url: String?, centerCrop: Boolean = false) {
    get(url).placeholder(R.drawable.shape_default_round_bg)
            .error(R.drawable.shape_default_round_bg)
            .transform(RoundedCornersTransformation(DisplayUtil.dp2px(context, 10f), 0, centerCrop = centerCrop))
            .into(this)
}

/**
 * 佔位符圓形
 */
fun ImageView.loadCircle(url: Drawable?) {
    get(url).placeholder(R.drawable.shape_default_circle_bg)
            .apply(RequestOptions.circleCropTransform())
            .error(R.drawable.shape_default_circle_bg)
            .into(this)
}

fun ImageView.loadCircle(url: String?) {
    get(url).placeholder(R.drawable.shape_default_circle_bg)
            .apply(RequestOptions.circleCropTransform())
            .error(R.drawable.shape_default_circle_bg)
            .into(this)
}

fun ImageView.get(url: String?): GlideRequest<Drawable> = GlideApp.with(context).load(url)
fun ImageView.get(url: Drawable?): GlideRequest<Drawable> = GlideApp.with(context).load(url)
複製程式碼

載入某個圖片之後,讓它呈現出圓角矩形的效果

holder.itemView.iv_game.loadRound(image_url)
複製程式碼

使用dsl進行封裝

class GlideWrapper {

    var url:String? = null

    var image: ImageView?=null

    var placeholder: Int = R.drawable.shape_default_rec_bg

    var error: Int = R.drawable.shape_default_rec_bg

    var transform: Transformation<Bitmap>? = null

}

fun load(init: GlideWrapper.() -> Unit) {

    val wrap = GlideWrapper()

    wrap.init()

    execute(wrap)
}

private fun execute(wrap:GlideWrapper) {

    wrap.image?.let {

        var request = it.get(wrap.url).placeholder(wrap.placeholder).error(wrap.error)

        if (wrap?.transform!=null) {

            request.transform(wrap.transform!!)
        }

        request.into(it)

    }

}
複製程式碼

仍然是載入該圖片,讓它呈現出圓角矩形的效果

            load {
                url = image_url
                image = holder.itemView.iv_game
                transform = RoundedCornersTransformation(DisplayUtil.dp2px(context, 10f), 0, centerCrop = false)
            }
複製程式碼

3.2 對toast的封裝

提示資訊是任何App必不可少的,在我們的專案中也使用擴充套件函式對toast進行封裝。

fun Toast.setGravityCenter(): Toast {
    setGravity(Gravity.CENTER, 0, 0)
    return this
}

/**
 * 設定Toast字型及背景顏色
 * @param messageColor
 * @param backgroundColor
 * @return
 */
fun Toast.setToastColor(@ColorInt messageColor: Int, @ColorInt backgroundColor: Int) {
    val view = view
    if (view != null) {
        val message = view.findViewById(android.R.id.message) as TextView
        message.setBackgroundColor(backgroundColor)
        message.setTextColor(messageColor)
    }
}

/**
 * 設定Toast字型及背景
 * @param messageColor
 * @param background
 * @return
 */
fun Toast.setBackground(@ColorInt messageColor: Int = Color.WHITE, @DrawableRes background: Int = R.drawable.shape_toast_bg): Toast {
    val view = view
    if (view != null) {
        val message = view.findViewById(android.R.id.message) as TextView
        view.setBackgroundResource(background)
        message.setTextColor(messageColor)
    }
    return this
}

//@SuppressLint("ShowToast")
fun toast(text: CharSequence): Toast = Toast.makeText(App.instance, text, Toast.LENGTH_LONG)
        .setGravityCenter()
        .setBackground()
//需要的地方呼叫withErrorIcon,預設不要新增
//        .withErrorIcon()


//@SuppressLint("ShowToast")
fun toast(@StringRes res: Int): Toast = Toast.makeText(App.instance, App.instance.resources.getString(res), Toast.LENGTH_LONG)
        .setGravityCenter()
        .setBackground()
//需要的地方呼叫withErrorIcon,預設不要新增
//        .withErrorIcon()

fun Toast.withErrorIcon(@DrawableRes iconRes: Int = R.drawable.ic_toast_error): Toast {
    val view = view
    if (view != null) {
        val layout = this.view as LinearLayout
        val layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
        val icon = ImageView(getApplicationContext())
        icon.setImageResource(iconRes)
        icon.setPadding(0, 0, Util.dip2px(8f), 0)
        icon.layoutParams = layoutParams
        layout.orientation = LinearLayout.HORIZONTAL
        layout.gravity = Gravity.CENTER_VERTICAL
        layout.addView(icon, 0)
    }
    return this
}

fun Toast.withSuccIcon(@DrawableRes iconRes: Int = R.drawable.ic_right_circle): Toast {
    val view = view
    if (view != null) {
        val layout = this.view as LinearLayout
        val layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
        val icon = ImageView(getApplicationContext())
        icon.setImageResource(iconRes)
        icon.setPadding(0, 0, Util.dip2px(8f), 0)
        icon.layoutParams = layoutParams
        layout.orientation = LinearLayout.HORIZONTAL
        layout.gravity = Gravity.CENTER_VERTICAL
        layout.addView(icon, 0)
    }
    return this
}
複製程式碼

要展示一個錯誤的提示,大致需要這樣寫。

toast(resources.getString(R.string.you_have_not_completed_the_email_address)).withErrorIcon().show()
複製程式碼

使用dsl進行封裝

class ToastWrapper {

    var text:String? = null

    var res:Int? = null

    var showSuccess:Boolean = false

    var showError:Boolean = false
}

fun toast(init: ToastWrapper.() -> Unit) {
    val wrap = ToastWrapper()

    wrap.init()

    execute(wrap)
}

private fun execute(wrap:ToastWrapper) {

    var taost:Toast?=null

    wrap.text?.let {

        taost = toast(it)
    }

    wrap.res?.let {

        taost = toast(it)
    }

    if (wrap.showSuccess) {

        taost?.withSuccIcon()
    } else if (wrap.showError) {

        taost?.withErrorIcon()
    }

    taost?.show()
}
複製程式碼

使用dsl的方式展示同樣的錯誤資訊。

                toast {

                    res = R.string.you_have_not_completed_the_email_address
                    showError = true
                }
複製程式碼

總結

目前該系列的文章整理得比較隨意,更像是一些常用的tips。

文中的dsl還是結合了擴充套件函式來使用的,個人認為是進一步的封裝。相比起鏈式呼叫,我還是比較偏向dsl。

該系列的相關文章:

使用Kotlin高效地開發Android App(五)完結篇

使用Kotlin高效地開發Android App(四)

使用Kotlin高效地開發Android App(二)

使用Kotlin高效地開發Android App(一)


Java與Android技術棧:每週更新推送原創技術文章,歡迎掃描下方的公眾號二維碼並關注,期待與您的共同成長和進步。

使用Kotlin高效地開發Android App(三)

相關文章