為什麼我們使用Kotlin開發LINE Creators Studio

前言

哈囉大家好,我是 Freddie Wang, LINE Fukuoka 開發室的軟體工程師。目前擔任 LINE Creators Studio Android 版本的開發。我們最近剛剛完成了 LINE Creators Studio 的 Android 版本。LINE Creators Sudio是 LINE 最新推出的應用程式,可以讓用戶輕鬆的製作自己的貼圖並且上架販賣。在 Android 的版本上我們使用 Kotlin 來開發這個應用程式。所以我想透過此篇文章來分享一下為什麼我們使用 Kotlin 開發以及我們使用了哪些功能。

目前 LINE Creators Studio 只在日本上架,預計之後會在其他地區推出。

Kotlin的優點與主要功能

在今年的 Google IO 17上, Google 正式宣佈將從 Android Studio 3.0 開始提供 Kotlin 的支援,這對 Android 的開發者來說是一個令人非常振奮的消息。在 2016 年底,我們開始了 LINE Creators Studio 的開發工作,並且需要在短時間完成一個 MVP (Minimum Viable Product)。在那個時間點,雖然 Kotlin 1.0 已經正式推出了,但我們小組裡面還沒有人有用過 Kotlin 開發產品的經驗,所以我們在正式開發前花了一點時間來研究 Kotlin,經過內部討論後,最後決定試著用 Kotlin 來開發 LINE Creators Studio。

為什麼用Kotlin

我們是根據以下幾個原因決定使用 Kotlin.

100% interoperable with Java

Kotlin 其中一個非常讚的特性是完全相容 Java,所以你可以在一個 project 裡面同時使用 Kotlin 和 Java 而不會有任何相容性的問題。雖然 LINE Creators Studio 是一個全新的專案,但是我們仍然需要使用一些很棒的函式庫,像 Dagger 2、Retrofit、RxJava 等等。

Concise syntax

Kotlin 最初是為了解決一些 Java 設計不便的地方而設計的,而其中一個主要的目的就是要能比 Java 寫出更簡潔的程式碼,而這一點在我們小組裡面,更是重要的一個因素。

Reduced dependencies

Kotlin 本身就帶了一個簡潔的標準函式庫,而且在大多數的應用上可以用來取代一些知名的大型 Java 函式庫,像是 Guava 等。在 Server 和 Desktop 應用領域上,使用這類大型的函式庫基本上沒有什麼問題。但是在 Android 的開發上就會造成一些麻煩。主要的原因就是 Android 的 Dalvik JVM 有上限 65K 的問題,基本上需要避免使用大型的 Java 函式庫。而最新的 Kotlin 標準函式庫 1.1.3-2 版只有6306個(參考連結) 函式, 所以可以有效地減輕這個問題。

Compatible with old Android devices

Kotlin 1.0 是基於 Java 6,所以使用 Kotlin 開發基本上可以在任何的 Android 2.3 版以上的裝置運行,這對於一個 Android 應用程式開發者來說是非常重要的,畢竟使用最新版本 Android 的用戶還是比較少一點。

我們使用的一些功能

Nullability

在所有 Kotlin 的功能裡面,我們最喜歡的應該算是 nullability 的支援。有了此功能,就可以讓 compiler 來幫忙檢查是否有 null 的情形發生,這可以讓我們的程式碼更清晰可讀,而且更安全。以下是幾個範例:

var output: String
output = null //compile complains

var output: String?
output = null //OK

//don't need to check null in every field
val name = bob?.department?.head?.name

Lambda expression

另外一個令人讚賞的功能就是 lambda expression 了。雖然 Java 8 之後也支援了 lambda, 但是目前只有新的 Android 裝置可以支援 Java 8。但是 Kotlin 不只支援 lambda,而且可以在大部分的 Android 裝置上執行。在 LINE Creators Studio 的 UI 開發上,我們使用了相當多的 lambda expression (搭配 anko)。所以我們不需要使用大量的 listener 或是使用其他像是 ButterKnife 之類的 view-binding 函式庫。

舉例來說,我們可以建立一個返回按鈕的 UI 如下, 這樣程式碼看起來比 Java 清爽多了,不是嗎?

backButton = imageButton {
    id = R.id.my_back_button
    imageResource = R.drawable.ic_back
    onClick {
        onBackPressed()
    }
}

Extension

Extension 也是 Kotlin 另外一個超棒的功能,透過 extension,我們可以添加額外的功能在 Android framework 上而不需要另外建立一個 base class。舉個例子,我們可以建立一個 Fragment 的 extension,之後就可以在任何的 Fragment 裡面切換到另外一個 Fragment,不需要另外再寫一個 BaseCustomFragment,然後讓所有的 Fragment 都繼承這個 BaseCustomFragment。

fun Fragment.addFragment(fragment: Fragment) {
    fragmentManager.beginTransaction()
            .setCustomAnimations(R.anim.slide_left_in,
                                 R.anim.slide_left_out, 
                                 R.anim.slide_right_in,
                                 R.anim.slide_right_out)
            .add(R.id.container, fragment)
            .addToBackStack(fragment::class.java.simpleName)
            .hide(this)
            .commit()
}

//In another Fragment, don't need to write a BaseCustomFragment class
onClick { addFragment(AnotherFragment()) }

另外一個範例是一個簡單的 Boolean-Int 轉換,當你需要從 json 轉換資料的時候,這個小小的 extension 就會非常有用,因為有時候你會需要把 integer 當成 boolean 使用。

fun Boolean.toInt(): Int {
    return if (this) 1 else 0
}

fun Int.toBoolean(): Boolean {
    return this != 0
}

//In another data class, we can use the extension like this
data.intValue = true.toInt()
data.booleanValue = 1.toBoolean() 

Named argument

在用 Kotlin 之前,如果在一個函式有稍微多一點參數的時候(大約四個以上),在使用上會造成一些困擾。但是如果使用 Kotlin 的 named argments 的話,你就可以很容易地寫出清晰易懂的程式碼。舉例來說我們在 LINE Creators Studio 的截圖功能裡面用到了 Catmull-Rom spline 這個演算法,而這個函式需要帶入七個參數。如果我們沒有使用 named arguments 的話,這個函式看起來非常的難懂,但如果用了 named arguments,就比較容易了解這個函式的用法。

fun catmullRomControlPoint(controlPoint: PointF, 
                           prevPoint: PointF,       
                           currentPoint: PointF,
                           nextPoint: PointF, 
                           delta1: Double, 
                           delta2: Double, 
                           alpha: Float) {
...
}

//When not using named arguments
catmullRomControlPoint(controlPoint1, 
                       point0, 
                       point1,
                       point2, 
                       delta1, 
                       delta2, 
                       alphaValue)

//When using named arguments
catmullRomControlPoint(controlPoint = controlPoint1, 
                       prevPoint = point0, 
                       currentPoint = point1,
                       nextPoint = point2, 
                       delta1 = delta1, 
                       delta2 = delta2, 
                       alpha = alphaValue)

Data class

不像其他的語言,Java 本身並不支援 struct,當要建立純物件 (POJO) 的時候,就需要產生相關的 getter 和 setter,這本身就是有點無聊跟煩人的工作。雖然 Java 的開發者可以使用 lombok 這樣的函式庫來簡化這流程,但是 Kotlin 所提供的 data class 讓這過程又變得更簡單了。如下面的範例,就只要宣告一個 data class 就好了! Kotlin compiler 會自動的產生 getter 和 setter。除此之外還會自動產生一些像是 equals(), hashCode()copy() 等等的基本實作。

//Just create the data class in one line, and that's it!
data class User(var name: String, var email: String, var age: Int)

如果你有使用 gson, 你也可以加上 gson 會用到的 annotations。

data class User(@SerializedName("userName") var name: String, 
                @SerializedName("userEmail") var email: String, 
                var age: Int)

Anko

Kotlin 除了本身新的語言特性之外,還提供了其他好用的函式庫像是 stdlibankoAnko 是用 Kotlin 寫的一組函式庫,目的是讓 Android 的開發可以更簡單快速,我們在 LINE Creators Studio 裡面主要就是用這兩個 Kotlin 函式庫。在 LINE Creators Studio 裡面我們用最多的是 Anko Layout。裡面提供了新的 DSL(Domain Specific Language) 來開發 Android UI,所以可以不需要使用傳統的 xml 方式。我們就可以用以下的範例來產生 UI:

//anko DSL
class MainActivityUi : AnkoComponent<MainActivity> {
    lateinit var aButton: Button
    override fun createView(ui: AnkoContext<MainActivity>): View = with(ui) {
        relativeLayout {
            aButton = button {
                textResource = R.string.ok
                onClick {
                    toast(“click button”)
                }
            }
        }.lparams(width = matchParent, height = wrapContent) {
            alignParentBottom()
        }
    }
}


在用了 Anko 之後,我們發現兩個優點:

  • 比 xml 更彈性的使用方式
  • 效能更好
  • 這是因為 Anko 是用 DSL (程式碼) 直接建立 UI layout,所以可以加入條件判斷 (其實就是 lambda expression)來產生不同的 UI。 如以下範例。

    如果使用 xml 的話,我們就沒有辦法做到一樣的事情。我們必須使用 ViewStub 或是動態地把 toolTipLayout 加到 RelativeLayout 裡面。在 LINE Creators Studio 裡面,我們完全沒有使用任何的 xml layout,唯一的例外是用 xml 來定義一些常數,例如 color、dimension 等等。另外在我們的內部測試裡面,用 anko 所產生出來的 UI,跑在舊款的 Android 裝置也有不錯的效能。

    relativeLayout {
        ...
        if (shouldShowToolTip) {
            toolTipLayout()
        }
    }
    

    Anko 的另外一個優點是可以讓開發者很容易做出 UI 而且不太需要依賴預覽工具,雖然 anko 本身也有提供了預覽工具,但是最新的版本只支援 Android Studio 2.4 以上,我們在開發的時候是使用 Android Studio 2.3。 雖然不能使用 anko 自帶的預覽工具但是這並沒有困擾我們,因為透過強大的 anko DSL,我們可以很輕鬆地使用結構化的方式來做 UI, 就跟使用 xml 的方式一樣簡單。

    除了 DSL 之外,anko 在處理非同步任務上也提供了一些實用的函式。像我們就用非同步的方式讀取 Bitmap 然後更新到 ImageView。

    doAsync {
        val bitmap = loadImage()
        uiThread {
            imageView.imageBitmap = bitmap
        }
    }
    

    Unit test與 Mockito


    Kotlin 跟Java不太一樣的地方是, 所有 class 預設都是 final。 但是使用 mockito 的話,只有 public class 可以被 mock。 在跑 unit tests 的時候這限制不是很方便。舉例來說,如果要寫 client-server 之間溝通的 protocol 的測試,因為會需要定義一些 data model,這時候就沒辦法直接用 mockito 來產生一些 mock data。幸運的是還是有一些辦法可以解決這問題:

  • 使用 Mockito 2,但還是需要建立一個 org.mockito.plugins.MockMaker 檔案
  • 使用 Kotlin 1.0.6 之後提供的 all-open plugin
  • 使用以 interface 為主的設計
  • 最終我們決定更改程式架構,以使用 interface 為主,所以我們就可以在 unit test 的時候 mock 這些 interface。 而這樣的改變也可以讓我們的程式碼比較沒有「程式碼壞味道」的問題。

    結論

    在我們開始開發 LINE Creators Studio之前,我們其實對使用 Kotlin 還是有一些疑問。但是實際用了 Kotlin 一陣子之後,我們深深覺得當初選擇使用 Kotlin 開發的決定是正確的。現在我們全部的 team member 都很享受使用 Kotlin。使用 Kotlin 提升了我們的開發效率,若你們還沒有使用 Kotlin 來開發 Android 應用程式,建議你們也考慮使用 Kotlin,絕對不會後悔的!