1 /*
<lambda>null2 * Copyright 2019 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 @file:OptIn(InternalComposeApi::class)
18
19 package androidx.compose.runtime
20
21 import androidx.compose.runtime.mock.Contact
22 import androidx.compose.runtime.mock.ContactModel
23 import androidx.compose.runtime.mock.Edit
24 import androidx.compose.runtime.mock.EmptyApplier
25 import androidx.compose.runtime.mock.InlineLinear
26 import androidx.compose.runtime.mock.Linear
27 import androidx.compose.runtime.mock.MockViewValidator
28 import androidx.compose.runtime.mock.Point
29 import androidx.compose.runtime.mock.Points
30 import androidx.compose.runtime.mock.Repeated
31 import androidx.compose.runtime.mock.Report
32 import androidx.compose.runtime.mock.ReportsReport
33 import androidx.compose.runtime.mock.ReportsTo
34 import androidx.compose.runtime.mock.SelectContact
35 import androidx.compose.runtime.mock.TestMonotonicFrameClock
36 import androidx.compose.runtime.mock.Text
37 import androidx.compose.runtime.mock.View
38 import androidx.compose.runtime.mock.ViewApplier
39 import androidx.compose.runtime.mock.compositionTest
40 import androidx.compose.runtime.mock.contact
41 import androidx.compose.runtime.mock.expectChanges
42 import androidx.compose.runtime.mock.expectNoChanges
43 import androidx.compose.runtime.mock.frameDelayMillis
44 import androidx.compose.runtime.mock.revalidate
45 import androidx.compose.runtime.mock.skip
46 import androidx.compose.runtime.mock.validate
47 import androidx.compose.runtime.snapshots.Snapshot
48 import kotlin.coroutines.CoroutineContext
49 import kotlin.math.min
50 import kotlin.random.Random
51 import kotlin.reflect.KProperty
52 import kotlin.test.Ignore
53 import kotlin.test.Test
54 import kotlin.test.assertEquals
55 import kotlin.test.assertFailsWith
56 import kotlin.test.assertFalse
57 import kotlin.test.assertNotEquals
58 import kotlin.test.assertTrue
59 import kotlin.time.Duration.Companion.seconds
60 import kotlinx.coroutines.CoroutineScope
61 import kotlinx.coroutines.CoroutineStart
62 import kotlinx.coroutines.Dispatchers
63 import kotlinx.coroutines.ExperimentalCoroutinesApi
64 import kotlinx.coroutines.Job
65 import kotlinx.coroutines.channels.Channel
66 import kotlinx.coroutines.channels.consumeEach
67 import kotlinx.coroutines.coroutineScope
68 import kotlinx.coroutines.delay
69 import kotlinx.coroutines.flow.MutableStateFlow
70 import kotlinx.coroutines.launch
71 import kotlinx.coroutines.test.TestCoroutineScheduler
72 import kotlinx.coroutines.test.TestScope
73 import kotlinx.coroutines.test.runTest
74 import kotlinx.coroutines.withContext
75
76 @Composable fun Container(content: @Composable () -> Unit) = content()
77
78 @Stable
79 @OptIn(InternalComposeApi::class)
80 @Suppress("unused")
81 class CompositionTests {
82 @Test
83 fun simple() = compositionTest {
84 compose { Text("Hello!") }
85
86 validate { Text("Hello!") }
87 }
88
89 @Test
90 fun simpleChanges() = compositionTest {
91 var name by mutableStateOf("Bob")
92 compose { Text("Hello $name") }
93
94 validate { Text("Hello $name") }
95
96 name = "Robert"
97
98 expectChanges()
99
100 validate { Text("Hello $name") }
101 }
102
103 @Test
104 fun testComposeAModel() = compositionTest {
105 val model = testModel()
106 compose { SelectContact(model) }
107
108 validate {
109 Linear {
110 Linear {
111 Text("Filter:")
112 Edit("")
113 }
114 Linear {
115 Text(value = "Contacts:")
116 Linear {
117 contact(bob)
118 contact(jon)
119 contact(steve)
120 }
121 }
122 }
123 }
124 }
125
126 @Test
127 fun testRecomposeWithoutChanges() = compositionTest {
128 val model = testModel()
129 compose { SelectContact(model) }
130
131 expectNoChanges()
132
133 validate { SelectContact(model) }
134 }
135
136 @Test
137 fun testInsertAContact() = compositionTest {
138 val model = testModel(mutableListOf(bob, jon))
139 var scope: RecomposeScope? = null
140
141 compose {
142 scope = currentRecomposeScope
143 SelectContact(model)
144 }
145
146 validate {
147 Linear {
148 skip()
149 Linear {
150 skip()
151 Linear {
152 contact(bob)
153 contact(jon)
154 }
155 }
156 }
157 }
158
159 model.add(steve, after = bob)
160 scope?.invalidate()
161 expectChanges()
162
163 validate {
164 Linear {
165 skip()
166 Linear {
167 skip()
168 Linear {
169 contact(bob)
170 contact(steve)
171 contact(jon)
172 }
173 }
174 }
175 }
176 }
177
178 @Test
179 fun testMoveAContact() = compositionTest {
180 val model = testModel(mutableListOf(bob, steve, jon))
181 var scope: RecomposeScope? = null
182
183 compose {
184 scope = currentRecomposeScope
185 SelectContact(model)
186 }
187
188 model.move(steve, after = jon)
189 scope?.invalidate()
190 expectChanges()
191
192 validate {
193 Linear {
194 skip()
195 Linear {
196 skip()
197 Linear {
198 contact(bob)
199 contact(jon)
200 contact(steve)
201 }
202 }
203 }
204 }
205 }
206
207 @Test
208 fun testSeveralArbitraryMoves() = compositionTest {
209 val random = Random(1337)
210 val list = mutableStateListOf(*List(10) { it }.toTypedArray())
211
212 var position by mutableStateOf(-1)
213
214 compose {
215 for (item in list) {
216 key(item) { Text("Item $item") }
217 }
218 }
219
220 fun validate() {
221 validate {
222 for (item in list) {
223 Text("Item $item")
224 }
225 }
226 }
227
228 validate()
229
230 repeat(10) {
231 position = it
232 list.shuffle(random)
233 expectChanges()
234 validate()
235 list.first()
236 }
237
238 position = -1
239 list.shuffle(random)
240 expectChanges()
241 validate()
242 }
243
244 @Test
245 fun testChangeTheFilter() = compositionTest {
246 val model = testModel(mutableListOf(bob, steve, jon))
247 var scope: RecomposeScope? = null
248
249 compose {
250 scope = currentRecomposeScope
251 SelectContact(model)
252 }
253
254 model.filter = "Jon"
255 scope?.invalidate()
256 expectChanges()
257
258 validate {
259 Linear {
260 skip()
261 Linear {
262 skip()
263 Linear { contact(jon) }
264 }
265 }
266 }
267 }
268
269 @Test
270 fun testConditionalNode() = compositionTest {
271 val condition = mutableStateOf(false)
272
273 compose {
274 InlineLinear {
275 Text("A")
276 Wrap {
277 if (condition.value) {
278 InlineLinear { Text("B") }
279 }
280 }
281 }
282 }
283
284 validate {
285 Linear {
286 Text("A")
287 if (condition.value) {
288 Linear { Text("B") }
289 }
290 }
291 }
292
293 repeat(10) {
294 condition.value = !condition.value
295 advance()
296 revalidate()
297 }
298 }
299
300 @Test
301 fun testComposeCompositionWithMultipleRoots() = compositionTest {
302 val reports = listOf(jim_reports_to_sally, rob_reports_to_alice, clark_reports_to_lois)
303
304 compose { ReportsReport(reports) }
305
306 validate { ReportsReport(reports) }
307 }
308
309 @Test
310 fun testMoveCompositionWithMultipleRoots() = compositionTest {
311 var reports = listOf(jim_reports_to_sally, rob_reports_to_alice, clark_reports_to_lois)
312 var scope: RecomposeScope? = null
313 compose {
314 scope = currentRecomposeScope
315 ReportsReport(reports)
316 }
317
318 reports = listOf(jim_reports_to_sally, clark_reports_to_lois, rob_reports_to_alice)
319 scope?.invalidate()
320 expectChanges()
321
322 validate { ReportsReport(reports) }
323 }
324
325 @Test
326 fun testReplace() = compositionTest {
327 var includeA = true
328 var scope: RecomposeScope? = null
329
330 @Composable
331 fun Composition() {
332 scope = currentRecomposeScope
333 Text("Before")
334 if (includeA) {
335 Linear { Text("A") }
336 } else {
337 Edit("B")
338 }
339 Text("After")
340 }
341
342 fun MockViewValidator.Composition() {
343 Text("Before")
344 if (includeA) {
345 Linear { Text("A") }
346 } else {
347 Edit("B")
348 }
349 Text("After")
350 }
351
352 compose { Composition() }
353
354 validate { this.Composition() }
355
356 includeA = false
357 scope?.invalidate()
358 expectChanges()
359 validate { this.Composition() }
360 includeA = true
361 scope?.invalidate()
362 expectChanges()
363 validate { this.Composition() }
364 scope?.invalidate()
365 expectNoChanges()
366 }
367
368 @Test
369 fun testInsertWithMultipleRoots() = compositionTest {
370 var chars = listOf('a', 'b', 'c')
371 var scope: RecomposeScope? = null
372
373 @Composable
374 fun TextOf(c: Char) {
375 Text(c.toString())
376 }
377
378 fun MockViewValidator.TextOf(c: Char) {
379 Text(c.toString())
380 }
381
382 @Composable
383 fun Chars(chars: Iterable<Char>) {
384 Repeated(of = chars) { c -> TextOf(c) }
385 }
386
387 fun MockViewValidator.validateChars(chars: Iterable<Char>) {
388 Repeated(of = chars) { c -> this.TextOf(c) }
389 }
390
391 compose {
392 scope = currentRecomposeScope
393 Chars(chars)
394 Chars(chars)
395 Chars(chars)
396 }
397
398 validate {
399 validateChars(chars)
400 validateChars(chars)
401 validateChars(chars)
402 }
403
404 chars = listOf('a', 'b', 'x', 'c')
405 scope?.invalidate()
406 expectChanges()
407
408 validate {
409 validateChars(chars)
410 validateChars(chars)
411 validateChars(chars)
412 }
413 }
414
415 @Test
416 fun testSimpleSkipping() = compositionTest {
417 val points = listOf(Point(1, 2), Point(2, 3))
418 var scope: RecomposeScope? = null
419 compose {
420 scope = currentRecomposeScope
421 Points(points)
422 }
423
424 validate { Points(points) }
425
426 scope?.invalidate()
427 expectNoChanges()
428 }
429
430 @Test
431 fun testMovingMemoization() = compositionTest {
432 var points = listOf(Point(1, 2), Point(2, 3), Point(4, 5), Point(6, 7))
433 var scope: RecomposeScope? = null
434 compose {
435 scope = currentRecomposeScope
436 Points(points)
437 }
438
439 validate { Points(points) }
440
441 points = listOf(Point(1, 2), Point(4, 5), Point(2, 3), Point(6, 7))
442 scope?.invalidate()
443 expectChanges()
444
445 validate { Points(points) }
446 }
447
448 @Test
449 fun testComponent() = compositionTest {
450 @Composable
451 fun Reporter(report: Report? = null) {
452 if (report != null) {
453 Text(report.from)
454 Text("reports to")
455 Text(report.to)
456 } else {
457 Text("no report to report")
458 }
459 }
460
461 @Composable
462 fun ReportsReport(reports: Iterable<Report>) {
463 Linear { Repeated(of = reports) { report -> Reporter(report) } }
464 }
465
466 val reports = listOf(jim_reports_to_sally, rob_reports_to_alice, clark_reports_to_lois)
467 compose { ReportsReport(reports) }
468
469 validate {
470 Linear {
471 ReportsTo(jim_reports_to_sally)
472 ReportsTo(rob_reports_to_alice)
473 ReportsTo(clark_reports_to_lois)
474 }
475 }
476
477 expectNoChanges()
478 }
479
480 @Test
481 fun testComposeTwoAttributeComponent() = compositionTest {
482 @Composable
483 fun Two2(first: Int = 1, second: Int = 2) {
484 Linear { Text("$first $second") }
485 }
486
487 fun MockViewValidator.two(first: Int, second: Int) {
488 Linear { Text("$first $second") }
489 }
490
491 compose { Two2(41, 42) }
492
493 validate { two(41, 42) }
494 }
495
496 @Test
497 fun testComposeThreeAttributeComponent() = compositionTest {
498 @Composable
499 fun Three3(first: Int = 1, second: Int = 2, third: Int = 3) {
500 Linear { Text("$first $second $third") }
501 }
502
503 fun MockViewValidator.Three(first: Int, second: Int, third: Int) {
504 Linear { Text("$first $second $third") }
505 }
506
507 compose { Three3(41, 42, 43) }
508
509 validate { Three(41, 42, 43) }
510 }
511
512 @Test
513 fun testComposeFourOrMoreAttributeComponent() = compositionTest {
514 @Composable
515 fun Four4(first: Int = 1, second: Int = 2, third: Int = 3, fourth: Int = 4) {
516 Linear { Text("$first $second $third $fourth") }
517 }
518
519 fun MockViewValidator.Four(first: Int, second: Int, third: Int, fourth: Int) {
520 Linear { Text("$first $second $third $fourth") }
521 }
522
523 compose { Four4(41, 42, 43, 44) }
524
525 validate { Four(41, 42, 43, 44) }
526 }
527
528 @Test
529 fun testSkippingACall() = compositionTest {
530 @Composable
531 fun Show(value: Int) {
532 Linear { Text("$value") }
533 Linear { Text("value") }
534 }
535
536 fun MockViewValidator.Show(value: Int) {
537 Linear { Text("$value") }
538 Linear { Text("value") }
539 }
540
541 @Composable
542 fun Test(showThree: Boolean) {
543 Show(1)
544 Show(2)
545 if (showThree) {
546 Show(3)
547 }
548 }
549
550 var showThree = false
551
552 var scope: RecomposeScope? = null
553
554 @Composable
555 fun Test() {
556 scope = currentRecomposeScope
557 Test(showThree)
558 }
559
560 fun MockViewValidator.Test(showThree: Boolean) {
561 this.Show(1)
562 this.Show(2)
563 if (showThree) {
564 this.Show(3)
565 }
566 }
567
568 fun validate() {
569 validate { this.Test(showThree) }
570 }
571
572 compose { Test() }
573
574 validate()
575
576 showThree = true
577 scope?.invalidate()
578 expectChanges()
579 validate()
580 }
581
582 @Test
583 fun testSkippingNestedLambda() = compositionTest {
584 val data = mutableStateOf(0)
585
586 itemRendererCalls = 0
587 scrollingListCalls = 0
588 compose { TestSkippingContent(data = data) }
589
590 data.value++
591 advance()
592
593 data.value++
594 advance()
595
596 data.value++
597 advance()
598
599 assertTrue(itemRendererCalls < scrollingListCalls)
600 }
601
602 @Test
603 fun testComponentWithVarConstructorParameter() = compositionTest {
604 @Composable
605 fun One(first: Int) {
606 Text("$first")
607 }
608
609 fun MockViewValidator.One(first: Int) {
610 Text("$first")
611 }
612
613 @Composable
614 fun CallOne(value: Int) {
615 One(first = value)
616 }
617
618 var value = 42
619 var scope: RecomposeScope? = null
620 compose {
621 scope = currentRecomposeScope
622 CallOne(value)
623 }
624
625 validate { this.One(42) }
626
627 value = 43
628 scope?.invalidate()
629 expectChanges()
630
631 validate { this.One(43) }
632 }
633
634 @Test
635 fun testComponentWithValConstructorParameter() = compositionTest {
636 @Composable
637 fun One(first: Int) {
638 Text("$first")
639 }
640
641 fun MockViewValidator.One(first: Int) {
642 Text("$first")
643 }
644
645 @Composable
646 fun CallOne(value: Int) {
647 One(first = value)
648 }
649
650 var value = 42
651 var scope: RecomposeScope? = null
652 compose {
653 scope = currentRecomposeScope
654 CallOne(value)
655 }
656
657 validate { this.One(42) }
658
659 value = 43
660 scope?.invalidate()
661 expectChanges()
662
663 validate { this.One(43) }
664
665 scope?.invalidate()
666 expectNoChanges()
667 }
668
669 @Test
670 fun testComposePartOfTree() = compositionTest {
671 var loisScope: RecomposeScope? = null
672
673 @Composable
674 fun Reporter(report: Report? = null) {
675 if (report != null) {
676 if (report.from == "Lois" || report.to == "Lois") loisScope = currentRecomposeScope
677 Text(report.from)
678 Text("reports to")
679 Text(report.to)
680 } else {
681 Text("no report to report")
682 }
683 }
684
685 @Composable
686 fun ReportsReport(reports: Iterable<Report>) {
687 Linear { Repeated(of = reports) { report -> Reporter(report) } }
688 }
689
690 val r = Report("Lois", "Perry")
691 val reports = listOf(jim_reports_to_sally, rob_reports_to_alice, clark_reports_to_lois, r)
692 compose { ReportsReport(reports) }
693
694 validate {
695 Linear {
696 ReportsTo(jim_reports_to_sally)
697 ReportsTo(rob_reports_to_alice)
698 ReportsTo(clark_reports_to_lois)
699 ReportsTo(r)
700 }
701 }
702
703 expectNoChanges()
704
705 // Demote Perry
706 r.from = "Perry"
707 r.to = "Lois"
708
709 // Compose only the Lois report
710 loisScope?.invalidate()
711
712 expectChanges()
713
714 validate {
715 Linear {
716 ReportsTo(jim_reports_to_sally)
717 ReportsTo(rob_reports_to_alice)
718 ReportsTo(clark_reports_to_lois)
719 ReportsTo(r)
720 }
721 }
722 }
723
724 @Test
725 fun testRecomposeWithReplace() = compositionTest {
726 var loisScope: RecomposeScope? = null
727 var key = 0
728
729 @Composable
730 fun Reporter(report: Report? = null) {
731 if (report != null) {
732 if (report.from == "Lois" || report.to == "Lois") loisScope = currentRecomposeScope
733 key(key) {
734 Text(report.from)
735 Text("reports to")
736 Text(report.to)
737 }
738 } else {
739 Text("no report to report")
740 }
741 }
742
743 @Composable
744 fun ReportsReport(reports: Iterable<Report>) {
745 Linear { Repeated(of = reports) { report -> Reporter(report) } }
746 }
747
748 val r = Report("Lois", "Perry")
749 val reports = listOf(jim_reports_to_sally, rob_reports_to_alice, clark_reports_to_lois, r)
750 compose { ReportsReport(reports) }
751
752 validate {
753 Linear {
754 ReportsTo(jim_reports_to_sally)
755 ReportsTo(rob_reports_to_alice)
756 ReportsTo(clark_reports_to_lois)
757 ReportsTo(r)
758 }
759 }
760
761 expectNoChanges()
762
763 // Demote Perry
764 r.from = "Perry"
765 r.to = "Lois"
766
767 // Cause a new group to be generated in the component
768 key = 2
769
770 // Compose only the Lois report
771 loisScope?.invalidate()
772
773 expectChanges()
774
775 validate {
776 Linear {
777 ReportsTo(jim_reports_to_sally)
778 ReportsTo(rob_reports_to_alice)
779 ReportsTo(clark_reports_to_lois)
780 ReportsTo(r)
781 }
782 }
783 }
784
785 @Test
786 fun testInvalidationAfterRemoval() = compositionTest {
787 var loisScope: RecomposeScope? = null
788 val key = 0
789
790 @Composable
791 fun Reporter(report: Report? = null) {
792 if (report != null) {
793 val scope = currentRecomposeScope
794 if (report.from == "Lois" || report.to == "Lois") loisScope = scope
795 key(key) {
796 Text(report.from)
797 Text("reports to")
798 Text(report.to)
799 }
800 } else {
801 Text("no report to report")
802 }
803 }
804
805 @Composable
806 fun ReportsReport(reports: Iterable<Report>, include: (report: Report) -> Boolean) {
807 Linear {
808 Repeated(of = reports) { report ->
809 if (include(report)) {
810 Reporter(report)
811 }
812 }
813 }
814 }
815
816 val r = Report("Lois", "Perry")
817 val reports = listOf(jim_reports_to_sally, rob_reports_to_alice, clark_reports_to_lois, r)
818 val all: (report: Report) -> Boolean = { true }
819 val notLois: (report: Report) -> Boolean = { it.from != "Lois" && it.to != "Lois" }
820
821 var filter = all
822 var scope: RecomposeScope? = null
823 compose {
824 scope = currentRecomposeScope
825 ReportsReport(reports, filter)
826 }
827
828 validate {
829 Linear {
830 ReportsTo(jim_reports_to_sally)
831 ReportsTo(rob_reports_to_alice)
832 ReportsTo(clark_reports_to_lois)
833 ReportsTo(r)
834 }
835 }
836
837 filter = notLois
838 scope?.invalidate()
839 expectChanges()
840
841 validate {
842 Linear {
843 ReportsTo(jim_reports_to_sally)
844 ReportsTo(rob_reports_to_alice)
845 }
846 }
847
848 // Invalidate Lois which is now removed.
849 loisScope?.invalidate()
850 expectNoChanges()
851
852 validate {
853 Linear {
854 ReportsTo(jim_reports_to_sally)
855 ReportsTo(rob_reports_to_alice)
856 }
857 }
858 }
859
860 // remember()
861
862 @Test
863 fun testSimpleRemember() = compositionTest {
864 var count = 0
865 var scope: RecomposeScope? = null
866
867 class Wrapper(val value: Int) {
868 init {
869 count++
870 }
871 }
872
873 @Composable
874 fun Test(value: Int) {
875 scope = currentRecomposeScope
876 val w = remember { Wrapper(value) }
877 Text("value = ${w.value}")
878 }
879
880 fun MockViewValidator.Test(value: Int) {
881 Text("value = $value")
882 }
883
884 compose { Test(1) }
885
886 validate { this.Test(1) }
887
888 assertEquals(1, count)
889
890 scope?.invalidate()
891 expectNoChanges()
892
893 // Expect the previous instance to be remembered
894 assertEquals(1, count)
895 }
896
897 @Test
898 fun testRememberOneParameter() = compositionTest {
899 var count = 0
900
901 class Wrapper(val value: Int) {
902 init {
903 count++
904 }
905 }
906
907 @Composable
908 fun Test(value: Int) {
909 val w = remember(value) { Wrapper(value) }
910 Text("value = ${w.value}")
911 }
912
913 fun MockViewValidator.Test(value: Int) {
914 Text("value = $value")
915 }
916
917 var value = 1
918 var scope: RecomposeScope? = null
919 compose {
920 scope = currentRecomposeScope
921 Test(value)
922 }
923
924 validate { this.Test(1) }
925
926 value = 2
927 scope?.invalidate()
928 expectChanges()
929
930 validate { this.Test(2) }
931
932 scope?.invalidate()
933 expectNoChanges()
934
935 validate { this.Test(2) }
936
937 assertEquals(2, count)
938 }
939
940 @Test
941 fun testRememberTwoParameters() = compositionTest {
942 var count = 0
943
944 class Wrapper(val a: Int, val b: Int) {
945 init {
946 count++
947 }
948 }
949
950 @Composable
951 fun Test(a: Int, b: Int) {
952 val w = remember(a, b) { Wrapper(a, b) }
953 Text("a = ${w.a} b = ${w.b}")
954 }
955
956 fun MockViewValidator.Test(a: Int, b: Int) {
957 Text("a = $a b = $b")
958 }
959
960 var p1 = 1
961 var p2 = 2
962 var scope: RecomposeScope? = null
963
964 compose {
965 scope = currentRecomposeScope
966 Test(p1, p2)
967 }
968
969 validate { this.Test(1, 2) }
970
971 p1 = 2
972 p2 = 3
973 scope?.invalidate()
974 expectChanges()
975
976 validate { this.Test(2, 3) }
977
978 scope?.invalidate()
979 expectNoChanges()
980
981 validate { this.Test(2, 3) }
982
983 assertEquals(2, count)
984 }
985
986 @Test
987 fun testRememberThreeParameters() = compositionTest {
988 var count = 0
989
990 class Wrapper(val a: Int, val b: Int, val c: Int) {
991 init {
992 count++
993 }
994 }
995
996 @Composable
997 fun Test(a: Int, b: Int, c: Int) {
998 val w = remember(a, b, c) { Wrapper(a, b, c) }
999 Text("a = ${w.a} b = ${w.b} c = ${w.c}")
1000 }
1001
1002 fun MockViewValidator.Test(a: Int, b: Int, c: Int) {
1003 Text("a = $a b = $b c = $c")
1004 }
1005
1006 var p3 = 3
1007 var scope: RecomposeScope? = null
1008 compose {
1009 scope = currentRecomposeScope
1010 Test(1, 2, p3)
1011 }
1012
1013 validate { this.Test(1, 2, 3) }
1014
1015 p3 = 4
1016 scope?.invalidate()
1017 expectChanges()
1018
1019 validate { this.Test(1, 2, 4) }
1020
1021 scope?.invalidate()
1022 expectNoChanges()
1023
1024 validate { this.Test(1, 2, 4) }
1025
1026 assertEquals(2, count)
1027 }
1028
1029 @Test
1030 fun testRememberFourParameters() = compositionTest {
1031 var count = 0
1032
1033 class Wrapper(val a: Int, val b: Int, val c: Int, val d: Int) {
1034 init {
1035 count++
1036 }
1037 }
1038
1039 @Composable
1040 fun Test(a: Int, b: Int, c: Int, d: Int) {
1041 val w = remember(a, b, c, d) { Wrapper(a, b, c, d) }
1042 Text("a = ${w.a} b = ${w.b} c = ${w.c} d = ${w.d}")
1043 }
1044
1045 fun MockViewValidator.Test(a: Int, b: Int, c: Int, d: Int) {
1046 Text("a = $a b = $b c = $c d = $d")
1047 }
1048
1049 var p3 = 3
1050 var p4 = 4
1051 var scope: RecomposeScope? = null
1052
1053 compose {
1054 scope = currentRecomposeScope
1055 Test(1, 2, p3, p4)
1056 }
1057
1058 validate { this.Test(1, 2, 3, 4) }
1059
1060 p3 = 4
1061 p4 = 5
1062 scope?.invalidate()
1063 expectChanges()
1064
1065 validate { this.Test(1, 2, 4, 5) }
1066
1067 scope?.invalidate()
1068 expectNoChanges()
1069
1070 validate { this.Test(1, 2, 4, 5) }
1071
1072 assertEquals(2, count)
1073 }
1074
1075 @Test
1076 fun testRememberFiveParameters() = compositionTest {
1077 var count = 0
1078
1079 class Wrapper(val a: Int, val b: Int, val c: Int, val d: Int, val e: Int) {
1080 init {
1081 count++
1082 }
1083 }
1084
1085 @Composable
1086 fun Test(a: Int, b: Int, c: Int, d: Int, e: Int) {
1087 val w = remember(a, b, c, d, e) { Wrapper(a, b, c, d, e) }
1088 Text("a = ${w.a} b = ${w.b} c = ${w.c} d = ${w.d} e = ${w.e}")
1089 }
1090
1091 fun MockViewValidator.Test(a: Int, b: Int, c: Int, d: Int, e: Int) {
1092 Text("a = $a b = $b c = $c d = $d e = $e")
1093 }
1094
1095 var lastParameter = 5
1096 var scope: RecomposeScope? = null
1097 compose {
1098 scope = currentRecomposeScope
1099 Test(1, 2, 3, 4, lastParameter)
1100 }
1101
1102 validate { this.Test(1, 2, 3, 4, 5) }
1103
1104 lastParameter = 6
1105 scope?.invalidate()
1106
1107 expectChanges()
1108
1109 validate { this.Test(1, 2, 3, 4, 6) }
1110
1111 expectNoChanges()
1112
1113 validate { this.Test(1, 2, 3, 4, 6) }
1114
1115 assertEquals(2, count)
1116 }
1117
1118 @Test
1119 fun testInsertGroupInContainer() = compositionTest {
1120 val values = mutableStateListOf(0)
1121
1122 @Composable
1123 fun Composition() {
1124 Linear {
1125 for (value in values) {
1126 Text("$value")
1127 }
1128 }
1129 }
1130
1131 fun MockViewValidator.Composition() {
1132 Linear { for (value in values) Text("$value") }
1133 }
1134
1135 compose { Composition() }
1136
1137 validate { this.Composition() }
1138
1139 for (i in 1..10) {
1140 values.add(i)
1141 expectChanges()
1142 validate { this.Composition() }
1143 }
1144 }
1145
1146 // b/148273328
1147 @Test
1148 fun testInsertInGroups() = compositionTest {
1149 var threeVisible by mutableStateOf(false)
1150
1151 @Composable
1152 fun Composition() {
1153 Linear {
1154 Text("one")
1155 Text("two")
1156 if (threeVisible) {
1157 Text("three")
1158 Text("four")
1159 }
1160 Linear { Text("five") }
1161 }
1162 }
1163
1164 fun MockViewValidator.Composition() {
1165 Linear {
1166 Text("one")
1167 Text("two")
1168 if (threeVisible) {
1169 Text("three")
1170 Text("four")
1171 }
1172 Linear { Text("five") }
1173 }
1174 }
1175
1176 compose { Composition() }
1177 validate { this.Composition() }
1178
1179 threeVisible = true
1180 expectChanges()
1181
1182 validate { this.Composition() }
1183 }
1184
1185 @Test
1186 fun testStartJoin() = compositionTest {
1187 var text by mutableStateOf("Starting")
1188
1189 @Composable
1190 fun Composition() {
1191 Linear { Text(text) }
1192 }
1193
1194 fun MockViewValidator.Composition() {
1195 Linear { Text(text) }
1196 }
1197
1198 compose { Composition() }
1199
1200 validate { this.Composition() }
1201
1202 text = "Ending"
1203
1204 expectChanges()
1205
1206 validate { this.Composition() }
1207 }
1208
1209 @Test
1210 fun testInvalidateJoin_End() = compositionTest {
1211 var text by mutableStateOf("Starting")
1212 var includeNested by mutableStateOf(true)
1213
1214 @Composable
1215 fun Composition() {
1216 Linear {
1217 Text(text)
1218 if (includeNested) {
1219 Text("Nested in $text")
1220 }
1221 }
1222 }
1223
1224 fun MockViewValidator.Composition() {
1225 Linear {
1226 Text(text)
1227 if (includeNested) {
1228 Text("Nested in $text")
1229 }
1230 }
1231 }
1232
1233 compose { Composition() }
1234
1235 validate { this.Composition() }
1236
1237 text = "Ending"
1238 includeNested = false
1239
1240 expectChanges()
1241
1242 validate { this.Composition() }
1243
1244 expectNoChanges()
1245
1246 validate { this.Composition() }
1247 }
1248
1249 @Test
1250 fun testInvalidateJoin_Start() = compositionTest {
1251 var text by mutableStateOf("Starting")
1252 var includeNested by mutableStateOf(true)
1253
1254 @Composable
1255 fun Composition() {
1256 Linear {
1257 if (includeNested) {
1258 Text("Nested in $text")
1259 }
1260 Text(text)
1261 }
1262 }
1263
1264 fun MockViewValidator.Composition() {
1265 Linear {
1266 if (includeNested) {
1267 Text("Nested in $text")
1268 }
1269 Text(text)
1270 }
1271 }
1272
1273 compose { Composition() }
1274
1275 validate { this.Composition() }
1276
1277 text = "Ending"
1278 includeNested = false
1279
1280 expectChanges()
1281
1282 validate { this.Composition() }
1283
1284 expectNoChanges()
1285
1286 validate { this.Composition() }
1287 }
1288
1289 // b/132638679
1290 @Test
1291 fun testJoinInvalidate() = compositionTest {
1292 var texts = 5
1293 var outerScope: RecomposeScope? = null
1294 var innerScope: RecomposeScope? = null
1295 var forceInvalidate = false
1296
1297 @Composable
1298 fun Composition() {
1299 Linear {
1300 outerScope = currentRecomposeScope
1301 for (i in 1..texts) {
1302 Text("Some text")
1303 }
1304
1305 Container {
1306 Text("Some text")
1307
1308 // Force the invalidation to survive the compose
1309 val innerInvalidate = currentRecomposeScope
1310 innerScope = innerInvalidate
1311 if (forceInvalidate) {
1312 innerInvalidate.invalidate()
1313 forceInvalidate = false
1314 }
1315 }
1316 }
1317 }
1318
1319 compose { Composition() }
1320
1321 texts = 4
1322 outerScope?.invalidate()
1323 innerScope?.invalidate()
1324 forceInvalidate = true
1325 expectChanges()
1326
1327 texts = 3
1328 outerScope?.invalidate()
1329 forceInvalidate = true
1330 expectChanges()
1331
1332 expectNoChanges()
1333 }
1334
1335 @Test
1336 fun testRememberObserver_Remember_Simple() = compositionTest {
1337 val rememberedObject =
1338 object : RememberObserver {
1339 var count = 0
1340
1341 override fun onRemembered() {
1342 count++
1343 }
1344
1345 override fun onForgotten() {
1346 count--
1347 }
1348
1349 override fun onAbandoned() {
1350 assertEquals(0, count, "onRemember called on an abandon object")
1351 }
1352 }
1353
1354 var scope: RecomposeScope? = null
1355
1356 @Composable
1357 fun Composition() {
1358 Linear {
1359 scope = currentRecomposeScope
1360 remember { rememberedObject }
1361 Text("Some text")
1362 }
1363 }
1364
1365 fun MockViewValidator.Composition() {
1366 Linear { Text("Some text") }
1367 }
1368
1369 compose { Composition() }
1370 validate { this.Composition() }
1371
1372 assertEquals(1, rememberedObject.count, "object should have been notified of a remember")
1373
1374 scope?.invalidate()
1375 expectNoChanges()
1376 validate { this.Composition() }
1377
1378 assertEquals(1, rememberedObject.count, "Object should have only been notified once")
1379 }
1380
1381 @Test
1382 fun testRememberObserver_Remember_SingleNotification() = compositionTest {
1383 val rememberedObject =
1384 object : RememberObserver {
1385 var count = 0
1386
1387 override fun onRemembered() {
1388 count++
1389 }
1390
1391 override fun onForgotten() {
1392 count--
1393 }
1394
1395 override fun onAbandoned() {
1396 assertEquals(0, count, "onRemember called on an abandon object")
1397 }
1398 }
1399
1400 var value by mutableStateOf(0)
1401 @Composable
1402 fun Composition() {
1403 Linear {
1404 val l = remember { rememberedObject }
1405 assertEquals(rememberedObject, l, "remembered object should be returned")
1406 Text("Some text $value")
1407 }
1408 Linear {
1409 val l = remember { rememberedObject }
1410 assertEquals(rememberedObject, l, "remembered object should be returned")
1411 Text("Some other text $value")
1412 }
1413 }
1414
1415 fun MockViewValidator.Composition() {
1416 Linear { Text("Some text $value") }
1417 Linear { Text("Some other text $value") }
1418 }
1419
1420 compose { Composition() }
1421 validate { this.Composition() }
1422
1423 assertEquals(2, rememberedObject.count, "object should have been notified remembered twice")
1424
1425 value++
1426 expectChanges()
1427 validate { this.Composition() }
1428
1429 assertEquals(2, rememberedObject.count, "Object should have only been notified twice")
1430 }
1431
1432 @Test
1433 fun testRememberObserver_Forget_Simple() = compositionTest {
1434 val rememberObject =
1435 object : RememberObserver {
1436 var count = 0
1437
1438 override fun onRemembered() {
1439 count++
1440 }
1441
1442 override fun onForgotten() {
1443 count--
1444 }
1445
1446 override fun onAbandoned() {
1447 assertEquals(0, count, "onRemember called on an abandon object")
1448 }
1449 }
1450
1451 @Composable
1452 fun Composition(includeRememberObject: Boolean) {
1453 Linear {
1454 if (includeRememberObject) {
1455 Linear {
1456 val l = remember { rememberObject }
1457 assertEquals(rememberObject, l, "Remember object should be returned")
1458 Text("Some text")
1459 }
1460 }
1461 }
1462 }
1463
1464 fun MockViewValidator.Composition(includeRememberObject: Boolean) {
1465 Linear {
1466 if (includeRememberObject) {
1467 Linear { Text("Some text") }
1468 }
1469 }
1470 }
1471
1472 var scope: RecomposeScope? = null
1473 var value = true
1474 compose {
1475 scope = currentRecomposeScope
1476 Composition(value)
1477 }
1478 validate { this.Composition(true) }
1479
1480 assertEquals(1, rememberObject.count, "object should have been notified of a remember")
1481
1482 scope?.invalidate()
1483 expectNoChanges()
1484 validate { this.Composition(true) }
1485
1486 assertEquals(1, rememberObject.count, "Object should have only been notified once")
1487
1488 value = false
1489 scope?.invalidate()
1490 expectChanges()
1491 validate { this.Composition(false) }
1492
1493 assertEquals(0, rememberObject.count, "Object should have been notified of a forget")
1494 }
1495
1496 @Test
1497 fun testRemember_Forget_ForgetOnRemember() = compositionTest {
1498 var expectedRemember = true
1499 var expectedForget = true
1500 val rememberObject =
1501 object : RememberObserver {
1502 var count = 0
1503
1504 override fun onRemembered() {
1505 val remembered = count++ == 0
1506 assertTrue(remembered && expectedRemember, "No remember expected")
1507 }
1508
1509 override fun onForgotten() {
1510 val forgotten = --count == 0
1511 assertTrue(forgotten && expectedForget, "No forget expected")
1512 }
1513
1514 override fun onAbandoned() {
1515 assertEquals(0, count, "onAbandon called after onRemember")
1516 }
1517 }
1518
1519 @Composable
1520 fun Composition(a: Boolean, b: Boolean, c: Boolean) {
1521 Linear {
1522 if (a) {
1523 key(1) {
1524 Linear {
1525 val l = remember { rememberObject }
1526 assertEquals(rememberObject, l, "Lifecycle object should be returned")
1527 Text("a")
1528 }
1529 }
1530 }
1531 if (b) {
1532 key(2) {
1533 Linear {
1534 val l = remember { rememberObject }
1535 assertEquals(rememberObject, l, "Lifecycle object should be returned")
1536 Text("b")
1537 }
1538 }
1539 }
1540 if (c) {
1541 key(3) {
1542 Linear {
1543 val l = remember { rememberObject }
1544 assertEquals(rememberObject, l, "Lifecycle object should be returned")
1545 Text("c")
1546 }
1547 }
1548 }
1549 }
1550 }
1551
1552 fun MockViewValidator.Composition(a: Boolean, b: Boolean, c: Boolean) {
1553 Linear {
1554 if (a) {
1555 Linear { Text("a") }
1556 }
1557 if (b) {
1558 Linear { Text("b") }
1559 }
1560 if (c) {
1561 Linear { Text("c") }
1562 }
1563 }
1564 }
1565
1566 expectedRemember = true
1567 expectedForget = false
1568
1569 var a = true
1570 var b = false
1571 var c = false
1572 var scope: RecomposeScope? = null
1573 compose {
1574 scope = currentRecomposeScope
1575 Composition(a = a, b = b, c = c)
1576 }
1577 validate { this.Composition(a = true, b = false, c = false) }
1578
1579 assertEquals(1, rememberObject.count, "object should have been notified of an enter")
1580
1581 expectedRemember = false
1582 expectedForget = false
1583 scope?.invalidate()
1584 expectNoChanges()
1585 validate { this.Composition(a = true, b = false, c = false) }
1586 assertEquals(1, rememberObject.count, "Object should have only been notified once")
1587
1588 expectedRemember = true
1589 expectedForget = true
1590 a = false
1591 b = true
1592 c = false
1593 scope?.invalidate()
1594 expectChanges()
1595 validate { this.Composition(a = false, b = true, c = false) }
1596 assertEquals(1, rememberObject.count, "No enter or leaves")
1597
1598 expectedRemember = true
1599 expectedForget = true
1600 a = false
1601 b = false
1602 c = true
1603 scope?.invalidate()
1604 expectChanges()
1605 validate { this.Composition(a = false, b = false, c = true) }
1606 assertEquals(1, rememberObject.count, "No enter or leaves")
1607
1608 expectedRemember = true
1609 expectedForget = true
1610 a = true
1611 b = false
1612 c = false
1613 scope?.invalidate()
1614 expectChanges()
1615 validate { this.Composition(a = true, b = false, c = false) }
1616 assertEquals(1, rememberObject.count, "No enter or leaves")
1617
1618 expectedRemember = false
1619 expectedForget = true
1620 a = false
1621 b = false
1622 c = false
1623 scope?.invalidate()
1624 expectChanges()
1625 validate { this.Composition(a = false, b = false, c = false) }
1626 assertEquals(0, rememberObject.count, "A leave")
1627 }
1628
1629 @Test
1630 fun testRemember_Forget_ForgetOnReplace() = compositionTest {
1631 val rememberObject1 =
1632 object : RememberObserver {
1633 var count = 0
1634
1635 override fun onRemembered() {
1636 count++
1637 }
1638
1639 override fun onForgotten() {
1640 count--
1641 }
1642
1643 override fun onAbandoned() {
1644 assertEquals(0, count, "onAbandon called after onRemember")
1645 }
1646 }
1647
1648 val rememberObject2 =
1649 object : RememberObserver {
1650 var count = 0
1651
1652 override fun onRemembered() {
1653 count++
1654 }
1655
1656 override fun onForgotten() {
1657 count--
1658 }
1659
1660 override fun onAbandoned() {
1661 assertEquals(0, count, "onAbandon called after onRemember")
1662 }
1663 }
1664
1665 var rememberObject: Any = rememberObject1
1666 var scope: RecomposeScope? = null
1667
1668 @Composable
1669 fun Composition(obj: Any) {
1670 Linear {
1671 key(1) {
1672 Linear {
1673 remember(obj) { obj }
1674 Text("Some value")
1675 }
1676 }
1677 }
1678 }
1679
1680 fun MockViewValidator.Composition() {
1681 Linear { Linear { Text("Some value") } }
1682 }
1683
1684 compose {
1685 scope = currentRecomposeScope
1686 Composition(obj = rememberObject)
1687 }
1688 validate { this.Composition() }
1689 assertEquals(1, rememberObject1.count, "first object should enter")
1690 assertEquals(0, rememberObject2.count, "second object should not have entered")
1691
1692 rememberObject = rememberObject2
1693 scope?.invalidate()
1694 expectChanges()
1695 validate { Composition() }
1696 assertEquals(0, rememberObject1.count, "first object should have left")
1697 assertEquals(1, rememberObject2.count, "second object should have entered")
1698
1699 rememberObject = object {}
1700 scope?.invalidate()
1701 expectChanges()
1702 validate { Composition() }
1703 assertEquals(0, rememberObject1.count, "first object should have left")
1704 assertEquals(0, rememberObject2.count, "second object should have left")
1705 }
1706
1707 @Test
1708 fun testRemember_forget_forgetAfterPredecessorReplaced() = compositionTest {
1709 val rememberObject =
1710 object : RememberObserver {
1711 var count = 0
1712
1713 override fun onRemembered() {
1714 count++
1715 }
1716
1717 override fun onForgotten() {
1718 count--
1719 }
1720
1721 override fun onAbandoned() {
1722 assertEquals(0, count, "onAbandon called after onRemember")
1723 }
1724
1725 override fun toString() = "rememberObject"
1726 }
1727
1728 var key = 0
1729 var include = true
1730 var scope: RecomposeScope? = null
1731 compose {
1732 scope = currentRecomposeScope
1733 if (include) {
1734 Linear {
1735 key(key) { Linear { Text("Some value") } }
1736 use(remember { rememberObject })
1737 }
1738 }
1739 }
1740
1741 fun MockViewValidator.Composition() {
1742 if (include) Linear { Linear { Text("Some value") } }
1743 }
1744
1745 validate { this.Composition() }
1746 assertEquals(1, rememberObject.count, "object should enter")
1747
1748 key++
1749 scope?.invalidate()
1750 expectChanges()
1751 validate { Composition() }
1752 assertEquals(1, rememberObject.count, "object should still be remembered")
1753
1754 include = false
1755 scope?.invalidate()
1756 expectChanges()
1757 validate { Composition() }
1758 assertEquals(0, rememberObject.count, "object should have left")
1759 }
1760
1761 @Test
1762 fun testRemember_RememberForgetNestedOrder() = compositionTest {
1763 var order = 0
1764 val objects = mutableListOf<Any>()
1765 val newRememberObject = { name: String ->
1766 object : RememberObserver, Counted, Ordered, Named {
1767 override var name = name
1768 override var count = 0
1769 override var rememberOrder = -1
1770 override var forgetOrder = -1
1771
1772 override fun onRemembered() {
1773 assertEquals(-1, rememberOrder, "Only one call to onRemembered expected")
1774 rememberOrder = order++
1775 count++
1776 }
1777
1778 override fun onForgotten() {
1779 assertEquals(-1, forgetOrder, "Only one call to onForgotten expected")
1780 forgetOrder = order++
1781 count--
1782 }
1783
1784 override fun onAbandoned() {
1785 assertEquals(0, count, "onAbandoned called after onRemembered")
1786 }
1787 }
1788 .also { objects.add(it) }
1789 }
1790
1791 @Suppress("UNUSED_PARAMETER") fun used(v: Any) {}
1792
1793 @Composable
1794 fun Tree() {
1795 used(remember { newRememberObject("L0B") })
1796 Linear {
1797 used(remember { newRememberObject("L1B") })
1798 Linear {
1799 used(remember { newRememberObject("L2B") })
1800 Linear {
1801 used(remember { newRememberObject("L3B") })
1802 Linear { used(remember { newRememberObject("Leaf") }) }
1803 used(remember { newRememberObject("L3A") })
1804 }
1805 used(remember { newRememberObject("L2A") })
1806 }
1807 used(remember { newRememberObject("L1A") })
1808 }
1809 used(remember { newRememberObject("L0A") })
1810 }
1811
1812 var includeTree by mutableStateOf(true)
1813 compose {
1814 if (includeTree) {
1815 Tree()
1816 }
1817 }
1818
1819 assertTrue(
1820 objects.mapNotNull { it as? Counted }.map { it.count == 1 }.all { it },
1821 "All object should have entered"
1822 )
1823
1824 includeTree = false
1825 expectChanges()
1826
1827 assertTrue(
1828 objects.mapNotNull { it as? Counted }.map { it.count == 0 }.all { it },
1829 "All object should have left"
1830 )
1831
1832 assertArrayEquals(
1833 "Expected enter order",
1834 arrayOf("L0B", "L1B", "L2B", "L3B", "Leaf", "L3A", "L2A", "L1A", "L0A"),
1835 objects
1836 .mapNotNull { it as? Ordered }
1837 .sortedBy { it.rememberOrder }
1838 .map { (it as Named).name }
1839 .toTypedArray()
1840 )
1841
1842 assertArrayEquals(
1843 "Expected exit order",
1844 arrayOf("L0A", "L1A", "L2A", "L3A", "Leaf", "L3B", "L2B", "L1B", "L0B"),
1845 objects
1846 .mapNotNull { it as? Ordered }
1847 .sortedBy { it.forgetOrder }
1848 .map { (it as Named).name }
1849 .toTypedArray()
1850 )
1851 }
1852
1853 @Test
1854 fun testRemember_RememberForgetNestedOrder_Inline() = compositionTest {
1855 var order = 0
1856 val objects = mutableListOf<Any>()
1857 val newRememberObject = { name: String ->
1858 object : RememberObserver, Counted, Ordered, Named {
1859 override var name = name
1860 override var count = 0
1861 override var rememberOrder = -1
1862 override var forgetOrder = -1
1863
1864 override fun onRemembered() {
1865 assertEquals(-1, rememberOrder, "Only one call to onRemembered expected")
1866 rememberOrder = order++
1867 count++
1868 }
1869
1870 override fun onForgotten() {
1871 assertEquals(-1, forgetOrder, "Only one call to onForgotten expected")
1872 forgetOrder = order++
1873 count--
1874 }
1875
1876 override fun onAbandoned() {
1877 assertEquals(0, count, "onAbandoned called after onRemembered")
1878 }
1879 }
1880 .also { objects.add(it) }
1881 }
1882
1883 @Suppress("UNUSED_PARAMETER") fun used(v: Any) {}
1884
1885 @Composable
1886 fun Tree() {
1887 used(remember { newRememberObject("L0B") })
1888 InlineLinear {
1889 used(remember { newRememberObject("L1B") })
1890 InlineLinear {
1891 used(remember { newRememberObject("L2B") })
1892 InlineLinear {
1893 used(remember { newRememberObject("L3B") })
1894 InlineLinear { used(remember { newRememberObject("Leaf") }) }
1895 used(remember { newRememberObject("L3A") })
1896 }
1897 used(remember { newRememberObject("L2A") })
1898 }
1899 used(remember { newRememberObject("L1A") })
1900 }
1901 used(remember { newRememberObject("L0A") })
1902 }
1903
1904 var includeTree by mutableStateOf(true)
1905 compose { if (includeTree) Tree() }
1906
1907 assertTrue(
1908 objects.mapNotNull { it as? Counted }.map { it.count == 1 }.all { it },
1909 "All object should have entered"
1910 )
1911
1912 includeTree = false
1913 expectChanges()
1914
1915 assertTrue(
1916 objects.mapNotNull { it as? Counted }.map { it.count == 0 }.all { it },
1917 "All object should have left"
1918 )
1919
1920 assertArrayEquals(
1921 "Expected enter order",
1922 arrayOf("L0B", "L1B", "L2B", "L3B", "Leaf", "L3A", "L2A", "L1A", "L0A"),
1923 objects
1924 .mapNotNull { it as? Ordered }
1925 .sortedBy { it.rememberOrder }
1926 .map { (it as Named).name }
1927 .toTypedArray()
1928 )
1929
1930 assertArrayEquals(
1931 "Expected exit order",
1932 arrayOf("L0A", "L1A", "L2A", "L3A", "Leaf", "L3B", "L2B", "L1B", "L0B"),
1933 objects
1934 .mapNotNull { it as? Ordered }
1935 .sortedBy { it.forgetOrder }
1936 .map { (it as Named).name }
1937 .toTypedArray()
1938 )
1939 }
1940
1941 @Test
1942 @Ignore // b/346821372
1943 fun testRemember_RememberForgetNestedOrder_Incremental() = compositionTest {
1944 var order = 0
1945 val objects = mutableListOf<Any>()
1946 val newRememberObject = { name: String ->
1947 object : RememberObserver, Counted, Ordered, Named {
1948 override var name = name
1949 override var count = 0
1950 override var rememberOrder = -1
1951 override var forgetOrder = -1
1952
1953 override fun onRemembered() {
1954 assertEquals(-1, rememberOrder, "Only one call to onRemembered expected")
1955 rememberOrder = order++
1956 count++
1957 }
1958
1959 override fun onForgotten() {
1960 assertEquals(-1, forgetOrder, "Only one call to onForgotten expected")
1961 forgetOrder = order++
1962 count--
1963 }
1964
1965 override fun onAbandoned() {
1966 assertEquals(0, count, "onAbandoned called after onRemembered")
1967 }
1968 }
1969 .also { objects.add(it) }
1970 }
1971
1972 @Suppress("UNUSED_PARAMETER") fun used(v: Any) {}
1973
1974 var level by mutableIntStateOf(0)
1975
1976 @Composable
1977 fun Tree() {
1978 used(remember { newRememberObject("L0B") })
1979 Linear {
1980 if (level >= 1) {
1981 used(remember { newRememberObject("L1B") })
1982 if (level >= 2) {
1983 Linear {
1984 used(remember { newRememberObject("L2B") })
1985 if (level >= 3) {
1986 Linear {
1987 used(remember { newRememberObject("L3B") })
1988 if (level >= 4) {
1989 Linear { used(remember { newRememberObject("Leaf") }) }
1990 }
1991 used(remember { newRememberObject("L3A") })
1992 }
1993 }
1994 used(remember { newRememberObject("L2A") })
1995 }
1996 }
1997 used(remember { newRememberObject("L1A") })
1998 }
1999 }
2000 used(remember { newRememberObject("L0A") })
2001 }
2002
2003 var includeTree by mutableStateOf(true)
2004 compose { if (includeTree) Tree() }
2005
2006 while (level < 4) {
2007 level++
2008 expectChanges()
2009 }
2010
2011 assertTrue(
2012 objects.mapNotNull { it as? Counted }.map { it.count == 1 }.all { it },
2013 "All object should have entered"
2014 )
2015
2016 includeTree = false
2017 expectChanges()
2018
2019 assertTrue(
2020 objects.mapNotNull { it as? Counted }.map { it.count == 0 }.all { it },
2021 "All object should have left"
2022 )
2023
2024 assertArrayEquals(
2025 "Expected enter order",
2026 arrayOf("L0B", "L0A", "L1B", "L1A", "L2B", "L2A", "L3B", "L3A", "Leaf"),
2027 objects
2028 .mapNotNull { it as? Ordered }
2029 .sortedBy { it.rememberOrder }
2030 .map { (it as Named).name }
2031 .toTypedArray()
2032 )
2033
2034 val forgetOrder = objects.mapNotNull { it as? Ordered }.sortedBy { it.forgetOrder }
2035
2036 // Even though the enter order is incremental, the order of onForgotten should
2037 // be called in the same order as if it came in all at once.
2038 assertArrayEquals(
2039 "Expected exit order",
2040 arrayOf("L0A", "L1A", "L2A", "L3A", "Leaf", "L3B", "L2B", "L1B", "L0A"),
2041 forgetOrder.map { (it as Named).name }.toTypedArray()
2042 )
2043 }
2044
2045 @Test
2046 fun testRemember_RememberForgetOrder() = compositionTest {
2047 var order = 0
2048 val objects = mutableListOf<Any>()
2049 val newRememberObject = { name: String ->
2050 object : RememberObserver, Counted, Ordered, Named {
2051 override var name = name
2052 override var count = 0
2053 override var rememberOrder = -1
2054 override var forgetOrder = -1
2055
2056 override fun onRemembered() {
2057 assertEquals(-1, rememberOrder, "Only one call to onRemembered expected")
2058 rememberOrder = order++
2059 count++
2060 }
2061
2062 override fun onForgotten() {
2063 assertEquals(-1, forgetOrder, "Only one call to onForgotten expected")
2064 forgetOrder = order++
2065 count--
2066 }
2067
2068 override fun onAbandoned() {
2069 assertEquals(0, count, "onAbandoned called after onRemembered")
2070 }
2071 }
2072 .also { objects.add(it) }
2073 }
2074
2075 @Composable
2076 fun RememberUser(name: String) {
2077 Linear {
2078 remember(name) { newRememberObject(name) }
2079 Text(value = name)
2080 }
2081 }
2082
2083 /*
2084 A
2085 |- B
2086 | |- C
2087 | |- D
2088 |- E
2089 |- F
2090 | |- G
2091 | |- H
2092 | |-I
2093 |- J
2094
2095 Should enter as: A, B, C, D, E, F, G, H, I, J
2096 Should leave as: J, I, H, G, F, E, D, C, B, A
2097 */
2098
2099 @Composable
2100 fun Tree() {
2101 Linear {
2102 RememberUser("A")
2103 Linear {
2104 RememberUser("B")
2105 Linear {
2106 RememberUser("C")
2107 RememberUser("D")
2108 }
2109 RememberUser("E")
2110 RememberUser("F")
2111 Linear {
2112 RememberUser("G")
2113 RememberUser("H")
2114 Linear { RememberUser("I") }
2115 }
2116 RememberUser("J")
2117 }
2118 }
2119 }
2120
2121 @Composable
2122 fun Composition(includeTree: Boolean) {
2123 Linear { if (includeTree) Tree() }
2124 }
2125
2126 var value by mutableStateOf(true)
2127
2128 compose { Composition(value) }
2129
2130 assertTrue(
2131 objects.mapNotNull { it as? Counted }.map { it.count == 1 }.all { it },
2132 "All object should have entered"
2133 )
2134
2135 value = false
2136 expectChanges()
2137
2138 assertTrue(
2139 objects.mapNotNull { it as? Counted }.map { it.count == 0 }.all { it },
2140 "All object should have left"
2141 )
2142
2143 assertArrayEquals(
2144 "Expected enter order",
2145 arrayOf("A", "B", "C", "D", "E", "F", "G", "H", "I", "J"),
2146 objects
2147 .mapNotNull { it as? Ordered }
2148 .sortedBy { it.rememberOrder }
2149 .map { (it as Named).name }
2150 .toTypedArray()
2151 )
2152
2153 assertArrayEquals(
2154 "Expected leave order",
2155 arrayOf("J", "I", "H", "G", "F", "E", "D", "C", "B", "A"),
2156 objects
2157 .mapNotNull { it as? Ordered }
2158 .sortedBy { it.forgetOrder }
2159 .map { (it as Named).name }
2160 .toTypedArray()
2161 )
2162 }
2163
2164 @Test
2165 fun testRemember_RememberForgetOrder_NonRestartable() = compositionTest {
2166 var order = 0
2167 val objects = mutableListOf<Any>()
2168 val newRememberObject = { name: String ->
2169 object : RememberObserver, Counted, Ordered, Named {
2170 override var name = name
2171 override var count = 0
2172 override var rememberOrder = -1
2173 override var forgetOrder = -1
2174
2175 override fun onRemembered() {
2176 assertEquals(-1, rememberOrder, "Only one call to onRemembered expected")
2177 rememberOrder = order++
2178 count++
2179 }
2180
2181 override fun onForgotten() {
2182 assertEquals(-1, forgetOrder, "Only one call to onForgotten expected")
2183 forgetOrder = order++
2184 count--
2185 }
2186
2187 override fun onAbandoned() {
2188 assertEquals(0, count, "onAbandoned called after onRemembered")
2189 }
2190
2191 override fun toString(): String =
2192 "$name: count($count), remember($rememberOrder), forgotten($forgetOrder)"
2193 }
2194 .also { objects.add(it) }
2195 }
2196
2197 @Composable
2198 @NonRestartableComposable
2199 fun RememberUser(name: String) {
2200 remember(name) { newRememberObject(name) }
2201 }
2202
2203 /*
2204 A
2205 |- B
2206 | |- C
2207 | |- D
2208 |- E
2209 |- F
2210 | |- G
2211 | |- H
2212 | |-I
2213 | |- J
2214 |- K
2215
2216 Should enter as: A, B, C, D, E, F, G, H, I, J, K
2217 Should leave as: K, J, I, H, G, F, E, D, C, B, A
2218 */
2219
2220 @Composable
2221 fun Tree() {
2222 Linear {
2223 RememberUser("A")
2224 Linear {
2225 RememberUser("B")
2226 Linear {
2227 RememberUser("C")
2228 RememberUser("D")
2229 }
2230 RememberUser("E")
2231 RememberUser("F")
2232 Linear {
2233 RememberUser("G")
2234 RememberUser("H")
2235 Linear { RememberUser("I") }
2236 RememberUser("J")
2237 }
2238 RememberUser("K")
2239 }
2240 }
2241 }
2242
2243 @Composable
2244 fun Composition(includeTree: Boolean) {
2245 Linear { if (includeTree) Tree() }
2246 }
2247
2248 var value by mutableStateOf(true)
2249
2250 compose { Composition(value) }
2251
2252 assertTrue(
2253 objects.mapNotNull { it as? Counted }.map { it.count == 1 }.all { it },
2254 "All object should have entered"
2255 )
2256
2257 value = false
2258 expectChanges()
2259
2260 assertTrue(
2261 objects.mapNotNull { it as? Counted }.map { it.count == 0 }.all { it },
2262 "All object should have left"
2263 )
2264
2265 val namesInRememberOrder =
2266 objects
2267 .mapNotNull { it as? Ordered }
2268 .sortedBy { it.rememberOrder }
2269 .map { (it as Named).name }
2270 .toTypedArray()
2271
2272 val namesInForgetOrder =
2273 objects
2274 .mapNotNull { it as? Ordered }
2275 .sortedBy { it.forgetOrder }
2276 .map { (it as Named).name }
2277 .toTypedArray()
2278
2279 assertArrayEquals(
2280 "Expected enter order",
2281 arrayOf("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"),
2282 namesInRememberOrder
2283 )
2284
2285 assertArrayEquals(
2286 "Expected leave order",
2287 arrayOf("K", "J", "I", "H", "G", "F", "E", "D", "C", "B", "A"),
2288 namesInForgetOrder
2289 )
2290 }
2291
2292 @Test
2293 fun testRememberObserver_RememberForgetOrder_ReplaceOnRecompose() = compositionTest {
2294 var order = 0
2295 val objects = mutableListOf<Any>()
2296 val newRememberObject = { name: String, data: Int ->
2297 object : RememberObserver, Counted, Ordered, Named, WithData {
2298 override var name = name
2299 override var data = data
2300 override var count = 0
2301 override var rememberOrder = -1
2302 override var forgetOrder = -1
2303
2304 override fun onRemembered() {
2305 assertEquals(-1, rememberOrder, "Only one call to onRemembered expected")
2306 rememberOrder = order++
2307 count++
2308 }
2309
2310 override fun onForgotten() {
2311 assertEquals(-1, forgetOrder, "Only one call to onForgotten expected")
2312 forgetOrder = order++
2313 count--
2314 }
2315
2316 override fun onAbandoned() {
2317 assertEquals(0, count, "onAbandoned called after onRemembered")
2318 }
2319
2320 override fun toString(): String =
2321 "$name: count($count), remember($rememberOrder), forgotten($forgetOrder)"
2322 }
2323 .also { objects.add(it) }
2324 }
2325
2326 var changing by mutableStateOf(0)
2327 val fixed = 10
2328
2329 @Composable
2330 @NonRestartableComposable
2331 fun RememberUser(name: String, data: Int) {
2332 remember(name, data) { newRememberObject(name, data) }
2333 }
2334
2335 @Composable
2336 fun Tree() {
2337 Linear {
2338 RememberUser("A", changing)
2339 RememberUser("B", fixed)
2340 }
2341 }
2342
2343 @Composable
2344 fun Composition(includeTree: Boolean) {
2345 Linear { if (includeTree) Tree() }
2346 }
2347
2348 var includeTree by mutableStateOf(true)
2349
2350 compose { Composition(includeTree) }
2351
2352 changing++
2353 advance()
2354
2355 includeTree = false
2356 advance()
2357
2358 val nameAndDataInForgetOrder =
2359 objects
2360 .mapNotNull { it as? Ordered }
2361 .sortedBy { it.forgetOrder }
2362 .map {
2363 val named = it as Named
2364 val withData = it as WithData
2365 "${named.name}:${withData.data}"
2366 }
2367 .joinToString()
2368
2369 assertEquals("A:0, B:10, A:1", nameAndDataInForgetOrder)
2370 }
2371
2372 @Test
2373 fun testRememberObserver_RememberForgetOrder_RelativeOrder() = compositionTest {
2374 var order = 0
2375 val objects = mutableListOf<Any>()
2376 val newRememberObject = { name: String, data: Int ->
2377 object : RememberObserver, Counted, Ordered, Named, WithData {
2378 override var name = name
2379 override var data = data
2380 override var count = 0
2381 override var rememberOrder = -1
2382 override var forgetOrder = -1
2383
2384 override fun onRemembered() {
2385 assertEquals(-1, rememberOrder, "Only one call to onRemembered expected")
2386 rememberOrder = order++
2387 count++
2388 }
2389
2390 override fun onForgotten() {
2391 assertEquals(-1, forgetOrder, "Only one call to onForgotten expected")
2392 forgetOrder = order++
2393 count--
2394 }
2395
2396 override fun onAbandoned() {
2397 assertEquals(0, count, "onAbandoned called after onRemembered")
2398 }
2399
2400 override fun toString(): String =
2401 "$name: count($count), remember($rememberOrder), forgotten($forgetOrder)"
2402 }
2403 .also { objects.add(it) }
2404 }
2405
2406 var changing by mutableStateOf(0)
2407 var includeChildren by mutableStateOf(true)
2408 val fixed = 10
2409
2410 @Composable
2411 @NonRestartableComposable
2412 fun RememberUser(name: String, data: Int) {
2413 remember(name, data) { newRememberObject(name, data) }
2414 }
2415
2416 @Composable
2417 @NonRestartableComposable
2418 fun Children() {
2419 RememberUser("A2", fixed)
2420 RememberUser("B2", fixed)
2421 }
2422
2423 @Composable @NonRestartableComposable fun NoChildren() {}
2424
2425 @Composable
2426 fun Composition() {
2427 InlineLinear {
2428 RememberUser("A1", changing)
2429 if (includeChildren) {
2430 Children()
2431 } else {
2432 NoChildren()
2433 }
2434 RememberUser("B1", changing)
2435 }
2436 }
2437
2438 compose { Composition() }
2439
2440 changing++
2441 includeChildren = false
2442 advance()
2443
2444 val nameAndDataInForgetOrder =
2445 objects
2446 .mapNotNull { item -> (item as? Ordered)?.takeIf { it.forgetOrder >= 0 } }
2447 .sortedBy { it.forgetOrder }
2448 .joinToString {
2449 val named = it as Named
2450 val withData = it as WithData
2451 "${named.name}:${withData.data}"
2452 }
2453
2454 assertEquals("B1:0, B2:10, A2:10, A1:0", nameAndDataInForgetOrder)
2455 }
2456
2457 @Test
2458 fun testRemember_RememberForgetOrder_keyChange() = compositionTest {
2459 var order = 0
2460 val objects = mutableListOf<Any>()
2461 val newRememberObject = { name: String ->
2462 object : RememberObserver, Counted, Ordered, Named {
2463 override var name = name
2464 override var count = 0
2465 override var rememberOrder = -1
2466 override var forgetOrder = -1
2467
2468 override fun onRemembered() {
2469 assertEquals(-1, rememberOrder, "Only one call to onRemembered expected")
2470 rememberOrder = order++
2471 count++
2472 }
2473
2474 override fun onForgotten() {
2475 assertEquals(-1, forgetOrder, "Only one call to onForgotten expected")
2476 forgetOrder = order++
2477 count--
2478 }
2479
2480 override fun onAbandoned() {
2481 assertEquals(0, count, "onAbandoned called after onRemembered")
2482 }
2483 }
2484 .also { objects.add(it) }
2485 }
2486
2487 @Suppress("UNUSED_PARAMETER") fun used(v: Any) {}
2488 var a by mutableIntStateOf(0)
2489 var b by mutableIntStateOf(0)
2490
2491 @Composable
2492 fun Test() {
2493 use(remember(a) { newRememberObject("A$a") })
2494 use(remember(b) { newRememberObject("B$b") })
2495 }
2496
2497 var include by mutableStateOf(true)
2498 compose {
2499 if (include) {
2500 Test()
2501 }
2502 }
2503
2504 a++
2505 advance()
2506 b++
2507 advance()
2508 a++
2509 advance()
2510 include = false
2511 advance()
2512
2513 assertTrue(
2514 objects.mapNotNull { it as? Counted }.map { it.count == 0 }.all { it },
2515 "All object should have left"
2516 )
2517
2518 assertArrayEquals(
2519 "Expected enter order",
2520 arrayOf("A0", "B0", "A1", "B1", "A2"),
2521 objects
2522 .mapNotNull { it as? Ordered }
2523 .sortedBy { it.rememberOrder }
2524 .map { (it as Named).name }
2525 .toTypedArray()
2526 )
2527
2528 val forgetOrder = objects.mapNotNull { it as? Ordered }.sortedBy { it.forgetOrder }
2529
2530 // Even though the enter order is incremental, the order of onForgotten should
2531 // be called in the same order as if it came in all at once.
2532 assertArrayEquals(
2533 "Expected exit order",
2534 arrayOf("A0", "B0", "A1", "B1", "A2"),
2535 forgetOrder.map { (it as Named).name }.toTypedArray()
2536 )
2537 }
2538
2539 @Test
2540 fun testRememberObserver_Abandon_Simple() = compositionTest {
2541 val abandonedObjects = mutableListOf<RememberObserver>()
2542 val observed =
2543 object : RememberObserver {
2544 override fun onAbandoned() {
2545 abandonedObjects.add(this)
2546 }
2547
2548 override fun onForgotten() {
2549 error("Unexpected call to onForgotten")
2550 }
2551
2552 override fun onRemembered() {
2553 error("Unexpected call to onRemembered")
2554 }
2555 }
2556
2557 assertFailsWith(IllegalStateException::class, message = "Throw") {
2558 compose {
2559 @Suppress("UNUSED_EXPRESSION") remember { observed }
2560 error("Throw")
2561 }
2562 }
2563
2564 assertArrayEquals(listOf(observed), abandonedObjects)
2565 }
2566
2567 @Test
2568 fun testRememberObserver_Abandon_Recompose() {
2569 val abandonedObjects = mutableListOf<RememberObserver>()
2570 val observed =
2571 object : RememberObserver {
2572 override fun onAbandoned() {
2573 abandonedObjects.add(this)
2574 }
2575
2576 override fun onForgotten() {
2577 error("Unexpected call to onForgotten")
2578 }
2579
2580 override fun onRemembered() {
2581 error("Unexpected call to onRemembered")
2582 }
2583 }
2584 assertFailsWith(IllegalStateException::class, message = "Throw") {
2585 compositionTest {
2586 val rememberObject = mutableStateOf(false)
2587
2588 compose {
2589 if (rememberObject.value) {
2590 @Suppress("UNUSED_EXPRESSION") remember { observed }
2591 error("Throw")
2592 }
2593 }
2594
2595 assertTrue(abandonedObjects.isEmpty())
2596
2597 rememberObject.value = true
2598
2599 advance(ignorePendingWork = true)
2600 }
2601 }
2602
2603 assertArrayEquals(listOf(observed), abandonedObjects)
2604 }
2605
2606 @Test
2607 @OptIn(ExperimentalCoroutinesApi::class)
2608 fun testRememberedObserver_Controlled_Dispose() = runTest {
2609 val recomposer = Recomposer(coroutineContext)
2610 val root = View()
2611 val controlled = ControlledComposition(ViewApplier(root), recomposer)
2612
2613 val abandonedObjects = mutableListOf<RememberObserver>()
2614 val observed =
2615 object : RememberObserver {
2616 override fun onAbandoned() {
2617 abandonedObjects.add(this)
2618 }
2619
2620 override fun onForgotten() {
2621 error("Unexpected call to onForgotten")
2622 }
2623
2624 override fun onRemembered() {
2625 error("Unexpected call to onRemembered")
2626 }
2627 }
2628
2629 controlled.composeContent {
2630 @Suppress("UNUSED_EXPRESSION") remember<RememberObserver> { observed }
2631 }
2632
2633 assertTrue(abandonedObjects.isEmpty())
2634
2635 controlled.dispose()
2636
2637 assertArrayEquals(listOf(observed), abandonedObjects)
2638 recomposer.close()
2639 }
2640
2641 @Test
2642 fun testCompoundKeyHashCodeStaysTheSameAfterRecompositions() = compositionTest {
2643 val outerKeys = mutableListOf<CompositeKeyHashCode>()
2644 val innerKeys = mutableListOf<CompositeKeyHashCode>()
2645 var previousOuterKeysSize = 0
2646 var previousInnerKeysSize = 0
2647 var outerScope: RecomposeScope? = null
2648 var innerScope: RecomposeScope? = null
2649
2650 @Composable
2651 fun Test() {
2652 outerScope = currentRecomposeScope
2653 outerKeys.add(currentComposer.compositeKeyHashCode)
2654 Container {
2655 Linear {
2656 innerScope = currentRecomposeScope
2657 innerKeys.add(currentComposer.compositeKeyHashCode)
2658 }
2659 }
2660 // asserts that the key is correctly rolled back after start and end of Observe
2661 assertEquals(outerKeys.last(), currentComposer.compositeKeyHashCode)
2662 }
2663
2664 compose { Test() }
2665
2666 assertNotEquals(previousOuterKeysSize, outerKeys.size)
2667 assertNotEquals(previousInnerKeysSize, innerKeys.size)
2668
2669 previousOuterKeysSize = outerKeys.size
2670 outerScope?.invalidate()
2671 expectNoChanges()
2672 assertNotEquals(previousOuterKeysSize, outerKeys.size)
2673
2674 previousInnerKeysSize = innerKeys.size
2675 innerScope?.invalidate()
2676 expectNoChanges()
2677 assertNotEquals(previousInnerKeysSize, innerKeys.size)
2678
2679 assertNotEquals(innerKeys[0], outerKeys[0])
2680 innerKeys.forEach { assertEquals(innerKeys[0], it) }
2681 outerKeys.forEach { assertEquals(outerKeys[0], it) }
2682 }
2683
2684 @Test // b/152753046
2685 fun testSwappingGroups() = compositionTest {
2686 val items = mutableListOf(0, 1, 2, 3, 4)
2687 var scope: RecomposeScope? = null
2688
2689 @Composable fun NoNodes() {}
2690
2691 @Composable
2692 fun Test() {
2693 scope = currentRecomposeScope
2694 for (item in items) {
2695 key(item) { NoNodes() }
2696 }
2697 }
2698
2699 compose { Test() }
2700
2701 // Swap 2 and 3
2702 items[2] = 3
2703 items[3] = 2
2704 scope?.invalidate()
2705
2706 expectChanges()
2707 }
2708
2709 @Test // b/154650546
2710 fun testInsertOnMultipleLevels() = compositionTest {
2711 val items =
2712 mutableListOf(1 to mutableListOf(0, 1, 2, 3, 4), 3 to mutableListOf(0, 1, 2, 3, 4))
2713
2714 val invalidates = mutableListOf<RecomposeScope>()
2715 fun invalidateComposition() {
2716 invalidates.forEach { it.invalidate() }
2717 invalidates.clear()
2718 }
2719
2720 @Composable
2721 fun Numbers(numbers: List<Int>) {
2722 Linear {
2723 Linear {
2724 invalidates.add(currentRecomposeScope)
2725 for (number in numbers) {
2726 Text("$number")
2727 }
2728 }
2729 }
2730 }
2731
2732 @Composable
2733 fun Item(number: Int, numbers: List<Int>) {
2734 Linear {
2735 invalidates.add(currentRecomposeScope)
2736 Text("$number")
2737 Numbers(numbers)
2738 }
2739 }
2740
2741 @Composable
2742 fun Test() {
2743 invalidates.add(currentRecomposeScope)
2744
2745 Linear {
2746 invalidates.add(currentRecomposeScope)
2747 for ((number, numbers) in items) {
2748 Item(number, numbers)
2749 }
2750 }
2751 }
2752
2753 fun MockViewValidator.validateNumbers(numbers: List<Int>) {
2754 Linear {
2755 Linear {
2756 for (number in numbers) {
2757 Text("$number")
2758 }
2759 }
2760 }
2761 }
2762
2763 fun MockViewValidator.validateItem(number: Int, numbers: List<Int>) {
2764 Linear {
2765 Text("$number")
2766 validateNumbers(numbers)
2767 }
2768 }
2769
2770 fun MockViewValidator.Test() {
2771 Linear {
2772 for ((number, numbers) in items) {
2773 validateItem(number, numbers)
2774 }
2775 }
2776 }
2777
2778 compose { Test() }
2779
2780 fun validate() {
2781 validate { this.Test() }
2782 }
2783
2784 validate()
2785
2786 // Add numbers to the list at 0 and 1
2787 items[0].second.add(2, 100)
2788 items[1].second.add(3, 200)
2789
2790 // Add a list to the root.
2791 items.add(1, 2 to mutableListOf(0, 1, 2))
2792
2793 invalidateComposition()
2794
2795 expectChanges()
2796 validate()
2797 }
2798
2799 @Test
2800 fun testInsertingAfterSkipping() = compositionTest {
2801 val items = mutableListOf(1 to listOf(0, 1, 2, 3, 4))
2802
2803 val invalidates = mutableListOf<RecomposeScope>()
2804 fun invalidateComposition() {
2805 invalidates.forEach { it.invalidate() }
2806 invalidates.clear()
2807 }
2808
2809 @Composable
2810 fun Test() {
2811 invalidates.add(currentRecomposeScope)
2812
2813 Linear {
2814 for ((item, numbers) in items) {
2815 Text(item.toString())
2816 Linear {
2817 invalidates.add(currentRecomposeScope)
2818 for (number in numbers) {
2819 Text(number.toString())
2820 }
2821 }
2822 }
2823 }
2824 }
2825
2826 fun MockViewValidator.Test() {
2827 Linear {
2828 for ((item, numbers) in items) {
2829 Text(item.toString())
2830 Linear {
2831 for (number in numbers) {
2832 Text(number.toString())
2833 }
2834 }
2835 }
2836 }
2837 }
2838
2839 compose { Test() }
2840
2841 validate { this.Test() }
2842
2843 items.add(2 to listOf(3, 4, 5, 6))
2844 invalidateComposition()
2845
2846 expectChanges()
2847 validate { this.Test() }
2848 }
2849
2850 @Test
2851 fun evenOddRecomposeGroup() = compositionTest {
2852 var includeEven = true
2853 var includeOdd = true
2854 val invalidates = mutableListOf<RecomposeScope>()
2855
2856 fun invalidateComposition() {
2857 for (scope in invalidates) {
2858 scope.invalidate()
2859 }
2860 invalidates.clear()
2861 }
2862
2863 @Composable
2864 fun Wrapper(content: @Composable () -> Unit) {
2865 content()
2866 }
2867
2868 @Composable
2869 fun EmitText() {
2870 invalidates.add(currentRecomposeScope)
2871 if (includeOdd) {
2872 key(1) { Text("odd 1") }
2873 }
2874 if (includeEven) {
2875 key(2) { Text("even 2") }
2876 }
2877 if (includeOdd) {
2878 key(3) { Text("odd 3") }
2879 }
2880 if (includeEven) {
2881 key(4) { Text("even 4") }
2882 }
2883 }
2884
2885 @Composable
2886 fun Test() {
2887 Linear {
2888 Wrapper { EmitText() }
2889 EmitText()
2890 Wrapper { EmitText() }
2891 EmitText()
2892 }
2893 }
2894
2895 fun MockViewValidator.Wrapper(children: () -> Unit) {
2896 children()
2897 }
2898
2899 fun MockViewValidator.EmitText() {
2900 if (includeOdd) {
2901 Text("odd 1")
2902 }
2903 if (includeEven) {
2904 Text("even 2")
2905 }
2906 if (includeOdd) {
2907 Text("odd 3")
2908 }
2909 if (includeEven) {
2910 Text("even 4")
2911 }
2912 }
2913
2914 fun MockViewValidator.Test() {
2915 Linear {
2916 this.Wrapper { this.EmitText() }
2917 this.EmitText()
2918 this.Wrapper { this.EmitText() }
2919 this.EmitText()
2920 }
2921 }
2922
2923 compose { Test() }
2924
2925 fun validate() {
2926 validate { this.Test() }
2927 }
2928 validate()
2929
2930 includeEven = false
2931 invalidateComposition()
2932 expectChanges()
2933 validate()
2934
2935 includeEven = true
2936 includeOdd = false
2937 invalidateComposition()
2938 expectChanges()
2939 validate()
2940
2941 includeEven = false
2942 includeOdd = false
2943 invalidateComposition()
2944 expectChanges()
2945 validate()
2946
2947 includeEven = true
2948 invalidateComposition()
2949 expectChanges()
2950 validate()
2951
2952 includeOdd = true
2953 invalidateComposition()
2954 expectChanges()
2955 validate()
2956 }
2957
2958 @Test // regression test for b/199136503
2959 fun testMovingSlotsButNotNodes() = compositionTest {
2960 val order = mutableStateListOf(1, 2, 3, 4, 5)
2961 val includeText = mutableStateMapOf(4 to 4)
2962 compose {
2963 for (i in order) {
2964 key(i) {
2965 if (i in includeText) {
2966 Text("Text for $i")
2967 }
2968 }
2969 }
2970 Text("End")
2971 }
2972
2973 validate {
2974 for (i in order) {
2975 if (i in includeText) {
2976 Text("Text for $i")
2977 }
2978 }
2979 Text("End")
2980 }
2981
2982 order.swap(3, 5)
2983 includeText.remove(4)
2984 includeText.set(3, 3)
2985 expectChanges()
2986 revalidate()
2987 }
2988
2989 @Test
2990 fun evenOddWithMovement() = compositionTest {
2991 var includeEven = true
2992 var includeOdd = true
2993 var order = listOf(1, 2, 3, 4)
2994 val invalidates = mutableListOf<RecomposeScope>()
2995
2996 fun invalidateComposition() {
2997 for (scope in invalidates) {
2998 scope.invalidate()
2999 }
3000 invalidates.clear()
3001 }
3002
3003 @Composable
3004 fun EmitText(all: Boolean) {
3005 invalidates.add(currentRecomposeScope)
3006 for (i in order) {
3007 if (i % 2 == 1 && (all || includeOdd)) {
3008 key(i) { Text("odd $i") }
3009 }
3010 if (i % 2 == 0 && (all || includeEven)) {
3011 key(i) { Text("even $i") }
3012 }
3013 }
3014 }
3015
3016 @Composable
3017 fun Test() {
3018 Linear {
3019 invalidates.add(currentRecomposeScope)
3020 for (i in order) {
3021 key(i) {
3022 Text("group $i")
3023 if (i == 2 || (includeEven && includeOdd)) {
3024 Text("including everything")
3025 } else {
3026 if (includeEven) {
3027 Text("including evens")
3028 }
3029 if (includeOdd) {
3030 Text("including odds")
3031 }
3032 }
3033 EmitText(i == 2)
3034 }
3035 }
3036 EmitText(false)
3037 }
3038 }
3039
3040 fun MockViewValidator.EmitText(all: Boolean) {
3041 for (i in order) {
3042 if (i % 2 == 1 && (includeOdd || all)) {
3043 Text("odd $i")
3044 }
3045 if (i % 2 == 0 && (includeEven || all)) {
3046 Text("even $i")
3047 }
3048 }
3049 }
3050
3051 fun MockViewValidator.Test() {
3052 Linear {
3053 for (i in order) {
3054 Text("group $i")
3055 if (i == 2 || (includeEven && includeOdd)) {
3056 Text("including everything")
3057 } else {
3058 if (includeEven) {
3059 Text("including evens")
3060 }
3061 if (includeOdd) {
3062 Text("including odds")
3063 }
3064 }
3065 this.EmitText(i == 2)
3066 }
3067 this.EmitText(false)
3068 }
3069 }
3070
3071 compose { Test() }
3072
3073 fun validate() {
3074 validate { this.Test() }
3075 }
3076 validate()
3077
3078 order = listOf(1, 2, 4, 3)
3079 includeEven = false
3080 invalidateComposition()
3081 expectChanges()
3082 validate()
3083
3084 order = listOf(1, 4, 2, 3)
3085 includeEven = true
3086 includeOdd = false
3087 invalidateComposition()
3088 expectChanges()
3089 validate()
3090
3091 order = listOf(3, 4, 2, 1)
3092 includeEven = false
3093 includeOdd = false
3094 invalidateComposition()
3095 expectChanges()
3096 validate()
3097
3098 order = listOf(4, 3, 2, 1)
3099 includeEven = true
3100 invalidateComposition()
3101 expectChanges()
3102 validate()
3103
3104 order = listOf(1, 2, 3, 4)
3105 includeOdd = true
3106 invalidateComposition()
3107 expectChanges()
3108 validate()
3109 }
3110
3111 /**
3112 * This tests behavior when changing the state object instances being observed - so not
3113 * `remember`ing the mutableStateOf calls is intentional, hence the Lint suppression.
3114 */
3115 @Suppress("UnrememberedMutableState")
3116 @Test
3117 fun testObservationScopes() = compositionTest {
3118 val states = mutableListOf<MutableState<Int>>()
3119 var iterations = 0
3120
3121 @Composable
3122 fun Test() {
3123 val s1 = mutableStateOf(iterations++)
3124 Text("s1 ${s1.value}")
3125 states.add(s1)
3126 val s2 = mutableStateOf(iterations++)
3127 Text("s2 ${s2.value}")
3128 states.add(s2)
3129 }
3130
3131 compose { Test() }
3132
3133 fun invalidateFirst() {
3134 states.first().value++
3135 }
3136
3137 fun invalidateLast() {
3138 states.last().value++
3139 }
3140
3141 repeat(10) {
3142 invalidateLast()
3143 expectChanges()
3144 }
3145
3146 invalidateFirst()
3147 expectNoChanges()
3148 }
3149
3150 @Suppress("UnrememberedMutableState")
3151 @Composable
3152 fun Indirect(iteration: Int, states: MutableList<MutableState<Int>>) {
3153 val state = mutableStateOf(Random.nextInt())
3154 states.add(state)
3155 Text("$iteration state = ${state.value}")
3156 }
3157
3158 @Composable
3159 fun ComposeIndirect(iteration: State<Int>, states: MutableList<MutableState<Int>>) {
3160 Text("Iteration ${iteration.value}")
3161 Indirect(iteration.value, states)
3162 }
3163
3164 @Test // Regression b/182822837
3165 fun testObservationScopes_IndirectInvalidate() = compositionTest {
3166 val states = mutableListOf<MutableState<Int>>()
3167 val iteration = mutableStateOf(0)
3168
3169 compose { ComposeIndirect(iteration, states) }
3170
3171 fun nextIteration() = iteration.value++
3172 fun invalidateLast() = states.last().value++
3173 fun invalidateFirst() = states.first().value++
3174
3175 repeat(10) {
3176 nextIteration()
3177 expectChanges()
3178 }
3179
3180 invalidateFirst()
3181 expectNoChanges()
3182 }
3183
3184 @Composable
3185 fun <T> calculateValue(state: State<T>): T {
3186 return remember { state.value }
3187 }
3188
3189 private var observationScopeTestCalls = 0
3190 private var observationScopeTestForwardWrite = false
3191
3192 @Composable
3193 fun <T> ObservationScopesTest(state: State<T>, forwardWrite: Boolean) {
3194 observationScopeTestCalls++
3195 calculateValue(state)
3196 observationScopeTestForwardWrite = forwardWrite
3197 }
3198
3199 @Composable
3200 fun ForwardWrite(state: MutableState<String>) {
3201 state.value += ", forward write"
3202 }
3203
3204 @Test // Regression test for b/186787946
3205 fun testObservationScopes_ReadInRemember() = compositionTest {
3206 val state = mutableStateOf("state")
3207 var mainState by mutableStateOf("main state")
3208 var doForwardWrite by mutableStateOf(false)
3209 compose {
3210 Text(mainState)
3211 ObservationScopesTest(state, doForwardWrite)
3212 if (doForwardWrite) ForwardWrite(state)
3213 }
3214
3215 // Set up the case by skipping ObservationScopeTest
3216 mainState += ", changed"
3217 advance()
3218
3219 // Do the forward write after skipping ObserveScopesTest.
3220 // This triggers a backward write in ForwardWrite because of the remember.
3221 // This backwards write is transitory as future writes will just be forward writes.
3222 doForwardWrite = true
3223 advance(ignorePendingWork = true)
3224
3225 // Assert we saw true. In the bug this is false because a stale value was used for
3226 // `doForwardWrite` because the scope callback lambda was not updated correctly.
3227 assertTrue(observationScopeTestForwardWrite)
3228 }
3229
3230 @Test
3231 fun testApplierBeginEndCallbacks() = compositionTest {
3232 val checks = mutableListOf<String>()
3233 compose {
3234 val myComposer = currentComposer
3235 val myApplier = myComposer.applier as ViewApplier
3236 assertEquals(0, myApplier.onBeginChangesCalled, "onBeginChanges during composition")
3237 assertEquals(0, myApplier.onEndChangesCalled, "onEndChanges during composition")
3238 checks += "composition"
3239
3240 SideEffect {
3241 assertEquals(1, myApplier.onBeginChangesCalled, "onBeginChanges during side effect")
3242 assertEquals(1, myApplier.onEndChangesCalled, "onEndChanges during side effect")
3243 checks += "SideEffect"
3244 }
3245
3246 // Memo to future self:
3247 // Without the explicit generic definition of RememberObserver here, the type of this
3248 // remember call is inferred to be `Unit` thanks to the call's position as the last
3249 // expression in a unit lambda (the argument to `compose {}`). The remember lambda is in
3250 // turn interpreted as returning Unit, the object expression is dropped on the floor for
3251 // the gc, and Unit is written into the slot table.
3252 remember<RememberObserver> {
3253 object : RememberObserver {
3254 override fun onRemembered() {
3255 assertEquals(
3256 1,
3257 myApplier.onBeginChangesCalled,
3258 "onBeginChanges during lifecycle observer"
3259 )
3260 assertEquals(
3261 1,
3262 myApplier.onEndChangesCalled,
3263 "onEndChanges during lifecycle observer"
3264 )
3265 checks += "RememberObserver"
3266 }
3267
3268 override fun onForgotten() {
3269 // Nothing to do
3270 }
3271
3272 override fun onAbandoned() {
3273 // Nothing to do
3274 }
3275 }
3276 }
3277 }
3278 assertEquals(
3279 listOf("composition", "RememberObserver", "SideEffect"),
3280 checks,
3281 "expected order of calls"
3282 )
3283 }
3284
3285 @Test // regression test for b/172660922
3286 fun testInvalidationOfRemovedContent() = compositionTest {
3287 var S1Scope: RecomposeScope? = null
3288 var viewS1 by mutableStateOf(true)
3289 var performBackwardsWrite = true
3290 @Composable
3291 fun S1() {
3292 S1Scope = currentRecomposeScope
3293 Text("In s1")
3294 }
3295
3296 @Composable
3297 fun S2() {
3298 Text("In s2")
3299 }
3300
3301 @Composable
3302 fun Test() {
3303 Text("$viewS1")
3304 Wrap {
3305 if (viewS1) {
3306 S1()
3307 }
3308 S2()
3309 }
3310
3311 if (performBackwardsWrite) {
3312 // This forces the equivalent of a backwards write.
3313 S1Scope?.invalidate()
3314 performBackwardsWrite = false
3315 }
3316 }
3317
3318 fun MockViewValidator.S1() {
3319 Text("In s1")
3320 }
3321
3322 fun MockViewValidator.S2() {
3323 Text("In s2")
3324 }
3325
3326 fun MockViewValidator.Test() {
3327 Text("$viewS1")
3328 if (viewS1) {
3329 this.S1()
3330 }
3331 this.S2()
3332 }
3333
3334 compose { Test() }
3335
3336 fun validate() = validate { this.Test() }
3337
3338 validate()
3339
3340 S1Scope?.invalidate()
3341 performBackwardsWrite = true
3342 expectNoChanges()
3343
3344 validate()
3345
3346 viewS1 = false
3347 performBackwardsWrite = true
3348 expectChanges()
3349 validate()
3350 }
3351
3352 @Test
3353 fun testModificationsPropagateToSubcomposition() = compositionTest {
3354 var value by mutableStateOf(0)
3355 val content: MutableState<@Composable () -> Unit> = mutableStateOf({})
3356 @Suppress("VARIABLE_WITH_REDUNDANT_INITIALIZER") var subCompositionOccurred = false
3357
3358 @Composable
3359 fun ComposeContent() {
3360 content.value()
3361 }
3362
3363 fun updateContent(parentValue: Int) {
3364 content.value = {
3365 subCompositionOccurred = true
3366 assertEquals(parentValue, value)
3367 }
3368 }
3369
3370 compose {
3371 updateContent(value)
3372 TestSubcomposition { ComposeContent() }
3373 }
3374
3375 subCompositionOccurred = false
3376
3377 value = 10
3378 expectChanges()
3379
3380 assertTrue(subCompositionOccurred)
3381 }
3382
3383 /**
3384 * This test checks that an updated ComposableLambda capture used in a subcomposition correctly
3385 * invalidates that subcomposition and schedules recomposition of that subcomposition.
3386 */
3387 @OptIn(ExperimentalCoroutinesApi::class)
3388 @Test
3389 fun testComposableLambdaSubcompositionInvalidation() = runTest {
3390 localRecomposerTest { recomposer ->
3391 val composition = Composition(EmptyApplier(), recomposer)
3392 try {
3393 var rootState by mutableStateOf(false)
3394 val composedResults = mutableListOf<Boolean>()
3395 Snapshot.notifyObjectsInitialized()
3396 composition.setContent {
3397 // Read into local variable, local will be captured below
3398 val capturedValue = rootState
3399 TestSubcomposition { composedResults.add(capturedValue) }
3400 }
3401 assertEquals(listOf(false), composedResults)
3402 rootState = true
3403 Snapshot.sendApplyNotifications()
3404 // expect lambda to invalidate on the same frame (regression test for b/320385076)
3405 testScheduler.advanceTimeByFrame(coroutineContext)
3406 assertEquals(listOf(false, true), composedResults)
3407 } finally {
3408 composition.dispose()
3409 }
3410 }
3411 }
3412
3413 @OptIn(ExperimentalCoroutinesApi::class)
3414 @Test
3415 fun testCompositionContextIsRemembered() = runTest {
3416 localRecomposerTest { recomposer ->
3417 val composition = Composition(EmptyApplier(), recomposer)
3418 try {
3419 lateinit var scope: RecomposeScope
3420 val parentReferences = mutableListOf<CompositionContext>()
3421 composition.setContent {
3422 scope = currentRecomposeScope
3423 parentReferences += rememberCompositionContext()
3424 }
3425 scope.invalidate()
3426 testScheduler.advanceUntilIdle()
3427 check(parentReferences.size > 1) { "expected to be composed more than once" }
3428 check(parentReferences.toSet().size == 1) {
3429 "expected all parentReferences to be the same; saw $parentReferences"
3430 }
3431 } finally {
3432 composition.dispose()
3433 }
3434 }
3435 }
3436
3437 @OptIn(ExperimentalCoroutinesApi::class)
3438 @Test
3439 fun testParentCompositionRecomposesFirst() = runTest {
3440 localRecomposerTest { recomposer ->
3441 val composition = Composition(EmptyApplier(), recomposer)
3442 val results = mutableListOf<String>()
3443 try {
3444 var firstState by mutableStateOf("firstInitial")
3445 var secondState by mutableStateOf("secondInitial")
3446 Snapshot.notifyObjectsInitialized()
3447 composition.setContent {
3448 results += firstState
3449 TestSubcomposition { results += secondState }
3450 }
3451 secondState = "secondSet"
3452 Snapshot.sendApplyNotifications()
3453 firstState = "firstSet"
3454 Snapshot.sendApplyNotifications()
3455 testScheduler.advanceUntilIdle()
3456 assertEquals(
3457 listOf("firstInitial", "secondInitial", "firstSet", "secondSet"),
3458 results,
3459 "Expected call ordering during recomposition of subcompositions"
3460 )
3461 } finally {
3462 composition.dispose()
3463 }
3464 }
3465 }
3466
3467 /**
3468 * An [Applier] may inadvertently (or on purpose) run arbitrary user code as a side effect of
3469 * performing tree manipulations as a [Composer] is applying changes. This can happen if the
3470 * tree type dispatches event callbacks when nodes are added or removed from a tree. These
3471 * callbacks may cause snapshot state writes, which can in turn invalidate scopes in the
3472 * composition that produced the tree in the first place. Ensure that the recomposition
3473 * machinery is robust to this, and that these invalidations are processed on a subsequent
3474 * recomposition.
3475 */
3476 @OptIn(ExperimentalCoroutinesApi::class)
3477 @Test
3478 fun testStateWriteInApplier() = runTest {
3479 class MutateOnRemoveApplier(private val removeCounter: MutableState<Int>) :
3480 AbstractApplier<Unit>(Unit) {
3481 var insertCount: Int = 0
3482 private set
3483
3484 override fun remove(index: Int, count: Int) {
3485 removeCounter.value += count
3486 }
3487
3488 override fun onClear() {
3489 // do nothing
3490 }
3491
3492 override fun insertTopDown(index: Int, instance: Unit) {
3493 insertCount++
3494 }
3495
3496 override fun insertBottomUp(index: Int, instance: Unit) {
3497 // do nothing
3498 }
3499
3500 override fun move(from: Int, to: Int, count: Int) {
3501 // do nothing
3502 }
3503 }
3504
3505 localRecomposerTest { recomposer ->
3506 val stateMutatedOnRemove = mutableStateOf(0)
3507 var shouldEmitNode by mutableStateOf(true)
3508 var compositionCount = 0
3509 Snapshot.notifyObjectsInitialized()
3510 val applier = MutateOnRemoveApplier(stateMutatedOnRemove)
3511 val composition = Composition(applier, recomposer)
3512 try {
3513 composition.setContent {
3514 compositionCount++
3515 // Read the state here so that the emit removal will invalidate it
3516 stateMutatedOnRemove.value
3517 if (shouldEmitNode) {
3518 ComposeNode<Unit, MutateOnRemoveApplier>({}) {}
3519 }
3520 }
3521 // Initial composition should not contain the node we will remove. We want to test
3522 // recomposition for this case in particular.
3523 assertEquals(1, applier.insertCount, "expected setup node not inserted")
3524 shouldEmitNode = false
3525 Snapshot.sendApplyNotifications()
3526 // Only advance one frame since the next frame will be automatically scheduled.
3527 testScheduler.advanceTimeByFrame(coroutineContext)
3528 assertEquals(1, stateMutatedOnRemove.value, "observable removals performed")
3529 // Only two composition passes should have been performed by this point; a state
3530 // invalidation in the applier should not be picked up or acted upon until after
3531 // this frame is complete.
3532 assertEquals(2, compositionCount, "expected number of (re)compositions performed")
3533 // After sending apply notifications we expect the snapshot state change made by
3534 // the applier to trigger one final recomposition.
3535 Snapshot.sendApplyNotifications()
3536 testScheduler.advanceUntilIdle()
3537 assertEquals(3, compositionCount, "expected number of (re)compositions performed")
3538 } finally {
3539 composition.dispose()
3540 }
3541 }
3542 }
3543
3544 @Test // Regression test for b/180124293
3545 @OptIn(ExperimentalCoroutinesApi::class)
3546 fun disposedCompositionShouldReportAsDisposed() = runTest {
3547 localRecomposerTest { recomposer ->
3548 val composition = Composition(EmptyApplier(), recomposer)
3549 assertFalse(composition.isDisposed)
3550 composition.dispose()
3551 assertTrue(composition.isDisposed)
3552 }
3553 }
3554
3555 @OptIn(ExperimentalCoroutinesApi::class)
3556 @Test
3557 fun testSubcomposeSingleComposition() = compositionTest {
3558 var flag by mutableStateOf(true)
3559 var flagCopy by mutableStateOf(true)
3560 var copyValue = true
3561 var rememberedValue = true
3562 compose {
3563 Text("Parent $flag")
3564 flagCopy = flag
3565 TestSubcomposition {
3566 copyValue = flagCopy
3567 rememberedValue = remember(flagCopy) { copyValue }
3568 }
3569 }
3570
3571 flag = false
3572 val count = advanceCount()
3573 assertFalse(copyValue)
3574 assertFalse(rememberedValue)
3575 assertEquals(1, count)
3576 }
3577
3578 @Test
3579 fun enumCompositeKeyShouldBeStable() = compositionTest {
3580 var parentHash = EmptyCompositeKeyHashCode
3581 var compositeHash = EmptyCompositeKeyHashCode
3582 compose {
3583 parentHash = currentCompositeKeyHashCode
3584 key(MyEnum.First) { compositeHash = currentCompositeKeyHashCode }
3585 }
3586
3587 val effectiveHash = compositeHash xor (parentHash rol 6)
3588 assertEquals(0, effectiveHash)
3589 }
3590
3591 @Test
3592 fun enumCompositeKeysShouldBeStable() = compositionTest {
3593 var parentHash = EmptyCompositeKeyHashCode
3594 var compositeHash = EmptyCompositeKeyHashCode
3595 compose {
3596 parentHash = currentCompositeKeyHashCode
3597 key(MyEnum.First, MyEnum.Second) { compositeHash = currentCompositeKeyHashCode }
3598 }
3599
3600 val effectiveHash = compositeHash xor (parentHash rol 6)
3601 assertEquals(8, effectiveHash)
3602 }
3603
3604 @Test // regression test for b/188015757
3605 fun testRestartOfDefaultFunctions() = compositionTest {
3606 @Composable
3607 fun Test() {
3608 Defaults()
3609 use(stateB)
3610 }
3611
3612 compose { Test() }
3613
3614 // Force Defaults to skip
3615 stateB++
3616 advance()
3617
3618 // Force Defaults to recompose
3619 stateA++
3620 advance()
3621 }
3622
3623 // Regression test for b/383769314
3624 @Test
3625 fun testRememberNotRecomputedInElidedGroupAfterMovableGroup() = compositionTest {
3626 var baseKey by mutableIntStateOf(0)
3627 var rememberInvocations = 0
3628
3629 compose {
3630 key(baseKey) { remember { baseKey.toString() } }
3631
3632 key(Unit) {}
3633
3634 remember {
3635 assertEquals(
3636 1,
3637 ++rememberInvocations,
3638 "Remember block should be invoked exactly once"
3639 )
3640 }
3641 }
3642
3643 baseKey++
3644 advance()
3645
3646 assertEquals(1, rememberInvocations, "Remember block should be invoked exactly once")
3647 }
3648
3649 // Regression test for b/383769314
3650 @Test
3651 fun testRememberNotRecomputedInElidedGroupAfterNestedMovableGroup() = compositionTest {
3652 var baseKey by mutableIntStateOf(0)
3653 var rememberInvocations = 0
3654
3655 compose {
3656 key(baseKey) { remember { baseKey.toString() } }
3657
3658 key(Unit) { key(Unit) {} }
3659
3660 remember {
3661 assertEquals(
3662 1,
3663 ++rememberInvocations,
3664 "Remember block should be invoked exactly once"
3665 )
3666 }
3667 }
3668
3669 baseKey++
3670 advance()
3671
3672 assertEquals(1, rememberInvocations, "Remember block should be invoked exactly once")
3673 }
3674
3675 // Regression test for b/383769314
3676 @Test
3677 fun testInvalidateCapturingLambdaInElidedGroupAfterMovableGroup() = compositionTest {
3678 var baseKey by mutableIntStateOf(0)
3679 var lastOuterSeen = ""
3680 var lastInnerSeen = ""
3681
3682 compose {
3683 val captor = key(baseKey) { remember { baseKey.toString() } }
3684
3685 key(Unit) {}
3686
3687 lastOuterSeen = captor
3688 Container { lastInnerSeen = captor }
3689 }
3690
3691 assertEquals("0", lastOuterSeen, "Outer scope did not compose")
3692 assertEquals("0", lastInnerSeen, "Inner scope did not compose")
3693
3694 baseKey++
3695 advance()
3696
3697 assertEquals("1", lastOuterSeen, "Outer scope did not recompose")
3698 assertEquals("1", lastInnerSeen, "Inner scope did not recompose")
3699 }
3700
3701 // Regression test for b/383769314
3702 @Test
3703 fun testInvalidateCapturingLambdaInElidedGroupAfterNestedMovableGroup() = compositionTest {
3704 var baseKey by mutableIntStateOf(0)
3705 var lastOuterSeen = ""
3706 var lastInnerSeen = ""
3707
3708 compose {
3709 val captor = key(baseKey) { remember { baseKey.toString() } }
3710
3711 key(Unit) { key(Unit) {} }
3712
3713 lastOuterSeen = captor
3714 Container { lastInnerSeen = captor }
3715 }
3716
3717 assertEquals("0", lastOuterSeen, "Outer scope did not compose")
3718 assertEquals("0", lastInnerSeen, "Inner scope did not compose")
3719
3720 baseKey++
3721 advance()
3722
3723 assertEquals("1", lastOuterSeen, "Outer scope did not recompose")
3724 assertEquals("1", lastInnerSeen, "Inner scope did not recompose")
3725 }
3726
3727 enum class MyEnum {
3728 First,
3729 Second
3730 }
3731
3732 /** set should set the value every time, update should only set after initial composition. */
3733 @OptIn(ExperimentalCoroutinesApi::class)
3734 @Test
3735 fun composeNodeSetVsUpdate() = runTest {
3736 localRecomposerTest { recomposer ->
3737 class SetUpdateNode(property: String) {
3738 var changeCount = 0
3739 var property: String = property
3740 set(value) {
3741 field = value
3742 changeCount++
3743 }
3744 }
3745 class SetUpdateNodeApplier : AbstractApplier<SetUpdateNode>(SetUpdateNode("root")) {
3746 override fun insertTopDown(index: Int, instance: SetUpdateNode) {}
3747
3748 override fun insertBottomUp(index: Int, instance: SetUpdateNode) {}
3749
3750 override fun remove(index: Int, count: Int) {}
3751
3752 override fun move(from: Int, to: Int, count: Int) {}
3753
3754 override fun onClear() {}
3755 }
3756 val composition = Composition(SetUpdateNodeApplier(), recomposer)
3757 val nodes = mutableListOf<SetUpdateNode>()
3758 fun makeNode(property: String) = SetUpdateNode(property).also { nodes += it }
3759
3760 var value by mutableStateOf("initial")
3761
3762 composition.setContent {
3763 ComposeNode<SetUpdateNode, SetUpdateNodeApplier>(
3764 factory = { makeNode(value) },
3765 update = { set(value) { property = value } }
3766 )
3767 ComposeNode<SetUpdateNode, SetUpdateNodeApplier>(
3768 factory = { makeNode(value) },
3769 update = { update(value) { property = value } }
3770 )
3771 }
3772
3773 assertEquals("initial", nodes[0].property, "node 0 initial composition value")
3774 assertEquals("initial", nodes[1].property, "node 1 initial composition value")
3775 assertEquals(1, nodes[0].changeCount, "node 0 initial composition changeCount")
3776 assertEquals(0, nodes[1].changeCount, "node 1 initial composition changeCount")
3777
3778 value = "changed"
3779 Snapshot.sendApplyNotifications()
3780 testScheduler.advanceUntilIdle()
3781
3782 assertEquals("changed", nodes[0].property, "node 0 recomposition value")
3783 assertEquals("changed", nodes[1].property, "node 1 recomposition value")
3784 assertEquals(2, nodes[0].changeCount, "node 0 recomposition changeCount")
3785 assertEquals(1, nodes[1].changeCount, "node 1 recomposition changeCount")
3786 }
3787 }
3788
3789 @Test
3790 fun internalErrorsAreReportedAsInternal() = compositionTest {
3791 expectError("internal") { compose { currentComposer.createNode { null } } }
3792 }
3793
3794 @Test
3795 fun textWithElvis() = compositionTest {
3796 compose {
3797 val value: String? = null
3798 value?.let { Text("Bye!") } ?: Text("Hello!")
3799 }
3800
3801 validate { Text("Hello!") }
3802 }
3803
3804 @Test
3805 fun textWithIfNotNull() = compositionTest {
3806 val condition = false
3807 compose {
3808 val result =
3809 if (condition) {
3810 Text("Bye!")
3811 } else null
3812
3813 if (result == null) {
3814 Text("Hello!")
3815 }
3816 }
3817
3818 validate { Text("Hello!") }
3819 }
3820
3821 @Test // Regression test for b/249050560
3822 fun testFunctionInstances() = compositionTest {
3823 var state by mutableStateOf(0)
3824 functionInstance = { -1 }
3825
3826 compose {
3827 val localStateCopy = state
3828 fun localStateReader() = localStateCopy
3829 updateInstance(::localStateReader)
3830 }
3831
3832 assertEquals(state, functionInstance())
3833
3834 state = 10
3835 advance()
3836 assertEquals(state, functionInstance())
3837 }
3838
3839 @Test
3840 fun testNonLocalReturn_CM1_RetFunc_FalseTrue() = compositionTest {
3841 var condition by mutableStateOf(false)
3842
3843 compose { test_CM1_RetFun(condition) }
3844
3845 validate { this.test_CM1_RetFun(condition) }
3846
3847 condition = true
3848
3849 expectChanges()
3850
3851 revalidate()
3852 }
3853
3854 @Test
3855 fun testNonLocalReturn_CM1_RetFunc_TrueFalse() = compositionTest {
3856 var condition by mutableStateOf(true)
3857
3858 compose { test_CM1_RetFun(condition) }
3859
3860 validate { this.test_CM1_RetFun(condition) }
3861
3862 condition = false
3863
3864 expectChanges()
3865
3866 revalidate()
3867 }
3868
3869 @Test
3870 fun test_CM1_CCM1_RetFun_FalseTrue() = compositionTest {
3871 var condition by mutableStateOf(false)
3872
3873 compose { test_CM1_CCM1_RetFun(condition) }
3874
3875 validate { this.test_CM1_CCM1_RetFun(condition) }
3876
3877 condition = true
3878
3879 expectChanges()
3880
3881 revalidate()
3882 }
3883
3884 @Test
3885 fun test_CM1_CCM1_RetFun_TrueFalse() = compositionTest {
3886 var condition by mutableStateOf(true)
3887
3888 compose { test_CM1_CCM1_RetFun(condition) }
3889
3890 validate { this.test_CM1_CCM1_RetFun(condition) }
3891
3892 condition = false
3893
3894 expectChanges()
3895
3896 revalidate()
3897 }
3898
3899 @Test
3900 fun test_returnConditionally_fromInlineLambda() = compositionTest {
3901 var condition by mutableStateOf(true)
3902
3903 compose {
3904 InlineWrapper {
3905 if (condition) return@InlineWrapper
3906 Text("Test")
3907 }
3908 }
3909
3910 validate {
3911 if (!condition) {
3912 Text("Test")
3913 }
3914 }
3915
3916 condition = false
3917
3918 expectChanges()
3919
3920 revalidate()
3921 }
3922
3923 @Test
3924 fun test_returnConditionally_fromInlineLambda_nonLocal() = compositionTest {
3925 var condition by mutableStateOf(true)
3926
3927 compose {
3928 InlineWrapper {
3929 M1 {
3930 if (condition) return@InlineWrapper
3931 Text("Test")
3932 }
3933 }
3934 }
3935
3936 validate {
3937 if (!condition) {
3938 Text("Test")
3939 }
3940 }
3941
3942 condition = false
3943
3944 expectChanges()
3945
3946 revalidate()
3947 }
3948
3949 @Test
3950 fun test_returnConditionally_fromLambda_nonLocal() = compositionTest {
3951 var condition by mutableStateOf(true)
3952
3953 compose {
3954 Wrap {
3955 M1 {
3956 if (condition) return@Wrap
3957 Text("Test")
3958 }
3959 }
3960 }
3961
3962 validate {
3963 if (!condition) {
3964 Text("Test")
3965 }
3966 }
3967
3968 condition = false
3969
3970 expectChanges()
3971
3972 revalidate()
3973 }
3974
3975 @Test // regression test for 264467571
3976 fun test_returnConditionally_fromNodeLambda_local_initial_return() = compositionTest {
3977 var condition by mutableStateOf(true)
3978
3979 compose {
3980 currentComposer.disableSourceInformation()
3981 Text("Before outer")
3982 InlineLinear {
3983 Text("Before inner")
3984 InlineLinear inner@{
3985 Text("Before return")
3986 if (condition) {
3987 return@inner
3988 }
3989 Text("After return")
3990 }
3991 Text("After inner")
3992 }
3993 Text("Before outer")
3994 }
3995
3996 validate {
3997 Text("Before outer")
3998 InlineLinear {
3999 Text("Before inner")
4000 InlineLinear inner@{
4001 Text("Before return")
4002 if (condition) return@inner
4003 Text("After return")
4004 }
4005 Text("After inner")
4006 }
4007 Text("Before outer")
4008 }
4009
4010 repeat(4) {
4011 condition = !condition
4012 expectChanges()
4013 revalidate()
4014 }
4015 }
4016
4017 @Test // regression test for 264467571
4018 fun test_returnConditionally_fromNodeLambda_local_initial_no_return() = compositionTest {
4019 var condition by mutableStateOf(true)
4020 compose {
4021 currentComposer.disableSourceInformation()
4022 Text("Before outer")
4023 InlineLinear {
4024 Text("Before inner")
4025 InlineLinear inner@{
4026 Text("Before return")
4027 if (condition) return@inner
4028 Text("After return")
4029 }
4030 Text("After inner")
4031 }
4032 Text("Before outer")
4033 }
4034
4035 validate {
4036 Text("Before outer")
4037 InlineLinear {
4038 Text("Before inner")
4039 InlineLinear inner@{
4040 Text("Before return")
4041 if (condition) return@inner
4042 Text("After return")
4043 }
4044 Text("After inner")
4045 }
4046 Text("Before outer")
4047 }
4048
4049 repeat(4) {
4050 condition = !condition
4051 expectChanges()
4052 revalidate()
4053 }
4054 }
4055
4056 @Test // regression test for 264467571
4057 fun test_returnConditionally_fromNodeLambda_nonLocal_initial_return() = compositionTest {
4058 var condition by mutableStateOf(true)
4059 compose {
4060 currentComposer.disableSourceInformation()
4061 Text("Before outer")
4062 InlineLinear outer@{
4063 Text("Before inner")
4064 InlineLinear {
4065 Text("Before return")
4066 if (condition) return@outer
4067 Text("After return")
4068 }
4069 Text("After inner")
4070 }
4071 Text("Before outer")
4072 }
4073
4074 validate {
4075 Text("Before outer")
4076 InlineLinear outer@{
4077 Text("Before inner")
4078 InlineLinear {
4079 Text("Before return")
4080 if (condition) return@outer
4081 Text("After return")
4082 }
4083 Text("After inner")
4084 }
4085 Text("Before outer")
4086 }
4087
4088 repeat(4) {
4089 condition = !condition
4090 expectChanges()
4091 verifyConsistent()
4092 revalidate()
4093 }
4094 }
4095
4096 @Test // regression test for 264467571
4097 fun test_returnConditionally_fromNodeLambda_nonLocal_initial_no_return() = compositionTest {
4098 var condition by mutableStateOf(true)
4099 compose {
4100 currentComposer.disableSourceInformation()
4101 Text("Before outer")
4102 InlineLinear outer@{
4103 Text("Before inner")
4104 InlineLinear {
4105 Text("Before return")
4106 if (condition) return@outer
4107 Text("After return")
4108 }
4109 Text("After inner")
4110 }
4111 Text("Before outer")
4112 }
4113
4114 validate {
4115 Text("Before outer")
4116 InlineLinear outer@{
4117 Text("Before inner")
4118 InlineLinear {
4119 Text("Before return")
4120 if (condition) return@outer
4121 Text("After return")
4122 }
4123 Text("After inner")
4124 }
4125 Text("Before outer")
4126 }
4127
4128 repeat(4) {
4129 condition = !condition
4130 expectChanges()
4131 revalidate()
4132 }
4133 }
4134
4135 @Test
4136 fun test_returnConditionally_fromConditionalNodeLambda_nonLocal_initial_no_return() =
4137 compositionTest {
4138 var condition by mutableStateOf(true)
4139 compose {
4140 currentComposer.disableSourceInformation()
4141 Text("Before outer")
4142 InlineLinear outer@{
4143 Text("Before inner")
4144 if (condition) {
4145 InlineLinear {
4146 Text("Before return")
4147 return@outer
4148 }
4149 }
4150 Text("After inner")
4151 }
4152 Text("Before outer")
4153 }
4154
4155 validate {
4156 Text("Before outer")
4157 InlineLinear outer@{
4158 Text("Before inner")
4159 if (condition) {
4160 InlineLinear {
4161 Text("Before return")
4162 return@outer
4163 }
4164 }
4165 Text("After inner")
4166 }
4167 Text("Before outer")
4168 }
4169
4170 repeat(4) {
4171 condition = !condition
4172 expectChanges()
4173 revalidate()
4174 }
4175 }
4176
4177 @Test
4178 fun test_returnConditionally_fromFunction_nonLocal() = compositionTest {
4179 val text = mutableStateOf<String?>(null)
4180
4181 compose { TextWithNonLocalReturn(text.value) }
4182
4183 validate {
4184 if (text.value != null) {
4185 Text(text.value!!)
4186 }
4187 }
4188
4189 text.value = "Test"
4190
4191 expectChanges()
4192
4193 revalidate()
4194 }
4195
4196 @Test // regression test for 274889428
4197 fun test_returnConditionally_simulatedIf() = compositionTest {
4198 val condition1 = mutableStateOf(true)
4199 val condition2 = mutableStateOf(true)
4200 val condition3 = mutableStateOf(true)
4201
4202 compose block@{
4203 Text("A")
4204 simulatedIf(condition1.value) {
4205 return@block
4206 }
4207 Text("B")
4208 simulatedIf(condition2.value) {
4209 return@block
4210 }
4211 Text("C")
4212 simulatedIf(condition3.value) {
4213 return@block
4214 }
4215 Text("D")
4216 }
4217
4218 validate block@{
4219 Text("A")
4220 this.simulatedIf(condition1.value) {
4221 return@block
4222 }
4223 Text("B")
4224 this.simulatedIf(condition2.value) {
4225 return@block
4226 }
4227 Text("C")
4228 this.simulatedIf(condition3.value) {
4229 return@block
4230 }
4231 Text("D")
4232 }
4233
4234 condition1.value = false
4235 expectChanges()
4236 revalidate()
4237
4238 condition2.value = false
4239 expectChanges()
4240 revalidate()
4241
4242 condition3.value = false
4243 expectChanges()
4244 revalidate()
4245 }
4246
4247 @Test // regression test for 267586102
4248 fun test_remember_in_a_loop() = compositionTest {
4249 var i1 = 0
4250 var i2 = 0
4251 var i3 = 0
4252 var recomposeCounter = 0
4253 var scope: RecomposeScope? = null
4254
4255 val content: @Composable SomeUnstableClass.() -> Unit = {
4256 for (index in 0 until recomposeCounter) {
4257 remember(this) { index }
4258 }
4259 scope = currentRecomposeScope
4260 // these remembered values are not expected to change, BUT they change on recompositions
4261 i1 = remember(this) { 1 }
4262 i2 = remember(this) { 2 }
4263 i3 = remember(this) { 3 }
4264 recomposeCounter++
4265 }
4266
4267 compose { content(SomeUnstableClass()) }
4268 advance()
4269 verifyConsistent()
4270
4271 assertEquals(1, i1)
4272 assertEquals(2, i2)
4273 assertEquals(3, i3)
4274 assertEquals(1, recomposeCounter)
4275
4276 scope!!.invalidate()
4277 advance()
4278 verifyConsistent()
4279
4280 assertEquals(2, recomposeCounter)
4281 assertEquals(1, i1)
4282 assertEquals(2, i2)
4283 assertEquals(3, i3)
4284
4285 scope!!.invalidate()
4286 advance()
4287 verifyConsistent()
4288
4289 assertEquals(3, recomposeCounter)
4290 assertEquals(1, i1)
4291 assertEquals(2, i2)
4292 assertEquals(3, i3)
4293 }
4294
4295 @Test
4296 fun testRememberAddedAndRemovedInALoop() = compositionTest {
4297 val iterations = 100
4298 val counter = mutableStateOf(0)
4299 var seen = emptyList<Any?>()
4300
4301 fun seen(list: List<Any>) {
4302 for (i in 0 until min(list.size, seen.size)) {
4303 assertEquals(list[i], seen[i])
4304 }
4305 seen = list
4306 }
4307
4308 compose {
4309 val list = mutableListOf<Any>()
4310 for (i in 0 until counter.value) {
4311 list.add(remember { Any() })
4312 }
4313 }
4314
4315 while (counter.value < iterations) {
4316 counter.value++
4317 advance()
4318 }
4319
4320 while (counter.value > 0) {
4321 counter.value--
4322 advance()
4323 }
4324 assertEquals(emptyList(), seen)
4325 }
4326
4327 @Test
4328 fun test_crossinline_differentComposition() = compositionTest {
4329 var branch by mutableStateOf(false)
4330 compose {
4331 if (branch) {
4332 Text("Content")
4333 }
4334 InlineSubcomposition {
4335 if (false) {
4336 remember { "Something" }
4337 }
4338 }
4339 }
4340 validate {
4341 if (branch) {
4342 Text("Content")
4343 }
4344 }
4345
4346 branch = true
4347 expectChanges()
4348 revalidate()
4349 }
4350
4351 @Test
4352 fun composableDelegates() = compositionTest {
4353 val local = compositionLocalOf { "Default" }
4354 val delegatedLocal by local
4355 compose {
4356 Text(delegatedLocal)
4357
4358 CompositionLocalProvider(local provides "Scoped") { Text(delegatedLocal) }
4359 }
4360 validate {
4361 Text("Default")
4362 Text("Scoped")
4363 }
4364 }
4365
4366 @Test
4367 fun testCompositionAndRecomposerDeadlock() {
4368 runTest(timeout = 10.seconds) {
4369 withGlobalSnapshotManager {
4370 repeat(100) {
4371 val job = Job(parent = coroutineContext[Job])
4372 val coroutineContext = Dispatchers.Unconfined + job
4373 val recomposer = Recomposer(coroutineContext)
4374
4375 launch(
4376 coroutineContext + BroadcastFrameClock(),
4377 start = CoroutineStart.UNDISPATCHED
4378 ) {
4379 recomposer.runRecomposeAndApplyChanges()
4380 }
4381
4382 val composition = Composition(EmptyApplier(), recomposer)
4383 composition.setContent {
4384 val innerComposition =
4385 Composition(
4386 EmptyApplier(),
4387 rememberCompositionContext(),
4388 )
4389
4390 DisposableEffect(composition) { onDispose { innerComposition.dispose() } }
4391 }
4392
4393 var value by mutableStateOf(1)
4394 launch(Dispatchers.Default + job) {
4395 while (true) {
4396 value += 1
4397 delay(1)
4398 }
4399 }
4400
4401 composition.dispose()
4402 recomposer.close()
4403 job.cancel()
4404 }
4405 }
4406 }
4407 }
4408
4409 @Test
4410 fun earlyComposableUnitReturn() = compositionTest {
4411 var state by mutableStateOf(true)
4412 compose {
4413 when (state) {
4414 true -> return@compose Text("true")
4415 false -> Text("false")
4416 }
4417 Text("after")
4418 }
4419 validate { Text("true") }
4420
4421 state = false
4422 expectChanges()
4423
4424 validate {
4425 Text("false")
4426 Text("after")
4427 }
4428 }
4429
4430 // Regression test for b/288717411
4431 @Test
4432 fun test_forgottenValue_isFreedFromSlotTable() = compositionTest {
4433 val value = Any()
4434 var rememberValue by mutableStateOf(false)
4435 val composers = mutableSetOf<Composer>()
4436 compose {
4437 composers += currentComposer
4438 if (rememberValue) {
4439 // <Any> prevents implicit coercion to Unit from inline lambda
4440 // https://youtrack.jetbrains.com/issue/KT-76579
4441 remember<Any> { value }
4442 }
4443 }
4444
4445 assertFalse(value in composition!!.getSlots())
4446
4447 rememberValue = true
4448 expectChanges()
4449
4450 assertTrue(value in composition!!.getSlots())
4451
4452 rememberValue = false
4453 expectChanges()
4454
4455 assertFalse(value in composition!!.getSlots())
4456 assertFalse(composers.any { value in it.getInsertTableSlots() })
4457 }
4458
4459 @Stable
4460 class VarargConsumer(var invokeCount: Int = 0) {
4461 @Composable
4462 fun Varargs(vararg ints: Int) {
4463 invokeCount++
4464 for (i in ints) {
4465 use(i)
4466 }
4467 }
4468 }
4469
4470 // Regression test for b/286132194
4471 @Test
4472 fun composableVarargs_skipped() = compositionTest {
4473 val consumer = VarargConsumer()
4474 var recomposeTrigger by mutableStateOf(0)
4475 compose {
4476 Linear {
4477 use(recomposeTrigger)
4478 consumer.Varargs(0, 1, 2, 3)
4479 }
4480 }
4481
4482 assertEquals(1, consumer.invokeCount)
4483
4484 recomposeTrigger = 1
4485 advance()
4486
4487 assertEquals(1, consumer.invokeCount)
4488 }
4489
4490 fun interface TestFunInterface {
4491 fun compute(value: Int)
4492 }
4493
4494 @Composable
4495 fun TestMemoizedFun(compute: TestFunInterface) {
4496 val oldCompute = remember { compute }
4497 assertEquals(oldCompute, compute)
4498 }
4499
4500 @Test
4501 fun funInterface_isMemoized() = compositionTest {
4502 var recomposeTrigger by mutableStateOf(0)
4503 val capture = 0
4504 compose {
4505 use(recomposeTrigger)
4506 TestMemoizedFun {
4507 // no captures
4508 use(it)
4509 }
4510 TestMemoizedFun {
4511 // stable captures
4512 use(capture)
4513 }
4514 }
4515
4516 recomposeTrigger++
4517 advance()
4518 }
4519
4520 // regression test for b/264467571, checks that composing with continue doesn't crash runtime
4521 @Test
4522 fun continueInALoop() = compositionTest {
4523 var iterations by mutableIntStateOf(5)
4524 compose {
4525 for (i in 1..iterations) {
4526 if (i == 4) continue
4527 Text(i.toString())
4528 }
4529 }
4530
4531 iterations++
4532 expectChanges()
4533 }
4534
4535 data class Foo(var i: Int = 0)
4536
4537 class UnstableCompConsumer(var invokeCount: Int = 0) {
4538 @Composable
4539 fun UnstableComp(foo: Foo) {
4540 use(foo)
4541 invokeCount++
4542 }
4543 }
4544
4545 @Test
4546 fun composableWithUnstableParameters_skipped() = compositionTest {
4547 val consumer = UnstableCompConsumer()
4548 var recomposeTrigger by mutableStateOf(0)
4549 val data = Foo()
4550 compose {
4551 Linear {
4552 use(recomposeTrigger)
4553 consumer.UnstableComp(foo = data)
4554 }
4555 }
4556
4557 assertEquals(1, consumer.invokeCount)
4558
4559 recomposeTrigger = 1
4560 data.i++
4561 advance()
4562
4563 assertEquals(1, consumer.invokeCount)
4564 }
4565
4566 // regression test from b/232007227 with forEach
4567 @Test
4568 fun slotsAreUsedCorrectly_forEach() = compositionTest {
4569 class Car(val model: String)
4570 class Person(val name: String, val car: MutableStateFlow<Car>)
4571
4572 val people =
4573 mutableListOf<MutableStateFlow<Person?>>(
4574 MutableStateFlow(Person("Ford", MutableStateFlow(Car("Model T")))),
4575 MutableStateFlow(Person("Musk", MutableStateFlow(Car("Model 3"))))
4576 )
4577 compose {
4578 people.forEach {
4579 val person = it.collectAsState().value
4580 Text(person?.name ?: "No person")
4581 if (person != null) {
4582 val car = person.car.collectAsState().value
4583 Text(" ${car.model}")
4584 }
4585 }
4586 }
4587
4588 validate {
4589 people.forEach {
4590 val person = it.value
4591 Text(person?.name ?: "No person")
4592 if (person != null) {
4593 val car = person.car.value
4594 Text(" ${car.model}")
4595 }
4596 }
4597 }
4598
4599 advanceTimeBy(16_000L)
4600 people[0].value = null
4601 advanceTimeBy(16_000L)
4602
4603 expectChanges()
4604 revalidate()
4605 }
4606
4607 @Test
4608 fun readingDerivedState_invalidatesWhenValueNotChanged() = compositionTest {
4609 var state by mutableStateOf(0)
4610 var condition by mutableStateOf(false)
4611 val derived by derivedStateOf { if (!condition) 0 else state }
4612 compose { Text(derived.toString()) }
4613 validate { Text(derived.toString()) }
4614
4615 condition = true
4616 expectNoChanges()
4617 revalidate()
4618
4619 state++
4620 expectChanges()
4621 revalidate()
4622 }
4623
4624 @Composable
4625 fun goBoom(): Boolean {
4626
4627 return true
4628 }
4629
4630 @Test
4631 fun earlyReturnFromInlined() = compositionTest {
4632 compose {
4633 run {
4634 if (true) {
4635 return@run
4636 } else {
4637 Text("")
4638 return@run
4639 }
4640 }
4641 }
4642
4643 validate {}
4644 }
4645
4646 @Composable private fun key() = "key"
4647
4648 @Test
4649 fun remember_withComposableParam() = compositionTest {
4650 compose {
4651 val text = remember(key()) { "" }
4652 Text(text)
4653 }
4654
4655 validate { Text("") }
4656 }
4657
4658 private val LocalNumber = compositionLocalOf { 0 }
4659
4660 @Composable
4661 fun Test(number: Int = LocalNumber.current) {
4662 val remembered = remember(number) { number + 1 }
4663 assertEquals(remembered, number + 1)
4664 }
4665
4666 @Test
4667 fun remember_defaultParamInRestartableFunction() = compositionTest {
4668 var state by mutableIntStateOf(0)
4669 compose { CompositionLocalProvider(LocalNumber provides state) { Test() } }
4670
4671 validate {}
4672
4673 state++
4674 advance()
4675 revalidate()
4676 }
4677
4678 @Test
4679 fun rememberAfterLoop() = compositionTest {
4680 // Removing the group around remember requires that updates to slots can be performed to
4681 // the parent even after we have moved off the parent to one of the children. Here this
4682 // inserts content as the child of the main composition. This moves the write to just after
4683 // the insert and then the `remember` call needs to update the `compose` group's slots.
4684 var count by mutableStateOf(1)
4685 compose {
4686 repeat(count) { Text("Some text") }
4687 val someRemember = remember(count) { count + 1 }
4688 Text("$someRemember")
4689 }
4690
4691 count++
4692 advance()
4693 }
4694
4695 @Test
4696 fun appendingRememberAfterLoop() = compositionTest {
4697 var count by mutableStateOf(1)
4698 compose {
4699 repeat(count) { Text("Some text") }
4700 repeat(count) { unused(remember { it }) }
4701 }
4702
4703 validate { repeat(count) { Text("Some text") } }
4704
4705 count++
4706 advance()
4707 revalidate()
4708
4709 count++
4710 advance()
4711 revalidate()
4712
4713 count = 1
4714 advance()
4715 revalidate()
4716 }
4717
4718 @Test
4719 fun composerCleanup() = compositionTest {
4720 var state by mutableStateOf(0)
4721
4722 compose { Text("State = $state") }
4723
4724 val stackSizes = (composition as CompositionImpl).composerStacksSizes()
4725 repeat(100) {
4726 state++
4727 advance()
4728 }
4729 assertEquals(stackSizes, (composition as CompositionImpl).composerStacksSizes())
4730 }
4731
4732 @Test
4733 fun movableContentNoopInDeactivatedComposition() = compositionTest {
4734 val state = mutableStateOf(false)
4735 val movableContent = movableContentOf { Text("Test") }
4736
4737 var composition: Composition? = null
4738 var context: CompositionContext? = null
4739 compose {
4740 context = rememberCompositionContext()
4741
4742 // read state to force recomposition
4743 state.value
4744 SideEffect { if (state.value) (composition as CompositionImpl).deactivate() }
4745 }
4746
4747 composition =
4748 CompositionImpl(context!!, ViewApplier(root)).apply {
4749 setContent {
4750 if (state.value) {
4751 movableContent()
4752 }
4753 }
4754 }
4755
4756 state.value = true
4757 advance()
4758 }
4759
4760 @Test // regression test for 339618126
4761 fun removeGroupAtEndOfGroup() = compositionTest {
4762 // Ensure the runtime handles aberrant code generation
4763 val state = mutableStateOf(true)
4764 compose {
4765 InlineLinear {
4766 InlineLinear {
4767 explicitStartReplaceGroup(-0x7e52e5de) { Text("Before") }
4768 explicitStartReplaceGroup(0x9222f9c, insertGroup = state.value) {}
4769 explicitStartReplaceGroup(0x22d2581c) { Text("After") }
4770 }
4771 InlineLinear { Text("State is ${state.value}") }
4772 if (state.value) {
4773 Text("State is on")
4774 }
4775 if (!state.value) {
4776 Text("State is off")
4777 }
4778 }
4779 }
4780
4781 validate {
4782 Linear {
4783 Linear {
4784 Text("Before")
4785 Text("After")
4786 }
4787 Linear { Text("State is ${state.value}") }
4788 if (state.value) {
4789 Text("State is on")
4790 }
4791 if (!state.value) {
4792 Text("State is off")
4793 }
4794 }
4795 }
4796
4797 state.value = false
4798 advance()
4799 revalidate()
4800
4801 state.value = true
4802 advance()
4803 revalidate()
4804 }
4805
4806 @Test // regression test for b/362291064
4807 fun avoidsThrashingTheSlotTable() = compositionTest {
4808 val count = 100
4809 var data by mutableIntStateOf(0)
4810 compose { repeat(count) { Linear { Text("Value: $it, data: $data") } } }
4811
4812 validate { repeat(count) { Linear { Text("Value: $it, data: $data") } } }
4813
4814 data++
4815 advance()
4816 revalidate()
4817 }
4818
4819 private inline fun CoroutineScope.withGlobalSnapshotManager(block: CoroutineScope.() -> Unit) {
4820 val channel = Channel<Unit>(Channel.CONFLATED)
4821 val job = launch { channel.consumeEach { Snapshot.sendApplyNotifications() } }
4822 val unregisterToken = Snapshot.registerGlobalWriteObserver { channel.trySend(Unit) }
4823 try {
4824 block()
4825 } finally {
4826 unregisterToken.dispose()
4827 job.cancel()
4828 }
4829 }
4830 }
4831
4832 class SomeUnstableClass(val a: Any = "abc")
4833
4834 @Composable
test_CM1_RetFunnull4835 fun test_CM1_RetFun(condition: Boolean) {
4836 Text("Root - before")
4837 M1 {
4838 Text("M1 - before")
4839 if (condition) return
4840 Text("M1 - after")
4841 }
4842 Text("Root - after")
4843 }
4844
MockViewValidatornull4845 fun MockViewValidator.test_CM1_RetFun(condition: Boolean) {
4846 Text("Root - before")
4847 Text("M1 - before")
4848 if (condition) return
4849 Text("M1 - after")
4850 Text("Root - after")
4851 }
4852
4853 @Composable
test_CM1_CCM1_RetFunnull4854 fun test_CM1_CCM1_RetFun(condition: Boolean) {
4855 Text("Root - before")
4856 M1 {
4857 Text("M1 - begin")
4858 if (condition) {
4859 Text("if - begin")
4860 M1 {
4861 Text("In CCM1")
4862 return@test_CM1_CCM1_RetFun
4863 }
4864 }
4865 Text("M1 - end")
4866 }
4867 Text("Root - end")
4868 }
4869
MockViewValidatornull4870 fun MockViewValidator.test_CM1_CCM1_RetFun(condition: Boolean) {
4871 Text("Root - before")
4872 Text("M1 - begin")
4873 if (condition) {
4874 Text("if - begin")
4875 Text("In CCM1")
4876 return
4877 }
4878 Text("M1 - end")
4879 Text("Root - end")
4880 }
4881
<lambda>null4882 var functionInstance: () -> Int = { 0 }
4883
4884 @Composable
updateInstancenull4885 fun updateInstance(newInstance: () -> Int) {
4886 functionInstance = newInstance
4887 }
4888
4889 var stateA by mutableStateOf(1000)
4890 var stateB by mutableStateOf(2000)
4891
usenull4892 fun use(@Suppress("UNUSED_PARAMETER") v: Int) {}
4893
calculateSomethingnull4894 fun calculateSomething() = 4
4895
4896 @Composable // used in testRestartOfDefaultFunctions
4897 fun Defaults(a: Int = 1, b: Int = 2, c: Int = 3, d: Int = calculateSomething()) {
4898 assertEquals(1, a)
4899 assertEquals(2, b)
4900 assertEquals(3, c)
4901 assertEquals(4, d)
4902 use(stateA)
4903 }
4904
4905 @OptIn(InternalComposeApi::class)
4906 @Composable
4907 internal fun TestSubcomposition(content: @Composable () -> Unit) {
4908 val parentRef = rememberCompositionContext()
4909 val currentContent by rememberUpdatedState(content)
<lambda>null4910 DisposableEffect(parentRef) {
4911 val subcomposition = Composition(EmptyApplier(), parentRef)
4912 // TODO: work around for b/179701728
4913 callSetContent(subcomposition) {
4914 // Note: This is in a lambda invocation to keep the currentContent state read
4915 // in the sub-composition's content composable. Changing this to be
4916 // subcomposition.setContent(currentContent) would snapshot read only on initial set.
4917 currentContent()
4918 }
4919 onDispose { subcomposition.dispose() }
4920 }
4921 }
4922
callSetContentnull4923 private fun callSetContent(composition: Composition, content: @Composable () -> Unit) {
4924 composition.setContent(content)
4925 }
4926
4927 class Ref<T : Any> {
4928 lateinit var value: T
4929 }
4930
4931 @Composable
NarrowInvalidateForReferencenull4932 fun NarrowInvalidateForReference(ref: Ref<CompositionContext>) {
4933 ref.value = rememberCompositionContext()
4934 }
4935
4936 @Composable
testDeferredSubcompositionnull4937 fun testDeferredSubcomposition(block: @Composable () -> Unit): () -> Unit {
4938 val container = remember { View() }
4939 val ref = Ref<CompositionContext>()
4940 NarrowInvalidateForReference(ref = ref)
4941 return { Composition(ViewApplier(container), ref.value).apply { setContent { block() } } }
4942 }
4943
4944 @OptIn(ExperimentalCoroutinesApi::class)
localRecomposerTestnull4945 internal suspend fun <R> TestScope.localRecomposerTest(block: CoroutineScope.(Recomposer) -> R) =
4946 coroutineScope {
4947 withContext(TestMonotonicFrameClock(this)) {
4948 val recomposer = Recomposer(coroutineContext)
4949 launch { recomposer.runRecomposeAndApplyChanges() }
4950 // ensure recomposition runner has started
4951 testScheduler.advanceUntilIdle()
4952 block(recomposer)
4953 // This call doesn't need to be in a finally; everything it does will be torn down
4954 // in exceptional cases by the coroutineScope failure
4955 recomposer.cancel()
4956 }
4957 }
4958
4959 /**
4960 * Advances this scheduler by exactly one frame, as defined by the [TestMonotonicFrameClock] in
4961 * [context].
4962 */
4963 @OptIn(ExperimentalCoroutinesApi::class)
TestCoroutineSchedulernull4964 private fun TestCoroutineScheduler.advanceTimeByFrame(context: CoroutineContext) {
4965 val testClock = context[MonotonicFrameClock] as TestMonotonicFrameClock
4966 advanceTimeBy(testClock.frameDelayMillis)
4967 // advanceTimeBy doesn't run tasks at exactly frameDelayMillis.
4968 runCurrent()
4969 }
4970
4971 @Composable
Wrapnull4972 fun Wrap(content: @Composable () -> Unit) {
4973 content()
4974 }
4975
4976 @Composable
Wrapnull4977 fun Wrap(count: Int, content: @Composable () -> Unit) {
4978 if (count > 1) Wrap(count - 1, content) else content()
4979 }
4980
assertArrayEqualsnull4981 private fun <T> assertArrayEquals(message: String, expected: Array<T>, received: Array<T>) {
4982 fun Array<T>.getString() = this.joinToString(", ") { it.toString() }
4983 fun err(msg: String): Nothing =
4984 error(
4985 "$message: $msg, expected: [${
4986 expected.getString()}], received: [${received.getString()}]"
4987 )
4988 if (expected.size != received.size) err("sizes are different")
4989 expected.indices.forEach { index ->
4990 if (expected[index] != received[index])
4991 err(
4992 "item at index $index was different (expected [${
4993 expected[index]}], received: [${received[index]}]"
4994 )
4995 }
4996 }
4997
4998 // Contact test data
4999 private val bob = Contact("Bob Smith", email = "bob@smith.com")
5000 private val jon = Contact(name = "Jon Alberton", email = "jon@alberton.com")
5001 private val steve = Contact("Steve Roberson", email = "steverob@somemail.com")
5002
testModelnull5003 private fun testModel(contacts: MutableList<Contact> = mutableListOf(bob, jon, steve)) =
5004 ContactModel(filter = "", contacts = contacts)
5005
5006 // Report test data
5007 private val jim_reports_to_sally = Report("Jim", "Sally")
5008 private val rob_reports_to_alice = Report("Rob", "Alice")
5009 private val clark_reports_to_lois = Report("Clark", "Lois")
5010
5011 internal interface Counted {
5012 val count: Int
5013 }
5014
5015 internal interface Ordered {
5016 val rememberOrder: Int
5017 val forgetOrder: Int
5018 }
5019
5020 internal interface Named {
5021 val name: String
5022 }
5023
5024 private interface WithData {
5025 val data: Int
5026 }
5027
isOddnull5028 private fun Int.isOdd() = this % 2 == 1
5029
5030 private fun Int.isEven() = this % 2 == 0
5031
5032 fun <T> MutableList<T>.swap(a: T, b: T) {
5033 val aIndex = indexOf(a)
5034 val bIndex = indexOf(b)
5035 require(aIndex >= 0 && bIndex >= 0)
5036 set(aIndex, b)
5037 set(bIndex, a)
5038 }
5039
5040 @Composable private inline fun InlineWrapper(content: @Composable () -> Unit) = content()
5041
5042 @Composable private inline fun M1(content: @Composable () -> Unit) = InlineWrapper { content() }
5043
5044 @Composable
TextWithNonLocalReturnnull5045 private fun TextWithNonLocalReturn(text: String?) {
5046 InlineWrapper {
5047 if (text == null) return
5048 Text(text)
5049 }
5050 }
5051
5052 @Composable
simulatedIfnull5053 private inline fun simulatedIf(condition: Boolean, block: () -> Unit) {
5054 if (condition) block()
5055 }
5056
simulatedIfnull5057 private inline fun MockViewValidator.simulatedIf(condition: Boolean, block: () -> Unit) {
5058 if (condition) block()
5059 }
5060
5061 @Composable
5062 private inline fun InlineSubcomposition(crossinline content: @Composable () -> Unit) =
<lambda>null5063 TestSubcomposition {
5064 content()
5065 }
5066
5067 @Composable
getValuenull5068 operator fun <T> CompositionLocal<T>.getValue(thisRef: Any?, property: KProperty<*>) = current
5069
5070 // for 274185312
5071
5072 var itemRendererCalls = 0
5073 var scrollingListCalls = 0
5074
5075 @Composable
5076 fun TestSkippingContent(data: State<Int>) {
5077 ScrollingList { viewItem ->
5078 Text("${data.value}")
5079 scrollingListCalls++
5080 ItemRenderer(viewItem)
5081 }
5082 }
5083
5084 @Composable
ItemRenderernull5085 fun ItemRenderer(viewItem: ListViewItem) {
5086 itemRendererCalls++
5087 Text("${viewItem.id}")
5088 }
5089
5090 @Composable
ScrollingListnull5091 private fun ScrollingList(
5092 itemRenderer: @Composable (ListViewItem) -> Unit,
5093 ) {
5094 ListContent(
5095 viewItems = remember { listOf(ListViewItem(0), ListViewItem(1)) },
5096 itemRenderer = itemRenderer
5097 )
5098 }
5099
5100 @Composable
ListContentnull5101 fun ListContent(viewItems: List<ListViewItem>, itemRenderer: @Composable (ListViewItem) -> Unit) {
5102 viewItems.forEach { viewItem ->
5103 ListContentItem(viewItem = viewItem, itemRenderer = itemRenderer)
5104 }
5105 }
5106
5107 @Composable
ListContentItemnull5108 fun ListContentItem(viewItem: ListViewItem, itemRenderer: @Composable (ListViewItem) -> Unit) {
5109 itemRenderer(viewItem)
5110 }
5111
5112 data class ListViewItem(val id: Int)
5113
unusednull5114 private fun <T> unused(@Suppress("UNUSED_PARAMETER") value: T) {}
5115
5116 // Part of regression test for 339618126
5117 @Composable
5118 @ExplicitGroupsComposable
5119 inline fun explicitStartReplaceGroup(
5120 key: Int,
5121 insertGroup: Boolean = true,
5122 content: @Composable () -> Unit
5123 ) {
5124 if (insertGroup) currentComposer.startReplaceGroup(key)
5125 content()
5126 if (insertGroup) currentComposer.endReplaceGroup()
5127 }
5128