I have Text elements that change font variation wh...
# compose
d
I have Text elements that change font variation when pagerState offset changes. What are some ways I could improve performance because currently it lags and even crashes the app after 10s.
🧵 3
z
Please keep long code snippets to the thread, it keeps the main channel more readable. Thanks!
d
Ok, here's the code:
Copy code
AppTabs.entries.forEachIndexed { index, tab ->
            val pageOffset by remember(pagerState, index) {
                derivedStateOf {
                    pagerState.calculateCurrentOffsetForPage(index).coerceIn(-1f, 1f)
                }
            }

            TopBarTab(
                label = tab.label.asString(),
                currentOffsetForPage = pageOffset,
                onTabClick = { scope.launch { pagerState.animateScrollToPage(tab.ordinal) } },
                onOverflowText = { fontSize *= 0.99f },
                fontSize = fontSize
            )
        }

@Composable
fun TopBarTab(
    label: String,
    currentOffsetForPage: Float,
    onTabClick: () -> Unit,
    fontSize: Float = 40f,
    onOverflowText: () -> Unit,
) {
    val fraction = abs(currentOffsetForPage)

    val width = lerp(MAX_WIDTH, MIN_WIDTH, fraction)
    val weight = lerp(MAX_WEIGHT, MIN_WEIGHT, fraction)

    val textStyle = MaterialTheme.typography.headlineLarge.copy(
        fontSize = fontSize.sp,
        fontFamily = FontFamily(
            Font(
                APP_FONT,
                variationSettings = FontVariation.Settings(
                    FontVariation.width(width),
                    FontVariation.weight(weight.toInt())
                )
            )
        )
    )

    val cachedTextStyle by remember(width, weight, fontSize) {
        derivedStateOf {
            textStyle
        }
    }

    Text(
        text = label,
        modifier = Modifier
            .clickable(
                indication = null,
                interactionSource = remember { MutableInteractionSource() }
            ) { onTabClick() },
        style = cachedTextStyle,
        softWrap = false,
        onTextLayout = {
            if (it.didOverflowWidth) {
                onOverflowText()
            }
        }
    )
}
z
A few notes: What is
calculateCurrentOffsetForPage
? If it's just doing math, and maps indices roughly 1-1 to offsets, it's not worth using
derivedStateOf
.
onOverflowText = { fontSize *= 0.99f }
It looks like this is trying to implement resizable text? This is a bad impl since it will require multiple frames to find the size that fits – you'll see the text animate smaller gradually, which is probably not what you want. Compose 1.8 has built-in resizable text, or before that you can use
TextMeasurer
.
val cachedTextStyle by remember(width, weight, fontSize) {
derivedStateOf {
textStyle
}
}
This
derivedStateOf
is pointless – it's not reading any snapshot state, and even if it
textStyle
were stored in a
mutableStateOf
, you're already calculating it in the composition anyway, and not doing any further calculations on the state, so it serves absolutely no purpose. Please read https://medium.com/androiddevelopers/jetpack-compose-when-should-i-use-derivedstateof-63ce7954c11b. Lastly, I don't know why the app is crashing, what is the exception and stack trace?
d
Thank you for the suggestions. I removed the `derivedStateOf`s. For the text sizing, I don't see how I could use the AutoSize, because I'm showing 3 `TopBarTab`s and I need them to size together. With AutoSize only one of them sizing and the other 2 stay the maxSize. I moved the offsetCalculation straight into
TopBarTab
so the parent doesn't get recomposed when offsetChanges. Here is the modified code again:
Copy code
@Composable
fun TopBarTab(
    pagerState: PagerState,
    index: Int,
    label: String,
    onTabClick: () -> Unit,
    fontSize: Float = 40f,
    onOverflowText: () -> Unit,
) {
    val currentOffsetForPage = pagerState.calculateCurrentOffsetForPage(index).coerceIn(-1f, 1f)

    val fraction = abs(currentOffsetForPage)

    val width = lerp(MAX_WIDTH, MIN_WIDTH, fraction)
    val weight = lerp(MAX_WEIGHT, MIN_WEIGHT, fraction)

    val textStyle = MaterialTheme.typography.headlineLarge.copy(
        fontSize = fontSize.sp,
        fontFamily = FontFamily(
            Font(
                APP_FONT,
                variationSettings = FontVariation.Settings(
                    FontVariation.width(width),
                    FontVariation.weight(weight.toInt())
                )
            )
        )
    )

    Text(
        text = label,
        modifier = Modifier
            .clickable(
                indication = null,
                interactionSource = remember { MutableInteractionSource() }
            ) { onTabClick() },
        style = textStyle,
        softWrap = false,
        onTextLayout = {
            if (it.didOverflowWidth) {
                onOverflowText()
            }
        },

    )
}

fun lerp(start: Float, stop: Float, fraction: Float): Float {
    return start + (stop - start) * fraction
}

fun PagerState.calculateCurrentOffsetForPage(page: Int): Float {
    println("calculated")
    return (currentPage - page) + currentPageOffsetFraction
}
I also attached a video to show what it actually looks like. But currently, after swiping for just a bit, the app's memory usage goes up to 5GB. Thank you again for the initial remarks, do you have any ideas on what else I could improve?
z
That is a lot of memory. So the crash is an OOM?
d
The crash on my phone is, I just confirmed with the stacktrace. It doesn't crash on the simulator just because my computer has enough ram to keep it alive.
z
What's the stacktrace?
I can't see where anything would be leaking there. I would run an allocation trace in Studio and see where all the allocations are coming from
d
It doesn't crash if I set the textstyle width and weight to be constant. I'll do the allocation trace in Studio right now.
z
i wonder if it's one of the text internal caching mechanisms that's leaking
but if that were the case i'd expect it to break any time anyone tries to animate text
d
Copy code
java.lang.OutOfMemoryError: Failed to allocate a 1684643 byte allocation with 1013888 free bytes and 990KB until OOM, target footprint 268435456, growth limit 268435456
                                                                                                    	at dalvik.system.VMRuntime.newNonMovableArray(Native Method)
                                                                                                    	at java.nio.DirectByteBuffer$MemoryRef.<init>(DirectByteBuffer.java:73)
                                                                                                    	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:347)
                                                                                                    	at android.graphics.fonts.Font$Builder.createBuffer(Font.java:294)
                                                                                                    	at android.graphics.fonts.Font$Builder.<init>(Font.java:202)
                                                                                                    	at android.graphics.Typeface$Builder.<init>(Typeface.java:562)
                                                                                                    	at android.graphics.Typeface$Builder.<init>(Typeface.java:549)
                                                                                                    	at androidx.compose.ui.text.font.TypefaceBuilderCompat.createFromAssets(AndroidPreloadedFont.android.kt:186)
                                                                                                    	at androidx.compose.ui.text.font.AndroidAssetFont.doLoad$ui_text_release(AndroidPreloadedFont.android.kt:80)
                                                                                                    	at androidx.compose.ui.text.font.AndroidPreloadedFont.loadCached$ui_text_release(AndroidPreloadedFont.android.kt:52)
                                                                                                    	at androidx.compose.ui.text.font.AndroidPreloadedFontTypefaceLoader.loadBlocking(AndroidPreloadedFont.android.kt:61)
                                                                                                    	at androidx.compose.ui.text.font.AndroidFontLoader.loadBlocking(AndroidFontLoader.android.kt:37)
                                                                                                    	at androidx.compose.ui.text.font.AndroidFontLoader.loadBlocking(AndroidFontLoader.android.kt:31)
                                                                                                    	at androidx.compose.ui.text.font.FontListFontFamilyTypefaceAdapterKt.firstImmediatelyAvailable(FontListFontFamilyTypefaceAdapter.kt:198)
                                                                                                    	at androidx.compose.ui.text.font.FontListFontFamilyTypefaceAdapterKt.access$firstImmediatelyAvailable(FontListFontFamilyTypefaceAdapter.kt:1)
                                                                                                    	at androidx.compose.ui.text.font.FontListFontFamilyTypefaceAdapter.resolve(FontListFontFamilyTypefaceAdapter.kt:138)
                                                                                                    	at androidx.compose.ui.text.font.FontFamilyResolverImpl$resolve$result$1.invoke(FontFamilyResolver.kt:95)
                                                                                                    	at androidx.compose.ui.text.font.FontFamilyResolverImpl$resolve$result$1.invoke(FontFamilyResolver.kt:94)
                                                                                                    	at androidx.compose.ui.text.font.TypefaceRequestCache.runCached(FontFamilyResolver.kt:197)
                                                                                                    	at androidx.compose.ui.text.font.FontFamilyResolverImpl.resolve(FontFamilyResolver.kt:94)
                                                                                                    	at androidx.compose.ui.text.font.FontFamilyResolverImpl.resolve-DPcqOEQ(FontFamilyResolver.kt:80)
                                                                                                    	at androidx.compose.ui.text.platform.AndroidParagraphIntrinsics$resolveTypeface$1.invoke-DPcqOEQ(AndroidParagraphIntrinsics.android.kt:94)
                                                                                                    	at androidx.compose.ui.text.platform.AndroidParagraphIntrinsics$resolveTypeface$1.invoke(AndroidParagraphIntrinsics.android.kt:91)
                                                                                                    	at androidx.compose.ui.text.platform.extensions.TextPaintExtensions_androidKt.applySpanStyle(TextPaintExtensions.android.kt:62)
                                                                                                    	at androidx.compose.ui.text.platform.AndroidParagraphIntrinsics.<init>(AndroidParagraphIntrinsics.android.kt:107)
                                                                                                    	at androidx.compose.ui.text.platform.AndroidParagraphIntrinsics_androidKt.ActualParagraphIntrinsics(AndroidParagraphIntrinsics.android.kt:183)
                                                                                                    	at androidx.compose.ui.text.ParagraphIntrinsicsKt.ParagraphIntrinsics(ParagraphIntrinsics.kt:126)
                                                                                                    	at androidx.compose.ui.text.MultiParagraphIntrinsics.<init>(MultiParagraphIntrinsics.kt:106)
                                                                                                    	at androidx.compose.foundation.text.modifiers.MultiParagraphLayoutCache.setLayoutDirection(MultiParagraphLayoutCache.kt:282)
                                                                                                    	at androidx.compose.foundation.text.modifiers.MultiParagraphLayoutCache.layoutText-K40F9xA(MultiParagraphLayoutCache.kt:307)
                                                                                                    	at androidx.compose.foundation.text.modifiers.MultiParagraphLayoutCache.layoutWithConstraints-K40F9xA(MultiParagraphLayoutCache.kt:163)
                                                                                                    	at androidx.compose.foundation.text.modifiers.TextAnnotatedStringNode.measure-3p2s80s(TextAnnotatedStringNode.kt:419)
                                                                                                    	at androidx.compose.ui.node.LayoutModifierNodeCoordinator.measure-BRTryo0(LayoutModifierNodeCoordinator.kt:190)
                                                                                                    	at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke(LayoutNodeLayoutDelegate.kt:359)
                                                                                                    	at androidx.compose.ui.node.LayoutNodeLayoutDelegate$performMeasureBlock$1.invoke(LayoutNodeLayoutDelegate.kt:358)
2025-02-07 20:07:48.159 23084-23084 AndroidRuntime          com...developer.esm_vertretungsplan  E  	at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2489) (Ask Gemini)
                                                                                                    	at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:460)
                                                                                                    	at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:244)
                                                                                                    	at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:124)
                                                                                                    	at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui_release(OwnerSnapshotObserver.kt:107)
                                                                                                    	at androidx.compose.ui.node.LayoutNodeLayoutDelegate.performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:1914)
                                                                                                    	at androidx.compose.ui.node.LayoutNodeLayoutDelegate.access$performMeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:39)
                                                                                                    	at androidx.compose.ui.node.LayoutNodeLayoutDelegate$MeasurePassDelegate.remeasure-BRTryo0(LayoutNodeLayoutDelegate.kt:745)
                                                                                                    	at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui_release(LayoutNode.kt:1138)
                                                                                                    	at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui_release$default(LayoutNode.kt:1131)
                                                                                                    	at androidx.compose.ui.node.MeasureAndLayoutDelegate.doRemeasure-sdFAvZA(MeasureAndLayoutDelegate.kt:366)
                                                                                                    	at androidx.compose.ui.node.MeasureAndLayoutDelegate.remeasureAndRelayoutIfNeeded(MeasureAndLayoutDelegate.kt:566)
                                                                                                    	at androidx.compose.ui.node.MeasureAndLayoutDelegate.onlyRemeasureIfScheduled(MeasureAndLayoutDelegate.kt:667)
                                                                                                    	at androidx.compose.ui.node.MeasureAndLayoutDelegate.forceMeasureTheSubtreeInternal(MeasureAndLayoutDelegate.kt:694)
                                                                                                    	at androidx.compose.ui.node.MeasureAndLayoutDelegate.forceMeasureTheSubtreeInternal(MeasureAndLayoutDelegate.kt:701)
                                                                                                    	at androidx.compose.ui.node.MeasureAndLayoutDelegate.forceMeasureTheSubtreeInternal(MeasureAndLayoutDelegate.kt:701)
                                                                                                    	at androidx.compose.ui.node.MeasureAndLayoutDelegate.forceMeasureTheSubtreeInternal(MeasureAndLayoutDelegate.kt:701)
                                                                                                    	at androidx.compose.ui.node.MeasureAndLayoutDelegate.forceMeasureTheSubtreeInternal(MeasureAndLayoutDelegate.kt:701)
                                                                                                    	at androidx.compose.ui.node.MeasureAndLayoutDelegate.forceMeasureTheSubtreeInternal(MeasureAndLayoutDelegate.kt:701)
                                                                                                    	at androidx.compose.ui.node.MeasureAndLayoutDelegate.forceMeasureTheSubtreeInternal(MeasureAndLayoutDelegate.kt:701)
z
oook so it's in the variable fonts support. That makes a bit more sense, i don't know how widely used that is so not entirely surprised if it hasn't been exercised as much
can you file a bug report on the google tracker with the info in this thread? All the code you shared and this stack trace, the video, and the bit about how the OOM doesn't happen if you don't animate? This is definitely something for the text team to look into
and post the link to the bug here?
d
I'm using compose multiplatform, support for variable fonts was added very recently in 1.8.0-alpha1 (I'm on 1.8.0-alpha02). This might be the commit https://github.com/JetBrains/compose-multiplatform-core/pull/1623. Should I file an issue on the JetBrains Youtrack, cause that's probably more to do with multiplatform, or on the Google issue tracker?
z
I'm seeing
AndroidFontLoader
which indicates you're running this on android, so this is google's code
but it might be an older version
can you make a minimal reproducer project that just does the animation with this font file and also crashes?
then if you can repro with the latest android-specific version of compose, that would be awesome. But regardless, that would be helpful to attach to the bug
d
I mean the leak also happens on iOS, it just doesn't crash because I'm running it on the simulator on my computer and it can sustain it.
z
(CMP is just android compose packaged up with all the non-android stuff that jetbrains does)
👍 1
d
Sure, then ill do an android only repro
z
Some of the font stuff is common code, so the bug might be there. But google also maintains that. If it happens on android at all, it's definitely in google's jurisdiction
d
Interesting, it is a CMP issue. I made an android repro but that doesn't crash, while the exact same code does on CMP.
z
if you use CMP in your repro, it crashes?
d
Yes
z
sounds like CMP is stuck on an older version of the android code, and it's been fixed later
d
But this is the version I used on the android project
composeBom = "2024.04.01"
and CMP
1.8.0-alpha02
was build against Compose
1.8.0-alpha07
which was released December 11, 2024.
So from my understanding, my android project is on a much earlier version
z
Did you try with a newer Android BOM?
d
Yeah I tried the latest version and even the exact version that CMP is built against
z
Hm i guess file it to jetbrains then.