一. ?、!! 、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(一)