Android Parcelize

Parcel是針對Android的優化序列化格式,旨在使咱們可以在進程之間傳輸數據。 這是大多數Android開發人員偶爾須要作的事情,但不是常常這樣作。 製做課程Pareclizable實際上須要一點點努力,可是有一個Kotlin擴展能夠極大地簡化事情。 在這篇文章中,咱們將看看@Parcelize以及它如何讓咱們的生活更輕鬆。原文java

我確信每次我須要實現Parcelable時我並不孤單我有點絕望,由於我知道我須要寫一些樣板代碼,我須要查閱文檔以提醒我如何去作 由於這是我不常常作的事情。 Kotlin來救援!android

一般,咱們須要在進程之間傳遞的數據類型能夠封裝在數據類中。 若是數據類實現了Parcelable,咱們一般須要包含一些方法:git

SimpleDataClassgithub

data class SimpleDataClass(
    val name: String,
    val age: Int
) : Parcelable {
    
    constructor(parcel: Parcel) : this(
        parcel.readString()!!,
        parcel.readInt()
    )

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(name)
        parcel.writeInt(age)
    }

    override fun describeContents(): Int {
        return 0
    }

    companion object CREATOR : Parcelable.Creator<SimpleDataClass> {
        override fun createFromParcel(parcel: Parcel): SimpleDataClass {
            return SimpleDataClass(parcel)
        }

        override fun newArray(size: Int): Array<SimpleDataClass?> {
            return arrayOfNulls(size)
        }
    }
}

值得慶幸的是,IntelliJ IDEA和Android Studio包含幫助程序以自動生成此代碼,但它仍然增長了咱們的數據類。 @Parcelize註釋是咱們的朋友! 它仍然處於實驗狀態,但已經存在了一段時間,因此咱們能夠但願它很快就能穩定下來。 很是感謝Olivier Genez指出@Parcelize是在Kotlin 1.3.40中進行實驗的(本文最初是在發佈以前編寫的)。 要在1.3.40以前的Kotlin版本中使用@Parcelize,咱們須要啓用實驗性Android擴展:swift

應用程序/的build.gradleapp

.
.
.
androidExtensions {
    experimental = true
}
.
.
.

有了@Parcelize,如今能夠很是簡化咱們的數據類:框架

SimpleDataClass.ktide

@Parcelize
data class SimpleDataClass(
    val name: String,
    val age: Int
) : Parcelable

雖然這彷佛是Styling Android歷史中最短的帖子,但它並不那麼簡單。 讓咱們看一個稍微複雜的例子:函數

CompoundDataClass.ktgradle

@Parcelize
data class CompoundDataClass @JvmOverloads constructor(
    val name: String,
    val simpleDataClass: SimpleDataClass,
    @Transient val transientString: String = ""
) : Parcelable

若是你喜歡一個謎題,那麼請研究那個片斷,看看你是否能夠找出不能按預期工做的東西。

乍一看,人們可能會認爲包括SimpleDataClass類型的字段可能會致使問題,但這實際上很好,由於它已是Parcelable,咱們可使用任何Parcelable類做爲字段。 問題其實是transientString字段,它看起來並不簡單。 讀取代碼,能夠理解的是,假設這個接收器在transientString字段中獲得一個空字符串,但這不會發生。 這個問題其實是雙重的:首先,Android框架只有一個Parcelable,當它有to;時,其次,@Parcelize不尊重@Transient註釋。

要演示第一個,請查看如下Fragment

MainFragment.kt

class MainFragment : Fragment() {

    private var simple: SimpleDataClass? = null
    private var compound: CompoundDataClass? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            simple = it.getParcelable(ARG_SIMPLE)
            compound = it.getParcelable(ARG_COMPOUND)
        }
        Timber.d("Simple: \"%s\"; Compound: \"%s\"", simple, compound)
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? = inflater.inflate(R.layout.fragment_main, container, false)

    companion object {
        private const val ARG_SIMPLE = "simple"
        private const val ARG_COMPOUND = "compound"

        @JvmStatic
        fun newInstance(simpleDataClass: SimpleDataClass, compound: CompoundDataClass) =
            MainFragment().apply {
                arguments = Bundle().apply {
                    putParcelable(ARG_SIMPLE, simpleDataClass)
                    putParcelable(ARG_COMPOUND, compound)
                }
            }
    }
}

若是咱們稱之爲以下,咱們可能指望不一樣的行爲而不是實際發生:

class MainActivity : AppCompatActivity() {

    private val simple = SimpleDataClass("Simple", 1)
    private val compound = CompoundDataClass("Compound", simple, "Transient")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        supportFragmentManager.beginTransaction().apply {
            replace(R.id.fragment_main, MainFragment.newInstance(simple, compound))
            commit()
        }
    }
}

儘管MainFragmentnewInstance()方法中突出顯示的行顯示咱們正在向Bundle添加Parcelable,但若是Android框架須要在整個流程邊界中展平它,它只會變得扁平化。 在這種狀況下,它在同一個Activity中傳遞,沒有理由將其展平,所以在onCreate()方法的突出顯示行中檢索CompoundDataClass的相同實例。

咱們能夠經過添加一些擴展函數來模擬Parcel的扁平化,這些擴展函數模仿Android框架將如何展平和擴展Parcels

inline fun <reified T : Parcelable> T.collapse(): ByteArray {
    val parcel = Parcel.obtain()
    parcel.writeParcelable(this, 0)
    val byteArray = parcel.marshall()
    parcel.recycle()
    return byteArray
}

inline fun <reified T : Parcelable> Class<T>.expand(byteArray: ByteArray): T {
    val parcel = Parcel.obtain()
    parcel.apply {
        unmarshall(byteArray, 0, byteArray.size)
        setDataPosition(0)
    }
    val parcelable =
        parcel.readParcelable<T>(this@expand.classLoader)
            ?: throw InstantiationException("Unable to expand $name")
    parcel.recycle()
    return parcelable
}

而後咱們能夠調用它們來強制扁平化數據類,而後再次展開它(這真的不是任何人在真實場景中須要作的事情,因此請注意代碼的註釋):

@JvmStatic
fun newInstance(simpleDataClass: SimpleDataClass, compound: CompoundDataClass) =
    MainFragment().apply {
        arguments = Bundle().apply {
            putParcelable(ARG_SIMPLE, simpleDataClass)
            putParcelable(
                ARG_COMPOUND,
                /*
                 * Don't do this.
                 *
                 * I've done this here in some sample code to demonstrate
                 * that things don't always get serialised. There is no
                 * reason than you'd actually want to do this in this context.
                 *
                 * So...really...don't do this.
                 *
                 * Look, I'm not joking, you really shouldn't do this.
                 *
                 * Even if you're being attacked by a pack of wild dogs and
                 * think that collapsing then immediately re-expanding a
                 * Parcelable will save your life, then I'm sorry, but
                 * it won't. Rest In Peace.
                 *
                 * Perhaps I forgot to mention: you really shouldn't do this.
                 */
                CompoundDataClass::class.java.expand(compound.collapse())
            )
        }
    }

即便咱們這樣作,咱們仍然獲得ARG_COMPOUND值中的「瞬態"值,咱們從MainFragmentonCreate()方法中的Fragment參數獲得。 正如我以前提到的,這是由於@Parcelize生成的字節碼將保留全部字段,包括那些標記爲@Transient的字段,這與在持久性期間一般處理瞬態字段的方式略有不一致,但咱們能夠經過查看Kotlin字節碼的反編譯來證實這一點:

CompoundDataClass.decompiled.java

@Parcelize
public final class CompoundDataClass implements Parcelable {
   @NotNull
   private final String name;
   @NotNull
   private final SimpleDataClass simpleDataClass;
   @NotNull
   private final transient String transientString;
   public static final android.os.Parcelable.Creator CREATOR = new CompoundDataClass.Creator();
    .
    .
    .
   public void writeToParcel(@NotNull Parcel parcel, int flags) {
      Intrinsics.checkParameterIsNotNull(parcel, "parcel");
      parcel.writeString(this.name);
      this.simpleDataClass.writeToParcel(parcel, 0);
      parcel.writeString(this.transientString);
   }

   @Metadata(
      mv = {1, 1, 15},
      bv = {1, 0, 3},
      k = 3
   )
   public static class Creator implements android.os.Parcelable.Creator {
      @NotNull
      public final Object[] newArray(int size) {
         return new CompoundDataClass[size];
      }

      @NotNull
      public final Object createFromParcel(@NotNull Parcel in) {
         Intrinsics.checkParameterIsNotNull(in, "in");
         return new CompoundDataClass(in.readString(), (SimpleDataClass)SimpleDataClass.CREATOR.createFromParcel(in), in.readString());
      }
   }
}

名爲transientString的字段與Parcel進行序列化,儘管使用Java transient關鍵字進行聲明。 若是咱們想要爲這個領域提供短暫性,咱們必須手動完成。 實際上值得覆蓋這個由於咱們可能還須要這樣作,若是咱們的字段是third-party對象而不是Parcelable而且咱們須要實現一個自定義機制來存儲足夠的數據到Parcel以容許咱們re-實例化特定對象時 Parcel擴展回對象實例:

CompoundDataClass.kt

@Parcelize
data class CompoundDataClass @JvmOverloads constructor(
    val name: String,
    val simpleDataClass: SimpleDataClass,
    @Transient val transientString: String = ""
) : Parcelable {

    companion object : Parceler<CompoundDataClass> {

        override fun create(parcel: Parcel): CompoundDataClass {
            val name: String = parcel.readString()!!
            val simple: SimpleDataClass =
                parcel.readParcelable(SimpleDataClass::class.java.classLoader)!!
            return CompoundDataClass(name, simple)
        }

        override fun CompoundDataClass.write(parcel: Parcel, flags: Int) {
            parcel.writeString(name)
            parcel.writeParcelable(simpleDataClass, flags)
        }
    }
}

擁有一個實現create()CompoundDataClass.write()方法的Parceler伴侶對象使咱們可以自定義Parcel的持久性,在這種狀況下,容許咱們省略transientString字段。 雖然@Transient註釋在這裏沒有達到直接目的,但我認爲將它留在這裏是好的,由於它使得閱讀代碼的任何人都更清楚,該字段將不會被持久化。 所以,咱們的代碼更易於維護和理解。

雖然可能有人認爲必須實施這些方法來執行包裹化,但這又回到了咱們剛開始的地方(即必須手動完成)。 可是,若是將其與第一個代碼片斷進行比較,仍然會爲咱們生成不少樣板代碼,而且使用Parcelize的結果仍然少得多。

here提供了本文的源代碼。

相關文章
相關標籤/搜索