【譯】將 Android 專案遷移到 Kotlin 語言

WilsonWu發表於2017-08-22

不久前我們開源了 Topeka,一個 Android 小測試程式。
這個程式是用 integration testsunit tests 進行測試的, 而且本身全部是用 Java 寫的。至少以前是這樣的...

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

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

從技術角度上來講,這次的移植並不是必須的,程式本身是十分穩定的,而(這次移植)主要是為了滿足我的好奇心。Topeka 成為了我學習一門新語言的媒介。

如果你好奇的話可以直接來看 GitHub 上的原始碼
目前 Kotlin 程式碼在一個獨立的分支上,但我們計劃在未來某個時刻將其合併到主程式碼中。

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


看上去依舊一樣

? 關鍵的幾點

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

移植到 Kotlin 的第一步

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

第一步和第二步對於學好 Kotlin 來說確實很有用。

然而第三步就要看我個人的造化了。

對於 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前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章