1 /*
2 * Copyright 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.lifecycle.viewmodel.testing
18
19 import androidx.lifecycle.LifecycleOwner
20 import androidx.lifecycle.VIEW_MODEL_STORE_OWNER_KEY
21 import androidx.lifecycle.ViewModel
22 import androidx.lifecycle.ViewModelProvider
23 import androidx.lifecycle.ViewModelProvider.Factory
24 import androidx.lifecycle.ViewModelStore
25 import androidx.lifecycle.ViewModelStoreOwner
26 import androidx.lifecycle.viewmodel.CreationExtras
27 import androidx.lifecycle.viewmodel.CreationExtras.Empty
28 import androidx.lifecycle.viewmodel.testing.internal.createScenarioExtras
29 import androidx.lifecycle.viewmodel.testing.internal.saveScenarioExtras
30 import androidx.lifecycle.viewmodel.viewModelFactory
31 import androidx.savedstate.SavedStateRegistryController
32 import androidx.savedstate.SavedStateRegistryOwner
33 import kotlin.reflect.KClass
34
35 /**
36 * [ViewModelScenario] provides API to start and drive a [ViewModel]'s lifecycle state for testing.
37 *
38 * [ViewModelScenario.recreate] allows you to simulate a System Process Death and restoration.
39 *
40 * [ViewModelScenario] does not clean up the [ViewModel] automatically. Call [close] in your test to
41 * clean up the state or use [AutoCloseable.use] to ensure [ViewModelStore.clear] and
42 * [ViewModel.onCleared] is invoked.
43 */
44 @OptIn(ExperimentalStdlibApi::class)
45 public class ViewModelScenario<VM : ViewModel>
46 @PublishedApi
47 internal constructor(
48 private val modelClass: KClass<VM>,
49 private val modelFactory: Factory,
50 initialModelExtras: CreationExtras = Empty
51 ) : AutoCloseable {
52
53 /**
54 * The current [CreationExtras] associated with the [viewModel]. This instance is updated when
55 * [recreate] is executed.
56 */
57 private var modelExtras = createScenarioExtras(initialModelExtras)
58
59 /**
60 * The current [ViewModelProvider] responsible for retrieve the [ViewModel]. This instance is
61 * updated when [recreate] is executed.
62 */
63 private var modelProvider =
64 ViewModelProvider.create(
65 owner = modelExtras[VIEW_MODEL_STORE_OWNER_KEY]!!,
66 factory = modelFactory,
67 extras = modelExtras,
68 )
69
70 /**
71 * The current [ViewModel] being managed by this scenario. This instance is change if the
72 * [ViewModelStore] is cleared or if [recreate] is invoked.
73 */
74 public val viewModel: VM
75 get() = modelProvider[modelClass]
76
77 /** Finishes the managed [ViewModel] and clear the [ViewModelStore]. */
closenull78 override fun close() {
79 modelExtras[VIEW_MODEL_STORE_OWNER_KEY]!!.viewModelStore.clear()
80 }
81
82 /**
83 * Simulates a System Process Death recreating the [ViewModel] and all associated components.
84 *
85 * This method:
86 * - Saves the state of the [ViewModel] with [SavedStateRegistryController.performSave].
87 * - Recreates the [ViewModelStoreOwner], [LifecycleOwner] and [SavedStateRegistryOwner].
88 * - Recreates the [ViewModelProvider] instance.
89 *
90 * Call this method to verify that the [ViewModel] correctly preserves and restores its state.
91 */
recreatenull92 public fun recreate() {
93 val savedState = saveScenarioExtras(modelExtras)
94 modelExtras =
95 createScenarioExtras(
96 initialExtras = modelExtras,
97 restoredState = savedState,
98 )
99 modelProvider =
100 ViewModelProvider.create(
101 owner = modelExtras[VIEW_MODEL_STORE_OWNER_KEY]!!,
102 factory = modelFactory,
103 extras = modelExtras,
104 )
105 }
106 }
107
108 /**
109 * Creates a [ViewModelScenario] using a given [VM] class as key, an [initializer] as a
110 * [ViewModelProvider.Factory] and a [creationExtras] as default extras.
111 *
112 * You should access the [ViewModel] instance using [ViewModelScenario.viewModel], and clear the
113 * [ViewModelStore] using [ViewModelScenario.close].
114 *
115 * Example usage:
116 * ```
117 * viewModelScenario { MyViewModel(parameters) }
118 * .use { scenario ->
119 * val vm = scenario.viewModel
120 * // Use the ViewModel
121 * }
122 * ```
123 *
124 * @param VM The reified [ViewModel] class to be created.
125 * @param creationExtras Additional data passed to the [Factory] during a [ViewModel] creation.
126 * @param initializer A [Factory] function to create a [ViewModel].
127 */
viewModelScenarionull128 public inline fun <reified VM : ViewModel> viewModelScenario(
129 creationExtras: CreationExtras = DefaultCreationExtras(),
130 noinline initializer: CreationExtras.() -> VM,
131 ): ViewModelScenario<VM> {
132 return viewModelScenario(
133 creationExtras = creationExtras,
134 factory = viewModelFactory { addInitializer(VM::class, initializer) },
135 )
136 }
137
138 /**
139 * Creates a [ViewModelScenario] using a given [VM] class as key, an [factory] and a
140 * [creationExtras] as default extras.
141 *
142 * You should access the [ViewModel] instance using [ViewModelScenario.viewModel], and clear the
143 * [ViewModelStore] using [ViewModelScenario.close].
144 *
145 * Example usage:
146 * ```
147 * val myFactory: ViewModelProvider.Factory = MyViewModelFactory()
148 * viewModelScenario<MyViewModel>(myFactory)
149 * .use { scenario ->
150 * val vm = scenario.viewModel
151 * // Use the ViewModel
152 * }
153 * ```
154 *
155 * @param VM The reified [ViewModel] class to be created.
156 * @param creationExtras Additional data passed to the [Factory] during a [ViewModel] creation.
157 * @param factory A [Factory] to create a [ViewModel].
158 */
viewModelScenarionull159 public inline fun <reified VM : ViewModel> viewModelScenario(
160 factory: Factory,
161 creationExtras: CreationExtras = DefaultCreationExtras(),
162 ): ViewModelScenario<VM> {
163 return ViewModelScenario(
164 modelClass = VM::class,
165 modelFactory = factory,
166 initialModelExtras = creationExtras,
167 )
168 }
169