【譯】將 Android 項目遷移到 Kotlin 語言

不久前咱們開源了 Topeka,一個 Android 小測試程序。
這個程序是用 integration testsunit tests 進行測試的, 並且自己所有是用 Java 寫的。至少之前是這樣的...html

聖彼得堡岸邊的那個島嶼叫什麼?

2017年穀歌在開發者大會上官方宣佈 支持 Kotlin 編程語言。從那時起,我便開始移植 Java 代碼,同時在過程當中學習 Kotlin。前端

從技術角度上來說,此次的移植並非必須的,程序自己是十分穩定的,而(此次移植)主要是爲了知足個人好奇心。Topeka 成爲了我學習一門新語言的媒介。java

若是你好奇的話能夠直接來看 GitHub 上的源代碼
目前 Kotlin 代碼在一個獨立的分支上,但咱們計劃在將來某個時刻將其合併到主代碼中。
react

這篇文章涵蓋了我在遷移代碼過程當中發現的一些關鍵點,以及 Android 開發新語言時有用的小竅門。android


看上去依舊同樣ios

🔑 關鍵的幾點

  • Kotlin 是一門有趣而強大的語言
  • 多測試才能心安
  • 平臺受限的狀況不多

移植到 Kotlin 的第一步

雖然不可能像 Bad Android Advice 所說的那麼簡單,但至少是個不錯的出發點。git

第一步和第二步對於學好 Kotlin 來講確實頗有用。github

然而第三步就要看我我的的造化了。數據庫

對於 Topeka 來講實際步驟以下:

  1. 學好 Kotlin 的基礎語法
  2. 經過使用 Koan 來逐步熟悉這門語言
  3. 使用 「⌥⇧⌘K」 保證(轉化後的文件)仍然能一個個經過測試
  4. 修改 Kotlin 文件使其更加符合語言習慣
  5. 重複第四步直到你和審覈你代碼的人都滿意
  6. 完工並上交

互通性

一步步去作是很明智的作法。
Kotlin 編譯爲 Java 字節碼後兩種語言能夠互相通用。並且同一個項目中兩種語言能夠共存,因此並不須要把所有代碼都移植成爲另外一種語言。
但若是你原本就想這麼作,那麼重複的改寫就是有意義的,這樣你在遷移代碼時能夠儘可能地維持項目的穩定性,並在此過程當中有所收穫。編程

多作測試才能更加安心

搭配使用單元和集成測試的好處不少。在絕大多數狀況下,這些測試是用來確保當前修改沒有損壞現有的功能。

我選擇在一開始使用一個不是很複雜的數據類。在整個項目中我一直在用這些類,它們的複雜性相比來講很低。這樣來看在學習新語言的過程當中這些類就成爲了最理想的出發點。

在經過使用 Android Studio 自帶的 Kotlin 代碼轉換器移植一部分代碼後,我開始執行並經過測試,直到最終將測試自己也移植爲 Kotlin 代碼。

若是沒有測試的話,我在每次改寫後都須要對可能受影響的功能手動進行測試。自動化的測試在我移植代碼的過程當中顯得更加快捷方便。

因此,若是你尚未對你的應用進行正確測試的話,以上就是你須要這麼作的又一個緣由。 👆

生成的代碼並非每一次都看起來很棒!!

在完成最開始幾乎自動化的移植代碼以後,我開始學習 Kotlin 代碼風格指南。 這使我發現還有一條很長的路要走。

整體來說,代碼生成器用起來很不錯。儘管有不少語言特徵和風格在轉換過程當中沒有被使用,但翻譯語言原本就是件很棘手的事,這麼作可能更好一些,尤爲是當這門語言所包含不少的特徵或者能夠經過不一樣方式進行表達的時候。

若是想要了解更多有關 Kotlin 轉換器的內容, Benjamin Baxter 寫過一些他本身的經歷:

‼️ ⁉

我發現自動轉換會生成不少的 ?!!
這些符號是用來定義可爲空的數值和保證其不爲空值的。他們反而會致使 空指針異常
我不由想到一條很恰當的名言

「過多使用感嘆號,」 他一邊搖頭一邊說道, 」是心理不正常的表現。」 — Terry Pratchett

在大部分狀況下它不會成爲空值,因此咱們不須要使用空值的檢查。同時也不必經過構造器來直接初始全部的數值,可使用 lateinit 或者委託來代替初始的流程。

然而這些方法也不是萬能的:

有時候變量會成爲空值。

看來我得從新把 view 定義爲可爲空值。

在其餘狀況下你仍是得檢查是否 null 存在。若是存在 supportActionBar 的話, *supportActionBar*?.setDisplayShowTitleEnabled(false) 纔會執行問號之後的代碼。
這意味着更少的基於 null 檢查的 if 條件聲明。 🔥

直接在非空數值上使用 stdlib 函數很是方便:

toolbarBack?.let {
    it.scaleX = 0f
    it.scaleY = 0f
}複製代碼

大規模地使用它...


變得愈來愈符合語言習慣

由於咱們能夠經過審覈者的反饋不斷地改寫生成的代碼來使其變得更加符合語言的習慣。這使代碼更加簡潔而且提高了可讀性。以上特色能夠證實 Kotlin 是門很強大的語言,

來看看我曾經遇到過的幾個例子吧。

少讀點兒並不必定是件壞事

咱們拿 adapter 裏面的 getView 來舉例:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
        if (null == convertView) {
           convertView = createView(parent);
        }
        bindView(convertView);
        return convertView;
}複製代碼

Java 中的 getView

override fun getView(position: Int, convertView: View?, parent: ViewGroup) =
    (convertView ?: createView(parent)).also { bindView(it) }複製代碼

Kotlin 的 getView

這兩段代碼在作同一件事:

先檢查 convertView 是否爲 null ,而後在 createView(...) 裏面建立一個新的 view ,或者返回 convertView。同時在最後調用 bindView(...).

兩端代碼都很清晰,不過能從八行代碼減到只有兩行確實讓我很驚訝。

數據類很神奇 🦄

爲了進一步展示 Kotlin 的精簡所在,使用數據類能夠輕鬆避免冗長的代碼:

public class Player {

    private final String mFirstName;
    private final String mLastInitial;
    private final Avatar mAvatar;

    public Player(String firstName, String lastInitial, Avatar avatar) {
        mFirstName = firstName;
        mLastInitial = lastInitial;
        mAvatar = avatar;
    }

    public String getFirstName() {
        return mFirstName;
    }

    public String getLastInitial() {
        return mLastInitial;
    }

    public Avatar getAvatar() {
        return mAvatar;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        Player player = (Player) o;

        if (mAvatar != player.mAvatar) {
            return false;
        }
        if (!mFirstName.equals(player.mFirstName)) {
            return false;
        }
        if (!mLastInitial.equals(player.mLastInitial)) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = mFirstName.hashCode();
        result = 31 * result + mLastInitial.hashCode();
        result = 31 * result + mAvatar.hashCode();
        return result;
    }
}複製代碼

下面咱們來看怎麼用 Kotlin 寫這段代碼:

data class Player( val firstName: String?, val lastInitial: String?, val avatar: Avatar?)複製代碼

是的,在保證功能的狀況下少了整整五十五行代碼。這就是數據類的神奇之處

擴展功能性

下面可能就是傳統 Android 開發者以爲奇怪的地方了。Kotlin 容許在一個給定範圍內建立你本身的 DSL。

來看看它是如何運做的

有時咱們會在 Topeka 裏經過
Parcel 傳遞 boolean。Android 框架的 API 沒法直接支持這項功能。在一開始實現這項功能的時候必須調用一個功能類函數例如ParcelableHelper.writeBoolean(parcel, value)
若是使用 Kotlin,擴展函數能夠解決以前的難題:

import android.os.Parcel

/**
 * 將一個 boolean 值寫入[Parcel]。
 * @param toWrite 是即將寫入的值。
 */
fun Parcel.writeBoolean(toWrite: Boolean) = writeByte(if (toWrite) 1 else 0)

/**
 * 從[Parcel]中獲得 boolean 值。
 */
fun Parcel.readBoolean() = 1 == this.readByte()複製代碼

當寫好以上代碼以後,咱們能夠把
parcel.writeBoolean(value)parcel.readBoolean() 當成框架的一部分直接調用。要不是由於 Android Studio 使用不一樣的高亮方式區分擴展函數,很難看出它們之間的區別。

擴展函數能夠提高代碼的可讀性。 來看看另外一個例子:在 view 的層次結構中替換 Fragment。

若是使用 Java 的話代碼以下:

getSupportFragmentManager().beginTransaction()
        .replace(R.id.quiz_fragment_container, myFragment)
        .commit();複製代碼

這幾行代碼其實寫的還不錯。但每次當 Fragment 被替換的時候你都要把這幾行代碼再寫一遍,或者在其餘的 Utils 類中建立一個函數。

若是使用 Kotlin,當咱們在 FragmentActivity 中須要替換 Fragment 的時候,只須要使用以下代碼調用 replaceFragment(R.id.container, MyFragment()) 便可:

fun FragmentActivity.replaceFragment(@IdRes id: Int, fragment: Fragment) {
    supportFragmentManager.beginTransaction().replace(id, fragment).commit()
}複製代碼

替換 Fragment 只需一行代碼

少一些形式,多一點兒功能

高階函數太令我震撼了。是的,我知道這不是什麼新的概念,但對於部分傳統 Android 開發者來講多是。我以前有據說過這類函數,也見有人寫過,但我從未在我本身的代碼中使用過它們。

在 Topeka 裏我有好幾回都是依靠 OnLayoutChangeListener 來實現注入行爲。若是沒有 Kotlin ,這樣作會生成一個包含重複代碼的匿名類。

遷移代碼以後,只須要調用如下代碼:
view.onLayoutChange { myAction() }

這其中的代碼被封裝到以下擴展函數中了:

/**
 * 當佈局改變時執行對應代碼
 */
inline fun View.onLayoutChange(crosssinline action: () -> Unit) {
    addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
        override fun onLayoutChange(v: View, left: Int, top: Int,
                                    right: Int, bottom: Int,
                                    oldLeft: Int, oldTop: Int,
                                    oldRight: Int, oldBottom: Int) {
            removeOnLayoutChangeListener(this)
            action()
        }
    })
}複製代碼

使用高階函數減小樣板代碼

另外一個例子能證實以上的功能一樣能夠被應用於數據庫的操做中:

inline fun SQLiteDatabase.transact(operation: SQLiteDatabase.() -> Unit) {
    try {
        beginTransaction()
        operation()
        setTransactionSuccessful()
    } finally {
        endTransaction()
    }
}複製代碼

少一些形式,多一些功能

這樣寫完後,API 使用者只須要調用 db.transact { operation() } 就能夠完成以上全部操做。

經過 Twitter 進行更新: 經過使用 SQLiteDatabase.() 而不是 () 能夠在 operation() 中傳遞函數並實現直接使用數據庫。🔥

不用我多說你應該已經懂了。

使用高階和擴展函數可以提高項目的可讀性,同時能去除冗長的代碼,提高性能並省略細節。


有待探索

目前爲止我一直在講代碼規範以及一些開發的慣例,都沒有提到有關 Android 開發的實踐經驗。

這主要是由於我對這門語言還不是很熟,或者說我尚未花太大精力去收集並發表這方面的內容。也許是由於我尚未碰到這類狀況,但彷佛還有至關多的平臺特定的語言風格。若是你知道這種狀況,請在評論區補充。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索