Kotlin 开发中遇到的坑(持续更新)

2021-03-02 10:53:35 浏览数 (1)

1、空类型安全

1.1、可空类型正确用法

kotlin是强类型判断的,每一个对象都有可以为空和不可以为空之分。

代码语言:javascript复制
var a: String = "abc"
a = null // 编译错误
 
var b: String? = "abc"
b = null // ok
 
val l = a.length // ok
 
val l = b.length // 编译错误:变量“b”可能为空
val l = b?.length ?: 0

1.2、可能出现异常的一些使用方式

1.2.1、getStringExtra 方法 可能导致的 null 异常

举个例子:

代码语言:javascript复制
private var mHomeWorkId  = ""
mHomeWorkId = intent.getStringExtra(INPUT_HOME_WORK_ID)

由代码可见,mHomeWorkId 是一个不能为null的String。然后通过intent.getStringExtra 方法给mHomeWorkId赋值。

代码语言:javascript复制
public String getStringExtra(String name) {
    return mExtras == null ? null : mExtras.getString(name);
}

getStringExtra() 方法的返回值是可能为 null的。因此,当getStringExtra返回值为null时,给mHomeWorkId赋值时,就会报错。

正确写法:

代码语言:javascript复制
mHomeWorkId = intent.getStringExtra(INPUT_HOME_WORK_ID)?:""

1.2.2、条件判断问题

看下面例子:

代码语言:javascript复制
var mUser:User? = null
if(mUser?.grade != -1){
    //做一些操作
}

上面代码看上去很简单,定义了一个 User类型的属性mUser,是可以为null的。在if条件中通过判断年级是否等于-1 来做一下操作。我们在写这段代码的时候想的可能是:当mUser不为null,而且年级不是-1的时候,通过条件判断,然后做一下操作。

但是在kotlin中,当mUser为null时,mUser?.grade的取值为null,而null != -1 ,在kotlin是成立的,这就不符合我们实际逻辑了。

解决方法如下:通过 ?: 操作,当mUser等于null时,给左边 一个默认值。

代码语言:javascript复制
var mUser:User? = null
if(mUser?.grade?:-1 != -1){
    //做一些操作
}

1.2.3、is、as 中的坑

obj is String 之后,作用域之中,类型就已经转换了。

代码语言:javascript复制
fun testAsIs() {
  var obj: Any? = null
 
  if (obj is String) {// 方法体内的作用域,obj 就是 String
      var length = obj.length
  }
}

as的两种不推荐写法,会抛出异常:TypeCastException: null cannot be cast to non-null type kotlin.String

代码语言:javascript复制
//错误写法1,text不是String或为空时,会报异常
var strAble1 = text as String
//错误写法2,text不是String时,同样会报异常
var strAble2 = text as String?

as的推荐写法:

代码语言:javascript复制
//正确写法,转换失败自动转换为空对象
var strAble = text as? String

2、TODO 语句报错问题

在kotlin开发中,当实现某个抽象方法时,会自动生成一条TODO语句。

代码语言:java复制
override fun cancelRequest() {
    TODO("Not yet implemented")
}

记得把TODO(“not implemented”)注释掉,不然会抛出An operation is not implemented: not implemented异常

3、尽量避免使用 !!

对于 Null 的检查是 Kotlin 的特点之一。强制你在编码过程中考虑变量是否可为 null,因此可以避免很多在 Java 中隐藏的 NullPointerException。!! 表示这个对象一定不为null。因此只有当你百分百确认该对象不可能为null的时候,才能使用!!。

举个例子:

代码语言:javascript复制
if (mPrimaryData != null) {
    mSecondaryData = mPrimaryData!!.secondaryList[0]
}

但是,当你用插件直接将 Java 代码转换为 Kotlin 时,你会发现有很多 !! 在里面。但其实 !! 意味着「有一个潜在未处理的 KotlinNullPointerException 在这里」。特别是在多线程开发环境中,而java代码又缺少了对null的强检查,这就更容易会出现空异常了。下面给大家减少及个避免 !! 的方法:

3.1、用 val 而不是 var

在 Kotlin 中 val 代表只读,var 代表可变。建议尽可能多的使用 val。val 是线程安全的,并且不需要担心 null 的问题。只需要注意 val 在某些情况下也是可变的就行了。对于普通变量来说,不可变( immutable )和只读( read-only )之间没什么区别。因为你没有办法复写一个 val 变量,所以在这时确实是不可变的。

但如果是对于类的成员变量来说,那只读和不可变的区别可就大了。在 Kotlin 的类中,val 和 var 是用于表示属性是否有 getter/setter:

  • var:同时有 getter 和 setter。
  • val:只有 getter。

但是可以通过自定义 getter 函数来返回不同的值:

代码语言:java复制
class Person(val birthDay: DateTime) {  
  val age: Int
    get() = yearsBetween(birthDay, DateTime.now())
}

可以看到,虽然没有方法来设置 age 的值,但会随着当前日期的变化而变化。

这种情况下,我建议不要自定义 val 属性的 getter 方法。如果一个只读的类属性会随着某些条件而变化,那么应当用函数来替代:

代码语言:java复制
class Person(val birthDay: DateTime) {  
  fun age(): Int = yearsBetween(birthDay, DateTime.now())
}

这也是 Kotlin 代码约定中所提到的,当具有下面列举的特点时使用属性,不然更推荐使用函数:

  • 不会抛出异常。
  • 具有 O(1) 的复杂度。
  • 计算时的消耗很少。
  • 同时多次调用有相同的返回值。

3.2、使用 lateinit

有些情况我们不能使用 val,比如,在 Android 中某些属性需要在 onCreate() 方法中初始化。对于这种情况,Kotlin 提供了 lateinit 关键字。

代码语言:java复制
private lateinit var mAdapter: RecyclerAdapter<Transaction>
 
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   mAdapter = RecyclerAdapter(R.layout.item_transaction)
}
 
fun updateTransactions() {
   if(this::mAdapter.isInitialized){
      mAdapter.notifyDataSetChanged()
   }
}

要注意,访问未初始化的 lateinit 属性会导致 UninitializedPropertyAccessException。因此在使用的时候最好判断是否初始化。

并且 lateinit 不支持基础数据类型,比如 Int。对于基础数据类型,我们可以这样:

代码语言:java复制
private var mNumber: Int by Delegates.notNull<Int>()

3.3、使用 let 函数

下面是 Kotlin 代码常见的编译错误:

许多开发者都会选择快速修复:

代码语言:java复制
private var mPhotoUrl: String? = null
fun uploadClicked() {
    if (mPhotoUrl != null) {
        uploadPhoto(mPhotoUrl!!)
    }
}

但这里选择 let 函数是一个更优雅的解决方法:

代码语言:java复制
private var mPhotoUrl: String? = null
fun uploadClicked() {
    mPhotoUrl?.let { uploadPhoto(it) }
}

3.4、创建全局函数来处理更复杂的情况

let 是一个对于 null 检查很好的替代品,但有时我们会遇到更复杂的情况。比如:

代码语言:java复制
if (mUserName != null && mPhotoUrl != null) {
   uploadPhoto(mUserName!!, mPhotoUrl!!)
}

你可以选择嵌套两个 let,但这样可读性并不好。这时你可以构建一个全局函数:

代码语言:java复制
fun <T1, T2> ifNotNull(value1: T1?, value2: T2?, bothNotNull: (T1, T2) -> (Unit)) {
   if (value1 != null && value2 != null) {
       bothNotNull(value1, value2)
   }
}

然后直接调用:

代码语言:java复制
ifNotNull(name,address){name,address->
    uploadPhoto(name,address)
}

3.5、使用 ?: 操作符

代码语言:java复制
fun getName():String{
    if(name!=null){
        return name!!
    }else{
        return "android coder"
    }
}

上面方法可以简化为:

代码语言:java复制
fun getName():String{
    return name?:"android coder"
}

3.6、自定义崩溃信息

如果我们使用 !!,那么当这个变量为 null 时,只会简单的抛出一个 KotlinNullPointerException。这时我们可以用 requireNotNull 或 checkNotNull 来附带异常信息,方便我们调试。

代码语言:java复制
uploadPhoto(requireNotNull(intent.getStringExtra("PHOTO_URL"), { 
    "Activity parameter 'PHOTO_URL' is missing" 
}))

总而言之,绝大多数情况下你都不需要 !!,可以用上面提到的 6 个技巧来消除 !!。这样能让代码更安全、更容易 debug 并且更干净。

4、Gson与Kotlin碰撞出的不安全操作

4.1、使用 data class 没有设置无参构造函数

在 Kotlin 中,不需要自己动手去写一个 JavaBean,可以直接使用 DataClass,使用 DataClass 编译器会默默地帮我们生成一些函数。例如:

代码语言:java复制
data class Person(var name: String, var age: Int) {}

这个Bean是用于接收服务器数据,通过Gson转化为对象的。例如:

代码语言:java复制
val gson = Gson()
val person = gson.fromJson<Person>("{"age":"12"}", Person::class.java)
println(person.name)

我们传递了一个json字符串,但是没有包含key为name的值,并且注意:

在Person中name的类型是String,也就是说是不允许name=null的

输出结果:

代码语言:java复制
null

是不是有些奇怪,感觉意外绕过了Kotlin的空类型检查。那么是什么原因导致的呢?

原因是:Person在被转Java代码时,只会生成一个包含两个参数的构造方法,没有提供默认的构造方法。Gson在通过反射创建对象时,会优先尝试获取无参构造函数。如果没有找到无参构造函数时,它就直接通过Unsafe的方法,绕过了构造方法,直接构建了一个对象。

因此我们在使用 data class,在遇到上面类似需求的时候,最好提供一个无参构造方法。

具体原因可以看这篇文章:https://cloud.tencent.com/developer/article/1788617

4.2、bean类继承了父类并在主构造函数中覆盖了父类的属性

使用Gson解析json时,如果bean类继承了父类并在主构造函数中覆盖了父类的属性,那么会报错:declares multiple JSON fields named name(声明多个名为name的JSON字段) 比如:

代码语言:java复制
open class Person: Serializable{
    open var name: String? = null
}
class SpecialPerson(override var name: String?) : Person() {
    override fun toString(): String {
        return name?: ""
    }
}

解决方法是,在子类中用init初始化块将构造函数中获取到的属性值赋给继承的属性,即:

代码语言:java复制
class SpecialPerson(var specialName: String?) : Person() {
    init {
        name = specialName
    }
    override fun toString(): String {
        return name?: ""
    }
}

5、Arouter中使用kotlin编写的Interceptor不生效的问题

问题原因在于kotlin文件中的@Interceptor注解没有被正确处理,因此没有将自定义的Interceptor加入到Interceptor集合中,解决方法为在module的build.gradle文件中:

第一:加入

代码语言:java复制
apply plugin: 'kotlin-kapt'

第二:使用

代码语言:java复制
kapt {
    arguments {
        arg("moduleName", project.getName())
    }
}

代替:

代码语言:java复制
javaCompileOptions {
    annotationProcessorOptions {
        arguments = [moduleName: project.getName()]
    }
}

第三:使用

代码语言:java复制
kapt 'com.alibaba:arouter-compiler:1.0.4'

代替:

代码语言:java复制
annotationProcessor 'com.alibaba:arouter-compiler:1.0.4'

kapt 可以替代annotationProcessor 注释java类

6、Kotlin 复写 Java 父类中的方法,这里有坑

Java 父类定义 onDialogCreate 方法

代码语言:java复制
// JavaKengBase.java
public class JavaKengBase {
    public void onDialogCreate(Object savedInstanceState) {
        // todo nothings
    }
}

Kotlin 继承并复写 JavaKengBase

代码语言:java复制
class Keng : JavaKengBase() {
    override fun onDialogCreate(savedInstanceState: Any) {// 注意:此处,是Any,不是Any?
        super.onDialogCreate(savedInstanceState)
    }
}

利用 Java 多态特性,调用 onDialogCreate,并传入 null 参数

代码语言:java复制
public class KengJava {
    public static void main(String[] args) {
        JavaKengBase keng = new Keng();
        keng.onDialogCreate(null);// 注意:空参数
    }
}

这里可以有两个问题:

第一个:"overrides nothing"

原因就在 onDialogCreate(savedInstanceState: Any) 方法定义中的:Any,不是Any?上。

注意:不要相信 AS 编译器,使用快捷键 Override Method 时,还是需要额外关注参数是否 Nullable?

第二个:IllegalArgumentException: Parameter specified as non-null is null

就算通过了编译,但在运行时,可能会抛出 Parameter specified as non-null is null异常,这个异常也是Java与Kotlin混合开发中的高频异常。

综上:上述问题,很好解决,只需要在方法参数后面,增加一个?即可。

代码语言:java复制
override fun onDialogCreate(savedInstanceState: Any?) 

7、kotlin中的单例模式

代码语言:java复制
class Singleton private constructor() {
    companion object {
        val instance: Singleton by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            Singleton()
        }
    }
}

8、Kotlin 使用@Parcelize注解实现Parcelable

这里不介绍@Parcelize注解的具体使用,只记录使用过程中遇到的问题。使用方法大家可以自己百度下,很简单。

8.1、apply plugin:'kotlin-android-extensions'引用问题

大家都知道@Parcelize使用需要在module的build.gradle中配置两个地方:

代码语言:java复制
//这个插件提供了很多新特性
apply plugin: 'kotlin-android-extensions'
代码语言:java复制
android{
    androidExtensions {
        //新版本不需要这个了
        experimental = true
        // 这个配置是为了禁用除了parcelize外的其他功能
        features = ["parcelize"]
    }
}

但是遇见了一个问题,添加完上面两个地方后,@Parcelize注解死活不能用,根本不能识别。

原来,第一处的kotlin-android扩展插件写的顺序是有要求的。我们必须先写apply plugin: 'kotlin-android',然后再写apply plugin: 'kotlin-android-extensions',如果顺序写反了就会出现不能识别的情况。

0 人点赞