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