Kotlin, Parcelable, Custom Class 和 List

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

而我的目標是
> 在不需要改寫原本的 primary constructor 以及把變數從 val 換成 var ，以及不需要多寫其他 class method 就可以達成

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

## 基本的東西

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

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

```kotlin:Media.kt
data class Media(val small: String?,
                 val large: String?)
```

## 讓 Media 實作 Parcelable 

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

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

```kotlin: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 

原本長這個樣子

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

寫一寫就卡在這邊：

```kotlin: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 的時候，看到這樣的東西：

```kotlin
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 的高階語法

宣告是這樣：

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

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

他有這樣的特性

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

於是就開始用用看，
單獨的寫的話是這樣：

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

喔喔！
看起來好像可行！

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


```kotlin: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 的目的了！

## 參考

- [Kotlin - apply](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/apply.html)
- [Keddit — Part 8: Orientation Change with Kotlin (Parcelable & Data Classes)](https://android.jlelse.eu/keddit-part-8-orientation-change-with-kotlin-parcelable-data-classes-f28136e8a6a8)
- [简述Kotlin中let, apply, run, with的区别](https://kotlintc.com/articles/1102)