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