在Kotlin中,是否有一种优雅的方法来保存和恢复视图状态?

17

在Android中编写自定义视图以在配置更改时保持其状态是繁琐的,仅为保存一个字段的状态而编写大量样板代码:

private class SavedState : BaseSavedState {
        var amount: Int = 0

        constructor(parcel: Parcel) : super(parcel) {
            amount = parcel.readInt()
        }

        constructor (parcelable: Parcelable?) : super(parcelable)

        override fun writeToParcel(parcel: Parcel, flags: Int) {
            super.writeToParcel(parcel, flags)
            parcel.writeInt(amount)
        }

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

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

Android Extensions插件提供了@Parcelize注释,可以用于自动生成Parcelable实现,但对于自定义视图,我们必须从BaseSavedState扩展,而不是直接从Parcelable扩展。

因此,像这样的代码无法编译:

@Parcelize
data class SavedState(val isLoading: Boolean = false): BaseSavedState()

我想知道是否有一种更简洁的方式来处理自定义视图中的状态恢复。 欢迎任何想法或建议。


但是在自定义视图的情况下,我们必须从BaseSavedState扩展而不是直接从Parcelable扩展。Parcelable是一个接口,你不能从它继承,而是要实现它。"像这样拥有东西是无法编译的" -- 尝试用BaseSavedState(), Parcelable替换BaseSavedState()@Parcelize可能仍然存在问题,但原则上应该没问题。 - CommonsWare
那也行不通,因为 BaseSavedState(parcel) 实际上需要一个类型为 Parcel 的参数。 - Andy Res
哦,没错。如果你有很多状态,你仍然可以使用@Parcelize将这些状态放入一个data类中。然后,你的自定义SavedState基本上就是你在第一个片段中所拥有的,只需用writeParcelable()readParcelable()替换writeInt()readInt()来写入/读取你的data类实例即可。 - CommonsWare
不完全是我寻找的解决方案,但在状态中有多个字段的情况下,您的想法仍然是更好的方法。谢谢。 - Andy Res
1
原则上,有人可以为视图状态创建一个类似于@Parcelize的东西 - 我没有查看是否有人已经这样做了。 - CommonsWare
3个回答

14

编写一个SaveState类是一种将需要保存的所有数据打包到一个类中的方法。另一种方法是重写onSaveInstanceStateonRestoreInstanceState并将参数放入bundle中。这样可以避免您提到的样板代码。如果有多个参数,只需使用带有@Parcelize注解的数据类,并使用bundle.putParcelable保存该类(而不是本示例中的数量)。还要记得将isSaveEnabled设置为true。

init { isSaveEnabled = true }

...

override fun onSaveInstanceState(): Parcelable {
   val bundle = Bundle()
   bundle.putInt("amount", amount)
   bundle.putParcelable("superState", super.onSaveInstanceState())
   return bundle
}
override fun onRestoreInstanceState(state: Parcelable) {
 var viewState = state
 if (viewState is Bundle) {
   amount = viewState.getInt("amount", 0)
   viewState = viewState.getParcelable("superState")
 }
 super.onRestoreInstanceState(viewState)
}

7

@Parcelize注释仍然可以使用,因为我们必须扩展BaseSavedState实现了Parcelable。这是一个示例,在此示例中,我需要在自定义视图中保存一些持续时间。

@Parcelize
internal class SavedState(state: Parcelable?, val duration: Long) : BaseSavedState(state)

如您所见,我们可以使用注释来仅定义要保存的状态和字段。一旦状态准备就绪,我们可以像这样保存/恢复它:

override fun onSaveInstanceState(): Parcelable {
    val parcel = super.onSaveInstanceState()
    return SavedState(parcel, currentDuration.timeInMillis)
}

override fun onRestoreInstanceState(state: Parcelable?) {
    if (state !is SavedState) {
        super.onRestoreInstanceState(state)
        return
    }

    super.onRestoreInstanceState(state.superState)
    setDurationInMillis(state.duration)
}

从这个实现开始,您可以为状态类添加Parcelable参数,并将其传递给基类。有了这个,一切都应该正常工作。


自定义的 SavedState 类中的非类字段 state 现在会抛出一个编译时异常,指出它必须声明为类字段。然而,试图将其声明为变量会提示不需要这样做。对此应该怎么办? - Edric
我不确定我理解你的意思。你能否更详细地解释一下(包括一些代码)? - Iulian Popescu
您当前的代码会导致 Kotlin 抛出错误,提示必须使用 val/var 关键字进行声明。但是添加该关键字后,会发现它并不是必需的(即 val/var 关键字被标记为灰色),因为它仅用作 BaseSavedState 的构造函数参数。 - Edric
代码运行正常,我不知道现在为什么会出问题。我猜测可能是其他的东西导致了问题,特别是我不记得曾经看到过你描述的这种编译错误。你能否添加完整的错误信息以及你的视图实现呢? - Iulian Popescu

0

在 @Mohsen 的回答基础上,这是我能够立即想到的最优雅的 Kotlin 解决方案:

override fun onSaveInstanceState(): Parcelable = bundleOf(
    "amount" to amount,
    "superState" to super.onSaveInstanceState()
)

override fun onRestoreInstanceState(state: Parcelable) = super.onRestoreInstanceState(
    if (state is Bundle) {
        amount = state.getInt("amount", 0)
        state.getParcelable("superState")
    } else {
        amount = 0
        state
    }
)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接