Android Parcelize
Parcel
是针对Android的优化序列化格式,旨在使我们能够在进程之间传输数据。 这是大多数Android开发人员偶尔需要做的事情,但不是经常这样做。 制作课程Pareclizable
实际上需要一点点努力,但是有一个Kotlin扩展可以极大地简化事情。 在这篇文章中,我们将看看@Parcelize
以及它如何让我们的生活更轻松。原文
我确信每次我需要实现Parcelable
时我并不孤单我有点绝望,因为我知道我需要写一些样板代码,我需要查阅文档以提醒我如何去做 因为这是我不经常做的事情。 Kotlin来救援!
通常,我们需要在进程之间传递的数据类型可以封装在数据类中。 如果数据类实现了Parcelable
,我们通常需要包含一些方法:
SimpleDataClass
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扩展:
应用程序/的build.gradle
. . . androidExtensions { experimental = true } . . .
有了@Parcelize
,现在可以非常简化我们的数据类:
SimpleDataClass.kt
@Parcelize data class SimpleDataClass( val name: String, val age: Int ) : Parcelable
虽然这似乎是Styling Android历史中最短的帖子,但它并不那么简单。 让我们看一个稍微复杂的例子:
CompoundDataClass.kt
@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() } } }
尽管MainFragment
的newInstance()
方法中突出显示的行显示我们正在向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>([email protected]) ?: 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
值中的“瞬态"值,我们从MainFragment
的onCreate()
方法中的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提供了本文的源代码。