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