1 /*
<lambda>null2 * 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
18
19 import androidx.compose.runtime.mock.InlineLinear
20 import androidx.compose.runtime.mock.Linear
21 import androidx.compose.runtime.mock.MockViewValidator
22 import androidx.compose.runtime.mock.View
23 import androidx.compose.runtime.mock.ViewApplier
24 import androidx.compose.runtime.mock.compositionTest
25 import androidx.compose.runtime.mock.expectChanges
26 import androidx.compose.runtime.mock.revalidate
27 import androidx.compose.runtime.mock.validate
28 import androidx.compose.runtime.mock.view
29 import androidx.compose.runtime.snapshots.Snapshot
30 import kotlin.random.Random
31 import kotlin.test.Test
32 import kotlin.test.assertEquals
33 import kotlin.test.assertFalse
34 import kotlin.test.assertNotEquals
35 import kotlin.test.assertSame
36 import kotlin.test.assertTrue
37
38 @Stable
39 class MovableContentTests {
40
41 @Test
42 fun testMovableContentSharesState() = compositionTest {
43 var lastPrivateState: State<Int> = mutableStateOf(0)
44 var portrait by mutableStateOf(false)
45
46 val content = movableContentOf {
47 val privateState = remember { mutableStateOf(0) }
48 lastPrivateState = privateState
49 Text("Some text")
50 Text("Some other text")
51 }
52
53 @Composable
54 fun Test() {
55 if (portrait) {
56 Column { content() }
57 } else {
58 Row { content() }
59 }
60 }
61
62 compose { Test() }
63
64 validate {
65 fun MockViewValidator.value() {
66 Text("Some text")
67 Text("Some other text")
68 }
69
70 if (portrait) {
71 Column { this.value() }
72 } else {
73 Row { this.value() }
74 }
75 }
76
77 val firstPrivateState = lastPrivateState
78 portrait = true
79 Snapshot.sendApplyNotifications()
80
81 expectChanges()
82 revalidate()
83
84 assertSame(firstPrivateState, lastPrivateState, "The state should be shared")
85 }
86
87 @Test
88 fun movableContentPreservesNodes() = compositionTest {
89 var portrait by mutableStateOf(false)
90
91 val content = movableContentOf {
92 Text("Some text")
93 Text("Some other text")
94 }
95
96 @Composable
97 fun Test() {
98 if (portrait) {
99 Column { content() }
100 } else {
101 Row { content() }
102 }
103 }
104
105 compose { Test() }
106
107 fun MockViewValidator.value() {
108 Text("Some text")
109 Text("Some other text")
110 }
111
112 validate {
113 if (portrait) {
114 Column { this.value() }
115 } else {
116 Row { this.value() }
117 }
118 }
119
120 val firstText = root.findFirst { it.name == "Text" }
121 portrait = true
122
123 expectChanges()
124 revalidate()
125
126 // Nodes should be shared
127 val newFirstText = root.findFirst { it.name == "Text" }
128 assertSame(firstText, newFirstText, "Text instance should be identical")
129 }
130
131 @Test
132 fun movingContent_mainComposer() = compositionTest {
133 val rememberedObject = mutableListOf<RememberedObject>()
134
135 @Composable
136 fun addRememberedObject() {
137 remember { RememberedObject().also { rememberedObject.add(it) } }
138 }
139
140 val content = movableContentOf {
141 Row {
142 addRememberedObject()
143 Text("Some text")
144 Marker()
145 }
146 }
147
148 fun MockViewValidator.validateContent() {
149 Row {
150 Text("Some text")
151 Marker()
152 }
153 }
154
155 var first by mutableStateOf(true)
156
157 compose {
158 Row {
159 if (first) content()
160 Text("Some other text")
161 }
162 Row {
163 Text("Some more text")
164 if (!first) content()
165 }
166 }
167
168 val marker: View = root.findFirst { it.name == "Marker" }
169
170 fun validate() {
171 validate {
172 Row {
173 if (first) validateContent()
174 Text("Some other text")
175 }
176 Row {
177 Text("Some more text")
178 if (!first) validateContent()
179 }
180 }
181
182 assertEquals(
183 expected = marker,
184 actual = root.findFirst { it.name == "Marker" },
185 message = "Expected marker node to move with the movable content"
186 )
187 assertTrue("Expected all remember observers to be kept alive") {
188 rememberedObject.all { it.isLive }
189 }
190 }
191
192 validate()
193
194 first = false
195 expectChanges()
196 validate()
197
198 first = true
199 expectChanges()
200 validate()
201 }
202
203 @Test
204 fun moveContent_subcompose() = compositionTest {
205 val rememberObservers = mutableListOf<RememberedObject>()
206
207 @Composable
208 fun addRememberObject() {
209 remember { RememberedObject().also { rememberObservers.add(it) } }
210 }
211
212 val content = movableContentOf {
213 Row {
214 addRememberObject()
215 Text("Text from value")
216 Marker()
217 }
218 }
219
220 fun MockViewValidator.validateContent() {
221 Row {
222 Text("Text from value")
223 Marker()
224 }
225 }
226
227 val inMain = 0
228 val inSubcompose1 = 1
229 val inSubcompose2 = 2
230
231 var position by mutableStateOf(inMain)
232
233 compose {
234 Row {
235 if (position == inMain) content()
236 Subcompose {
237 Row {
238 if (position == inSubcompose1) content()
239 Text("Some other text")
240 }
241 }
242 Subcompose {
243 Row {
244 Text("Some more text")
245 if (position == inSubcompose2) content()
246 }
247 }
248 }
249 }
250
251 val marker: View = root.findFirst { it.name == "Marker" }
252
253 fun validate() {
254 validate {
255 Row {
256 if (position == inMain) validateContent()
257 Subcompose {
258 Row {
259 if (position == inSubcompose1) validateContent()
260 Text("Some other text")
261 }
262 }
263 Subcompose {
264 Row {
265 Text("Some more text")
266 if (position == inSubcompose2) validateContent()
267 }
268 }
269 }
270 }
271
272 assertEquals(
273 expected = marker,
274 actual = root.findFirst { it.name == "Marker" },
275 message = "Expected marker node to move with the movable content"
276 )
277 assertTrue("Expected all remember observers to be kept alive") {
278 rememberObservers.all { it.isLive }
279 }
280 }
281
282 validate()
283
284 for (newPosition in
285 listOf(inSubcompose1, inSubcompose2, inSubcompose1, inMain, inSubcompose2, inMain)) {
286 position = newPosition
287 expectChanges()
288 validate()
289 }
290 }
291
292 @Test
293 fun normalMoveWithContentMove() = compositionTest {
294 val random = Random(1337)
295 val list = mutableStateListOf(*List(10) { it }.toTypedArray())
296
297 val content = movableContentOf { Marker() }
298 var position by mutableStateOf(-1)
299
300 compose {
301 Column {
302 if (position == -1) content()
303 for (item in list) {
304 key(item) {
305 Text("Item $item")
306 if (item == position) content()
307 }
308 }
309 }
310 }
311
312 val marker: View = root.findFirst { it.name == "Marker" }
313
314 fun validate() {
315 validate {
316 Column {
317 if (position == -1) Marker()
318 for (item in list) {
319 Text("Item $item")
320 if (item == position) Marker()
321 }
322 }
323 }
324
325 assertEquals(
326 expected = marker,
327 actual = root.findFirst { it.name == "Marker" },
328 message = "Expected marker node to move with the movable content"
329 )
330 }
331
332 validate()
333
334 repeat(10) {
335 position = it
336 list.shuffle(random)
337 expectChanges()
338 validate()
339 }
340
341 position = -1
342 list.shuffle(random)
343 expectChanges()
344 validate()
345 }
346
347 @Test
348 fun removeAndInsertWithMoveAway() = compositionTest {
349 var position by mutableStateOf(0)
350 var skipItem by mutableStateOf(5)
351
352 val content = movableContentOf { Marker() }
353 compose {
354 Row {
355 if (position == -1) content()
356 Column {
357 repeat(10) { item ->
358 key(item) {
359 if (skipItem != item) Text("Item $item")
360 if (position == item) content()
361 }
362 }
363 }
364 }
365 }
366
367 val marker: View = root.findFirst { it.name == "Marker" }
368
369 fun validate() {
370 validate {
371 Row {
372 if (position == -1) Marker()
373 Column {
374 repeat(10) { item ->
375 if (skipItem != item) Text("Item $item")
376 if (position == item) Marker()
377 }
378 }
379 }
380 }
381 assertEquals(
382 expected = marker,
383 actual = root.findFirst { it.name == "Marker" },
384 message = "Expected marker node to move with the movable content"
385 )
386 }
387
388 validate()
389
390 repeat(10) { markerPosition ->
391 repeat(10) { skip ->
392 position = -1
393 skipItem = -1
394 expectChanges()
395 validate()
396
397 // Move the marker and delete an item.
398 position = markerPosition
399 skipItem = skip
400 expectChanges()
401 validate()
402
403 // Move the marker away and insert an item
404 position = -1
405 skipItem = -1
406 expectChanges()
407
408 // Move the marker back
409 position = markerPosition
410 expectChanges()
411 validate()
412
413 // Move the marker way and delete an item
414 position = -1
415 skipItem = skip
416 expectChanges()
417 validate()
418 }
419 }
420 }
421
422 @Test
423 fun invalidationsMoveWithContent() = compositionTest {
424 var data by mutableStateOf(0)
425 var position by mutableStateOf(-1)
426 val content = movableContentOf { Text("data = $data") }
427
428 compose {
429 Row {
430 if (position == -1) content()
431 repeat(10) { item ->
432 key(item) {
433 Text("Item $item")
434 if (position == item) content()
435 }
436 }
437 }
438 }
439
440 validate {
441 fun MockViewValidator.content() {
442 Text("data = $data")
443 }
444 Row {
445 if (position == -1) this.content()
446 repeat(10) { item ->
447 Text("Item $item")
448 if (position == item) this.content()
449 }
450 }
451 }
452
453 repeat(10) { newData ->
454 data = newData
455 position = newData
456 expectChanges()
457 revalidate()
458 }
459 }
460
461 @Test
462 fun projectedBinaryTree() = compositionTest {
463 class Node(value: Int, left: Node? = null, right: Node? = null) {
464 var value by mutableStateOf(value)
465 var left by mutableStateOf(left)
466 var right by mutableStateOf(right)
467
468 fun validateNode(validator: MockViewValidator) {
469 with(validator) { Marker(value) }
470 left?.validateNode(validator)
471 right?.validateNode(validator)
472 }
473
474 fun forEach(block: (node: Node) -> Unit) {
475 block(this)
476 left?.forEach(block)
477 right?.forEach(block)
478 }
479
480 fun swap() {
481 val oldLeft = left
482 val oldRight = right
483 left = oldRight
484 right = oldLeft
485 }
486
487 override fun toString(): String = "$value($left, $right)"
488 }
489
490 fun buildTree(level: Int): Node {
491 var index = 0
492 fun build(level: Int): Node =
493 if (level > 1) Node(index++, build(level - 1), build(level - 1)) else Node(index++)
494 return build(level)
495 }
496
497 val tree = buildTree(6)
498
499 val contents = mutableMapOf<Node?, @Composable () -> Unit>()
500 tree.forEach { node ->
501 contents[node] = movableContentOf {
502 Marker(node.value)
503 contents[node.left]?.invoke()
504 contents[node.right]?.invoke()
505 }
506 }
507
508 compose { contents[tree]?.invoke() }
509
510 validate { tree.validateNode(this) }
511
512 tree.forEach { it.swap() }
513
514 expectChanges()
515
516 revalidate()
517
518 tree.forEach { it.swap() }
519
520 expectChanges()
521
522 revalidate()
523 }
524
525 @Test
526 fun multipleContentsMovingIntoCommonParent() = compositionTest {
527 val content1 = movableContentOf {
528 Text("1-1")
529 Text("1-2")
530 Text("1-3")
531 }
532 val content2 = movableContentOf {
533 Text("2-4")
534 Text("2-5")
535 Text("2-6")
536 }
537 val content3 = movableContentOf {
538 Text("3-7")
539 Text("3-8")
540 Text("3-9")
541 }
542
543 var case by mutableStateOf(0)
544 var level by mutableStateOf(0)
545
546 @Composable
547 fun sep() {
548 Text("-----")
549 }
550
551 @Composable
552 fun cases() {
553 when (case) {
554 0 -> {
555 sep()
556 content1()
557 sep()
558 content2()
559 sep()
560 content3()
561 sep()
562 }
563 1 -> {
564 content2()
565 sep()
566 content3()
567 sep()
568 content1()
569 }
570 2 -> {
571 sep()
572 content3()
573 content1()
574 content2()
575 sep()
576 }
577 }
578 }
579
580 compose {
581 Column {
582 if (level == 0) {
583 cases()
584 }
585 Column {
586 if (level == 1) {
587 cases()
588 }
589 }
590 }
591 }
592
593 validate {
594 fun MockViewValidator.sep() {
595 Text("-----")
596 }
597
598 fun MockViewValidator.value1() {
599 Text("1-1")
600 Text("1-2")
601 Text("1-3")
602 }
603
604 fun MockViewValidator.value2() {
605 Text("2-4")
606 Text("2-5")
607 Text("2-6")
608 }
609
610 fun MockViewValidator.value3() {
611 Text("3-7")
612 Text("3-8")
613 Text("3-9")
614 }
615
616 fun MockViewValidator.cases() {
617 when (case) {
618 0 -> {
619 this.sep()
620 this.value1()
621 this.sep()
622 this.value2()
623 this.sep()
624 this.value3()
625 this.sep()
626 }
627 1 -> {
628 this.value2()
629 this.sep()
630 this.value3()
631 this.sep()
632 this.value1()
633 }
634 2 -> {
635 this.sep()
636 this.value3()
637 this.value1()
638 this.value2()
639 this.sep()
640 }
641 }
642 }
643
644 Column {
645 if (level == 0) {
646 this.cases()
647 }
648 Column {
649 if (level == 1) {
650 this.cases()
651 }
652 }
653 }
654 }
655
656 fun textMap(): Map<String?, View> {
657 val result = mutableMapOf<String?, View>()
658 fun collect(view: View) {
659 if (view.name == "Text") {
660 if (view.text?.contains('-') == false) result[view.text] = view
661 }
662 for (child in view.children) {
663 collect(child)
664 }
665 }
666 collect(root)
667 return result
668 }
669
670 val initialMap = textMap()
671
672 fun validateInstances() {
673 val currentMap = textMap()
674 for (entry in currentMap) {
675 if (initialMap[entry.key] !== entry.value) {
676 error("The text value ${entry.key} had a different instance created")
677 }
678 }
679 }
680
681 fun test(l: Int, c: Int) {
682 case = c
683 level = l
684 advance(ignorePendingWork = true)
685 revalidate()
686 validateInstances()
687 }
688
689 test(0, 0)
690 test(1, 1)
691 test(0, 2)
692 test(1, 0)
693 test(0, 1)
694 test(1, 2)
695 }
696
697 @Test
698 fun childIndexesAreCorrectlyCalculated() = compositionTest {
699 val content = movableContentOf { Marker(0) }
700
701 var vertical by mutableStateOf(false)
702 compose {
703 if (vertical) {
704 Row {
705 Empty()
706 content()
707 }
708 } else {
709 Column {
710 Empty()
711 content()
712 }
713 }
714 }
715
716 validate {
717 if (vertical) {
718 Row { Marker(0) }
719 } else {
720 Column { Marker(0) }
721 }
722 }
723
724 vertical = true
725 expectChanges()
726 revalidate()
727 }
728
729 @Test
730 fun validateRecomposeScopesDoNotGetLost() = compositionTest {
731 var isHorizontal by mutableStateOf(false)
732 val displayValue = mutableStateOf(0)
733 val content = movableContentOf { DisplayInt(displayValue) }
734
735 compose { Stack(isHorizontal) { Row { content() } } }
736
737 validate { Stack(isHorizontal) { Row { DisplayInt(displayValue) } } }
738
739 displayValue.value++
740 expectChanges()
741 revalidate()
742
743 isHorizontal = true
744 Snapshot.sendApplyNotifications()
745 advanceTimeBy(10)
746
747 displayValue.value++
748 expectChanges()
749 revalidate()
750 }
751
752 @Test
753 fun compositionLocalsShouldBeAvailable() = compositionTest {
754 var someValue by mutableStateOf(0)
755 val local = staticCompositionLocalOf<Int> { error("No value provided for local") }
756
757 compose {
758 Wrap(20) {
759 CompositionLocalProvider(local provides 10) {
760 // Remember is missing intentionally so it creates a new value to ensure the
761 // new values see the correct provider scope.
762 val content = movableContentOf {
763 Text("Local = ${local.current}")
764 Text("SomeValue = $someValue")
765 }
766 if (someValue % 2 == 0) content() else content()
767 }
768 }
769 }
770
771 validate {
772 Text("Local = 10")
773 Text("SomeValue = $someValue")
774 }
775
776 someValue++
777 advance()
778
779 revalidate()
780 }
781
782 @Test
783 fun compositionLocalsShouldBeAvailableInNestedContent() = compositionTest {
784 var someValue by mutableStateOf(0)
785 val local = staticCompositionLocalOf<Int> { error("No value provided for local") }
786
787 val parent = movableContentOf<@Composable () -> Unit> { child -> Wrap { child() } }
788
789 val child = movableContentOf {
790 Text("Local = ${local.current}")
791 Text("SomeValue = $someValue")
792 }
793
794 compose {
795 Wrap {
796 CompositionLocalProvider(local provides 10) {
797 // Remember is missing intentionally so it creates a new value to ensure the
798 // new values see the correct provider scope.
799 if (someValue % 2 == 0)
800 parent {
801 Wrap {
802 Text("One")
803 child()
804 }
805 }
806 else
807 parent {
808 child()
809 Text("Two")
810 }
811 }
812 }
813 }
814
815 validate {
816 if (someValue % 2 == 0) {
817 Text("One")
818 Text("Local = 10")
819 Text("SomeValue = $someValue")
820 } else {
821 Text("Local = 10")
822 Text("SomeValue = $someValue")
823 Text("Two")
824 }
825 }
826
827 someValue++
828 advance()
829
830 revalidate()
831 }
832
833 @Test
834 fun subcomposeLifetime_no_movable_content() = compositionTest {
835 val rememberObject = RememberedObject()
836 var useInMain by mutableStateOf(false)
837 var useInSub1 by mutableStateOf(false)
838 var useInSub2 by mutableStateOf(false)
839
840 @Composable
841 fun use() {
842 remember { rememberObject }
843 }
844 compose {
845 if (useInMain) use()
846 Subcompose { if (useInSub1) use() }
847 Subcompose { if (useInSub2) use() }
848 }
849
850 fun expectUnused() {
851 advance()
852 assertFalse(rememberObject.isLive, "RememberObject unexpectedly used")
853 }
854 fun expectUsed() {
855 advance()
856 assertTrue(rememberObject.isLive, "Expected RememberObject to be used")
857 }
858
859 expectUnused()
860
861 // Add a use in main
862 useInMain = true
863 expectUsed()
864
865 // Add in sub-composes
866 useInSub1 = true
867 useInSub2 = true
868 expectUsed()
869
870 // Remove it from main
871 useInMain = false
872 expectUsed()
873
874 // Remove it from sub1
875 useInSub1 = false
876 expectUsed()
877
878 // Transfer it from sub1 to sub2
879 useInSub1 = false
880 useInSub2 = true
881 expectUsed()
882
883 // Remove it altogether
884 useInMain = false
885 useInSub1 = false
886 useInSub2 = false
887 expectUnused()
888 }
889
890 @Test
891 fun subcomposeLifetime_with_movable_content() = compositionTest {
892 val rememberObject = RememberedObject()
893 var useInMain by mutableStateOf(false)
894 var useInSub1 by mutableStateOf(false)
895 var useInSub2 by mutableStateOf(false)
896
897 @Suppress("UNUSED_VARIABLE") // Object not remembered without
898 val rememberTheObject = movableContentOf {
899 val obj = remember { rememberObject }
900 }
901
902 @Composable
903 fun use() {
904 rememberTheObject()
905 }
906 compose {
907 if (useInMain) use()
908 Subcompose { if (useInSub1) use() }
909 Subcompose { if (useInSub2) use() }
910 }
911
912 fun expectUnused() {
913 advance()
914 assertFalse(rememberObject.isLive, "RememberObject unexpectedly used")
915 }
916 fun expectUsed() {
917 advance()
918 assertTrue(rememberObject.isLive, "Expected RememberObject to be used")
919 }
920
921 expectUnused()
922
923 // Add a use in main
924 useInMain = true
925 expectUsed()
926
927 // Add in sub-composes
928 useInSub1 = true
929 useInSub2 = true
930 expectUsed()
931
932 // Remove it from main
933 useInMain = false
934 expectUsed()
935
936 // Remove it from sub1
937 useInSub1 = false
938 expectUsed()
939
940 // Transfer it from sub1 to sub2
941 useInSub1 = false
942 useInSub2 = true
943 expectUsed()
944
945 // Remove it altogether
946 useInMain = false
947 useInSub1 = false
948 useInSub2 = false
949 expectUnused()
950 }
951
952 @Test // Regression test for 230830644 and 235398298
953 fun deferredSubcompose_conditional_rootLevelChildren() = compositionTest {
954 var subcompose by mutableStateOf(false)
955 var lastPrivateState: State<Int> = mutableStateOf(0)
956
957 val content = movableContentOf {
958 lastPrivateState = remember { mutableStateOf(0) }
959 Text("Movable content")
960 }
961
962 compose {
963 Text("Main content start")
964 if (!subcompose) {
965 content()
966 }
967 Text("Main content end")
968 if (subcompose) {
969 DeferredSubcompose {
970 Text("Sub-composed content start")
971 content()
972 Text("Sub-composed content end")
973 }
974 }
975 }
976
977 validate {
978 Text("Main content start")
979 if (!subcompose) {
980 Text("Movable content")
981 }
982 Text("Main content end")
983 if (subcompose) {
984 DeferredSubcompose {
985 Text("Sub-composed content start")
986 Text("Movable content")
987 Text("Sub-composed content end")
988 }
989 }
990 }
991
992 val expectedState = lastPrivateState
993 subcompose = true
994 expectChanges()
995 revalidate()
996
997 assertEquals(expectedState, lastPrivateState, "Movable content was unexpectedly recreated")
998
999 subcompose = false
1000 expectChanges()
1001 revalidate()
1002
1003 assertEquals(expectedState, lastPrivateState, "Movable content was unexpectedly recreated")
1004 }
1005
1006 @Test // Regression test for 230830644 and 235398298
1007 fun deferredSubcompose_conditional_nestedChildren() = compositionTest {
1008 var subcompose by mutableStateOf(false)
1009 var lastPrivateState: State<Int> = mutableStateOf(0)
1010
1011 val content = movableContentOf {
1012 lastPrivateState = remember { mutableStateOf(0) }
1013 Text("Movable content")
1014 }
1015
1016 compose {
1017 Text("Main content start")
1018 if (!subcompose) {
1019 content()
1020 }
1021 Text("Main content end")
1022 if (subcompose) {
1023 DeferredSubcompose {
1024 Column {
1025 Text("Sub-composed content start")
1026 content()
1027 Text("Sub-composed content end")
1028 }
1029 }
1030 }
1031 }
1032
1033 validate {
1034 Text("Main content start")
1035 if (!subcompose) {
1036 Text("Movable content")
1037 }
1038 Text("Main content end")
1039 if (subcompose) {
1040 DeferredSubcompose {
1041 Column {
1042 Text("Sub-composed content start")
1043 Text("Movable content")
1044 Text("Sub-composed content end")
1045 }
1046 }
1047 }
1048 }
1049
1050 val expectedState = lastPrivateState
1051 subcompose = true
1052 expectChanges()
1053 revalidate()
1054
1055 assertEquals(expectedState, lastPrivateState, "Movable content was unexpectedly recreated")
1056
1057 subcompose = false
1058 expectChanges()
1059 revalidate()
1060
1061 assertEquals(expectedState, lastPrivateState, "Movable content was unexpectedly recreated")
1062 }
1063
1064 @Test // Regression test for 230830644
1065 fun deferredSubcompose_conditional_and_invalid() = compositionTest {
1066 var subcompose by mutableStateOf(false)
1067 var lastPrivateState: State<Int> = mutableStateOf(0)
1068 var state by mutableStateOf("one")
1069
1070 val content = movableContentOf {
1071 lastPrivateState = remember { mutableStateOf(0) }
1072 Text("Movable content state: $state")
1073 }
1074
1075 compose {
1076 Text("Main content start")
1077 if (!subcompose) {
1078 content()
1079 }
1080 Text("Main content end")
1081 if (subcompose) {
1082 DeferredSubcompose {
1083 Text("Sub-composed content start")
1084 content()
1085 Text("Sub-composed content end")
1086 }
1087 }
1088 }
1089
1090 validate {
1091 Text("Main content start")
1092 if (!subcompose) {
1093 Text("Movable content state: $state")
1094 }
1095 Text("Main content end")
1096 if (subcompose) {
1097 DeferredSubcompose {
1098 Text("Sub-composed content start")
1099 Text("Movable content state: $state")
1100 Text("Sub-composed content end")
1101 }
1102 }
1103 }
1104
1105 val expectedState = lastPrivateState
1106 subcompose = true
1107 state = "two"
1108 expectChanges()
1109 revalidate()
1110
1111 assertEquals(expectedState, lastPrivateState)
1112 }
1113
1114 @Test
1115 fun movableContentParameters_One() = compositionTest {
1116 val data = mutableStateOf(0)
1117 val content = movableContentOf<Int> { p1 -> Text("Value p1=$p1, data=${data.value}") }
1118
1119 compose {
1120 content(1)
1121 content(2)
1122 }
1123
1124 validate {
1125 Text("Value p1=1, data=${data.value}")
1126 Text("Value p1=2, data=${data.value}")
1127 }
1128
1129 data.value++
1130 expectChanges()
1131 revalidate()
1132 }
1133
1134 @Test
1135 fun movableContentParameters_Two() = compositionTest {
1136 val data = mutableStateOf(0)
1137 val content =
1138 movableContentOf<Int, Int> { p1, p2 ->
1139 Text("Value p1=$p1, p2=$p2, data=${data.value}")
1140 }
1141
1142 compose {
1143 content(1, 2)
1144 content(3, 4)
1145 }
1146
1147 validate {
1148 Text("Value p1=1, p2=2, data=${data.value}")
1149 Text("Value p1=3, p2=4, data=${data.value}")
1150 }
1151
1152 data.value++
1153 expectChanges()
1154 revalidate()
1155 }
1156
1157 @Test
1158 fun movableContentParameters_Three() = compositionTest {
1159 val data = mutableStateOf(0)
1160 val content =
1161 movableContentOf<Int, Int, Int> { p1, p2, p3 ->
1162 Text("Value p1=$p1, p2=$p2, p3=$p3, data=${data.value}")
1163 }
1164
1165 compose {
1166 content(1, 2, 3)
1167 content(4, 5, 6)
1168 }
1169
1170 validate {
1171 Text("Value p1=1, p2=2, p3=3, data=${data.value}")
1172 Text("Value p1=4, p2=5, p3=6, data=${data.value}")
1173 }
1174
1175 data.value++
1176 expectChanges()
1177 revalidate()
1178 }
1179
1180 @Test
1181 fun movableContentParameters_Four() = compositionTest {
1182 val data = mutableStateOf(0)
1183 val content =
1184 movableContentOf<Int, Int, Int, Int> { p1, p2, p3, p4 ->
1185 Text("Value p1=$p1, p2=$p2, p3=$p3, p4=$p4, data=${data.value}")
1186 }
1187
1188 compose {
1189 content(1, 2, 3, 4)
1190 content(5, 6, 7, 8)
1191 }
1192
1193 validate {
1194 Text("Value p1=1, p2=2, p3=3, p4=4, data=${data.value}")
1195 Text("Value p1=5, p2=6, p3=7, p4=8, data=${data.value}")
1196 }
1197
1198 data.value++
1199 expectChanges()
1200 revalidate()
1201 }
1202
1203 @Test
1204 fun movableContentReceiver_None() = compositionTest {
1205 val data = mutableStateOf(0)
1206 val content =
1207 movableContentWithReceiverOf<Int>() { Text("Value this=$this, data=${data.value}") }
1208 val receiver1 = 100
1209 val receiver2 = 200
1210
1211 compose {
1212 receiver1.content()
1213 receiver2.content()
1214 }
1215
1216 validate {
1217 Text("Value this=100, data=${data.value}")
1218 Text("Value this=200, data=${data.value}")
1219 }
1220
1221 data.value++
1222 expectChanges()
1223 revalidate()
1224 }
1225
1226 @Test
1227 fun movableContentReceiver_One() = compositionTest {
1228 val data = mutableStateOf(0)
1229 val content =
1230 movableContentWithReceiverOf<Int, Int>() { p1 ->
1231 Text("Value this=$this, p1=$p1, data=${data.value}")
1232 }
1233 val receiver1 = 100
1234 val receiver2 = 200
1235
1236 compose {
1237 receiver1.content(1)
1238 receiver2.content(2)
1239 }
1240
1241 validate {
1242 Text("Value this=100, p1=1, data=${data.value}")
1243 Text("Value this=200, p1=2, data=${data.value}")
1244 }
1245
1246 data.value++
1247 expectChanges()
1248 revalidate()
1249 }
1250
1251 @Test
1252 fun movableContentReceiver_Two() = compositionTest {
1253 val data = mutableStateOf(0)
1254 val content =
1255 movableContentWithReceiverOf<Int, Int, Int>() { p1, p2 ->
1256 Text("Value this=$this, p1=$p1, p2=$p2, data=${data.value}")
1257 }
1258 val receiver1 = 100
1259 val receiver2 = 200
1260
1261 compose {
1262 receiver1.content(1, 2)
1263 receiver2.content(3, 4)
1264 }
1265
1266 validate {
1267 Text("Value this=100, p1=1, p2=2, data=${data.value}")
1268 Text("Value this=200, p1=3, p2=4, data=${data.value}")
1269 }
1270
1271 data.value++
1272 expectChanges()
1273 revalidate()
1274 }
1275
1276 @Test
1277 fun movableContentReceiver_Three() = compositionTest {
1278 val data = mutableStateOf(0)
1279 val content =
1280 movableContentWithReceiverOf<Int, Int, Int, Int>() { p1, p2, p3 ->
1281 Text("Value this=$this, p1=$p1, p2=$p2, p3=$p3, data=${data.value}")
1282 }
1283 val receiver1 = 100
1284 val receiver2 = 200
1285
1286 compose {
1287 receiver1.content(1, 2, 3)
1288 receiver2.content(4, 5, 6)
1289 }
1290
1291 validate {
1292 Text("Value this=100, p1=1, p2=2, p3=3, data=${data.value}")
1293 Text("Value this=200, p1=4, p2=5, p3=6, data=${data.value}")
1294 }
1295
1296 data.value++
1297 expectChanges()
1298 revalidate()
1299 }
1300
1301 @Test
1302 fun movableContentParameters_changedParameter() = compositionTest {
1303 val data = mutableStateOf(0)
1304 val location = mutableStateOf(0)
1305 val content = movableContentOf<Int> { d -> Text("d=$d") }
1306
1307 compose {
1308 if (location.value == 0) content(data.value)
1309 Column { if (location.value == 1) content(data.value) }
1310 Row { if (location.value == 2) content(data.value) }
1311 }
1312
1313 validate {
1314 if (location.value == 0) Text("d=${data.value}")
1315 Column { if (location.value == 1) Text("d=${data.value}") }
1316 Row { if (location.value == 2) Text("d=${data.value}") }
1317 }
1318
1319 location.value++
1320 data.value++
1321 expectChanges()
1322 revalidate()
1323
1324 location.value++
1325 expectChanges()
1326 revalidate()
1327
1328 location.value++
1329 data.value++
1330 expectChanges()
1331 revalidate()
1332 }
1333
1334 @Test
1335 fun movableContentOfTheSameFunctionShouldHaveStableKeys() = compositionTest {
1336 val hashList1 = mutableListOf<CompositeKeyHashCode>()
1337 val hashList2 = mutableListOf<CompositeKeyHashCode>()
1338 val composable1: @Composable () -> Unit = { hashList1.add(currentCompositeKeyHashCode) }
1339 val composable2: @Composable () -> Unit = { hashList2.add(currentCompositeKeyHashCode) }
1340 val movableContent1A = movableContentOf(composable1)
1341 val movableContent1B = movableContentOf(composable1)
1342 val movableContent2A = movableContentOf(composable2)
1343 val movableContent2B = movableContentOf(composable2)
1344 compose {
1345 movableContent1A()
1346 movableContent1B()
1347 movableContent1A()
1348 movableContent1B()
1349 movableContent2A()
1350 movableContent2B()
1351 movableContent2A()
1352 movableContent2B()
1353 }
1354
1355 fun List<CompositeKeyHashCode>.assertAllTheSame() = forEach { assertEquals(it, first()) }
1356 hashList1.assertAllTheSame()
1357 hashList2.assertAllTheSame()
1358 assertNotEquals(hashList1.first(), hashList2.first())
1359 }
1360
1361 @Test
1362 fun keyInsideMovableContentShouldntChangeWhenRecomposed() = compositionTest {
1363 val hashList = mutableListOf<CompositeKeyHashCode>()
1364 val counter = mutableStateOf(0)
1365 val movableContent = movableContentOf {
1366 hashList.add(currentCompositeKeyHashCode)
1367 Text("counter=${counter.value}")
1368 }
1369 compose { movableContent() }
1370
1371 validate { Text("counter=${counter.value}") }
1372
1373 counter.value++
1374 expectChanges()
1375 revalidate()
1376
1377 assertEquals(2, hashList.size)
1378 assertEquals(hashList[0], hashList[1])
1379 }
1380
1381 @Test
1382 fun parameterPassingThroughDeferredSubcompose() = compositionTest {
1383 var state by mutableStateOf(false)
1384 var lastSeen: Boolean? = null
1385 val content = movableContentOf { parameter: Boolean -> Container { lastSeen = parameter } }
1386
1387 compose {
1388 if (state) {
1389 content(true)
1390 } else {
1391 DeferredSubcompose { content(state) }
1392 }
1393 }
1394
1395 advanceTimeBy(5_000)
1396
1397 assertEquals(state, lastSeen)
1398
1399 repeat(5) {
1400 state = !state
1401
1402 expectChanges()
1403
1404 assertEquals(state, lastSeen, "Failed in iteration $it")
1405 }
1406 }
1407
1408 @Test
1409 fun stateChangesWhilePendingMove() = compositionTest {
1410 var state = 0
1411 var lastSeen: Int? = null
1412 var deferred by mutableStateOf(false)
1413 var scope: RecomposeScope? = null
1414
1415 val content = movableContentOf {
1416 Container {
1417 lastSeen = state
1418 scope = currentRecomposeScope
1419 }
1420 }
1421
1422 compose {
1423 if (deferred) {
1424 DeferredSubcompose { content() }
1425 SideEffect {
1426 state++
1427 scope?.invalidate()
1428 }
1429 } else {
1430 content()
1431 }
1432 }
1433
1434 advanceTimeBy(5_000)
1435
1436 assertEquals(state, lastSeen)
1437
1438 deferred = true
1439
1440 advance()
1441
1442 assertEquals(state, lastSeen)
1443 }
1444
1445 @Test
1446 fun movableContent_moveRow() = compositionTest {
1447 var condition by mutableStateOf(true)
1448
1449 val movableContent1 = movableContentOf { Text("First") }
1450 val movableContent2 = movableContentOf { Text("Second") }
1451
1452 compose {
1453 if (condition) {
1454 Linear {
1455 Linear { movableContent1() }
1456 movableContent2()
1457 }
1458 } else {
1459 Linear {
1460 Linear { movableContent1() }
1461 movableContent2()
1462 }
1463 }
1464 }
1465
1466 validate {
1467 Linear {
1468 Linear { Text("First") }
1469 Text("Second")
1470 }
1471 }
1472
1473 condition = false
1474 expectChanges()
1475
1476 revalidate()
1477 }
1478
1479 @Test
1480 fun movableContent_rememberOrdering() = compositionTest {
1481 val movableContent1 = movableContentOf { repeat(100) { Text("Some content") } }
1482 var includeContent by mutableStateOf(true)
1483 var rememberKey by mutableStateOf(0)
1484
1485 compose {
1486 if (includeContent) {
1487 movableContent1()
1488 }
1489 val a = remember(rememberKey) { SimpleRememberedObject("Key $rememberKey") }
1490 Text(a.name)
1491 }
1492
1493 rememberKey++
1494 expectChanges()
1495
1496 includeContent = false
1497 rememberKey++
1498 expectChanges()
1499 }
1500
1501 @Test
1502 fun movableContent_nestedMovableContent() = compositionTest {
1503 var data = 0
1504
1505 var condition by mutableStateOf(true)
1506
1507 val nestedContent = movableContentOf {
1508 val state = remember { data++ }
1509 Text("Generated state: $state")
1510 }
1511
1512 val contentHost = movableContentOf {
1513 Text("Host")
1514 if (condition) {
1515 nestedContent()
1516 }
1517 }
1518
1519 compose {
1520 if (condition) {
1521 contentHost()
1522 }
1523 Text("Outer")
1524 if (!condition) {
1525 contentHost()
1526 nestedContent()
1527 }
1528 }
1529
1530 validate {
1531 if (condition) {
1532 Text("Host")
1533 Text("Generated state: 0")
1534 }
1535 Text("Outer")
1536 if (!condition) {
1537 Text("Host")
1538 Text("Generated state: 0")
1539 }
1540 }
1541
1542 condition = false
1543 expectChanges()
1544 revalidate()
1545 verifyConsistent()
1546
1547 condition = true
1548 expectChanges()
1549 println("Done")
1550 revalidate()
1551 verifyConsistent()
1552 }
1553
1554 @Test // 362539770
1555 fun movableContent_nestedMovableContent_direct() = compositionTest {
1556 var data = 0
1557
1558 var condition by mutableStateOf(true)
1559
1560 val common = movableContentOf {
1561 val state = remember { data++ }
1562 Text("Generated state: $state")
1563 }
1564
1565 val wrapper = movableContentOf {
1566 Text("Wrapper start")
1567 common()
1568 Text("Wrapper end")
1569 }
1570
1571 compose {
1572 Text("Outer")
1573 if (condition) {
1574 wrapper()
1575 } else {
1576 common()
1577 }
1578 }
1579
1580 validate {
1581 Text("Outer")
1582 if (condition) {
1583 Text("Wrapper start")
1584 }
1585 Text("Generated state: 0")
1586 if (condition) {
1587 Text("Wrapper end")
1588 }
1589 }
1590
1591 condition = false
1592 expectChanges()
1593 revalidate()
1594
1595 condition = true
1596 expectChanges()
1597 revalidate()
1598 }
1599
1600 @Test // 362539770
1601 @OptIn(ExperimentalComposeApi::class)
1602 fun movableContent_nestedMovableContent_disabled() = compositionTest {
1603 var data = 0
1604
1605 var condition by mutableStateOf(true)
1606
1607 val common = movableContentOf {
1608 val state = remember { data++ }
1609 Text("Generated state: $state")
1610 }
1611
1612 val wrapper = movableContentOf {
1613 Text("Wrapper start")
1614 common()
1615 Text("Wrapper end")
1616 }
1617
1618 compose {
1619 Text("Outer")
1620 if (condition) {
1621 wrapper()
1622 } else {
1623 common()
1624 }
1625 }
1626
1627 var expectedState = 0
1628 validate {
1629 Text("Outer")
1630 if (condition) {
1631 Text("Wrapper start")
1632 }
1633 Text("Generated state: $expectedState")
1634 if (condition) {
1635 Text("Wrapper end")
1636 }
1637 }
1638
1639 ComposeRuntimeFlags.isMovingNestedMovableContentEnabled = false
1640 try {
1641 // With moving nested content disabled the call to common() will generate new
1642 // state when it moves out of the containing movable content.
1643 expectedState = 1
1644 condition = false
1645 expectChanges()
1646 revalidate()
1647
1648 condition = true
1649 expectChanges()
1650 revalidate()
1651 } finally {
1652 ComposeRuntimeFlags.isMovingNestedMovableContentEnabled = true
1653 }
1654 }
1655
1656 @Test
1657 fun movableContent_nestedMovableContent_simpleMove() = compositionTest {
1658 var data = 0
1659
1660 var condition by mutableStateOf(true)
1661
1662 val common = movableContentOf {
1663 val state = remember { data++ }
1664 Text("Generated state: $state")
1665 }
1666
1667 val wrapper = movableContentOf {
1668 Text("Wrapper start")
1669 common()
1670 Text("Wrapper end")
1671 }
1672
1673 compose {
1674 Text("Outer")
1675 if (condition) {
1676 Linear { wrapper() }
1677 } else {
1678 wrapper()
1679 }
1680 }
1681
1682 validate {
1683 Text("Outer")
1684 if (condition) {
1685 Linear {
1686 Text("Wrapper start")
1687 Text("Generated state: 0")
1688 Text("Wrapper end")
1689 }
1690 } else {
1691 Text("Wrapper start")
1692 Text("Generated state: 0")
1693 Text("Wrapper end")
1694 }
1695 }
1696
1697 condition = false
1698 expectChanges()
1699 revalidate()
1700
1701 condition = true
1702 expectChanges()
1703 revalidate()
1704 }
1705
1706 @Test
1707 fun movableContent_nestedMovableContent_tree() = compositionTest {
1708 var data = 0
1709
1710 @Composable
1711 fun Leaf() {
1712 val value = remember { data++ }
1713 Text("Data $value")
1714 }
1715
1716 val level0 = Array(16) { movableContentOf { Leaf() } }
1717 val level1 =
1718 Array(8) { it ->
1719 movableContentOf {
1720 level0[it * 2]()
1721 level0[it * 2 + 1]()
1722 }
1723 }
1724 val level2 =
1725 Array(4) {
1726 movableContentOf {
1727 level1[it * 2]()
1728 level1[it * 2 + 1]()
1729 }
1730 }
1731 val level3 =
1732 Array(2) {
1733 movableContentOf {
1734 level2[it * 2]()
1735 level2[it * 2 + 1]()
1736 }
1737 }
1738
1739 var displayTree by mutableStateOf(false)
1740
1741 compose { if (displayTree) level3.forEach { it() } else level0.forEach { it() } }
1742
1743 validate { repeat(16) { Text("Data $it") } }
1744
1745 displayTree = true
1746 expectChanges()
1747 revalidate()
1748
1749 displayTree = false
1750 expectChanges()
1751 revalidate()
1752 }
1753
1754 @Test // 343178423
1755 fun movableContent_movingContentOutOfDeferredSubcomposition() = compositionTest {
1756 var toggle by mutableStateOf(true)
1757 val content = movableContentOf { Text("Toggle = $toggle") }
1758
1759 compose {
1760 if (toggle) {
1761 DeferredSubcompose { content() }
1762 } else {
1763 content()
1764 }
1765 }
1766 advanceTimeBy(5_000)
1767
1768 validate {
1769 if (toggle) {
1770 DeferredSubcompose { Text("Toggle = $toggle") }
1771 } else {
1772 Text("Toggle = false")
1773 }
1774 }
1775
1776 toggle = !toggle
1777 expectChanges()
1778 revalidate()
1779 }
1780
1781 @Test // 365802563
1782 fun movableContent_movingChildOfDeletedNode() = compositionTest {
1783 var index by mutableIntStateOf(0)
1784 val content = movableContentOf { Text("Some text") }
1785 compose {
1786 for (i in index..3) {
1787 InlineLinear { content() }
1788 }
1789 for (i in 0..index) {
1790 InlineLinear { content() }
1791 }
1792 }
1793 validate {
1794 for (i in index..3) {
1795 Linear { Text("Some text") }
1796 }
1797 for (i in 0..index) {
1798 Linear { Text("Some text") }
1799 }
1800 }
1801
1802 index++
1803 advance()
1804
1805 index++
1806 advance()
1807
1808 index++
1809 advance()
1810 }
1811 }
1812
1813 @Composable
Rownull1814 private fun Row(content: @Composable () -> Unit) {
1815 ComposeNode<View, ViewApplier>(factory = { View().also { it.name = "Row" } }, update = {}) {
1816 content()
1817 }
1818 }
1819
Rownull1820 private fun MockViewValidator.Row(block: MockViewValidator.() -> Unit) {
1821 view("Row", block)
1822 }
1823
1824 @Composable
Columnnull1825 private fun Column(content: @Composable () -> Unit) {
1826 ComposeNode<View, ViewApplier>(factory = { View().also { it.name = "Column" } }, update = {}) {
1827 content()
1828 }
1829 }
1830
Emptynull1831 @Composable private fun Empty() {}
1832
Columnnull1833 private fun MockViewValidator.Column(block: MockViewValidator.() -> Unit) {
1834 view("Column", block)
1835 }
1836
1837 @Composable
Textnull1838 private fun Text(text: String) {
1839 ComposeNode<View, ViewApplier>(
1840 factory = { View().also { it.name = "Text" } },
1841 update = { set(text) { attributes["text"] = it } }
1842 )
1843 }
1844
Textnull1845 private fun MockViewValidator.Text(text: String) {
1846 view("Text")
1847 assertEquals(text, view.attributes["text"])
1848 }
1849
1850 @Composable
Markernull1851 private fun Marker() {
1852 ComposeNode<View, ViewApplier>(factory = { View().also { it.name = "Marker" } }, update = {})
1853 }
1854
Markernull1855 private fun MockViewValidator.Marker() {
1856 view("Marker")
1857 }
1858
1859 @Composable
Markernull1860 private fun Marker(value: Int) {
1861 ComposeNode<View, ViewApplier>(
1862 factory = { View().also { it.name = "Marker" } },
1863 update = { set(value) { attributes["value"] = it } }
1864 )
1865 }
1866
1867 @Composable
Stacknull1868 private fun Stack(isHorizontal: Boolean, block: @Composable () -> Unit) {
1869 if (isHorizontal) {
1870 Column(block)
1871 } else {
1872 Row(block)
1873 }
1874 }
1875
MockViewValidatornull1876 private fun MockViewValidator.Stack(isHorizontal: Boolean, block: MockViewValidator.() -> Unit) {
1877 if (isHorizontal) {
1878 Column(block)
1879 } else {
1880 Row(block)
1881 }
1882 }
1883
1884 @Composable
DisplayIntnull1885 private fun DisplayInt(value: State<Int>) {
1886 Text("value=${value.value}")
1887 }
1888
DisplayIntnull1889 private fun MockViewValidator.DisplayInt(value: State<Int>) {
1890 Text("value=${value.value}")
1891 }
1892
Markernull1893 private fun MockViewValidator.Marker(value: Int) {
1894 view("Marker")
1895 assertEquals(value, view.attributes["value"])
1896 }
1897
1898 @Composable
Subcomposenull1899 private fun Subcompose(content: @Composable () -> Unit) {
1900 val host = View().also { it.name = "SubcomposeHost" }
1901 ComposeNode<View, ViewApplier>(factory = { host }, update = {})
1902 val parent = rememberCompositionContext()
1903 val composition = Composition(ViewApplier(host), parent)
1904 composition.setContent(content)
1905 DisposableEffect(Unit) { onDispose { composition.dispose() } }
1906 }
1907
1908 @Composable
DeferredSubcomposenull1909 private fun DeferredSubcompose(content: @Composable () -> Unit) {
1910 val host = View().also { it.name = "DeferredSubcompose" }
1911 ComposeNode<View, ViewApplier>(factory = { host }, update = {})
1912 val parent = rememberCompositionContext()
1913 val composition = remember { Composition(ViewApplier(host), parent) }
1914 LaunchedEffect(content as Any) { composition.setContent(content) }
1915 DisposableEffect(Unit) { onDispose { composition.dispose() } }
1916 }
1917
Subcomposenull1918 private fun MockViewValidator.Subcompose(content: MockViewValidator.() -> Unit) {
1919 view("SubcomposeHost", content)
1920 }
1921
MockViewValidatornull1922 private fun MockViewValidator.DeferredSubcompose(content: MockViewValidator.() -> Unit) {
1923 view("DeferredSubcompose", content)
1924 }
1925
1926 class RememberedObject : RememberObserver {
1927 var count: Int = 0
1928 val isLive: Boolean
1929 get() = count > 0
1930
1931 private var rememberedCount = 0
1932 private var forgottenCount = 0
1933 private var abandonedCount = 0
1934
1935 private var died: Boolean = false
1936
onRememberednull1937 override fun onRemembered() {
1938 check(!died) { "Remember observer was resurrected" }
1939 rememberedCount++
1940 count++
1941 }
1942
onForgottennull1943 override fun onForgotten() {
1944 check(count > 0) { "Abandoned or forgotten more times than remembered" }
1945 forgottenCount++
1946 count--
1947 if (count == 0) died = true
1948 }
1949
onAbandonednull1950 override fun onAbandoned() {
1951 check(count > 0) { "Abandoned or forgotten more times than remembered" }
1952 abandonedCount++
1953 count--
1954 if (count == 0) died = true
1955 }
1956 }
1957
1958 class SimpleRememberedObject(val name: String) : RememberObserver {
onRememberednull1959 override fun onRemembered() {}
1960
onForgottennull1961 override fun onForgotten() {}
1962
onAbandonednull1963 override fun onAbandoned() {}
1964 }
1965