1 /*
2  * Copyright 2020 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.compose.runtime.saveable
18 
19 import android.os.Bundle
20 import androidx.activity.ComponentActivity
21 import androidx.compose.runtime.CompositionLocalProvider
22 import androidx.compose.runtime.MutableState
23 import androidx.compose.runtime.getValue
24 import androidx.compose.runtime.mutableStateOf
25 import androidx.compose.runtime.remember
26 import androidx.compose.runtime.setValue
27 import androidx.compose.ui.test.junit4.StateRestorationTester
28 import androidx.compose.ui.test.junit4.createAndroidComposeRule
29 import androidx.test.ext.junit.runners.AndroidJUnit4
30 import androidx.test.filters.MediumTest
31 import com.google.common.truth.Truth.assertThat
32 import org.junit.Rule
33 import org.junit.Test
34 import org.junit.runner.RunWith
35 
36 @Suppress("RememberReturnType")
37 @MediumTest
38 @RunWith(AndroidJUnit4::class)
39 class SaveableStateHolderTest {
40 
41     @get:Rule val rule = createAndroidComposeRule<Activity>()
42 
43     private val restorationTester = StateRestorationTester(rule)
44 
45     @Test
stateIsRestoredWhenGoBackToScreen1null46     fun stateIsRestoredWhenGoBackToScreen1() {
47         var increment = 0
48         var screen by mutableStateOf(Screens.Screen1)
49         var numberOnScreen1 = -1
50         var restorableNumberOnScreen1 = -1
51         restorationTester.setContent {
52             val holder = rememberSaveableStateHolder()
53             holder.SaveableStateProvider(screen) {
54                 if (screen == Screens.Screen1) {
55                     numberOnScreen1 = remember { increment++ }
56                     restorableNumberOnScreen1 = rememberSaveable { increment++ }
57                 } else {
58                     // screen 2
59                     remember { 100 }
60                 }
61             }
62         }
63 
64         rule.runOnIdle {
65             assertThat(numberOnScreen1).isEqualTo(0)
66             assertThat(restorableNumberOnScreen1).isEqualTo(1)
67             screen = Screens.Screen2
68         }
69 
70         // wait for the screen switch to apply
71         rule.runOnIdle {
72             numberOnScreen1 = -1
73             restorableNumberOnScreen1 = -1
74             // switch back to screen1
75             screen = Screens.Screen1
76         }
77 
78         rule.runOnIdle {
79             assertThat(numberOnScreen1).isEqualTo(2)
80             assertThat(restorableNumberOnScreen1).isEqualTo(1)
81         }
82     }
83 
84     @Test
simpleRestoreOnlyOneScreennull85     fun simpleRestoreOnlyOneScreen() {
86         var increment = 0
87         var number = -1
88         var restorableNumber = -1
89         restorationTester.setContent {
90             val holder = rememberSaveableStateHolder()
91             holder.SaveableStateProvider(Screens.Screen1) {
92                 number = remember { increment++ }
93                 restorableNumber = rememberSaveable { increment++ }
94             }
95         }
96 
97         rule.runOnIdle {
98             assertThat(number).isEqualTo(0)
99             assertThat(restorableNumber).isEqualTo(1)
100             number = -1
101             restorableNumber = -1
102         }
103 
104         restorationTester.emulateSavedInstanceStateRestore()
105 
106         rule.runOnIdle {
107             assertThat(number).isEqualTo(2)
108             assertThat(restorableNumber).isEqualTo(1)
109         }
110     }
111 
112     @Test
switchToScreen2AndRestorenull113     fun switchToScreen2AndRestore() {
114         var increment = 0
115         var screen by mutableStateOf(Screens.Screen1)
116         var numberOnScreen2 = -1
117         var restorableNumberOnScreen2 = -1
118         restorationTester.setContent {
119             val holder = rememberSaveableStateHolder()
120             holder.SaveableStateProvider(screen) {
121                 if (screen == Screens.Screen2) {
122                     numberOnScreen2 = remember { increment++ }
123                     restorableNumberOnScreen2 = rememberSaveable { increment++ }
124                 }
125             }
126         }
127 
128         rule.runOnIdle { screen = Screens.Screen2 }
129 
130         // wait for the screen switch to apply
131         rule.runOnIdle {
132             assertThat(numberOnScreen2).isEqualTo(0)
133             assertThat(restorableNumberOnScreen2).isEqualTo(1)
134             numberOnScreen2 = -1
135             restorableNumberOnScreen2 = -1
136         }
137 
138         restorationTester.emulateSavedInstanceStateRestore()
139 
140         rule.runOnIdle {
141             assertThat(numberOnScreen2).isEqualTo(2)
142             assertThat(restorableNumberOnScreen2).isEqualTo(1)
143         }
144     }
145 
146     @Test
stateOfScreen1IsSavedAndRestoredWhileWeAreOnScreen2null147     fun stateOfScreen1IsSavedAndRestoredWhileWeAreOnScreen2() {
148         var increment = 0
149         var screen by mutableStateOf(Screens.Screen1)
150         var numberOnScreen1 = -1
151         var restorableNumberOnScreen1 = -1
152         restorationTester.setContent {
153             val holder = rememberSaveableStateHolder()
154             holder.SaveableStateProvider(screen) {
155                 if (screen == Screens.Screen1) {
156                     numberOnScreen1 = remember { increment++ }
157                     restorableNumberOnScreen1 = rememberSaveable { increment++ }
158                 } else {
159                     // screen 2
160                     remember { 100 }
161                 }
162             }
163         }
164 
165         rule.runOnIdle {
166             assertThat(numberOnScreen1).isEqualTo(0)
167             assertThat(restorableNumberOnScreen1).isEqualTo(1)
168             screen = Screens.Screen2
169         }
170 
171         // wait for the screen switch to apply
172         rule.runOnIdle {
173             numberOnScreen1 = -1
174             restorableNumberOnScreen1 = -1
175         }
176 
177         restorationTester.emulateSavedInstanceStateRestore()
178 
179         // switch back to screen1
180         rule.runOnIdle { screen = Screens.Screen1 }
181 
182         rule.runOnIdle {
183             assertThat(numberOnScreen1).isEqualTo(2)
184             assertThat(restorableNumberOnScreen1).isEqualTo(1)
185         }
186     }
187 
188     @Test
weCanSkipSavingForCurrentScreennull189     fun weCanSkipSavingForCurrentScreen() {
190         var increment = 0
191         var screen by mutableStateOf(Screens.Screen1)
192         var restorableStateHolder: SaveableStateHolder? = null
193         var restorableNumberOnScreen1 = -1
194         restorationTester.setContent {
195             val holder = rememberSaveableStateHolder()
196             restorableStateHolder = holder
197             holder.SaveableStateProvider(screen) {
198                 if (screen == Screens.Screen1) {
199                     restorableNumberOnScreen1 = rememberSaveable { increment++ }
200                 } else {
201                     // screen 2
202                     remember { 100 }
203                 }
204             }
205         }
206 
207         rule.runOnIdle {
208             assertThat(restorableNumberOnScreen1).isEqualTo(0)
209             restorableNumberOnScreen1 = -1
210             restorableStateHolder!!.removeState(Screens.Screen1)
211             screen = Screens.Screen2
212         }
213 
214         rule.runOnIdle {
215             // switch back to screen1
216             screen = Screens.Screen1
217         }
218 
219         rule.runOnIdle { assertThat(restorableNumberOnScreen1).isEqualTo(1) }
220     }
221 
222     @Test
weCanRemoveAlreadySavedStatenull223     fun weCanRemoveAlreadySavedState() {
224         var increment = 0
225         var screen by mutableStateOf(Screens.Screen1)
226         var restorableStateHolder: SaveableStateHolder? = null
227         var restorableNumberOnScreen1 = -1
228         restorationTester.setContent {
229             val holder = rememberSaveableStateHolder()
230             restorableStateHolder = holder
231             holder.SaveableStateProvider(screen) {
232                 if (screen == Screens.Screen1) {
233                     restorableNumberOnScreen1 = rememberSaveable { increment++ }
234                 } else {
235                     // screen 2
236                     remember { 100 }
237                 }
238             }
239         }
240 
241         rule.runOnIdle {
242             assertThat(restorableNumberOnScreen1).isEqualTo(0)
243             restorableNumberOnScreen1 = -1
244             screen = Screens.Screen2
245         }
246 
247         rule.runOnIdle {
248             // switch back to screen1
249             restorableStateHolder!!.removeState(Screens.Screen1)
250             screen = Screens.Screen1
251         }
252 
253         rule.runOnIdle { assertThat(restorableNumberOnScreen1).isEqualTo(1) }
254     }
255 
256     @Test
restoringStateOfThePreviousPageAfterCreatingBundlenull257     fun restoringStateOfThePreviousPageAfterCreatingBundle() {
258         var showFirstPage by mutableStateOf(true)
259         var firstPageState: MutableState<Int>? = null
260 
261         rule.setContent {
262             val holder = rememberSaveableStateHolder()
263             holder.SaveableStateProvider(showFirstPage) {
264                 if (showFirstPage) {
265                     firstPageState = rememberSaveable { mutableStateOf(0) }
266                 }
267             }
268         }
269 
270         rule.runOnIdle {
271             assertThat(firstPageState!!.value).isEqualTo(0)
272             // change the value, so we can assert this change will be restored
273             firstPageState!!.value = 1
274             firstPageState = null
275             showFirstPage = false
276         }
277 
278         rule.runOnIdle {
279             rule.activity.doFakeSave()
280             showFirstPage = true
281         }
282 
283         rule.runOnIdle { assertThat(firstPageState!!.value).isEqualTo(1) }
284     }
285 
286     @Test
saveNothingWhenNoRememberSaveableIsUsedInternallynull287     fun saveNothingWhenNoRememberSaveableIsUsedInternally() {
288         var showFirstPage by mutableStateOf(true)
289         val registry = SaveableStateRegistry(null) { true }
290 
291         rule.setContent {
292             CompositionLocalProvider(LocalSaveableStateRegistry provides registry) {
293                 val holder = rememberSaveableStateHolder()
294                 holder.SaveableStateProvider(showFirstPage) {}
295             }
296         }
297 
298         rule.runOnIdle { showFirstPage = false }
299 
300         rule.runOnIdle {
301             val savedData = registry.performSave()
302             assertThat(savedData).isEqualTo(emptyMap<String, List<Any?>>())
303         }
304     }
305 
306     class Activity : ComponentActivity() {
doFakeSavenull307         fun doFakeSave() {
308             onSaveInstanceState(Bundle())
309         }
310     }
311 }
312 
313 enum class Screens {
314     Screen1,
315     Screen2,
316 }
317