https://kotlinlang.org logo
#squarelibraries
Title
# squarelibraries
t

Trevor Stone

01/27/2023, 5:32 PM
I was able to get it working! I'm not sure if this is exactly what you meant by the above, but here is my sample code in 🧵
Copy code
fun interface StringListScope {
    fun addString(string: String)
}

@Composable
fun StringListBuilder(content: @Composable StringListScope.() -> Unit): List<String> = run {
    (mutableListOf<String>()).also {
        StringListScope { string: String -> it.add(string) }.content()
    }
}

@Composable fun StringListScope.Hello(prefix: String) = addString("$prefix Hello")

@Composable fun StringListScope.World(suffix: String) = addString("World $suffix")

@Composable
fun StringListOutput(count: Int) = StringListBuilder {
    Hello("$count")
    World("$count")
}

val flow =
    moleculeFlow(RecompositionClock.Immediate) {
        var count by remember { mutableStateOf(0) }

        LaunchedEffect(Unit) {
            while (true) {
                delay(500)
                count++
            }
        }

        StringListOutput(count)
    }
j

jw

01/28/2023, 2:34 AM
Hmm I see. I'm not entirely sure this is safe.
t

Trevor Stone

01/28/2023, 2:34 AM
Yeah I don't love it, but it appears to be working so far
j

jw

01/28/2023, 2:34 AM
If you emit a constant string with no dependency on
count
is it included in every emission? Or only the first?
t

Trevor Stone

01/28/2023, 2:38 AM
Copy code
fun interface StringListScope {
        fun addString(string: String)
    }

    @Composable
    fun StringListBuilder(content: @Composable StringListScope.() -> Unit): List<String> = run {
        (mutableListOf<String>()).also {
            StringListScope { string: String -> it.add(string) }.content()
        }
    }

    @Composable fun StringListScope.Hello(prefix: String) = addString("$prefix Hello")

    @Composable fun StringListScope.World() = addString("World")

    @Composable
    fun StringListOutput(count: Int) = StringListBuilder {
        Hello("$count")
        World()
    }

    val flow: Flow<List<String>> =
        moleculeFlow(RecompositionClock.Immediate) {
            var count by remember { mutableStateOf(0) }

            LaunchedEffect(Unit) {
                while (true) {
                    delay(500)
                    count++
                }
            }

            StringListOutput(count)
        }

    @Test
    fun sanityCheck() = runTest {
        flow.test {
            println("first" + awaitItem())
            cancelAndIgnoreRemainingEvents()
        }
        flow.test {
            println("second" + awaitItem())
            println("second" + awaitItem())
            println("second" + awaitItem())
            println("second" + awaitItem())

            cancelAndIgnoreRemainingEvents()
        }
    }
This outputs
Copy code
first[0 Hello, World]
second[0 Hello, World]
second[1 Hello, World]
second[2 Hello, World]
second[3 Hello, World]
So yes
I was looking into Mosaic a bit and was wondering if I could leverage something closer to how Nodes seem to work
but instead of rendering to a canvas I somehow can aggregate them to a data structure output
j

jw

01/28/2023, 2:42 AM
Absolutely
In this example each
StringListScope.something()
function would call
ComposeNode
to emit something into the tree
and it doesn't even have to be a tree. If none of the calls to
ComposeNode
use the
children
parameter, the result will be a root node and a single set of children (basically, a list)
t

Trevor Stone

01/28/2023, 2:45 AM
Would this still work with Molecule to have the result emit as a Flow? Or would this need to move away from Molecule
but this sounds really promising
j

jw

01/28/2023, 2:45 AM
You would need to replace about 50% of Molecule, but Molecule is only about 40 lines of code.
You would need to create a real applier
Pass that to the Composition with a root node
and then you create the composable function that emits a node
that's about it
Mosaic is a good place to look. It has an incredibly simple applier and node hierarchy
t

Trevor Stone

01/28/2023, 2:48 AM
Other than asking you, are there any good resources you know of to get a better handle on the internals of compose? I'm looking closer at Molecule's implementation right now
j

jw

01/28/2023, 2:52 AM
Jetpack Compose Internals book by Jorge
Conference talks by Leland on Compose team
I learned a lot by reading the unit tests of the Compose runtime
t

Trevor Stone

01/28/2023, 2:52 AM
Ah I was looking at https://jorgecastillo.dev/diving-into-mosaic earlier today
Otherwise do you think that the initial solution posted is viable after seeing the outputs?
j

jw

01/28/2023, 3:00 AM
I mean if it works then yeah. The general pattern there is create a mutable object, abstract over it with an interface, and pass that around (either explicitly, as a receiver, or as a fancy new context), and finally return the mutable object (potentially as a read-only/immutable version of itself)
t

Trevor Stone

01/29/2023, 11:52 PM
While poking around at this a bit more I think I have a solid path forward except I don’t see where I would hook into for the emit. From my understanding Molecule emits whatever the return value is, and essentially is called at the correct time due to it being called in
setContent
. When the
body
recomposes the new return value gets returned and emitted. Based upon mosaic, I have a root node that I want to emit instead. mosaic seems to update the output on a 50ms interval, but I was hoping to have a solution that emits nothing unless a change has occurred. Is there a place in the recomposition lifecycle I could be emitting from?
j

jw

01/30/2023, 12:18 AM
Mosaic adds a frame waiter to the frame clock to wait for state changes to redraw
t

Trevor Stone

01/30/2023, 12:58 AM
Is that triggered by
Copy code
override fun setContent(content: @Composable () -> Unit) {
				composition.setContent(content)
				hasFrameWaiters = true
			}
I’ve got it working now! Thanks for your help
4 Views