Can I render a `@Composable` outside of `setConten...
# compose
g
Can I render a
@Composable
outside of
setContent {}
so that I could get the size of one child
Composable
within? I don’t care doing that in the UI thread right now, just want to know if it’s possible!
s
You may want to make a layout where you can measure that child directly and get its size?
g
Thanks I’ll have a look at that
@Stylianos Gakis How do you make it run outside of
setContent {}
, like can I trigger this
layout
to compose outside of the UI hierarchy?
s
Hmm, I don’t think you can get this sort of data outside of the “setContent” (which gets you a @Composable context), afaik you need to be inside that context at least. I know we’re getting a notion of “composable as a value” soon in the new alphas, but I don’t think even that provides the size of the composable for example. The only way I have successfully worked with doing such things have been with
layout{}
and doing everything inside that block. One term I’ve heard thrown around in this slack has been “SubComposition” which I don’t know what it is but maybe this potentially helps solve your problem in some way? And another less optimal way I’ve seen people do something like this has been by laying it out normally and using
onGloballyPositioned
to get data about it’s place/size? If none of the above, I am not quite sure myself, maybe someone else might have another idea. Or maybe you should reconsider what you’re trying to achieve and go about it with a different approach?
g
I’m trying to know what the size of an image will be on screen to prefetch an image in advance so when it will be effectively shown on the screen, the image of the right size will be waiting in the in-memory cache of images
But you gave me a few ideas how I could try to achieve that, so I’ll try a few things now
a
Modifier.onSizeChanged
is likely to be an efficient answer to what you're trying to do here. Alternatively, we've worked with the Coil library authors to help with this case in the past as well, that might be an easy drop-in solution
today i learned 2
g
@Adam Powell Thanks for the info! I’m doing the following, but I don’t think the composition is running, any idea why?
Copy code
var imageSize: IntSize? = null
                ComposeView(context).apply {
                    compositionContext = Recomposer(<http://Dispatchers.IO|Dispatchers.IO>)
                    setContent {
                        HouzzTheme {
                            AdItem(ad = safeAd) { imageSize = it }
                        }
                    }
                    measure(ViewMeasureUtils.exact(width), ViewMeasureUtils.exact(height))
                    layout(0, 0, width, width)
                }
                delay(5000)
                logger().d("HomeFeedComposedAdFetcher", "ImageSize = $imageSize")
a
Several, but I'm more interested in why you're creating a whole recomposer and ComposeView to get the measurement of a single layout element to load an image 🙂
g
The idea here is to load the image to be ready in memory in a cache at the exact size its view will have so there’s no loading time for those images in ads when the user scrolls the screen
That’s what was already implemented in that screen and I’m trying to reproduce that result for Jetpack Compose so that we’re actually allowed to rewrite the whole feature in JC 😄
@Adam Powell Any pointer would be really appreciated 😊
a
what are you using to determine what size its view will have?
i.e. where are you getting
width
and
height
in the snippet above?
g
That’s a good point. At first I tried passing to the ViewModel the IntSize the
LazyColumn
would get in its
onSizeChanged
modifier:
Copy code
).onSizeChanged {
     homeFeedViewModel.screenSize = it
}
Ugly, I know, but I have to make this work at any price. I thought this was working but got lots of
null
at the time I am trying to run the snippet above, so I just used constants to go forward for now:
Copy code
val width = 1080
val height = 2400
Not pretty as well, I agree….
Ok adding a little delay I can manage to wait for the Composable to have a size and I do get it correctly (I can wait even longer to make sure):
Copy code
2022-02-10 22:27:20.733 16222-16222/com.some.app D/fillDescriptor: called with safeSize = 1080 x 1865
Now, the question is why I can’t get that value out of the callback of the Composable, as it also uses internally
onSizeChanged
to notify us of its size:
Copy code
fun AdItem(
    ad: Ad,
    modifier: Modifier = Modifier,
    onImageSizeChanged: ((IntSize) -> Unit)? = null,
) {
        SomeImage(
            placeHolder = defaultPlaceholder(),
            imageDescriptor = ad.AdSpace.image1Descriptor(),
            modifier = Modifier
                .fillMaxWidth()
                .height(375.dp)
                .onSizeChanged {
                    onImageSizeChanged?.invoke(it)
                }
        )
}
And the snippet, again:
Copy code
var imageSize: IntSize? = null
                ComposeView(context).apply {
                    compositionContext = Recomposer(Dispatchers.Main)
                    setContent {
                        HouzzTheme {
                            AdItem(ad = safeAd) { imageSize = it }
                        }
                    }
                    measure(ViewMeasureUtils.exact(width), ViewMeasureUtils.exact(height))
                    layout(0, 0, width, width)
                }
Does the composition happen only following a lifecycle event maybe? I thought it would measure and layout itself when the
ComposeView
does 🤔
Still not running, but I could make some progress with the following:
Copy code
class VagabondComposeView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AbstractComposeView(context, attrs, defStyleAttr) {

    private val content = mutableStateOf<(@Composable () -> Unit)?>(null)

    @Suppress("RedundantVisibilityModifier")
    protected override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
        private set

    @Composable
    override fun Content() {
        content.value?.invoke()
    }


    fun setContent(content: @Composable () -> Unit) {
        this.content.value = content
        createComposition()
    }
}
My
Composable
AdItem
is still not composed…
a
It won't recompose unless you call
runRecomposeAndApplyChanges
on the recomposer, but I still think this is overkill for the problem space. If you know what the size of your container is in your main composition there should be no need for this parallel universe
g
The issue is that indeed I do not know the size of the image, and there’s no way to get that size before being rendered in its LazyColumn, right? The image in the old UI ToolKit is setup inside another layout, with 3 hierarchies, like this:
Copy code
<com.company.app.views.MyImageView
    android:id="@+id/image"
    style="@style/image_bg_gray"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>
So the width depends on the parent layout which has a parent layout which has another parent layout. I’ve made the equivalent in Compose like this:
Copy code
CustomImage(
    placeHolder = defaultPlaceholder(),
    imageDescriptor = someData.imageDescriptor(), // image descriptor is url + enum for a size
    modifier = Modifier
        .fillMaxWidth()
        .onSizeChanged {
            onImageSizeChanged?.invoke(it)        
    }
)
I think you’re getting my point, I can’t just say “its the width of the list minus 4 dp for margins and that’ll be the final size…” I wish! Nothing here would beat getting it calculated by the view itself, and using that exact size to prefetch that image way in advance, when the data is loaded, to be ready for the image to show itself directly when the user scrolls. Let’s be clear here, I also think it’s overkill but that’s the requirement set up for this, sadly.
@Adam Powell So as a conclusion, we decide to re-use our old image size calculation logic and
AndroidView
so that it matches what we calculated… I’m sad I have to do so just for knowing in advance the size of a
Composable
😢
a
this may or may not have other side effects for what you're doing but it might be worth trying since it's small and a lot less complex/overhead:
Copy code
fun Modifier.invisible() = layout { measurable, constraints ->
    val placeable = measurable.measure(constraints)
    layout(placeable.width, placeable.height) {
        // Do not place
    }
}
stick your
LazyColumn
in a
Box
with an instance of your item you're trying to phantom-measure in that box along with it. Give it a
fillMaxWidth
, your desired fixed/minimum height if needed, and one of these or something like it. The key thing this makes use of is that if a layout or layout modifier doesn't place a measured item, it's not attached to the layout node tree
👍 1
hmm, trying this out, you can leave out the callback since
onSizeChanged
will still fire
edited the example so this is no longer a relevant comment
not placing a child like this is more or less similar to setting a view's visibility to
INVISIBLE
- it still measures but it doesn't draw and you can't interact with it
today i learned 1
so if you're going to create a copy of a composable up front to measure it, might as well do it in the same environment here
if you want to avoid other side effects of this invisible element continuing to exist in your composition you can wrap it in an
if
and omit it after you have what you need, but if you want to be aware of any resizes that happen you might want to have it stick around
I'd still put this in the class of workarounds for something missing, though. It'd be worth filing a feature request for LazyColumn/LazyListState to be able to do something like explicitly pre-compose a particular item, which would let the LazyColumn use the actual instance of the item as the real UI later when it's needed
maybe something in the lazy layout dsl to mark particular items as
PreCompose.Eager
or similar?
cc @Andrey Kulikov to think about during work hours at some point 🙂
there's a bunch of current discussion around the topics of what to do with a focused item that scrolls out of view in a LazyColumn, how to focus search forward in a LazyColumn, and what to do with a LazyColumn item that has an active connection to the soft keyboard when it scrolls away
all of which RecyclerView had historically unsatisfying answers for
💯 1
all of these keep coming back to a related topic of controlling the composition and retention of lazy layout items not currently visible
💯 1
g
@Adam Powell Thank you so much for your input! 🤯 Yes it’s a workaround and it would be amazing to have an API to calculate sizes or even render of Composables offscreen (thinking about taking view screenshots), or as you mentioned some control on items before showing them in
LazyColumn
. Using your snippet I’ve come to build it in a modifier, which I think looks quite good:
Copy code
fun Modifier.prefetchAdsImages(
    scope: CoroutineScope,
    ads: List<Ad>,
) = composed {
    // remember the types of Ads we already calculated the size to not recompose that again
    val composedAdTypes by remember { mutableStateOf(mutableListOf<AdType>()) }
    // run on all the ads
    ads.forEach { ad ->
        // only when not already composed we'll compose
        if (!composedAdTypes.contains(ad.Type)) {
            composedAdTypes.add(ad.Type)
            // setup a basic layout to be able to calculate the size of the image inside the adItem
            Box(modifier = Modifier.fillMaxSize()) {
                LazyColumn(modifier = Modifier.fillMaxSize()) {
                    items(listOf(ad)) { item ->
                        AdItem(
                            ad = item,
                            imageModifier = Modifier
                                .onSizeSet { size ->
                                    scope.launchOnBackground {
                                        AndroidCore
                                            .services()
                                            .imageLoaderTaskManager()
                                            .prefetch(ad.AdSpace.image1Descriptor(), size.width, size.height, false, null)
                                    }
                                }
                        )
                    }
                }
            }
        }
    }
    this
}
onSizeSet {}
is just a modifier to not get a size change twice
Copy code
fun Modifier.onSizeSet(
    onSizeSet: (IntSize) -> Unit
) = composed {
    var isSet by remember { mutableStateOf(false) }
    return@composed onSizeChanged {
        if (!isSet && it.width > 0 && it.height > 0) {
            isSet = true
            onSizeSet(it)
        }
    }
}
What do you think? Maybe there’s a perf issue with this that I don’t see yet?
s
Haven’t read everything you posted, but as a nit in your code snippet regarding this:
val composedAdTypes by remember { mutableStateOf(mutableListOf<AdType>()) }
it may not be doing what you want it to. Please read this article, to see why.
💯 1
a
Avoid emitting UI in a
Modifier.composed
- the behavior of this will not be what you want and is more or less undefined. We'll likely use the new applier inference feature to make the snippet above a compiler error. Using a
LazyColumn
in your speculative layout will have the same issues you're facing in your original problem - things that don't fit won't be composed because the layout is lazy.
I think you'll also have some issues trying to do, "only once" measurement of this; things resize for a number of reasons, animation, general setup of a container, etc.
g
Hmmm I see, thanks
Is there some doc about this new applier inference feature? I'm hungry to learn more about what's behind the Compose curtain! 😊
a