Android

Kotlin使用注意细节

2019-04-03  本文已影响0人  钢镚koala

摘要:Kotlin 是一种针对 Java 平台的新编程语言。它简洁、安全、务实,并且专注于与 Java 代码的互操作性。它几乎可以用在现在Java 使用的任何地方 :服务器端开发、Android 应用等。相信移动端的小伙伴早已从纯 Java 开发 Android 转为了混合使用开发,甚至是 Kotlin 开发。它是google官方推荐使用的语言,特点是 简洁优雅,提高了代码的可读性和可维护性,节省了开发时间,提高了开发效率。那本文我们主要讲使用过程中需要注意的细节。

1. 避免使用 IDE 自带的插件转换 Java 代码

IDE 里面的插件 "Covert Java File To Kotlin File" 早已被大家熟知,要是不知道的小伙伴,赶紧写个 Java 文件,尝试点击 Android Studio 工具栏的 Code 下面的 "Convert Java File To Kotlin File"。

这样的方式足够地快,但却会出现很多很多的 !!,这是由于 Kotlin 的 null safety 特性。这是 Kotlin 在 Android 开发中的很牛逼的一大特性,想必不少小伙伴都被此 Android 的 NullPointException 困扰许久。我们直接转换 Java 文件造成的各种 !! ,其实也就意味着你可能存在潜在的未处理的 KotlinNullPointException。

2. 尽量地使用 val

val 是线程安全的,并且不需要担心 null 的问题,我们自然应该尽可能地使用它。
比如我们常用的 Android 解析的服务器数据,我们应该为自己的 data class 设置为 val,因为它本身就不应该是可写的。

第一次使用 Kotlin 的时候,我以为val 和 var 的区别在于val 代表不可变,而 var 代表是可变的。但事实是:val 不代表不可变,val 意味着只读。这意味着你不允许明确声明为 val,它就不能保证它是不可变的。

对于普通变量来说,不可变和只读之间并没什么区别,因为你没办法复写一个 val 变量,所以在此时却是是不可变的。但在 class 的成员变量中,只读和不可变的区别就大了。

在 Kotlin 的类中,val 和 var 是用于表示属性是否有 getter/setter:
var:同时有 getter 和 setter。
val:只有 getter。

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

class A(val createTime: Date) {  
  val count: Int
    get() = timeBetween(createTime.time())
}

虽然没有方法来设置 count 的值,但会随着创建日期的变化而变化。这种情况下,建议不要自定义 val 属性的 getter 方法。如果一个只读的类属性会随着某些条件而变化,那么应当用函数来替代

class A(val createTime: Date) {  
  fun count(): Int = timeBetween(createTime.time())
}

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

因此上面提到的,自定义 getter 方法并随着当前时间的不同而返回不同的值违反了最后一条原则。大家也要尽量的避免这种情况。

3. 好好注意一下伴生对象

伴生对象通过在类中使用 companion object 来创建,用来替代静态成员,类似于 Java 中的静态内部类。所以在伴生对象中声明常量是很常见的做法,但如果写法不对,可能就会产生额外开销。

比如下面的这段代码

class CompanionKotlin {
    companion object {
        val DATA = "CompanionKotlin_DATA"
    }

    fun getData(): String = DATA
}

挺简洁地一段代码。但将这段简洁的 Kotlin 代码转换为等同的 Java 代码后,却显的晦涩难懂。

public final class CompanionKotlin {
   @NotNull
   private static final String DATA = "CompanionKotlin_DATA";
   public static final CompanionKotlin.Companion Companion = new CompanionKotlin.Companion((DefaultConstructorMarker)null);

   @NotNull
   public final String getData() {
      return DATA;
   }
    // ...
   public static final class Companion {
      @NotNull
      public final String getDATA() {
         return CompanionKotlin.DATA;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

与 Java 直接读取一个常量不同,Kotlin 访问一个伴生对象的私有常量字段需要经过以下方法:

调用伴生对象的静态方法
调用伴生对象的实例方法
调用主类的静态方法
读取主类中的静态字段

为了访问一个常量,而多花费调用4个方法的开销,这样的 Kotlin 代码无疑是低效的。
我们可以通过以下解决方法来减少生成的字节码:

对于基本类型和字符串,可以使用 const 关键字将常量声明为编译时常量。
对于公共字段,可以使用 @JvmField 注解。
对于其他类型的常量,最好在它们自己的主类对象而不是伴生对象中来存储公共的全局常量。

4. kotlin中 的object

我们在 Kotlin 中发现了 object 这个东西,我以前就一直对这个东西很好奇,不知道这是个什么。之前有人写过这样的代码,表示很不解,一个接口类型的成员变量,访问外部类的成员变量 name。认为这是理所应当的?

interface Runnable {
    fun run()
}

class Test {
    private val name: String = "nice"

    object impl : Runnable {
        override fun run() {
            // 这里编译器会报红报错。对 name
            println(name)
        }
    }
}

查看了文档之后,大致对object的解释是这样的:Kotlin 使用 object 代替 Java 匿名内部类实现。

很明显,即便如此,这里的访问应该也是合情合理的。从匿名内部类中访问成员变量在 Java 语言中是完全允许的。
下面是上面的例子,编译生成Java 字节码再反编译成 Java后的代码。

public final class Test {
   private final String name = "nice";
   public static final class impl implements Runnable {
      public static final Test.impl INSTANCE;

      public void run() {
      }

      static {
         Test.impl var0 = new Test.impl();
         INSTANCE = var0;
      }
   }
}

public interface Runnable {
   void run();
}

静态内部类!Java 中静态内部类是不允许访问外部类的成员变量的。但,说好的 object 代替的是 Java 的匿名内部类呢?

这里一定要注意,如果你只是这样声明了一个object,Kotlin认为你是需要一个静态内部类。而如果你用一个变量去接收object表达式,Kotlin认为你需要一个匿名内部类对象。

因此,这个类应该改进:

interface Runnable {
    fun run()
}

class Test {
    private val name: String = "nice"

    private val impl = object : Runnable {
        override fun run() {
            println(name)
        }
    }
}

为了避免出现这个问题,谨记一个原则:如果 object 只是声明,它代表一个静态内部类。如果用变量接收 object 表达式,它代表一个匿名内部类对象。
讲到这,我们知道了 Kotlin 对 object 的三个作用:

简化生成静态内部类
生成匿名内部类对象
生成单例对象

5. by lazy 和 lateinit 相爱相杀

在 Android 开发中,我们经常会有不少的成员变量需要在 onCreate() 中对其进行初始化,特别是我们在 XML 中使用的各种控件,而 Kotlin 要求声明成员变量的时候默认需要为它声明一个初始值。这时候就会出现不少的下面这样的代码。

private var textView:TextView? = null

迫于暂时的无奈,我们不能不为这些 View 加上 ? 代表它们可以为空,然后为它们赋值为 null。实际上,我们在使用中一点都不希望它们为空。这样造成的后果就是,我们每次要使用它的时候都必须去先判断它不为空。这样无用的代码,无疑是浪费了我们的工作时间。
好在 Kotlin 推出了 lateinit 关键字:延迟加载。这样我们可以先绕过 kotlin 的强制要求,在后面使用的时候,再也不需要先判断它是否为空了。但要注意,访问未初始化的 lateinit 属性会导致UninitializedPropertyAccessException。
并且 lateinit 不支持基础数据类型,比如 Int。对于基础数据类型,我们可以这样:

private var mNumber: Int by Delegates.notNull<Int>()

lateinit 用于只能生命周期流程中进行获取或者初始化的变量,比如 Android 的 onCreate()。

lateinit var pagerAdapter:FragmentStatePagerAdapter

我们前面说了,在一些明知是只读不可写不可变的变量,我们尽可能地用 val 去修饰它,而
lazy 应用于单例模式(if-null-then-init-else-return),而且当且仅当变量被第一次调用的时候,委托方法才会执行。

lazy()是接受一个 lambda 并返回一个 Lazy <T> 实例的函数,返回的实例可以作为实现延迟属性的委托: 第一次调用 get() 会执行已传递给 lazy() 的 lambda 表达式并记录结果, 后续调用 get() 只是返回记录的结果。

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main(args: Array<String>) {
    println(lazyValue)
    println(lazyValue)
}

打印结果
computed!
Hello

Hello

比如这样的常见操作,只获取,不赋值,并且多次使用的对象

 private val mUserMannager: UserMannager by lazy {
        UserMannager.getInstance()
    }
6. DexGuard 混淆 crash (NoSuchMethodError

项目中采用dexguard来加固混淆会碰到:

 val drawable = imageView?.drawable ?: ColorDrawable(Color.LTGRAY)
 transitionDrawable = TransitionDrawable(arrayOf(drawable, ColorDrawable()))

这段代码在没有混淆之前是ok的,但混淆之后直接crash。为空条件不是什么时候都可以允许,改成:

if(imageView == null) return
 val drawable = imageView.drawable ?: ColorDrawable(Color.LTGRAY)
7. isNullOrEmpty / isNullOrBlank / isEmpty / isNotBlank 四个方法的区别

isNullOrEmpty : 为空指针或者字串长度为0时返回true,非空串与可空串均可调用。

isNullOrBlank : 为空指针或者字串长度为0或者全为空格时返回true,非空串与可空串均可调用。

isEmpty : 字串长度为0时返回true,只有非空串可调用。

isBlank : 字串长度为0或者全为空格时返回true,只有非空串可调用。

isNotEmpty : 字串长度大于0时返回true,只有非空串可调用。

isNotBlank : 字串长度大于0且不是全空格串时返回true,只有非空串可调用。

上一篇下一篇

猜你喜欢

热点阅读