A library that provides type-safe access to saved instance state.
This library provides the following features.
- Provides delegated properties for type-safe access to ViewModel's SavedStateHandle.
- Generates ViewModelArgs classes to initialize the
SavedStateHandle
in a type-safe manner.
This library officially supports KSP.
Please add the following entries to your build.gradle
file.
plugins {
id 'com.google.devtools.ksp' version 'x.x.x-x.x.x'
}
dependencies {
implementation 'com.linecorp.lich:savedstate:x.x.x'
ksp 'com.linecorp.lich:savedstate-compiler:x.x.x'
}
Alternatively, you can use kapt instead of KSP.
In that case, change build.gradle
as follows.
plugins {
id 'org.jetbrains.kotlin.kapt'
}
dependencies {
implementation 'com.linecorp.lich:savedstate:x.x.x'
kapt 'com.linecorp.lich:savedstate-compiler:x.x.x'
}
This library provides delegated properties for SavedStateHandle.
You can access the values of SavedStateHandle
via delegated properties as follows.
class SampleViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
// A delegated property that accesses the value associated with "param1".
// This is equivalent to the code below.
// ```
// private var param1: String?
// get() = savedStateHandle["param1"]
// set(value) { savedStateHandle["param1"] = value }
// ```
private var param1: String? by savedStateHandle
// A delegated property that accesses the value associated with "param2".
// The value is initialized with "abc" unless specified by a `ViewModelArgs`.
// This is equivalent to the code below.
// ```
// init {
// if ("param2" !in savedStateHandle) {
// savedStateHandle["param2"] = "abc"
// }
// }
//
// private var param2: String
// get() = savedStateHandle["param2"]!!
// set(value) { savedStateHandle["param2"] = value }
// ```
private var param2: String by savedStateHandle.initial("abc")
// A delegated property that accesses the value associated with "param3".
// The value must be initialized by a `ViewModelArgs`, otherwise IllegalStateException is thrown.
// This is equivalent to the code below.
// ```
// init {
// check("param3" in savedStateHandle) { "param3 is not specified in the arguments." }
// }
//
// private var param3: String
// get() = savedStateHandle["param3"]!!
// set(value) { savedStateHandle["param3"] = value }
// ```
private var param3: String by savedStateHandle.required()
// A delegated property of a `MutableLiveData` that accesses the value associated with "param4".
// This is equivalent to the code below.
// ```
// private val param4: MutableLiveData<String> = savedStateHandle.getLiveData("param4")
// ```
private val param4: MutableLiveData<String> by savedStateHandle.liveData()
// A delegated property of a `MutableLiveData` that accesses the value associated with "param5".
// The value is initialized with "abc" unless specified by a `ViewModelArgs`.
// This is equivalent to the code below.
// ```
// private val param5: MutableLiveData<String> = savedStateHandle.getLiveData("param5", "abc")
// ```
private val param5: MutableLiveData<String> by savedStateHandle.liveDataWithInitial("abc")
// snip...
}
This library can generate ViewModelArgs
classes to initialize SavedStateHandle
.
To generate a ViewModelArgs
class, specify
@GenerateArgs and
@Argument annotations to a ViewModel class as follows.
@GenerateArgs
class FooViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
@Argument
private val userName: String by savedStateHandle.required()
@Argument(isOptional = true)
private val tags: Array<String> by savedStateHandle.initial(arrayOf("normal"))
@Argument
private var attachment: Parcelable? by savedStateHandle
@Argument
private val message: MutableLiveData<CharSequence> by savedStateHandle.liveData()
// snip...
}
Then, the following class will be generated.
public class FooViewModelArgs(
public val userName: String,
public val tags: Array<String>? = null,
public val attachment: Parcelable?,
public val message: CharSequence
) : ViewModelArgs {
public override fun toBundle(): Bundle = Bundle().also {
it.putString("userName", this.userName)
if (this.tags != null) it.putSerializable("tags", this.tags)
it.putParcelable("attachment", this.attachment)
it.putCharSequence("message", this.message)
}
}
You can use Intent.putViewModelArgs
/ Fragment.setViewModelArgs to set
a ViewModelArgs
object to the Activity / Fragment that hosts the ViewModel.
class FooFragment : Fragment() {
private val fooViewModel: FooViewModel by viewModels()
// snip...
}
val fooFragment = FooFragment().also {
// This `FooViewModelArgs` is used to initialize the `savedStateHandle` of `FooFragment.fooViewModel`.
it.setViewModelArgs(FooViewModelArgs(userName = "John", attachment = null, message = "Hello."))
}
Alternatively, you can use the generated ViewModelArgs
class in the argument of the viewModels
extension function, as shown below.
class FooFragment : Fragment() {
private val fooViewModel: FooViewModel by viewModels {
SavedStateViewModelFactory(
requireActivity().application,
this,
FooViewModelArgs(userName = "John", attachment = null, message = "Hello.").toBundle()
)
}
// snip...
}
You can use createSavedStateHandleForTesting
to initialize SavedStateHandle
for unit tests of ViewModels.
@RunWith(AndroidJUnit4::class)
class FooViewModelTest {
@Test
fun testFooViewModel() {
val savedStateHandle = createSavedStateHandleForTesting(
FooViewModelArgs(userName = "John", attachment = null, message = "Hello.")
)
val fooViewModel = FooViewModel(savedStateHandle)
// snip...
}
}