1. 程式人生 > >給寫Kotlin 開發 Android 小夥伴的一些小建議

給寫Kotlin 開發 Android 小夥伴的一些小建議

Kotlin 有著諸多的特性,比如空指標安全、方法擴充套件、支援函數語言程式設計、豐富的語法糖等。這些特性使得 Kotlin 的程式碼比 Java 簡潔優雅許多,提高了程式碼的可讀性和可維護性,節省了開發時間,提高了開發效率,但同樣作為 Kotlin 使用者的你,我相信你一定也有不少小建議和小技巧,一直想迫不及待地分享給大家。

**那就給你一個機會,願你把你的黑科技悄悄留言在本文下方!

我想給大家的一些小建議

這麼有趣的活動,那我作為一名兩個月的 Kotlin 開發,自然也應該來這個活動湊湊熱鬧。

1. 避免使用 IDE 自帶的外掛轉換 Java 程式碼

想必 IDE 裡面的外掛 "Covert Java File To Kotlin File" 早已被大家熟知,要是不知道的小夥伴,趕緊寫個 Java 檔案,嘗試點選 Android Studio 工具欄的 Code 下面的 "Convert Java File To Kotlin File",看看都有什麼小妙用。

這也是南塵最開始喜歡使用的方式,沒有技術卻有一顆裝 ✘ 的內心,直接寫成 Java 檔案,再直接一鍵轉換為 Kotlin。甚至寶寶想告訴你,我 GitHub 上 1k Star 的 AiYaGilr 專案的 Kotlin 分支,也是這樣而來。但真是踩了不少的坑。

這樣的方式足夠地快,但卻會出現很多很多的 !!,這是由於 Kotlin 的 null safety 特性。這是 Kotlin 在 Android 開發中的很牛逼的一大特性,想必不少小夥伴都被此 Android 的 NullPointException 困擾許久。我們直接轉換 Java 檔案造成的各種 !! ,其實也就意味著你可能存在潛在的未處理的 KotlinNullPointException

2. 儘量地使用 val

val 是執行緒安全的,並且不需要擔心 null 的問題,我們自然應該儘可能地使用它。

比如我們常用的 Android 解析的伺服器資料,我們應該為自己的 data class 設定為 val,因為它本身就不應該是可寫的。

當我第一次使用 Kotlin 的時候,我以為valvar 的區別在於val 代表不可變,而 var 代表是可變的。但事實比這更加微妙:val 不代表不可變,val 意味著只讀。。這意味著你不允許明確宣告為 val,它就不能保證它是不可變的。

對於普通變數來說,「不可變」和「只讀」之間並沒什麼區別,因為你沒辦法複寫一個 val 變數,所以在此時卻是是不可變的。但在 class 的成員變數中,「只讀」和「不可變」的區別就大了。

在 Kotlin 的類中,val 和 var 是用於表示屬性是否有 getter/setter:

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

這裡是可以通過自定義 getter 函式來返回不同的值:

class Person(val birthDay: DateTime) {  
  val age: Int
    get() = yearsBetween(birthDay, DateTime.now())
}

可以看到,雖然沒有方法來設定 age 的值,但會隨著當前日期的變化而變化。

這種情況下,我建議不要自定義 val 屬性的 getter 方法。如果一個只讀的類屬性會隨著某些條件而變化,那麼應當用函式來替代:

class Person(val birthDay: DateTime) {  
  fun age(): Int = yearsBetween(birthDay, DateTime.now())
}

這也是 Kotlin 程式碼約定 中所提到的,當具有下面列舉的特點時使用屬性,不然更推薦使用函式:

  • 不會丟擲異常。
  • 具有 O(1) 的複雜度。
  • 計算時的消耗很少。
  • 同時多次呼叫有相同的返回值。

因此上面提到的,自定義 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 程式碼無疑是低效的。

我們可以通過以下解決方法來減少生成的位元組碼:

  1. 對於基本型別和字串,可以使用 const 關鍵字將常量宣告為編譯時常量。
  2. 對於公共欄位,可以使用 @JvmField 註解。
  3. 對於其他型別的常量,最好在它們自己的主類物件而不是伴生物件中來儲存公共的全域性常量。

4. @JvmStatic、@JvmFiled 和 object 還有這種故事?

我們在 Kotlin 中發現了 object 這個東西,我以前就一直對這個東西很好奇,不知道這是個什麼玩意兒。

object ?難道又一個物件?

之前有人寫過這樣的程式碼,表示很不解,一個介面型別的成員變數,訪問外部類的成員變數 name。這不是理所應當的麼?

interface Runnable {
    fun run()
}

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

    object impl : Runnable {
        override fun run() {
            // 這裡編譯器會報紅報錯。對 name
            println(name)
        }
    }
}

即使檢視 Kotlin 官方文件,也有這樣一段描述:

Sometimes we need to create an object of a slight modification of some class, without explicitly declaring a new subclass for it. Java handles this case with anonymous inner classes. Kotlin slightly generalizes this concept with object expressions and object declarations.

核心意思是:Kotlin 使用 object 代替 Java 匿名內部類實現。

很明顯,即便如此,這裡的訪問應該也是合情合理的。從匿名內部類中訪問成員變數在 Java 語言中是完全允許的。

這個問題很有意思,解答這個我們需要生成 Java 位元組碼,再反編譯成 Java 看看具體生成的程式碼是什麼。

public final class Test {
   private final String name = "nanchen";
   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 = "nanchen"

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

為了避免出現這個問題,謹記一個原則:如果 object 只是宣告,它代表一個靜態內部類。如果用變數接收 object 表示式,它代表一個匿名內部類物件。

講到這,自然也就知道了 Kotlin 對 object 的三個作用:

  • 簡化生成靜態內部類
  • 生成匿名內部類物件
  • 生成單例物件

咳咳,說了那麼多,到底和 @JvmStatic 和 @JvmField 有啥關係呢?

實際上,目前我們大多數的 Android 專案都是 Java 和 Kotlin 混編的,包括我們的專案在內也是如此。所以我們總是免不了 Java 和 Kotlin 互調的情況。我們可能經常會在程式碼中這樣編寫:

object Test1 {
    val NAME = "nanchen"
    fun getAge() = 18
}

在 Java 中會呼叫是這樣的:

System.out.println("name:"+Test1.INSTANCE.getNAME()+",age:"+Test1.INSTANCE.getAge());

作為強迫症重度患者的我,自然是無法接受上面這樣奇怪的程式碼。所以我強烈建議大家在 object 和 companion object 中分別為變數和方法增加上 @JvmField 和 @JvmStatic 註解。

object Test1 {
    @JvmField
    val NAME = "nanchen"
    @JvmStatic
    fun getAge() = 18
}

這樣外面 Java 呼叫起來就好看多了。

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>()

當然,我們還可以使用 let 函式來進行上面的這種情況,但無疑都是畫蛇添足的。

我們前面說了,在一些明知是隻讀不可寫不可變的變數,我們儘可能地用 val 去修飾它。而 lateinit 僅僅能修飾 var 變數,所以 by lazy 懶載入,是時候表演真正的技術了。

對於很多不可變的變數,比如上個頁面通過 bundle 傳遞過來的用於該頁面請求網路的引數,比如 MVP 架構開發中的 Presenter,我們都應該用 by lazy 關鍵字去初始化它。

lazy() 委託屬性可以用於只讀屬性的惰性載入,但是在使用 lazy() 時經常被忽視的地方就是有一個可選的model引數:

  • LazyThreadSafetyMode.SYNCHRONIZED:初始化屬性時會有雙重鎖檢查,保證該值只在一個執行緒中計算,並且所有執行緒會得到相同的值。
  • LazyThreadSafetyMode.PUBLICATION:多個執行緒會同時執行,初始化屬性的函式會被多次呼叫,但是隻有第一個返回的值被當做委託屬性的值。
  • LazyThreadSafetyMode.NONE:沒有雙重鎖檢查,不應該用在多執行緒下。

lazy() 預設情況下會指定 LazyThreadSafetyMode.SYNCHRONIZED,這可能會造成不必要執行緒安全的開銷,應該根據實際情況,指定合適的model來避免不需要的同步鎖。

6.注意 Kotlin 中的 for 迴圈

Kotlin提供了 downTostepuntilreversed 等函式來幫助開發者更簡單的使用 For 迴圈,如果單一的使用這些函式確實是方便簡潔又高效,但要是將其中兩個結合呢?比如下面這樣:

class A {
    fun loop() {
        for (i in 10 downTo 0 step 3) {
            println(i)
        }
    }
}

上面使用了 downTo 和 step 兩個關鍵字,我們看看 Java 是怎樣實現的。

public final class A {
   public final void loop() {
      IntProgression var10000 = RangesKt.step(RangesKt.downTo(10, 0), 3);
      int i = var10000.getFirst();
      int var2 = var10000.getLast();
      int var3 = var10000.getStep();
      if (var3 > 0) {
         if (i > var2) {
            return;
         }
      } else if (i < var2) {
         return;
      }

      while(true) {
         System.out.println(i);
         if (i == var2) {
            return;
         }

         i += var3;
      }
   }
}

毫無疑問:IntProgression var10000 = RangesKt.step(RangesKt.downTo(10, 0), 3); 一行程式碼就建立了兩個 IntProgression 臨時物件,增加了額外的開銷。

7. 注意 Kotlin 的可空和不可空

最近鬧了一個笑話,在專案中需要寫一個上傳跳繩資料的功能。於是有了下面的程式碼。

public interface ISkipService {
    /**
     * 上傳使用者跳繩資料
     */
    @POST("v2/rope/upload_jump_data")
    Observable<BaseResponse<Object>> uploadJumpData(@Field("data") List<SkipHistoryBean> data);
}

寫畢上面的介面,我們再到 ViewModel 中進行網路請求。

private List<SkipHistoryBean> list = new ArrayList<>();

public void uploadClick() {
    mNavigator.showProgressDialog();
    list.add(bean);
    RetrofitManager.create(ISkipService.class)
        .uploadJumpData(list)
        .compose(RetrofitUtil.schedulersAndGetData())
        .subscribe(new BaseSubscriber<Object>() {
            @Override
            protected void onSuccess(Object data) {
                mNavigator.hideProgressDialog();
                mNavigator.uploadDataSuccess();
                // 點選上傳成功,刪除資料庫
                deleteDataFromDB();
            }

            @Override
            protected void onFail(ErrorBean errorBean) {
                super.onFail(errorBean);
                mNavigator.hideProgressDialog();
                mNavigator.uploadDataFailed(errorBean.error_description);
            }
        });
}

執行其實並沒有什麼問題。但由於某些原因,當我把上面的 ISkipService 類修改為了 Kotlin 實現,卻發生了崩潰,從程式碼上暫時沒看出問題。

interface ISkipService {
    /**
     * 上傳使用者跳繩資料
     */
    @POST("v2/rope/upload_jump_data")
    fun uploadJumpData(@Field("data") data: List<SkipHistoryBean>): Observable<BaseResponse<Any>>
}

但確實就是崩潰了。仔細一看,發現 Java 編寫這個介面的時候,會被認為這個引數 "data" 對應的 "value" 是可以為 null 的,而改為 Kotlin 後,由於 Kotlin 預設不為空的機制,所以需要的引數是一個不可以為 null 的 List 集合。而我們的 ViewModel 中使用的 Java 程式碼,由於 Java 認為我們的 List 是可以為 null 的,所以導致了型別不匹配的崩潰。