一. ?、!! 、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) {
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.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 preventDuplicateClicksTransformer(windowDuration:Long=1000,timeUnit: TimeUnit=TimeUnit.MILLISECONDS): ObservableTransformer {
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 = GlideApp.with(context).load(url)
fun ImageView.get(url: Drawable?): GlideRequest = 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? = 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(一)