1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

View.onSaveInstanceState()を簡単に実装する方法

Last updated at Posted at 2021-07-30

概要

Android の onSaveInstanceState() 及び onRestoreInstanceState(Parcelable) を簡単に実装する方法のメモです。

例えば、foo, bar, hoge, fuga という4つのプロパティを保存したいなら以下のような感じになります。

override fun onSaveInstanceState(): Parcelable = onSaveInstanceState(target)
override fun onRestoreInstanceState(state: Parcelable) = onRestoreInstanceState(state, target)

private val target = listOf(::foo, ::bar, ::hoge, ::fuga)

方式

onSaveInstanceState() 及び onRestoreInstanceState(Parcelable) の処理を行う汎用的なコードに処理を移譲する方式になります。

実装方法概要

◆ super を移譲する版(API26以上のみ可)

// valueOrValueRetrievers は、保存したい情報の vararg もしくは List
@SuppressLint("MissingSuperCall")
override fun onSaveInstanceState(): Parcelable = onSaveInstanceState(valueOrValueRetrievers)

// restoreValuesは、保存したい情報の vararg もしくは List
@SuppressLint("MissingSuperCall")
override fun onRestoreInstanceState(state: Parcelable) = onRestoreInstanceState(state, restoreValues)

◆ super を移譲しない版

override fun onSaveInstanceState(): Parcelable = onSaveInstanceState(super.onSaveInstanceState(), valueOrValueRetrievers)
override fun onRestoreInstanceState(state: Parcelable) = super.onRestoreInstanceState(restoreThisStateAndReturnSuperState(state, restoreValues))

保存・復元対象の設定方法

◆ 順序

保存・復元対象の設定は、valueOrValueRetrieversrestoreValues の部分に、保存と復元の情報を同じ順番で記述します。

◆ 保存・復元対象のスコープ

super を移譲する版の場合は public にする必要があります。
※ inline 関数から呼ばれるため

◆ 保存・復元される情報の型

保存・復元対象の情報は、Parcelable に詰められるものに限定されます。

◆ 保存・復元方法の指定

☆ 保存方法の指定

保存情報の指定は、以下のいづれかで行います。

  • Function 型の値
  • KCallable 型の値
  • KProperty 型の値

☆ 復元情報の指定

  • Function 型の値
  • KCallable 型の値
  • KMutableProperty 型の値

◆ 3つのプロパティを扱う場合

例えば、以下の3つの情報の保存と復元を行う場合は、

var myString: String? = null
var myInt: Int? = null
var myChar: Char? = null

シンプルなのは以下のような形です。

@SuppressLint("MissingSuperCall")
override fun onSaveInstanceState(): Parcelable = onSaveInstanceState(target)

@SuppressLint("MissingSuperCall")
override fun onRestoreInstanceState(state: Parcelable) = onRestoreInstanceState(state, target)
    
private val target get() = listOf(::myString, ::myInt, ::myChar)

以下のようにすることもできます。(する意味が分かりませんがw)

@SuppressLint("MissingSuperCall")
override fun onSaveInstanceState(): Parcelable =
    onSaveInstanceState(myString, ::myInt, { myChar })

@SuppressLint("MissingSuperCall")
override fun onRestoreInstanceState(state: Parcelable) =
    onRestoreInstanceState(state, { it: String? -> myString = it }, ::myInt.setter, ::myChar)

◆ オーバーロード対応

TextView の text を指定するような場合、名前だけでは型が確定できないので、型を明確に指定する必要があります。

クリックすると時刻が表示されるTextViewの抜粋無しのソース
package com.objectfanatics.chrono0022

import android.annotation.SuppressLint
import android.content.Context
import android.os.Parcelable
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import com.objectfanatics.commons.android.view.instancestate.onRestoreInstanceState
import com.objectfanatics.commons.android.view.instancestate.onSaveInstanceState
import java.text.SimpleDateFormat
import java.util.*

class ClickAndShowCurrentTimeView : AppCompatTextView {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    init {
        setOnClickListener { text = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.JAPAN).format(Date()) }
    }

    @SuppressLint("MissingSuperCall")
    override fun onSaveInstanceState(): Parcelable = onSaveInstanceState(::getText as () -> CharSequence?)

    @SuppressLint("MissingSuperCall")
    override fun onRestoreInstanceState(state: Parcelable) = onRestoreInstanceState(state, { it: CharSequence? -> text = it })
}

考察

結構限界までラクチンさを追求したつくりになっていると思います。
基本的には property を作ると思うので、::prop という形式だけで事足りると思います。

◆ ちゃぶ台返し

しかしながら、メッチャちゃぶ台返しなのですが、View 単位で状態を保存・復元する方式は基本的にバッドプラクティスだと思っています。理由は以下:

  • id の付け忘れとかでバグが起きやすい。(idをつけないと保存・復元対象にならない)
  • id をつけても、id が重複すると後勝ちになってしまう。また、そうなることを事前に防ぐ方法が無い。1
  • ViewModel を用いた保存・復元コードと、各 View 単位での保存・復元コードをすべて把握しながら保守するのは現実的ではない。
  • ViewModel での復元忘れがあっても View 単位で復元してしまうケースがかなりヤバい。ViewModel 側で復元を忘れているので ViewModel 内部で情報が失われているのだが View には一見正しい情報が表示されているという発見しにくいバグが発生する。当然ながら ViewModel 側の情報を取得すると null が返るのだが、『ViewModel の内容が View に bind されてるのになんだこの現象は!ViewModel のバグだ!!』という謎理論が展開され、ワークアラウンドが行われ、ViewModel のバグ、View 側の復元に気づかないという見落とし、さらにワークアラウンドでごまかすという負債の三重奏が鳴り響き、そして往々にしてコードを書いた人が転職した後に問題が発生し、そして保守者が死ぬ(合掌)。
  • kill からの復元は ViewModel を復元するだけにしたほうがシンプルで、実質これ以外の方法はまともに保守できる気がしない。2

◆ ありがとう、そしてさようなら

ということで、今後おいらはこの仕組みを使うことはおそらくないであろう。
ありがとう onSaveInstanceState() ... そしてさようなら (´・ω・`)

注意

今回の方法に関するコードについては、コンセプトの検証目的であり、あまり細かく検証していません。

必要が生じたらまともな品質に持っていこうかなと思います。3

付録1:ライブラリ的なコード

@file:Suppress("unused")

package com.objectfanatics.commons.android.view.instancestate

import android.annotation.TargetApi
import android.os.Parcelable
import android.view.View
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType
import kotlin.reflect.KCallable
import kotlin.reflect.KMutableProperty
import kotlin.reflect.KProperty

// ---------------------------------------------------------
// common for onSaveInstanceState and onRestoreInstanceState
// ---------------------------------------------------------

@Parcelize
data class FrozenState(
    val superState: Parcelable?,
    val thisState: List<@RawValue Any?>
) : Parcelable

// ---------------------------------------------------------
// for onSaveInstanceState
// ---------------------------------------------------------

// using 'inline' since this function is caller sensitive.
@TargetApi(26)
inline fun <reified T> T.onSaveInstanceState(vararg valueOrValueRetrievers: Any?): Parcelable =
    onSaveInstanceState(valueOrValueRetrievers.toList())

// using 'inline' since this function is caller sensitive.
@TargetApi(26)
inline fun <reified T> T.onSaveInstanceState(valueOrValueRetrievers: List<Any?>): Parcelable =
    onSaveInstanceState(superOnSaveInstanceState(), valueOrValueRetrievers = valueOrValueRetrievers)

fun onSaveInstanceState(superState: Parcelable?, vararg valueOrValueRetrievers: Any?): FrozenState =
    onSaveInstanceState(superState, valueOrValueRetrievers.toList())

fun onSaveInstanceState(superState: Parcelable?, valueOrValueRetrievers: List<Any?>): FrozenState =
    FrozenState(superState, valueOrValueRetrievers.map { getValue(it) })

// ---------------------------------------------------------
// for onRestoreInstanceState
// ---------------------------------------------------------

// using 'inline' since this function is caller sensitive.
@TargetApi(26)
inline fun <reified T> T.onRestoreInstanceState(frozenState: Parcelable, vararg restoreValues: Any?) {
    onRestoreInstanceState(frozenState, restoreValues.toList())
}

// using 'inline' since this function is caller sensitive.
@TargetApi(26)
inline fun <reified T> T.onRestoreInstanceState(frozenState: Parcelable, restoreValues: List<Any?>) =
    superOnRestoreInstanceState(restoreThisStateAndReturnSuperState(frozenState, restoreValues))

fun restoreThisStateAndReturnSuperState(frozenState: Parcelable, vararg restoreValues: Any?): Parcelable? {
    return restoreThisStateAndReturnSuperState(frozenState, restoreValues.toList())
}

fun restoreThisStateAndReturnSuperState(frozenState: Parcelable, restoreValues: List<Any?>): Parcelable? {
    restoreValues.forEachIndexed { index, restoreValue -> setValue(restoreValue, (frozenState as FrozenState).thisState[index]) }
    return (frozenState as FrozenState).superState
}

// ---------------------------------------------------------
// for calling super
// ---------------------------------------------------------

// Using 'inline' since 'MethodHandles.lookup()' is caller sensitive.
@TargetApi(26)
inline fun <reified T> T.superOnSaveInstanceState(): Parcelable {
    val methodName = "onSaveInstanceState"
    val methodType = MethodType.methodType(Parcelable::class.java)
    return superMethod<T>(methodName, methodType).invokeWithArguments(this) as Parcelable
}

// Using 'inline' since 'MethodHandles.lookup()' is caller sensitive.
@TargetApi(26)
inline fun <reified T> T.superOnRestoreInstanceState(state: Parcelable?) {
    val methodName = "onRestoreInstanceState"
    val methodType = MethodType.methodType(Void::class.javaPrimitiveType, Parcelable::class.java)
    superMethod<T>(methodName, methodType).invokeWithArguments(this, state)
}

@TargetApi(26)
inline fun <reified T> superMethod(methodName: String, methodType: MethodType?): MethodHandle =
    MethodHandles.lookup().findSpecial(
        View::class.java,
        methodName,
        methodType,
        T::class.java
    )

// ---------------------------------------------------------
// misc
// ---------------------------------------------------------

private fun getValue(valueOrValueRetriever: Any?): Any? =
    when (valueOrValueRetriever) {
        is KProperty<*> -> valueOrValueRetriever.getter.call()
        is KCallable<*> -> valueOrValueRetriever.call()
        is Function<*>  -> valueOrValueRetriever::class.java.getMethod("invoke").invoke(valueOrValueRetriever)
        else            -> valueOrValueRetriever
    }

private fun setValue(restoreValue: Any?, value: Any?) =
    when (restoreValue) {
        is KMutableProperty<*> -> restoreValue.setter.call(value)
        is KCallable<*>        -> restoreValue.call(value)
        is Function1<*, *>     -> restoreValue::class.java.getMethod("invoke", Any::class.java).invoke(restoreValue, value)
        else                   -> throw IllegalArgumentException("restoreValue.javaClass = ${restoreValue?.javaClass?.name}")
    }

付録2:github

git clone git@github.com:beyondseeker/chrono0022.git -b qiita_ver_0001; cd chrono0022
  1. 将来的にどのように使われるかなんてわからない。

  2. ViewModel と SavedStateHandle を組み合わせたうえで、content view の isSaveFromParentEnabled を false にしちゃうのが最強な気がする。

  3. しかしながら、ちゃぶ台返しで語ったように、今後 View 単位でのデータ復元をプロダクションに入れることはなさそうな気がする (´・ω・`)

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?