一. ?、!! 、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(五)完結篇
Java與Android技術棧:每週更新推送原創技術文章,歡迎掃描下方的公眾號二維碼並關注,期待與您的共同成長和進步。