Android
Kotlin
Parcelable

Kotlin, Parcelable, Custom Class 和 List

More than 1 year has passed since last update.

在介接 API 的時候碰到一個東西,要把自定義的 data class 實作 Parcelable ,而這個 data class 裡面又有包含另外一個 Parcelable 的 List。

而我的目標是

在不需要改寫原本的 primary constructor 以及把變數從 val 換成 var ,以及不需要多寫其他 class method 就可以達成

中間試了很多方法,看了很多文章有用了 library 的,和一些 Java 的實作方式。
但是看到 Java 的實作方式裡面的屬性都沒有被加上 final ,也就是可變化(mutable)、在 Kotlin 的話是 var ,因此都不是我想要的做法。

基本的東西

還沒加上 Parcelable 各自像是這樣:

Product.kt
data class Product(val id: String?, 
                   val name: String?,
                   val images: List<Media>?)
Media.kt
data class Media(val small: String?,
                 val large: String?)

讓 Media 實作 Parcelable

處理這部分的實作的時候。我的順序是從內實作到外,也就是先從 Media 開始,因為沒有包含其他自定義型別,所以處理上比較簡單。

在 class name 定義後面加上 Parcelable 之後, Android Studio 會有紅色燈泡,點下去就可以自動生成所有所需要的 methods ,完成之後像是這樣:

Media.kt
// package 略
// import 略

data class Media(val small: String?,
                 val large: String?): Parcelable {

    constructor(parcel: Parcel) : this(
            parcel.readString(), // Parcelable 有辦法處理 nullable types, 因此這樣寫沒有問題
            parcel.readString())

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(small)
        parcel.writeString(large)
    }

    override fun describeContents(): Int = 0

    companion object CREATOR : Parcelable.Creator<Media> {
        override fun createFromParcel(parcel: Parcel): Media = Media(parcel)
        override fun newArray(size: Int): Array<Media?> = arrayOfNulls(size)
    }
}

*在這裡部分覆寫的函式只有一行,我就拿掉 Android Studio 產生給我的 { } ,直接用 = 的方式寫

讓 Product 實作 Parcelable

原本長這個樣子

Product.kt
data class Product(val id: String?, 
                   val name: String?,
                   val images: List<Media>?)

寫一寫就卡在這邊:

Product.kt
// package 略
// import 略

data class Product(val id: String?, 
                   val name: String?,
                   val images: List<Media>?): Parcelable {

    constructor(parcel: Parcel) : this(
            parcel.readString(),
            parcel.readString(),
            /** 這邊要怎麼把它讀出來?! **/) // <<<< ---- 就是這裡

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(id)
        parcel.writeString(name)
        parcel.writeTypedList(images) // 怎麼塞進去是沒問題
    }

    override fun describeContents(): Int = 0

    companion object CREATOR : Parcelable.Creator<Product> {
        override fun createFromParcel(parcel: Parcel): Product = Product(parcel)
        override fun newArray(size: Int): Array<Product?> = arrayOfNulls(size)
    }

}

一開始查到怎麼處理這個 List 的時候,看到這樣的東西:

val medium = mutableListOf<Media>() 
parcel.readTypedList(medium, Media.CREATOR)

因為 readTypedList 這個方法是沒有回傳值的,所以如果這樣寫的話,就一定要找個辦法把生成完的 medium 塞回原本的 images 屬性裡面

但是因為 images 他是一個 immutable 變數(val),所以一者不是要讓他變成 var ,在 Parcel 用的 constructor 才有辦法修改這個值;不然就是要另外寫一個 class method (也就是 companion object),先處理完這個 List 再去執行初始化。

但是這樣子就只是因為實作 Parcel ,原本簡潔漂亮的 Kotlin data class ,就會變得醜醜的,其實不是那麼地願意。

apply

當絕望之間,無意間就發現 apply 的這個東西
他是屬於 Kotlin standard library 的高階語法

宣告是這樣:

public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

在呼叫這個方法之後,會有個回傳值,並且是回傳自己。

他有這樣的特性

  • 在 apply 的 block 中,可以用 this 代表呼叫 apply 的物件
  • 當要使用該物件的方法時,可以省略 this
  • 一定會有返回值,返回值則是是呼叫 apply 的物件。這個回傳則不用自己寫 return this

於是就開始用用看,
單獨的寫的話是這樣:

val medium = mutableListOf<Media>().apply {
    parcel.readTypedList(this, Media.CREATOR)
}

喔喔!
看起來好像可行!

把它加回去 Product 的 Parcel 用 constructor 看看:

Product.kt
// package 略
// import 略

data class Product(val id: String?, 
                   val name: String?,
                   val images: List<Media>?): Parcelable {

    constructor(parcel: Parcel) : this(
            parcel.readString(),
            parcel.readString(),
            mutableListOf<Media>().apply { parcel.readTypedList(this, Media.CREATOR) }) 
            // ↑↑↑ 這一段

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(id)
        parcel.writeString(name)
        parcel.writeTypedList(images)
    }

    override fun describeContents(): Int = 0

    companion object CREATOR : Parcelable.Creator<Product> {
        override fun createFromParcel(parcel: Parcel): Product = Product(parcel)
        override fun newArray(size: Int): Array<Product?> = arrayOfNulls(size)
    }

}

建置之後,該處理的資料也有如預期順利運作
終於順利在沒有修改原有設計的方式,達成漂亮的掛上 Parcelable 的目的了!

參考