<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Blogs by Pushpal Roy]]></title><description><![CDATA[Android engineer by profession. Dedicated to bringing creative ideas to life in the digital realm. Exploring the limitless possibilities of technology.]]></description><link>https://blog.pushpalroy.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1709432362255/y8nbdFmlc.png</url><title>Blogs by Pushpal Roy</title><link>https://blog.pushpalroy.com</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 12 Apr 2026 09:55:26 GMT</lastBuildDate><atom:link href="https://blog.pushpalroy.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Container Transform Animation with Lookahead in Jetpack Compose]]></title><description><![CDATA[This post is a continuation of my previous blog about the LookaheadScope API in Jetpack Compose, where I delved into various APIs and demonstrated an example animation utilizing Box layouts. If you haven’t read it yet, I strongly encourage you to do ...]]></description><link>https://blog.pushpalroy.com/container-transform-animation-with-lookahead-in-jetpack-compose</link><guid isPermaLink="true">https://blog.pushpalroy.com/container-transform-animation-with-lookahead-in-jetpack-compose</guid><category><![CDATA[container-transform-animation]]></category><category><![CDATA[container-transform]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[compose]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><category><![CDATA[animation]]></category><category><![CDATA[lookahead]]></category><category><![CDATA[lookaheadscope]]></category><category><![CDATA[lookaheadlayout]]></category><category><![CDATA[UI Design]]></category><category><![CDATA[UI]]></category><category><![CDATA[UIUX]]></category><dc:creator><![CDATA[Pushpal Roy]]></dc:creator><pubDate>Wed, 13 Mar 2024 21:02:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710362762253/aceeefd7-2f45-4bb2-9544-4028c2a018a1.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<p>This post is a continuation of my <a target="_blank" href="https://blog.pushpalroy.com/animations-with-lookahead-in-jetpack-compose">previous blog about the LookaheadScope API</a> in Jetpack Compose, where I delved into various APIs and demonstrated an example animation utilizing Box layouts. If you haven’t read it yet, I strongly encourage you to do so before proceeding. The concepts covered in that post will be foundational to our discussion here.</p>
<p>This post aims to provide a clear and straightforward explanation of how to create a container transform animation using Lookahead APIs. In the end, we will end up creating a UX like this:</p>
<p><img src="https://cdn-images-1.medium.com/max/1600/1*UDelbmLmIm1Bqn2wAoHWDQ.gif" alt="Animation we want to achieve" class="image--center mx-auto" /></p>
<h2 id="heading-quick-recap">Quick recap</h2>
<p>Let's quickly go through some points we know about <code>LookaheadScope</code>. These concepts are discussed in more detail in my previous article.</p>
<ol>
<li><p>The <strong>LookaheadScope</strong> API allows us to “look ahead” and pre-calculate the size and position of the target destination while animating. It achieves that using a “lookahead pass” before the actual measure/layout pass.</p>
</li>
<li><p>Using these pre-calculated values, we can decide an approach logic in the “approach pass” of how to gradually reach this destination state, which will define the animation.</p>
</li>
</ol>
<h3 id="heading-what-is-a-container-transform-animation">What is a container transform animation?</h3>
<p><a target="_blank" href="https://m3.material.io/styles/motion/transitions/transition-patterns#b67cba74-6240-4663-a423-d537b6d21187">Container transform</a> animation is a design pattern that morphs one UI container into another, often employing a shared element to bridge two UI components. It uses persistent elements to create a smooth and seamless transition between the starting and ending states.</p>
<h2 id="heading-lets-dive-into-the-code">Let’s dive into the code</h2>
<p>In the <a target="_blank" href="https://blog.pushpalroy.com/animations-with-lookahead-in-jetpack-compose">previous article</a>, we explored the <code>movableContent</code> and <code>movableContentWithReceiverOf</code> APIs of Jetpack Compose. The latter allows the creation of movable content within a receiver context, enabling the preservation of state while content is repositioned. We will extensively leverage this API in conjunction with <code>LookaheadScope</code> to create the UX described earlier.</p>
<p>The UX design in question consists of two main components: a top/center header card layout featuring a circle and two rounded rectangles, and a lower layout composed of four colored attribute cards, each adorned with a circle and one or two rounded rectangles. This setup is conceptual, and designed for simplicity. In practical applications, these shapes could be replaced with Image and Text composables without affecting the functionality.</p>
<h3 id="heading-step-1-create-a-custom-modifier-using-approachlayout-modifier">Step 1: Create a custom modifier using approachLayout Modifier</h3>
<p>As elaborated in the <a target="_blank" href="https://blog.pushpalroy.com/animations-with-lookahead-in-jetpack-compose">previous</a> post, we will now develop a custom modifier named <code>animateLayout</code> by leveraging the <code>approachLayout</code> Modifier. The <code>approachLayout</code> modifier plays a crucial role during the <strong>approach pass</strong> and is utilized within <code>LookaheadScope</code> to delineate the method by which a composable should measure and place its content throughout the animation process.</p>
<p>Within the <code>isMeasurementApproachComplete</code> and <code>isPlacementApproachComplete</code> checks, we'll determine whether our measurement and placement processes have reached their intended destinations. To accomplish this, we will employ the lambda arguments <code>lookaheadSize</code> and <code>lookaheadCoordinates</code> to craft our logic. In this scenario, we're opting for a tween animation with a duration of 1800 milliseconds to slow down the animation, enhancing its visibility and allowing for a more detailed observation.</p>
<p>In the trailing lambda of <code>approachMeasure</code>, we will define an interpolation method for the animation. This interpolation will smoothly transition the measurement or placement from its previous size and position to the designated target size and position. We'll ascertain the target size using <code>lookaheadSize</code>, and the target position through <code>lookaheadCoordinates</code> and <code>localLookaheadPositionOf</code>.</p>
<p>It’s important to note that <code>localLookaheadPositionOf</code> is utilized for obtaining the converted offset relative to a specific position coordinate within the Lookahead coordinate space. This technique ensures that the animation transitions are smooth and precise, directly correlating the starting points to their final destinations based on the defined animation parameters.</p>
<pre><code class="lang-kotlin">context (LookaheadScope)
<span class="hljs-meta">@OptIn(ExperimentalAnimatableApi::class, ExperimentalComposeUiApi::class)</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> Modifier.<span class="hljs-title">animateLayout</span><span class="hljs-params">()</span></span>: Modifier = composed {
    <span class="hljs-keyword">val</span> sizeAnim = remember { DeferredTargetAnimation(IntSize.VectorConverter) }
    <span class="hljs-keyword">val</span> offsetAnim = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
    <span class="hljs-keyword">val</span> scope = rememberCoroutineScope()
    <span class="hljs-keyword">this</span>.approachLayout(
        isMeasurementApproachComplete = { lookaheadSize -&gt;
            sizeAnim.updateTarget(lookaheadSize, scope, tween(<span class="hljs-number">1800</span>))
            sizeAnim.isIdle
        },
        isPlacementApproachComplete = { lookaheadCoordinates -&gt;
            <span class="hljs-keyword">val</span> target = lookaheadScopeCoordinates.localLookaheadPositionOf(lookaheadCoordinates)
            offsetAnim.updateTarget(target.round(), scope, tween(<span class="hljs-number">1800</span>))
            offsetAnim.isIdle
        }
    ) { measurable, _ -&gt;
        <span class="hljs-keyword">val</span> (animWidth, animHeight) = sizeAnim.updateTarget(lookaheadSize, scope)
        measurable.measure(Constraints.fixed(animWidth, animHeight))
            .run {
                layout(width, height) {
                    coordinates?.let {
                        <span class="hljs-keyword">val</span> target = lookaheadScopeCoordinates.localLookaheadPositionOf(it).round()
                        <span class="hljs-keyword">val</span> animOffset = offsetAnim.updateTarget(target, scope)
                        <span class="hljs-keyword">val</span> current = lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero).round()
                        <span class="hljs-keyword">val</span> (x, y) = animOffset - current
                        place(x, y)
                    } ?: place(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>)
                }
            }
    }
}
</code></pre>
<p>After creating the <code>animateLayout</code> modifier, it becomes a powerful tool that we can employ to animate our UI elements in subsequent sections of the code.</p>
<h3 id="heading-step-2-define-the-parent-and-animation-states"><strong>Step 2:</strong> <strong>Define the parent and animation states</strong></h3>
<p>First, we’ll construct the top/center header card layout followed by the bottom layout. It’s crucial to recognize that the layout transitions between two states. These states switch upon tapping the screen or parent container. To manage this behavior, we’ll begin by defining our parent composable. We will also introduce a mutable <code>Boolean</code> state, named <code>isExpanded</code>, to represent the toggle state. This flag will be instrumental in controlling the animation, dictating whether the layout is in its expanded or non-expanded form based on user interaction.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ContainerTransformAnimationWithLookahead</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">var</span> isExpanded <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-literal">false</span>) }
    Surface(
        modifier = Modifier.fillMaxSize().clickable { isExpanded = !isExpanded },
        color = Color(<span class="hljs-number">0xFFFFFFFF</span>)
    ) {
        <span class="hljs-comment">// Content here</span>
    }
}
</code></pre>
<h3 id="heading-step-3-define-the-movable-contents-for-the-header-section"><strong>Step 3:</strong> Define the movable contents for the header section</h3>
<p>For the top header card view, we have 4 movable contents:</p>
<ol>
<li><p>Header image (Circle)</p>
</li>
<li><p>Header title (Long rounded rectangle)</p>
</li>
<li><p>Header subtitle (Short rounded rectangle)</p>
</li>
<li><p>Header container (Card container)</p>
</li>
</ol>
<p>For each one of these, we have to define remembered tracking composable lambdas. We will leverage <code>movableContentWithReceiverOf</code> with <code>LookaheadScope</code> for this.</p>
<p>Let’s do it first for the header <strong>image</strong>:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> headerImage = remember {
    movableContentWithReceiverOf&lt;LookaheadScope, Modifier&gt; { modifier -&gt;
        Box(
            modifier = Modifier
                .animateLayout()
                .then(modifier)
                .drawBehind {
                    drawCircle(Color(<span class="hljs-number">0xFF949494</span>))
                }
        )
    }
}
</code></pre>
<p><strong>Here’s what’s happening:</strong> Each component is encapsulated within a remembered composable lambda, enabling it to be tracked. This ability to track compositions facilitates the creation of a composable that dynamically switches its content layout between a row and a column, contingent upon a specific parameter. We’ve employed the <code>animateLayout()</code> Modifier, introduced earlier, to generate the actual animation effect.</p>
<p>The <code>LookaheadScope</code> is utilized as the receiver context, a necessity for the <code>animateLayout()</code> modifier to function properly. Moreover, we've incorporated a <code>Modifier</code> as a parameter, allowing for external style adjustments that correspond with the layout transitions. This modifier is appended using <code>.then(modifier)</code> after applying <code>animateLayout()</code>. It's important to note that styling elements intended to remain consistent across different layout (or animated) states should be explicitly defined within this composable at the time of its creation, ensuring a seamless transition and maintaining visual coherence throughout the animation process.</p>
<p>Similarly, we will define the header <strong>title</strong> and <strong>subtitle</strong>:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> headerTitle = remember {
    movableContentWithReceiverOf&lt;LookaheadScope, Modifier&gt; { modifier -&gt;
        Box(
            modifier = Modifier
                .animateLayout()
                .then(modifier)
                .graphicsLayer {
                    clip = <span class="hljs-literal">true</span>
                    shape = RoundedCornerShape(<span class="hljs-number">100</span>)
                }
                .background(Color(<span class="hljs-number">0xFFACACAC</span>))
        )
    }
}
<span class="hljs-keyword">val</span> headerSubTitle = remember {
    movableContentWithReceiverOf&lt;LookaheadScope, Modifier&gt; { modifier -&gt;
        Box(
            modifier = Modifier
                .animateLayout()
                .then(modifier)
                .graphicsLayer {
                    clip = <span class="hljs-literal">true</span>
                    shape = RoundedCornerShape(<span class="hljs-number">100</span>)
                }
                .background(Color(<span class="hljs-number">0xFFC2C2C2</span>))
        )
    }
}
</code></pre>
<p>Now, we will define the header <strong>container</strong>, which will hold the above three elements. This is a bit different from the above implementations. Let's see the code:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> headerContainer = remember {
    movableContentWithReceiverOf&lt;LookaheadScope, <span class="hljs-meta">@Composable</span> () -&gt; <span class="hljs-built_in">Unit</span>&gt; { content -&gt;
        Box(
            modifier = Modifier
                .animateLayout()
                .padding(<span class="hljs-number">16</span>.dp)
                .graphicsLayer {
                    clip = <span class="hljs-literal">true</span>
                    shape = RoundedCornerShape(<span class="hljs-number">10</span>)
                }
                .background(Color(<span class="hljs-number">0xFFE7E7E7</span>))
        ) {
            content()
        }
    }
}
</code></pre>
<p>In this scenario, everything remains consistent with the previous components, with the notable exception of the argument type: instead of a <code>Modifier</code>, a <code>Composable</code> function is used. This adjustment is made because a <code>Modifier</code> isn't required for this specific use case, as the card's styling remains constant across both states. Instead, what's needed is a way to pass content into this <code>Box</code>—specifically, the three UI elements: image, title, and subtitle. The rationale behind this approach and its practical application will become more apparent through examples showcasing its usage.</p>
<h3 id="heading-step-4-use-the-defined-movable-contents-for-the-header-section">Step 4: Use the defined movable contents for the header section</h3>
<p>Now, we will employ the movable contents defined earlier to construct two layouts, toggling between them based on the <code>isExpanded</code> Boolean flag. All of these operations will occur within the <code>LookAheadScope</code>. It's crucial to understand that without <code>LookAheadScope</code>, our movable contents cannot be utilized. This scope provides the necessary context for our movable content to function correctly, enabling the seamless transition between expanded and non-expanded states as dictated by the <code>isExpanded</code> condition.</p>
<pre><code class="lang-kotlin">LookaheadScope {
    Column(
        modifier = Modifier.padding(horizontal = <span class="hljs-number">16</span>.dp, vertical = <span class="hljs-number">32</span>.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Box(
            modifier = Modifier.fillMaxSize().weight(<span class="hljs-keyword">if</span> (isExpanded) <span class="hljs-number">1f</span> <span class="hljs-keyword">else</span> <span class="hljs-number">4f</span>),
            contentAlignment = <span class="hljs-keyword">if</span> (isExpanded) Alignment.TopStart <span class="hljs-keyword">else</span> Alignment.Center
        ) {
            <span class="hljs-keyword">if</span> (isExpanded) {
                headerContainer {
                    Row(
                        modifier = Modifier.padding(<span class="hljs-number">16</span>.dp),
                        verticalAlignment = Alignment.CenterVertically
                    ) {
                        headerImage(Modifier.size(<span class="hljs-number">48</span>.dp))
                        Spacer(Modifier.width(<span class="hljs-number">16</span>.dp))
                        Column {
                            headerTitle(Modifier.height(<span class="hljs-number">20</span>.dp).width(<span class="hljs-number">280</span>.dp))
                            Spacer(Modifier.height(<span class="hljs-number">16</span>.dp))
                            headerSubTitle(Modifier.height(<span class="hljs-number">20</span>.dp).width(<span class="hljs-number">172</span>.dp))
                        }
                    }
                }
            } <span class="hljs-keyword">else</span> {
                headerContainer {
                    Column(
                        modifier = Modifier.padding(<span class="hljs-number">16</span>.dp),
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        headerImage(Modifier.size(<span class="hljs-number">120</span>.dp))
                        Spacer(Modifier.height(<span class="hljs-number">32</span>.dp))
                        headerTitle(Modifier.height(<span class="hljs-number">24</span>.dp).width(<span class="hljs-number">280</span>.dp))
                        Spacer(Modifier.height(<span class="hljs-number">16</span>.dp))
                        headerSubTitle(Modifier.height(<span class="hljs-number">24</span>.dp).width(<span class="hljs-number">172</span>.dp))
                    }
                }
            }
        }
      <span class="hljs-comment">// Remaining code of the screen to be continued here</span>
    }
}
</code></pre>
<p>The provided code outlines two distinct layouts: one displaying the header card section at the top of the screen (in its expanded state) and the other showing it in the middle of the screen (in its non-expanded state). For both layouts, we’ve utilized the <code>headerContainer</code> as the movable content container, incorporating <code>headerImage</code>, <code>headerTitle</code>, and <code>headerSubTitle</code> as its contents. Additionally, we've applied specific styling to these elements using the previously defined <code>Modifier</code> argument.</p>
<p>This setup allows for dynamic styling adjustments — specifically, the width and height of the elements — based on whether the layout is in its expanded or non-expanded state, as well as its positioning. The Lookahead API is tasked with animating these changes, ensuring a smooth transition between states.</p>
<p>With this, we conclude the discussion on the header section.</p>
<h3 id="heading-step-5-define-the-movable-contents-for-the-attribute-section">Step 5: Define the movable contents for the attribute section</h3>
<p>Moving on to the attribute cards section, I’ll keep the explanation brief as we’ll be applying the same concepts introduced previously.</p>
<p>Let’s define the movable contents for the <strong>attributes</strong> container for four colored Boxes, the <strong>image</strong>, <strong>title</strong>, and <strong>subtitle</strong>.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> attributeColors = listOf(
    Color(<span class="hljs-number">0xFFFF928D</span>),
    Color(<span class="hljs-number">0xFFFFDB8D</span>),
    Color(<span class="hljs-number">0xFFA7E5FF</span>),
    Color(<span class="hljs-number">0xFFB6E67F</span>),
)
<span class="hljs-keyword">val</span> attributes = remember {
    movableContentWithReceiverOf&lt;LookaheadScope, Modifier, <span class="hljs-meta">@Composable</span> () -&gt; <span class="hljs-built_in">Unit</span>&gt; { modifier, content -&gt;
        attributeColors.forEach { color -&gt;
            Box(
                Modifier
                    .animateLayout()
                    .graphicsLayer {
                        clip = <span class="hljs-literal">true</span>
                        shape = RoundedCornerShape(<span class="hljs-number">10</span>)
                    }
                    .background(color)
                    .then(modifier)
            ) {
                content()
            }
        }
    }
}
<span class="hljs-keyword">val</span> attributeImage = remember {
    movableContentWithReceiverOf&lt;LookaheadScope, Modifier&gt; { modifier -&gt;
        Box(
            modifier = Modifier
                .animateLayout()
                .then(modifier)
                .drawBehind {
                    drawCircle(Color(<span class="hljs-number">0x99FFFFFF</span>))
                }
        )
    }
}
<span class="hljs-keyword">val</span> attributeTitle = remember {
    movableContentWithReceiverOf&lt;LookaheadScope, Modifier&gt; { modifier -&gt;
        Box(
            modifier = Modifier
                .animateLayout()
                .then(modifier)
                .graphicsLayer {
                    clip = <span class="hljs-literal">true</span>
                    shape = RoundedCornerShape(<span class="hljs-number">100</span>)
                }
                .background(Color(<span class="hljs-number">0x99FFFFFF</span>))
        )
    }
}
<span class="hljs-keyword">val</span> attributeSubtitle = remember {
    movableContentWithReceiverOf&lt;LookaheadScope, Modifier&gt; { modifier -&gt;
        Box(
            modifier = Modifier
                .animateLayout()
                .then(modifier)
                .graphicsLayer {
                    clip = <span class="hljs-literal">true</span>
                    shape = RoundedCornerShape(<span class="hljs-number">100</span>)
                }
                .background(Color(<span class="hljs-number">0x99FFFFFF</span>))
        )
    }
}
</code></pre>
<p>Note that here, for the <code>attributes</code> component, the <code>movableContentWithReceiverOf</code> takes both a <code>Modifier</code> and <code>Composable</code> content as arguments. The modifier will help us to pass styles for the Box container while using them in animation.</p>
<h3 id="heading-step-6-use-the-defined-movable-contents-for-the-attributes-section">Step 6: Use the defined movable contents for the attributes section</h3>
<p>Similar to what we have done previously, we will use the movable contents to define two layouts: one for the expanded state and another for the non-expanded state. Also, we will switch between them based on the <code>isExpanded</code> flag.</p>
<pre><code class="lang-kotlin">LookaheadScope {
    Column(
        modifier = Modifier.padding(horizontal = <span class="hljs-number">16</span>.dp, vertical = <span class="hljs-number">32</span>.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        <span class="hljs-comment">// Previous code for the header section</span>
        Box(
            modifier = Modifier
                .weight(<span class="hljs-keyword">if</span> (isExpanded) <span class="hljs-number">4f</span> <span class="hljs-keyword">else</span> <span class="hljs-number">1f</span>)
                .padding(horizontal = <span class="hljs-number">8</span>.dp)
        ) {
            <span class="hljs-keyword">if</span> (isExpanded) {
                Column(
                    verticalArrangement = Arrangement.spacedBy(<span class="hljs-number">8</span>.dp)
                ) {
                    attributes(
                        Modifier.height(<span class="hljs-number">140</span>.dp).fillMaxWidth()
                    ) {
                        Row(
                            modifier = Modifier.padding(<span class="hljs-number">16</span>.dp).fillMaxSize(),
                            verticalAlignment = Alignment.CenterVertically
                        ) {
                            attributeImage(Modifier.size(<span class="hljs-number">64</span>.dp))
                            Spacer(Modifier.width(<span class="hljs-number">16</span>.dp))
                            Column {
                                attributeTitle(Modifier.height(<span class="hljs-number">20</span>.dp).fillMaxWidth())
                                Spacer(Modifier.height(<span class="hljs-number">16</span>.dp))
                                attributeSubtitle(Modifier.height(<span class="hljs-number">20</span>.dp).fillMaxWidth())
                            }
                        }
                    }
                }
            } <span class="hljs-keyword">else</span> {
                Row(
                    horizontalArrangement = Arrangement.spacedBy(<span class="hljs-number">8</span>.dp)
                ) {
                    attributes(
                        Modifier.size(width = <span class="hljs-number">64</span>.dp, height = <span class="hljs-number">80</span>.dp)
                    ) {
                        Column(
                            modifier = Modifier.fillMaxSize().padding(<span class="hljs-number">8</span>.dp),
                            horizontalAlignment = Alignment.CenterHorizontally
                        ) {
                            attributeImage(Modifier.size(<span class="hljs-number">32</span>.dp))
                            Spacer(Modifier.height(<span class="hljs-number">8</span>.dp))
                            attributeTitle(Modifier.height(<span class="hljs-number">16</span>.dp).fillMaxWidth())
                        }
                    }
                }
            }
        }
    }
}
</code></pre>
<p>Note that here, we have omitted the <code>attributeSubtitle</code> in the non-expanded state. This is because we do not want it for the smaller card view in the non-expanded condition.</p>
<p>Also, it’s important to note that we adjusted the <code>weight</code> of the <code>Box</code> composables for header and attribute sections based on the <code>isExpanded</code> flag. This was important for our layouts to move up and down accordingly.</p>
<p>That’s it. If we run the above code now, we will see the desired animation as shared in the beginning.</p>
<p>We can also experiment by not omitting the <code>attributeSubtitle</code> rather, changing its size to <code>0.dp</code> for the non-expanded state:</p>
<pre><code class="lang-kotlin">attributeSubtitle(Modifier.size(<span class="hljs-number">0</span>.dp))
</code></pre>
<p>This will create an effect where the <strong>subtitle</strong> in the attributes section will shrink and disappear after animating to the non-expanded state:</p>
<p><img src="https://cdn-images-1.medium.com/max/1600/1*M9KKQp1P_sab_pLEpJnP9w.gif" alt="Animation with shrinking attribute subtitle" class="image--center mx-auto" /></p>
<p>The full code for this is <a target="_blank" href="https://github.com/pushpalroy/ComposeLayoutPlayground/blob/main/app/src/main/java/com/appmason/composelayoutplayground/ui/screens/lookaheadlayout/ContainerTransformAnimationWithLookahead.kt">available here</a>.</p>
<hr />
<h2 id="heading-additional-resources">Additional resources</h2>
<p>If you want to learn more about how Lookahead works internally, I highly recommend this blog “<strong>Introducing LookaheadLayout”</strong> by <a target="_blank" href="https://twitter.com/JorgeCastilloPr"><strong>Jorge Castillo</strong></a><strong>:</strong> <a target="_blank" href="https://substack.com/inbox/post/64358322">https://substack.com/inbox/post/64358322</a>. </p>
<p>Also, the <strong>official docs</strong> for <strong>LookaheadScope:</strong> <a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/LookaheadScope">https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/LookaheadScope</a>.</p>
<p>And, my previous blog: <a target="_blank" href="https://blog.pushpalroy.com/animations-with-lookahead-in-jetpack-compose">Animations with Lookahead in Jetpack Compose</a>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I hope this article sheds light on the diverse possibilities that the <code>LookaheadScope</code> APIs can unlock. Although this API is currently experimental and subject to future changes, one thing is certain: it represents a promising tool that could significantly simplify animation in Compose development.</p>
]]></content:encoded></item><item><title><![CDATA[Animations with Lookahead in Jetpack Compose]]></title><description><![CDATA[The LookaheadScope (replaced by the previous LookaheadLayout) is a new experimental API in Jetpack Compose that allows for predictive animations based on the future state of the layout by pre-calculating size and position. It supports a lookahead pas...]]></description><link>https://blog.pushpalroy.com/animations-with-lookahead-in-jetpack-compose</link><guid isPermaLink="true">https://blog.pushpalroy.com/animations-with-lookahead-in-jetpack-compose</guid><category><![CDATA[lookaheadlayout]]></category><category><![CDATA[lookaheadscope]]></category><category><![CDATA[lookahead]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><category><![CDATA[android development]]></category><category><![CDATA[animations]]></category><category><![CDATA[compose]]></category><dc:creator><![CDATA[Pushpal Roy]]></dc:creator><pubDate>Tue, 12 Mar 2024 05:32:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710216168369/62a844a6-186b-42dc-a3d0-5dac54fb2334.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The <strong>LookaheadScope</strong> (replaced by the previous <code>LookaheadLayout</code>) is a new experimental API in Jetpack Compose that allows for predictive animations based on the future state of the layout by pre-calculating size and position. It supports a lookahead pass of measure and layout before the actual measure/layout. Thus the latter can make use of the values pre-calculated during the previous to update the node on every frame.</p>
<p><strong>LookaheadLayout</strong> was first introduced in <strong>Version 1.3.0-alpha01</strong> (<a target="_blank" href="https://android-review.googlesource.com/c/platform/frameworks/support/+/1961800">see commit</a>). Since then the API has gone through many changes. Check for some of the important changes listed at the end of this article.</p>
<p>As mentioned by <strong>Jaewoong Eum</strong> in this <a target="_blank" href="https://twitter.com/github_skydoves/status/1531115821161799680">tweet</a>: “This allows us to look ahead and calculate a new layout while allowing the actual measurement &amp; placement of every frame to be different than the pre-calculation.” Also, <strong>Doris Liu</strong> in this <a target="_blank" href="https://twitter.com/doris4lt/status/1531365874828865536">tweet</a> shared an amazing example of lookahead with movable content, which made it possible to move UI between a single-column and a double-column layout without losing their animation states.</p>
<p>In this article, I will try to explain the various functions and interfaces that I have explored so far in this API and then analyze an official example for animating four Box composables provided in the document.</p>
<h2 id="heading-how-does-it-work"><strong>How does it work?</strong></h2>
<h3 id="heading-quick-overview">Quick Overview</h3>
<ol>
<li><p><strong>Lookahead pass</strong>: All layouts will first determine their target or destination layout. This allows a pre-calculation of the layout when it changes and use this information to measure and place layouts during each frame of the animation (next point).</p>
</li>
<li><p><strong>Approach pass</strong>: Then they will run the measurement and placement approach or logic to reach the destination gradually.</p>
</li>
<li><p>The <strong>LookaheadScope</strong> interface creates a scope for the above two steps.</p>
</li>
<li><p>The measurement and placement approach in the <strong>approach pass</strong> is defined by the <code>Modifier.approachLayout</code> or by the <code>ApproachLayoutModifierNode</code> interface (creating custom approach logic in an explicit Modifier Node).</p>
</li>
</ol>
<h2 id="heading-lets-understand-the-apis">Let's understand the APIs</h2>
<h3 id="heading-lookaheadscope"><strong>LookaheadScope</strong></h3>
<p>There is a <code>LookaheadScope</code> interface and a <code>LookaheadScope</code> composable. The interface is a receiver scope for all child layouts within the composable. It provides access to the <code>lookaheadScopeCoordinates</code> from any child's <code>PlacementScope</code>. This allows any child to convert <code>LayoutCoordinates</code> to the <code>LayoutCoordinates</code> in lookahead coordinate space using the <code>toLookaheadCoordinates()</code> function. This is particularly useful for animations where we want to calculate positions based on the future state of the layout.</p>
<h3 id="heading-approachmeasurescope">ApproachMeasureScope</h3>
<p>A scope that provides access to the lookahead results. The <code>ApproachLayoutModifierNode</code> can leverage these results to define how measurements and placements approach their destination.</p>
<h3 id="heading-approachlayout-modifier-and-approachlayoutmodifiernode">approachLayout Modifier and ApproachLayoutModifierNode</h3>
<p>The <code>approachLayout</code> modifier is instrumental in managing the <strong>approach pass</strong> (it replaces the now-deprecated <code>Modifier.intermediateLayout</code>). It operates within the <code>LookaheadScope</code> to establish the methodology for measuring and positioning a composable's content throughout an animation. This modifier constructs an "approach layout" that facilitates a gradual transition towards the target layout defined during the lookahead pass.</p>
<p>Internally, the <code>approachLayout</code> modifier utilizes the <code>ApproachLayoutModifierNode</code> API, a new Modifier Node tailored for the destination layout as determined in the <strong>lookahead phase</strong>. This Node relies on input from the users to confirm the completion of measurement and placement processes. Such confirmations enable the system to bypass the approach phase once concluded, thereby enhancing layout performance by avoiding unnecessary recalculations.</p>
<h3 id="heading-deferredtargetanimation">DeferredTargetAnimation</h3>
<p>The <code>DeferredTargetAnimation</code> is a new experimental API for creating those animations whose target is unknown at instantiation. It can be used for size or position animations where the target size or position stays unknown until the later measure and placement phase. It has an <code>updateTarget</code> function, which sets up an animation or updates an already running animation. It returns the current value of the animation, after launching the animation in the given <code>coroutineScope</code>. It also has an <code>isIdle</code> Boolean property which returns true when the animation has finished running and reached its destination, or when the animation has not been set up.</p>
<h3 id="heading-some-important-concepts-here">Some important concepts here:</h3>
<ol>
<li><p><code>lookaheadSize</code>: The destination/target size of the layout obtained in the <code>ApproachMeasureScope</code>. This is the size of the <code>ApproachLayoutModifierNode</code> measured during the <strong>lookahead pass</strong>.</p>
</li>
<li><p><code>localLookaheadPositionOf</code>: It calculates the local position in the Lookahead coordinate space. It is used to obtain the target position during placement by using <code>LookaheadScope.localLookaheadPositionOf</code> and the lookahead <code>LayoutCoordinates</code> in the <code>ApproachMeasureScope</code>.</p>
</li>
<li><p>By knowing the target size and position, animations or other layout adjustments can be defined in the <code>ApproachLayoutModifierNode</code> to morph the layout gradually in both size and position to arrive at its precalculated bounds.</p>
</li>
<li><p><code>isMeasurementApproachComplete</code>: A block that indicates whether the <strong>measurement</strong> has reached the destination size. It is invoked after the destination has been determined by the lookahead pass, before <code>approachMeasure</code> is invoked. It receives lookahead size in the argument to decide whether the destination size has been reached.</p>
</li>
<li><p><code>isPlacementApproachComplete</code>: A block that indicates whether the <strong>position</strong> has approached the destination defined by the lookahead. Based on this, the system can decide whether additional approach placements are necessary. It is invoked after the destination position has been determined by the lookahead pass, and before the placement phase in <code>approachMeasure</code>.</p>
</li>
<li><p>Once both of the above two blocks return <code>true</code>, the system may skip <strong>approach pass</strong> until additional approach passes are necessary as indicated by them. If the above two approach callbacks are incomplete for a long time, the system might skip approach pass whenever possible. Hence it’s important to be accurate about these two callbacks.</p>
</li>
</ol>
<h2 id="heading-implement-in-code">Implement in code</h2>
<p>We have learned a lot of concepts. But we do not know how to use them yet. Let’s write some code with these APIs.</p>
<p>Let’s start with an example where we want to animate four colored Boxes smoothly from a <code>Row</code> view to a <code>Column</code> view and vice-versa, on the click of the container. This example has been taken from the <a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/LookaheadScope">official docs here</a> for <code>LookAheadScope</code>. We want to create something like this:</p>
<p><img src="https://cdn-images-1.medium.com/max/1600/1*y2TcVNniFzCQ1Zb1x2h30w.gif" alt /></p>
<p><strong>Now, we can follow two ways to use Lookahead APIs:</strong></p>
<ol>
<li><p>Write a custom implementation of the <code>ApproachLayoutModifierNode</code></p>
</li>
<li><p>Use the <code>approachLayout</code> Modifier (internally uses <code>ApproachLayoutModifierNode</code>)</p>
</li>
</ol>
<p><strong>Both are ideally doing the same thing</strong>: defining an approach or logic to reach the destination. I will show both ways separately to obtain the desired animation shown above.</p>
<h3 id="heading-1-writing-a-custom-implementation-of-the-approachlayoutmodifiernode">1. Writing a custom implementation of the <code>ApproachLayoutModifierNode</code></h3>
<h4 id="heading-step-1create-movable-contents-and-switch-layouts"><strong>Step 1:Create movable contents and switch layouts</strong></h4>
<p>First, we will create movable contents for the four colored Boxes. For this, we will use the <code>movableContentOf</code> API. This enables us to move around content without the need for recomposition. It works by receiving a composable lambda function that it will remember and move to wherever it is invoked. Then using a Boolean mutable state we will toggle them between a <code>Row</code> and a <code>Column</code> on the click of the parent <code>Box</code> container.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">LookAheadWithSimpleMovableContent</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">val</span> colors = listOf(
        Color(<span class="hljs-number">0xffff6f69</span>),
        Color(<span class="hljs-number">0xffffcc5c</span>),
        Color(<span class="hljs-number">0xff264653</span>),
        Color(<span class="hljs-number">0xFF679138</span>),
    )
    <span class="hljs-keyword">var</span> isInColumn <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-literal">true</span>) }
    <span class="hljs-keyword">val</span> items = remember {
        movableContentOf {
            colors.forEach { color -&gt;
                Box(
                    Modifier
                        .padding(<span class="hljs-number">8</span>.dp)
                        .size(<span class="hljs-number">80</span>.dp)
                        .background(color, RoundedCornerShape(<span class="hljs-number">10</span>))
                )
            }
        }
    }

    Box(
        modifier = Modifier.fillMaxSize().clickable { isInColumn = !isInColumn },
        contentAlignment = Alignment.Center
    ) {
        <span class="hljs-keyword">if</span> (isInColumn) {
            Column { items() }
        } <span class="hljs-keyword">else</span> {
            Row { items() }
        }
    }
}
</code></pre>
<p>This will create a behavior like this. The layouts are switching but there is no animation as we have not added any.</p>
<p><img src="https://cdn-images-1.medium.com/max/1600/1*X5tbelHoD64RU0HReLIssg.gif" alt /></p>
<p><strong>Step 2: Use the</strong><code>LookaheadScope</code><strong>API</strong></p>
<p>Next, we will wrap our content with the <code>LookaheadScope</code> composable. This will provide us with the scope to “look ahead” so that we can determine the destination of our Box layouts through a lookahead pass.</p>
<p>So, our code will now look like this:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">LookAheadWithSimpleMovableContent</span><span class="hljs-params">()</span></span> {
    <span class="hljs-comment">// ...</span>
    <span class="hljs-keyword">var</span> isInColumn <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-literal">true</span>) }
    LookaheadScope { <span class="hljs-comment">// Wrapped with LookaheadScope</span>
        <span class="hljs-comment">// ...</span>
        <span class="hljs-keyword">val</span> items = remember {
            movableContentOf {
                <span class="hljs-comment">// ...</span>
            }
        }

        Box(
            modifier = Modifier.fillMaxSize().clickable { isInColumn = !isInColumn },
            contentAlignment = Alignment.Center
        ) {
            <span class="hljs-comment">// Remaining code</span>
        }
    }
}
</code></pre>
<h4 id="heading-step-3-a-custom-implementation-of-approachlayoutmodifiernode">Step 3: A custom implementation of <code>ApproachLayoutModifierNode</code></h4>
<p>Now the most crucial part. We will use the <code>LookaheadScope</code> from the above step to create a custom implementation of the <code>ApproachLayoutModifierNode</code>.</p>
<p>For this, we have to override 3 functions:</p>
<ol>
<li><p><code>isMeasurementApproachComplete</code></p>
</li>
<li><p><code>isPlacementApproachComplete</code></p>
</li>
<li><p><code>approachMeasure</code></p>
</li>
</ol>
<p>First, we have to create an offset animation of the type <code>DeferredTargetAnimation</code>, the target of which will be known during placement.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> offsetAnimation: DeferredTargetAnimation&lt;IntOffset, AnimationVector2D&gt; = 
    DeferredTargetAnimation(IntOffset.VectorConverter)
</code></pre>
<p>Then override <code>isMeasurementApproachComplete</code>. As we are only animating the placement here, we can consider the measurement approach complete. So we can simply return a <code>true</code> here.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">isMeasurementApproachComplete</span><span class="hljs-params">(lookaheadSize: <span class="hljs-type">IntSize</span>)</span></span>: <span class="hljs-built_in">Boolean</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
}
</code></pre>
<p>Now we’ll override the <code>isPlacementApproachComplete</code>.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> Placeable.PlacementScope.<span class="hljs-title">isPlacementApproachComplete</span><span class="hljs-params">(
    lookaheadCoordinates: <span class="hljs-type">LayoutCoordinates</span>
)</span></span>: <span class="hljs-built_in">Boolean</span> {
    <span class="hljs-keyword">val</span> target: IntOffset = with(lookaheadScope) {
        lookaheadScopeCoordinates.localLookaheadPositionOf(lookaheadCoordinates).round()
    }
    offsetAnimation.updateTarget(target, coroutineScope)
    <span class="hljs-keyword">return</span> offsetAnimation.isIdle
}
</code></pre>
<p><strong>Let’s try to understand what is happening in the above code.</strong> Our main objective of this function is to return <code>true</code> when the offset animation is complete, and <code>false</code> otherwise. First, we are acquiring the <code>target</code> position of the layout using the <code>localLookaheadPositionOf</code> with the layout’s coordinates. We have discussed previously that <code>localLookaheadPositionOf</code> calculates the local position of the layout in the Lookahead coordinate space. Then we will call <code>updateTarget</code> on the <code>offsetAnimation</code> that we created at the beginning with this acquired <code>target</code> position. This function will set up the animation or will update the already running animation based on the <code>target</code>. Finally, we will return <code>offsetAnimation.isIdle</code> which will return <code>true</code> when the animation has finished running.</p>
<p>Now, we’ll override the <code>approachMeasure</code>.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> ApproachMeasureScope.<span class="hljs-title">approachMeasure</span><span class="hljs-params">(
    measurable: <span class="hljs-type">Measurable</span>,
    constraints: <span class="hljs-type">Constraints</span>
)</span></span>: MeasureResult {
    <span class="hljs-keyword">val</span> placeable = measurable.measure(constraints)
    <span class="hljs-keyword">return</span> layout(placeable.width, placeable.height) {
        <span class="hljs-keyword">val</span> coordinates = coordinates
        <span class="hljs-keyword">if</span> (coordinates != <span class="hljs-literal">null</span>) {
            <span class="hljs-keyword">val</span> target = with(lookaheadScope) {
                lookaheadScopeCoordinates.localLookaheadPositionOf(coordinates).round()
            }
            <span class="hljs-keyword">val</span> animatedOffset = offsetAnimation.updateTarget(target, coroutineScope)
            <span class="hljs-keyword">val</span> placementOffset = with(lookaheadScope) {
                lookaheadScopeCoordinates.localPositionOf(coordinates, Offset.Zero).round()
            }
            <span class="hljs-keyword">val</span> (x, y) = animatedOffset - placementOffset
            placeable.place(x, y)
        } <span class="hljs-keyword">else</span> {
            placeable.place(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>)
        }
    }
}
</code></pre>
<p><strong>Let’s try to understand what is happening in the above code.</strong> The function <code>approachMeasure</code> accepts a <code>Measurable</code> and <code>Constraints</code> and returns a <code>MeasureResult</code>. So we are first measuring our measurable (which is our layout) using the constraints. This generates a <code>Placeable</code>. By definition, we know that a Placeable corresponds to a child layout that can be positioned by its parent layout. So we need to position or <code>place</code> this Placeable. Now, similar to the previous step, we are calculating the target offset within the <code>lookaheadScope</code>, using the available<code>LayoutCoordinates</code>. Then using the <code>updateTarget</code> function and the <code>target</code> offset, we are starting an offset animation. This gives us the animated offset or position. Then, we are calculating the current offset within the given <code>LookaheadScope</code> using the <code>localPositionOf</code> function. We then calculate the delta between the animated position and the current position in <code>lookaheadScope</code>. Finally, we put the child layout in the animated position using the <code>place</code> function.</p>
<blockquote>
<p>Both <code>localLookaheadPositionOf</code> and <code>localPositionOf</code> are used to get the converted offset relative to a specific coordinate. The only difference is that unlike <code>localPositionOf</code>, <code>localLookaheadPositionOf</code> uses the lookahead position for coordinate calculation.</p>
</blockquote>
<p>Till here we are done with the custom implementation of the <code>ApproachLayoutModifierNode</code> and overridden all three functions.</p>
<h4 id="heading-step-4-create-a-custom-node-element">Step 4: Create a custom node element</h4>
<p>Now we have to create a custom node element for the <code>AnimatedPlacementModifierNode</code> created above by implementing the <code>ModifierNodeElement</code> abstract class. It takes a <code>LookaheadScope</code> instance in the constructor and creates/updates our custom <code>ApproachLayoutModifierNode</code>.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AnimatePlacementNodeElement</span></span>(<span class="hljs-keyword">val</span> lookaheadScope: LookaheadScope) :
    ModifierNodeElement&lt;AnimatedPlacementModifierNode&gt;() {

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">update</span><span class="hljs-params">(node: <span class="hljs-type">AnimatedPlacementModifierNode</span>)</span></span> {
        node.lookaheadScope = lookaheadScope
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">create</span><span class="hljs-params">()</span></span>: AnimatedPlacementModifierNode {
        <span class="hljs-keyword">return</span> AnimatedPlacementModifierNode(lookaheadScope)
    }
}
</code></pre>
<h4 id="heading-step-5-create-a-modifier-to-use-the-custom-node-element">Step 5: Create a <code>Modifier</code> to use the custom node element</h4>
<p>We will create a <code>Modifier</code> that will internally use the custom node element <code>AnimatePlacementNodeElement</code> created above. This Modifier will be used with the colored Boxes that we want to animate.</p>
<pre><code class="lang-kotlin"><span class="hljs-function"><span class="hljs-keyword">fun</span> Modifier.<span class="hljs-title">animatePlacementInScope</span><span class="hljs-params">(lookaheadScope: <span class="hljs-type">LookaheadScope</span>)</span></span>: Modifier {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.then(AnimatePlacementNodeElement(lookaheadScope))
}
</code></pre>
<h4 id="heading-step-6-use-the-above-createdmodifier"><strong>Step 6: Use the above-created</strong><code>Modifier</code></h4>
<p>Going back to the initial code we wrote, we will use this Modifier with the movable content Boxes. And we will pass the current <code>LookaheadScope</code> instance using this as we already wrapped it inside the <code>LookaheadScope</code> composable.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">LookAheadWithSimpleMovableContent</span><span class="hljs-params">()</span></span> {
    <span class="hljs-comment">// ...</span>
    <span class="hljs-keyword">var</span> isInColumn <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-literal">true</span>) }
    LookaheadScope {
        <span class="hljs-keyword">val</span> items = remember {
            movableContentOf {
                colors.forEach { color -&gt;
                    Box(
                        Modifier
                            .padding(<span class="hljs-number">8</span>.dp)
                            .size(<span class="hljs-number">80</span>.dp)
                            .animatePlacementInScope(<span class="hljs-keyword">this</span>) <span class="hljs-comment">// Add Modifier here</span>
                            .background(color, RoundedCornerShape(<span class="hljs-number">10</span>))
                    )
                }
            }
        }
      <span class="hljs-comment">// Remaining code</span>
    }
}
</code></pre>
<p>Our implementation of Lookahead APIs with movable content is completed and now we will see the desired behavior. A smooth animation of the four Boxes between <code>Row</code> to <code>Column</code> layouts.</p>
<p><img src="https://cdn-images-1.medium.com/max/1600/1*y2TcVNniFzCQ1Zb1x2h30w.gif" alt /></p>
<p><strong>See the complete code for this example here:</strong><a target="_blank" href="https://github.com/pushpalroy/ComposeLayoutPlayground/blob/main/app/src/main/java/com/appmason/composelayoutplayground/ui/screens/lookaheadlayout/LookAheadWithCustomApproachLayoutModifierNode.kt">https://github.com/pushpalroy/ComposeLayoutPlayground/…/LookAheadWithCustomApproachLayoutModifierNode.kt</a></p>
<h3 id="heading-2-using-the-approachlayout-modifier">2. <strong>Using the</strong> <code>approachLayout</code> Modifier</h3>
<p>This approach will be very similar to the previous way, with some differences.</p>
<h4 id="heading-step-1-create-movable-contents-with-lookaheadscope-receiver">Step 1: Create movable contents with <code>LookaheadScope</code> receiver</h4>
<p>In the former approach, we used the <code>movableContentOf</code> API. In this approach, we will use the <code>movableContentWithReceiverOf</code> API. This is also used to create movable content but with a receiver context, which in our case will be the <code>LookaheadScope</code>. So our previous code to initialize the items of colored Boxes will become:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> items = remember {
    movableContentWithReceiverOf&lt;LookaheadScope&gt; {
        colors.forEach { color -&gt;
            Box(
                Modifier
                    .padding(<span class="hljs-number">8</span>.dp)
                    .size(<span class="hljs-number">80</span>.dp)
                    .background(color, RoundedCornerShape(<span class="hljs-number">10</span>))
            )
        }
    }
}
</code></pre>
<p>The remaining code for switching the layout between <code>Row</code> and <code>Column</code> will remain the same as the former code.</p>
<h4 id="heading-step-2-create-a-modifier-using-the-approachlayout-modifier">Step 2: Create a Modifier using the <code>approachLayout</code> Modifier</h4>
<p>This step is very similar to how we created the custom implementation of <code>ApproachLayoutModifierNode</code> before. The <code>approachLayout</code> Modifier also provides us the three lambdas to invoke: <code>isMeasurementApproachComplete</code>, <code>isPlacementApproachComplete</code> and <code>approachMeasure</code>. This is exactly the same as how we have overridden the three functions previously. Also, we are going to write the same login inside each of these lambdas for our use case.</p>
<p>Note how we marked the entire Modifier function with <code>context (LookaheadScope)</code> at the top. This means our <code>animateBounds</code> Modifier will only work in the context of a <code>LookaheadScope</code>. This also helps to access the <code>lookaheadScopeCoordinates</code> for calculating the <code>localPosition</code> in the Lookahead coordinate space.</p>
<pre><code class="lang-kotlin">context (LookaheadScope)
<span class="hljs-meta">@OptIn(ExperimentalAnimatableApi::class, ExperimentalComposeUiApi::class)</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> Modifier.<span class="hljs-title">animateBounds</span><span class="hljs-params">()</span></span>: Modifier = composed {
    <span class="hljs-keyword">val</span> offsetAnim = remember { DeferredTargetAnimation(IntOffset.VectorConverter) }
    <span class="hljs-keyword">val</span> scope = rememberCoroutineScope()
    <span class="hljs-keyword">this</span>.approachLayout(
        isMeasurementApproachComplete = {
            <span class="hljs-literal">true</span>
        },
        isPlacementApproachComplete = {
            <span class="hljs-keyword">val</span> target = lookaheadScopeCoordinates.localLookaheadPositionOf(it)
            offsetAnim.updateTarget(target.round(), scope)
            offsetAnim.isIdle
        }
    ) { measurable, constraints -&gt;
        measurable.measure(constraints)
            .run {
                layout(width, height) {
                    coordinates?.let {
                        <span class="hljs-keyword">val</span> target = lookaheadScopeCoordinates.localLookaheadPositionOf(it).round()
                        <span class="hljs-keyword">val</span> animOffset = offsetAnim.updateTarget(target, scope)
                        <span class="hljs-keyword">val</span> current = lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero).round()
                        <span class="hljs-keyword">val</span> (x, y) = animOffset - current
                        place(x, y)
                    } ?: place(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>)
                }
            }
    }
}
</code></pre>
<p><strong>Note:</strong> If we <a target="_blank" href="https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadScope.kt;l=138?q=Modifier.approachLayout&amp;sq=">see the internals</a> of the <code>approachLayout</code> Modifier, we will find that it is in fact creating a <code>ApproachLayoutElement</code> data class which is an implementation of the <code>ModifierNodeElement</code> abstract class (Step 4 of our former way). And a custom implementation of <code>ApproachLayoutModifierNode</code> is there, called the <code>ApproachLayoutModifierNodeImpl</code> (Step 3 of our former way). So basically both the ways we just covered do the same thing.</p>
<h4 id="heading-step-3-use-the-above-created-modifier-in-our-layout">Step 3: Use the above-created Modifier in our layout</h4>
<p>Now the obvious step, we will use the animateBounds modifier in our layout. This is similar to Step 6 of our former way. As we used <code>movableContentWithReceiverOf&lt;LookaheadScope&gt;</code> the <code>animateBounds()</code> will execute in the context of <code>LookaheadScope</code>, which we need.</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// Remaining Code</span>
<span class="hljs-comment">// ..</span>
<span class="hljs-keyword">val</span> items = remember {
    movableContentWithReceiverOf&lt;LookaheadScope&gt; {
        colors.forEach { color -&gt;
            Box(
                Modifier
                    .padding(<span class="hljs-number">8</span>.dp)
                    .size(<span class="hljs-number">80</span>.dp)
                    .animateBounds() <span class="hljs-comment">// Use modifier here</span>
                    .background(color, RoundedCornerShape(<span class="hljs-number">10</span>))
            )
        }
    }
}
<span class="hljs-comment">// ..</span>
<span class="hljs-comment">// Remaining Code</span>
</code></pre>
<p>All the other codes will remain the same. If we run the above code it will result in the same animation we obtained earlier.</p>
<p><strong>See the complete code for this example here:</strong></p>
<p><a target="_blank" href="https://github.com/pushpalroy/ComposeLayoutPlayground/blob/main/app/src/main/java/com/appmason/composelayoutplayground/ui/screens/lookaheadlayout/LookAheadWithApproachLayoutModifier.kt">https://github.com/pushpalroy/ComposeLayoutPlayground/…/LookAheadWithApproachLayoutModifier.kt</a></p>
<p>It’s obvious that the second approach, that is using the <code>approachLayout</code> Modifier is an easier way and requires writing less code for our use case.</p>
<h2 id="heading-customize-animations">Customize animations</h2>
<p>In the <code>isPlacementApproachComplete</code> lambda, we are updating the offset animation using the <code>updateTarget</code> function:</p>
<pre><code class="lang-kotlin">offsetAnim.updateTarget(target.round(), scope)
</code></pre>
<p>The <code>updateTarget</code> has a default <code>animationSpec</code> of <code>spring()</code>, which we can customize. For example, we can pass a <code>tween()</code> like this:</p>
<pre><code class="lang-kotlin">offsetAnim.updateTarget(target.round(), scope, tween(durationMillis = <span class="hljs-number">800</span>))
</code></pre>
<p>This will create an animation like this:</p>
<p><img src="https://cdn-images-1.medium.com/max/1600/1*gxByJjVPbZux28y3p3I_IQ.gif" alt /></p>
<h2 id="heading-animate-size-along-with-placement">Animate size along with placement</h2>
<p>If we remember, in the previous animation we have just animated the placement of the Boxes. As we only animated the placement here and not the size, we considered the measurement was approach complete. So we returned a <code>true</code> inside the <code>isMeasurementApproachComplete</code> lambda.</p>
<p>What if we want to animate the size as well? Let’s do that.</p>
<p>Let’s modify the last code and switch the size of each <code>Box</code> between <code>80.dp</code> and <code>20.dp</code> based on whether the layout is a <code>Row</code> or <code>Column</code>.</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// ..</span>
<span class="hljs-keyword">var</span> isInColumn <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-literal">true</span>) }
<span class="hljs-keyword">val</span> items = remember {
    movableContentWithReceiverOf&lt;LookaheadScope&gt; {
        colors.forEach { color -&gt;
            Box(
                Modifier
                    .padding(<span class="hljs-number">8</span>.dp)
                    .size(<span class="hljs-keyword">if</span> (isInColumn) <span class="hljs-number">80</span>.dp <span class="hljs-keyword">else</span> <span class="hljs-number">20</span>.dp)
                    .animateBounds()
                    .background(color, RoundedCornerShape(<span class="hljs-number">10</span>))
            )
        }
    }
}
<span class="hljs-comment">// Remaining Code</span>
</code></pre>
<p>So now based on the <code>isInColumn</code> flag, we are changing the size. That means if the layout is a Column <code>80.dp</code> will be used else <code>20.dp</code> will be used.</p>
<p>Also, let’s increase the animation duration of the offset inside <code>isPlacementApproachComplete</code> so that we see clearly what is happening. Let's use a <code>tween</code> animation for 3000ms.</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// ..</span>
isPlacementApproachComplete = {
    <span class="hljs-keyword">val</span> target = lookaheadScopeCoordinates.localLookaheadPositionOf(it)
    offsetAnim.updateTarget(target.round(), scope, tween(<span class="hljs-number">3000</span>))
    offsetAnim.isIdle
}
<span class="hljs-comment">// ..</span>
</code></pre>
<p>Now without any further modification, if we run the code now, we will see this:</p>
<p><img src="https://cdn-images-1.medium.com/max/1600/1*b9vj64n7yVI9bZV3BJ1Fmw.gif" alt /></p>
<p>If we look closely, the size is changing but there is no animation for that. We can only see animation for placement. The size is just snapping from <code>80.dp</code> to <code>20.dp</code> quickly.</p>
<p><strong>Let’s add animation for the size change:</strong></p>
<p>Similar to the <code>offsetAnim</code>, we will create a <code>DeferredTargetAnimation</code> called the <code>sizeAnim</code>, which will track the animation for size. Then instead of returning <code>true</code> inside <code>isMeasurementApproachComplete</code>, now we will do <code>updateTarget</code> on <code>sizeAnim</code> and return if it’s idle:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// ..</span>
<span class="hljs-keyword">this</span>.approachLayout(
  isMeasurementApproachComplete = {
      sizeAnim.updateTarget(it, scope, tween(<span class="hljs-number">2000</span>))
      sizeAnim.isIdle
  },
  isPlacementApproachComplete = {
    <span class="hljs-comment">// Same as before</span>
  }
<span class="hljs-comment">// ..</span>
</code></pre>
<p>The above code will return <code>true</code> inside <code>isMeasurementApproachComplete</code> when the size animation is complete. Note that we have used a <code>tween</code> animation of duration 2000ms for the size animation.</p>
<p>Now, inside the <code>approachMeasure</code> block, we will do <code>sizeAnim.updateTarget</code> with the <code>lookaheadSize</code> to get the <code>animWidth</code> and <code>animHeight</code>. This is the measured size of the layout while animating. We will then use this constraint to measure the measurable: <code>Constraints.fixed(animWidth, animHeight)</code>.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">this</span>.approachLayout(
    isMeasurementApproachComplete = {
        <span class="hljs-comment">// ..</span>
    },
    isPlacementApproachComplete = {
        <span class="hljs-comment">// ..</span>
    }
) { measurable, _ -&gt;
    <span class="hljs-keyword">val</span> (animWidth, animHeight) = sizeAnim.updateTarget(lookaheadSize, scope)
    measurable.measure(Constraints.fixed(animWidth, animHeight))
        .run {
            layout(width, height) {
                coordinates?.let {
                    <span class="hljs-keyword">val</span> target = lookaheadScopeCoordinates.localLookaheadPositionOf(it).round()
                    <span class="hljs-keyword">val</span> animOffset = offsetAnim.updateTarget(target, scope)
                    <span class="hljs-keyword">val</span> current = lookaheadScopeCoordinates.localPositionOf(it, Offset.Zero).round()
                    <span class="hljs-keyword">val</span> (x, y) = animOffset - current
                    place(x, y)
                } ?: place(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>)
            }
        }
}
</code></pre>
<p>This will create an effect like this:</p>
<p><img src="https://cdn-images-1.medium.com/max/1600/1*rlDMkVy7Kk1bO2HAmWfBTg.gif" alt /></p>
<p>Now we can see that the Boxes' size and placement are getting animated smoothly.</p>
<p><strong>See the complete code for this example here:</strong></p>
<p><a target="_blank" href="https://github.com/pushpalroy/ComposeLayoutPlayground/blob/main/app/src/main/java/com/appmason/composelayoutplayground/ui/screens/lookaheadlayout/LookAheadWithApproachLayoutModifier2.kt">https://github.com/pushpalroy/ComposeLayoutPlayground/…/LookAheadWithApproachLayoutModifier2.kt</a></p>
<hr />
<p>You can find all the examples along with some other examples of Lookahead in this repository:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/pushpalroy/ComposeLayoutPlayground">https://github.com/pushpalroy/ComposeLayoutPlayground</a></div>
<p> </p>
<h2 id="heading-some-important-api-changes">Some important API changes</h2>
<ol>
<li><p><a target="_blank" href="https://github.com/pushpalroy/ComposeLayoutPlayground/blob/main/app/src/main/java/com/appmason/composelayoutplayground/ui/screens/lookaheadlayout/LookAheadWithApproachLayoutModifier2.kt">In <strong>Version 1.7.0-al</strong></a><a target="_blank" href="https://developer.android.com/jetpack/androidx/releases/compose-ui#1.7.0-alpha03"><strong>pha03</strong></a>, the <code>ApproachLayoutModifierNode</code> API <a target="_blank" href="https://github.com/pushpalroy/ComposeLayoutPlayground/blob/main/app/src/main/java/com/appmason/composelayoutplayground/ui/screens/lookaheadlayout/LookAheadWithApproachLayoutModifier2.kt">has been added to support creating custom approach lo</a>gic in an explicit Modifier Node.</p>
</li>
<li><p>In <a target="_blank" href="https://developer.android.com/jetpack/androidx/releases/compose-animation#1.6.0-alpha03"><strong>Version 1.6.0-alpha03</strong></a>, the <code>LookaheadScope</code> composable and interfaces are made stable. Also, a new enter/exit transition to scale content to container size has been added. <a target="_blank" href="https://developer.android.com/jetpack/androidx/releases/compose-animation#1.6.0-alpha03">Read here</a>.</p>
</li>
<li><p>In <a target="_blank" href="https://developer.android.com/jetpack/androidx/releases/compose-ui#1.6.0-alpha02"><strong>Version 1.6.0-alpha02</strong></a>, <code>LookaheadLayout</code> and <code>LookaheadLayoutScope</code> have been finally removed and replaced by <code>LookaheadScope</code> APIs.</p>
</li>
<li><p>In <a target="_blank" href="https://developer.android.com/jetpack/androidx/releases/compose-ui#1.6.0-alpha01"><strong>Version 1.6.0-alpha01</strong></a>, support for lookahead has been added to <code>LazyList</code>.</p>
</li>
<li><p>In <a target="_blank" href="https://developer.android.com/jetpack/androidx/releases/compose-ui#1.5.0-alpha03"><strong>Version 1.5.0-alpha03</strong></a>, new behavior has been added due to which <code>SubcomposeLayouts</code> that don’t have conditional slots work nicely with lookahead animations.</p>
</li>
<li><p>In <a target="_blank" href="https://developer.android.com/jetpack/androidx/releases/compose-ui#1.5.0-alpha01"><strong>Version 1.5.0-alpha01</strong></a>, <code>LookaheadLayout</code> has been replaced by <code>LookaheadScope</code>, which is no longer a Layout.</p>
</li>
<li><p>In <a target="_blank" href="https://developer.android.com/jetpack/androidx/releases/compose-ui#1.3.0-alpha01"><strong>Version 1.3.0-alpha01</strong></a>, <code>LookaheadLayout</code> was first introduced. See the <a target="_blank" href="https://android-review.googlesource.com/c/platform/frameworks/support/+/1961800">commit here</a>.</p>
</li>
</ol>
<h2 id="heading-additional-resources">Additional resources</h2>
<p>Learn more about LookAheadLayout from these resources:</p>
<ol>
<li><p><strong>Official Docs:</strong><br /> <a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/LookaheadScope">https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/LookaheadScope</a></p>
</li>
<li><p><strong>Introducing LookaheadLayout by</strong><a target="_blank" href="https://twitter.com/JorgeCastilloPr"><strong>Jorge Castillo</strong></a>**:<br /> **<a target="_blank" href="https://substack.com/inbox/post/64358322">https://substack.com/inbox/post/64358322</a></p>
</li>
<li><p>This blog by <a target="_blank" href="https://jisungbin.medium.com/?source=user_profile-------------------------------------"><strong>Ji Sungbin</strong></a>:<br /> <a target="_blank" href="https://betterprogramming.pub/introducing-jetpack-composes-new-layout-lookaheadlayout-eb30406f715">https://betterprogramming.pub/introducing-jetpack-composes-new-layout-lookaheadlayout-eb30406f715</a></p>
</li>
</ol>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>Lookahead is a potent API within Jetpack Compose that, when utilized correctly, enables the creation of stunning and highly performant animations, including shared element transitions among others. This article aims to provide an initial exploration and shed light on various aspects of this new API, encouraging further experimentation.</p>
<p>Check out the follow-up article where I demonstrate how to create a container transform animation using Lookahead: <a target="_blank" href="https://hashnode.com/post/cltqaexhy000309jx3nnu04s3"><strong>Container Transform Animation with Lookahead in Jetpack Compose</strong></a></p>
<p>Follow me: <a target="_blank" href="https://twitter.com/pushpalroy">@pushpalroy</a></p>
]]></content:encoded></item><item><title><![CDATA[Create Instagram-like Long Press and Draggable Carousel Indicators in Jetpack Compose]]></title><description><![CDATA[We must have used this UX in the Instagram mobile app, where we can long press the carousel indicators section and then swipe left or right for fast scrolling. This is an extremely useful user experience when it comes to quick viewing of the images.
...]]></description><link>https://blog.pushpalroy.com/create-instagram-like-long-press-and-draggable-carousel-indicators-in-jetpack-compose</link><guid isPermaLink="true">https://blog.pushpalroy.com/create-instagram-like-long-press-and-draggable-carousel-indicators-in-jetpack-compose</guid><category><![CDATA[drag gesture]]></category><category><![CDATA[gesture]]></category><category><![CDATA[compose_ui]]></category><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[UIUX]]></category><category><![CDATA[UI Design]]></category><category><![CDATA[UI]]></category><category><![CDATA[Gesture Detector]]></category><category><![CDATA[carousel]]></category><dc:creator><![CDATA[Pushpal Roy]]></dc:creator><pubDate>Mon, 04 Mar 2024 00:08:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709510466624/2f2bde22-7c30-4a36-94d4-5ea3dbce5099.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We must have used this UX in the Instagram mobile app, where we can long press the carousel indicators section and then swipe left or right for fast scrolling. This is an extremely useful user experience when it comes to quick viewing of the images.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:640/1*eeIicZiiLx5Jv1_lSwIG9Q.gif" alt class="image--center mx-auto" /></p>
<p>If we look closely, the indicator sizes also diminish for images further away from the current selection, providing a focused visual effect.</p>
<p>In this article, we will learn how to create this UX using Jetpack Compose. The idea is to create a composable named <code>DraggableIndicator</code> and use it along with a <code>HorizontalPager</code>.</p>
<p>So let’s get started.</p>
<h2 id="heading-step-by-step-approach"><strong>Step-by-step Approach</strong></h2>
<h3 id="heading-step-1-set-up-the-basic-composable-structure"><strong>Step 1: Set up the basic composable structure</strong></h3>
<p>We have to create a composable called <code>DraggableIndicator</code> which will look like this. It will have the following arguments:</p>
<ul>
<li><p><code>pagerState</code>: A <code>PagerState</code> instance which will be used by the <code>HorizontalPager</code>.</p>
</li>
<li><p><code>itemCount</code>: The total count of items or images in the pager.</p>
</li>
<li><p><code>onPageSelected</code>: A callback function that is invoked when a new page is selected through drag gestures.</p>
</li>
</ul>
<p>We’ll start the parent with a simple <code>Box</code>.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">DraggableIndicator</span><span class="hljs-params">(
    modifier: <span class="hljs-type">Modifier</span> = Modifier,
    state: <span class="hljs-type">PagerState</span>,
    itemCount: <span class="hljs-type">Int</span>,
    onPageSelect: (<span class="hljs-type">Int</span>) -&gt; <span class="hljs-type">Unit</span>,
)</span></span> {
    Box(modifier = modifier) {
        <span class="hljs-comment">// Indicators will be added here</span>
    }
}
</code></pre>
<h3 id="heading-step-2-add-the-indicators"><strong>Step 2: Add the indicators</strong></h3>
<p>Next, we will draw each indicator using a <code>Box</code> and <code>drawBehind</code> modifier with an initial style of<code>Gray</code> color and add them in a horizontal row using a <code>LazyRow</code>.</p>
<pre><code class="lang-kotlin">LazyRow(
    modifier = Modifier.padding(<span class="hljs-number">8</span>.dp).widthIn(max = <span class="hljs-number">100</span>.dp),
    horizontalArrangement = Arrangement.spacedBy(<span class="hljs-number">8</span>.dp),
    verticalAlignment = Alignment.CenterVertically
) {
    items(itemCount) { index -&gt;
        Box(
            modifier = Modifier
                .size(<span class="hljs-number">10</span>.dp)
                .drawBehind {
                  drawCircle(color)
                }
        )
    }
}
</code></pre>
<h3 id="heading-step-3-adjust-indicator-size-and-color-based-on-the-current-page"><strong>Step 3: Adjust indicator size and color based on the current page</strong></h3>
<p>Now as we discussed at the start, we want to create a visual effect for our indicators, such that the indicator sizes diminish for images further away from the current selection. To achieve this, we have to calculate a <code>scaleFactor</code> for each indicator dot, based on the <code>currentPage</code> state and the indicator <code>index</code>. The <code>currentPage</code> here is simply the <code>Int</code> state obtained from the <code>PagerState</code> of the pager. Also, we are switching the <code>color</code> of each indicator between 2 values based on the currently selected page.</p>
<pre><code class="lang-kotlin">items(itemCount) { i -&gt;
    <span class="hljs-keyword">val</span> scaleFactor = <span class="hljs-number">1f</span> - (<span class="hljs-number">0.1f</span> * abs(i - currentPage)).coerceAtMost(<span class="hljs-number">0.4f</span>)
    <span class="hljs-keyword">val</span> color = <span class="hljs-keyword">if</span> (i == currentPage) Color(<span class="hljs-number">0xFF03A9F4</span>) <span class="hljs-keyword">else</span> Color.Gray
    Box(
        modifier = Modifier
            .size(<span class="hljs-number">10</span>.dp)
            .graphicsLayer {
                scaleX = scaleFactor
                scaleY = scaleFactor
            }
            .drawBehind {
                drawCircle(color)
            }
    )
}
</code></pre>
<p>The formula we are using to calculate the scale factor for each indicator here is:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> scaleFactor = <span class="hljs-number">1f</span> - (<span class="hljs-number">0.1f</span> * abs(i - currentPage)).coerceAtMost(<span class="hljs-number">0.4f</span>)
</code></pre>
<p><strong>Let’s try to understand what is happening in the above formula:</strong></p>
<ul>
<li><p><code>1f</code>: This represents the base scale factor, implying that in the default state (when the indicator is the current page), it should not be scaled down at all (i.e., it retains its original size).</p>
</li>
<li><p><code>0.1f * abs(i - currentPage)</code>: This portion of the formula calculates the difference in position between the current indicator (<code>i</code>) and the currently selected page (<code>currentPage</code>). The absolute value (<code>abs</code>) ensures that the distance is always a positive number, regardless of whether the current indicator is before or after the current page. This distance is then multiplied by <code>0.1f</code>, determining how much the scale factor decreases as the distance from the current page increases. This means, that for each step away from the current page, the indicator scales down by 10%.</p>
</li>
<li><p><code>.coerceAtMost(0.4f)</code>: This method caps the maximum scale-down effect to 40% (<code>0.4f</code>). Without this cap, indicators far away from the current page could become too small or even disappear. By limiting the scale factor reduction to a maximum of 40%, we ensure that all indicators remain visible and maintain a minimum scale, contributing to a balanced and harmonious visual effect across the carousel.</p>
</li>
</ul>
<p>The result is a dynamic scaling effect where the size of each indicator smoothly decreases as the distance from the current page increases, up to a maximum scale reduction.</p>
<h3 id="heading-step-4-enable-long-press-and-drag-gestures"><strong>Step 4: Enable long-press and drag gestures</strong></h3>
<p>Now, the most interesting part. We need to implement a gesture that will enable us to first long-press on the indicators and then scroll left and right. For this will use the <code>pointerInput</code> Modifier along with the <code>detectDragGesturesAfterLongPress</code> suspend function which is an extension of the <code>PointerInputScope</code>. This is a very useful function of the <code>androidx.compose.foundation.gestures</code> package.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> accumulatedDragAmount = remember { mutableFloatStateOf(<span class="hljs-number">0f</span>) }
<span class="hljs-keyword">var</span> enableDrag <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-literal">false</span>) }

Modifier.pointerInput(<span class="hljs-built_in">Unit</span>) {
    detectDragGesturesAfterLongPress(
        onDragStart = {
            enableDrag = <span class="hljs-literal">true</span>
            accumulatedDragAmount.floatValue = <span class="hljs-number">0f</span>
        },
        onDrag = { change, dragAmount -&gt;
            change.consume()
            accumulatedDragAmount.floatValue += dragAmount.x
            <span class="hljs-comment">// Logic to update currentPage based on drag</span>
        },
        onDragEnd = {
            enableDrag = <span class="hljs-literal">false</span>
            accumulatedDragAmount.floatValue = <span class="hljs-number">0f</span>
        }
    )
}
</code></pre>
<p><strong>Let’s try to understand what is happening:</strong></p>
<p>Here the gesture detector waits for the pointer down and long press, after which it calls <code>onDrag</code> for each drag event, which is exactly what we need. <code>onDragStart</code> is called when a long press is detected and <code>onDragEnd</code> is called after all pointers are up. The <code>enableDrag</code> flag tracks the point when drag is enabled and disabled. This is important because we want the drag to be disabled as soon as the user leaves the long press. The <code>accumulatedDragAmount</code> tracks the total drag distance, which will be used later to decide page scrolling.</p>
<h3 id="heading-step-5-change-pages-on-drag"><strong>Step 5: Change pages on drag</strong></h3>
<p>Now inside the <code>onDrag</code> lambda, we will use the value of <code>accumulatedDragAmount</code> to incorporate the logic to change the current page.</p>
<p>First, we need to calculate a drag <code>threshold</code>. The <code>threshold</code> represents the minimum distance the user needs to drag to trigger a page change. This mechanism ensures that slight, unintentional drags do not cause the carousel to switch pages. We will not hardcode this value to a fixed dp because the <code>threshold</code> should change based on the number of items in the carousel. Otherwise, the user might have to swipe more or less if the item count changes causing a weird experience. This is a one-time calculation that we need to add at the top of our composable. <em>Feel free to play around with this calculation to create a desired user experience.</em></p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> density = LocalDensity.current
<span class="hljs-keyword">val</span> threshold = remember {
    with(density) {
        ((<span class="hljs-number">80</span>.dp / (itemCount.coerceAtLeast(<span class="hljs-number">1</span>))) + <span class="hljs-number">10</span>.dp).toPx()
    }
}
</code></pre>
<p>Now using the <code>threshold</code> and <code>accumulatedDragAmount</code> we will write the logic to change the current page inside <code>onDrag</code>.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">if</span> (abs(accumulatedDragAmount.value) &gt;= threshold) {
    <span class="hljs-keyword">val</span> nextPage = <span class="hljs-keyword">if</span> (accumulatedDragAmount.value &lt; <span class="hljs-number">0</span>) state.currentPage + <span class="hljs-number">1</span> <span class="hljs-keyword">else</span> state.currentPage - <span class="hljs-number">1</span>
    <span class="hljs-keyword">val</span> correctedNextPage = nextPage.coerceIn(<span class="hljs-number">0</span>, itemCount - <span class="hljs-number">1</span>)

    <span class="hljs-keyword">if</span> (correctedNextPage != state.currentPage) {
        onPageSelect(correctedNextPage)
    }
    accumulatedDragAmount.value = <span class="hljs-number">0f</span>
}
</code></pre>
<p><strong>Let’s try to understand what is happening:</strong></p>
<ul>
<li><p><code>abs(accumulatedDragAmount.value) &gt;= threshold</code>: This condition checks if the absolute value of the accumulated drag amount (i.e., the total distance the user has dragged, disregarding direction) has reached or exceeded a predefined threshold. This determines <code>nextPage</code>.</p>
</li>
<li><p>If <code>accumulatedDragAmount.value &lt; 0</code>, it implies the user has dragged to the left, intending to move to the next page. Therefore, <code>state.currentPage + 1</code> is computed.</p>
</li>
<li><p>Conversely, if <code>accumulatedDragAmount.value &gt; 0</code>, the user has dragged to the right, indicating a move to the previous page, hence <code>state.currentPage - 1</code>.</p>
</li>
<li><p><code>correctedNextPage</code>: The <code>nextPage</code> value is then coerced within the bounds of <code>0</code> and <code>itemCount - 1</code> using <code>coerceIn</code>. This step ensures that the page index stays within the valid range of pages available in the carousel, preventing index out-of-bounds errors.</p>
</li>
<li><p>If <code>correctedNextPage</code> is different from <code>state.currentPage</code>, the <code>onPageSelect(correctedNextPage)</code> callback is invoked. This action effectively updates the carousel to display the new page selected by the user's drag gesture.</p>
</li>
<li><p>Finally, <code>accumulatedDragAmount.value</code> is reset to <code>0f</code>. This reset is crucial for starting the drag distance calculation afresh for subsequent drag gestures, ensuring that each drag gesture is evaluated independently of the previous ones.</p>
</li>
</ul>
<h3 id="heading-step-6-scroll-to-the-indicator-position-on-the-drag"><strong>Step 6: Scroll to the indicator position on the drag</strong></h3>
<p>Our implementation of the drag gesture is completed. Now we need to make sure that the <code>LazyRow</code> of indicators scroll automatically when the user drags. For this, we will add this <code>LaunchedEffect</code> at the top of our composable which animate scroll to the <code>currentPage</code> index whenever the page changes.</p>
<pre><code class="lang-kotlin">LaunchedEffect(currentPage) {
    coroutineScope.launch {
        lazyListState.animateScrollToItem(index = currentPage)
    }
}
</code></pre>
<h3 id="heading-step-7-add-interactive-haptic-feedback"><strong>Step 7: Add interactive haptic feedback!</strong></h3>
<p>Now when everything is done, it will be cool to add some kind of haptic feedback effect (sensory effect that is created by a device vibrating) so that users get feedback with the interaction. We will provide feedback at 2 points: One when drag is enabled after long-press and then when each page changes.</p>
<p>To do this first we need to initialize a <code>hapticFeedback</code> at the top:</p>
<pre><code class="lang-kotlin"> <span class="hljs-keyword">val</span> haptics = LocalHapticFeedback.current
</code></pre>
<p>Then use it when inside <code>onDragStart</code> when drag is enabled after long-press on the indicators:</p>
<pre><code class="lang-kotlin">.pointerInput(<span class="hljs-built_in">Unit</span>) {
  detectDragGesturesAfterLongPress(
      onDragStart = {
          haptics.performHapticFeedback(HapticFeedbackType.LongPress)
          accumulatedDragAmount.floatValue = <span class="hljs-number">0f</span>
          enableDrag = <span class="hljs-literal">true</span>
      },
<span class="hljs-comment">// Remaining code</span>
</code></pre>
<p>Also inside onDrag when the page changes:</p>
<pre><code class="lang-kotlin">onDrag = { change, dragAmount -&gt;
  <span class="hljs-keyword">if</span> (enableDrag) {
      change.consume()
      accumulatedDragAmount.floatValue += dragAmount.x
      <span class="hljs-keyword">if</span> (abs(accumulatedDragAmount.floatValue) &gt;= threshold) {
          <span class="hljs-comment">// Remaining code</span>
          <span class="hljs-keyword">if</span> (correctedNextPage != state.currentPage) {
              haptics.performHapticFeedback(HapticFeedbackType.TextHandleMove)
              onPageSelect(correctedNextPage)
          }
          <span class="hljs-comment">// Remaining code</span>
      }
  }
},
<span class="hljs-comment">// Remaining code</span>
</code></pre>
<p>That’s it.</p>
<p>We have come a long way to create our composable <code>DraggableIndicator</code>.</p>
<p>Now we will use this in our code.</p>
<h2 id="heading-use-the-composable-in-our-code"><strong>Use the composable in our code</strong></h2>
<p>Consider this sample code for usage. Here we are using a <code>HorizontalPager</code> to show a list of image items in a carousel. Below the Pager, we are showing the indicators using <code>DraggableIndicator</code>.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> state = rememberPagerState(
    initialPage = <span class="hljs-number">0</span>,
    initialPageOffsetFraction = <span class="hljs-number">0f</span>
) { list.size } <span class="hljs-comment">// Here, list is images list</span>

<span class="hljs-keyword">val</span> coroutineScope = rememberCoroutineScope()
Column(
    modifier = Modifier.padding(<span class="hljs-number">24</span>.dp),
    verticalArrangement = Arrangement.Center,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    HorizontalPager(
        modifier = Modifier.height(<span class="hljs-number">280</span>.dp),
        state = state,
    ) { page -&gt;
        <span class="hljs-comment">// Pager content: Image, Card, etc.</span>
    }
    Spacer(modifier = Modifier.height(<span class="hljs-number">16</span>.dp))
    <span class="hljs-comment">// Using our composable here</span>
    DraggableIndicator(
        modifier = Modifier,
        state = state,
        itemCount = colorList.size,
        onPageSelect = { page -&gt;
            coroutineScope.launch {
                state.scrollToPage(page)
            }
        },
    )
}
</code></pre>
<h3 id="heading-final-result"><strong>Final Result</strong></h3>
<p><img src="https://miro.medium.com/v2/resize:fit:1200/1*TJxMUUsb7QWUxHXVNvdznA.gif" alt /></p>
<h3 id="heading-source-code"><strong>Source code</strong></h3>
<p>The entire source code is available here:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/pushpalroy/JetDraggableIndicators">https://github.com/pushpalroy/JetDraggableIndicators</a></div>
]]></content:encoded></item><item><title><![CDATA[Overcoming Common Performance Pitfalls in Jetpack Compose]]></title><description><![CDATA[Jetpack Compose has a dedicated optimization strategy, but we must remember several things while writing code. These points are like rules that we need to follow daily to improve performance and save our application from severe performance pitfalls.
...]]></description><link>https://blog.pushpalroy.com/overcoming-common-performance-pitfalls-in-jetpack-compose</link><guid isPermaLink="true">https://blog.pushpalroy.com/overcoming-common-performance-pitfalls-in-jetpack-compose</guid><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[android development]]></category><category><![CDATA[Performance Optimization]]></category><category><![CDATA[common mistakes]]></category><category><![CDATA[best practices]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Android]]></category><dc:creator><![CDATA[Pushpal Roy]]></dc:creator><pubDate>Sun, 03 Mar 2024 00:06:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709423925050/97ab996b-8cbb-4e95-896b-03950d07560a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Jetpack Compose has a dedicated optimization strategy, but we must remember several things while writing code. These points are like rules that we need to follow daily to improve performance and save our application from severe performance pitfalls.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">As of today, while writing this blog, “Strong Skipping Mode” is experimental, and is currently under testing. It can be a game changer shortly and it might be possible that we do not need to worry about most of the things (if enabled in code) that will be discussed in this blog. But since then (or maybe even after that), these rules will be very crucial for development in Compose. Read more about the Strong Skipping Mode <a target="_blank" class="ba xx" href="https://medium.com/androiddevelopers/jetpack-compose-strong-skipping-mode-explained-cbdb2aa4b900">here</a>.</div>
</div>

<h2 id="heading-a-quick-recap-to-the-rendering">A Quick Recap to the Rendering</h2>
<p>Jetpack Compose renders a frame in the following 3 phases: <strong>Composition</strong>, <strong>Layout</strong>, and <strong>Drawing</strong>. On a high level, the responsibilities of these phases are: “What to show”, “Where to show” and “How to show” respectively. In the <strong>Composition phase</strong>, the runtime executes the composable functions and generates a UI tree consisting of layout nodes, that contain all information for the next 2 phases. In the <strong>Layout phase</strong>, with the help of the UI tree information, each node’s size and location are determined in 2D space. The entire UI tree is traversed and each node does these: Measure its children if any, based on the measurements decide on its own size, and each child node is placed in 2D space relative to the nodes’s own position. In the <strong>Draw phase</strong>, each node is traversed again from the top of the tree to the bottom, and based on the size and position measurements from the previous phase, they are drawn on the screen.</p>
<p>These are repeated in every frame where data is changed. Also, you can skip some phase(s) if data is not changed. Compose tracks what state is read within each of them. This allows Compose to notify only the specific phases that need to perform work for each affected element of your UI. This is extremely important for us to understand because based on the code we are writing we can skip some phases in the rendering, which can improve performance.</p>
<h2 id="heading-a-quick-recap-to-stability-in-compose"><strong>A Quick Recap to Stability in Compose</strong></h2>
<p>Compose considers types to be either <code>stable</code> or <code>unstable</code>. A type is <strong>stable</strong> if it is immutable, or if Compose can know whether its value has changed between recompositions (notify Composition upon mutating). A type is <strong>unstable</strong> if Compose can’t know whether its value has changed between recompositions. If a composable has <strong>stable parameters</strong> that have not changed, Compose skips it. If a composable has <strong>unstable parameters</strong>, Compose always recomposes it when it recomposes the component’s parent. We can use the <a target="_blank" href="https://developer.android.com/jetpack/compose/performance/stability/diagnose#compose-compiler">Compose compiler reports</a>, using which the Compose compiler can output the results of its stability inference for inspection. The compiler marks functions to be either <code>skippable</code> or <code>restartable</code>. A <strong>skippable</strong> function denotes Compose can skip it during recomposition if all its arguments are equal with their previous values. Normally a function is marked as skippable if all its parameters are <code>stable</code> and thus Compose can infer when it has or has not changed. A <strong>restartable</strong> function denotes that this composable can serve as a restarting “scope”. This means that whenever this Composable needs to recompose, it will not trigger the recomposition of its parent scope. Rather this function itself can be a point of entry for where Compose can start re-executing code for recomposition after state changes.</p>
<h2 id="heading-rules-to-overcome-common-pitfalls"><strong>Rules to Overcome Common Pitfalls</strong></h2>
<h3 id="heading-1-avoid-using-unstable-collections-as-much-as-possible"><strong>1. Avoid using unstable collections as much as possible</strong></h3>
<p>The Compose compiler cannot be completely sure that collections such as <code>List</code>, <code>Map</code>, and <code>Set</code> are truly immutable and therefore mark them as unstable. This means, that if we use unstable collections as an argument in a composable function, the argument will be marked as unstable. This even the argument does not change, the composable will recompose if the parent recomposes.</p>
<p><strong>Consider this example to understand:</strong> Here when the <code>FavoriteButton</code> was toggled, the list <code>articles</code> would also be recomposed as it has an <strong>unstable</strong> parameter type: <code>List</code>. Even if the <code>Article</code> class is stable in this example, that won’t help because the List is <strong>unstable</strong>.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">UnstableListScreen</span><span class="hljs-params">(viewModel: <span class="hljs-type">ListViewModel</span> = viewModel()</span></span>) {
    <span class="hljs-keyword">var</span> favorite <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-literal">false</span>) }
    Column(
        modifier = Modifier.padding(<span class="hljs-number">16</span>.dp)
    ) {
        FavoriteButton(isFavorite = favorite, onToggle = { favorite = !favorite })
        Spacer(modifier = Modifier.height(<span class="hljs-number">16</span>.dp))
        UnstableList(viewModel.articles)
    }
}

<span class="hljs-meta">@Composable</span>
<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">UnstableList</span><span class="hljs-params">(
    articles: <span class="hljs-type">List</span>&lt;<span class="hljs-type">Article</span>&gt;, <span class="hljs-comment">// List = Unstable, Article = Stable</span>
    modifier: <span class="hljs-type">Modifier</span> = Modifier <span class="hljs-comment">// Stable</span>
)</span></span> {
    LazyColumn(modifier = modifier) {
        items(articles) { article -&gt;
            Text(text = article.name)
        }
    }
}
</code></pre>
<p><strong>Overcome:</strong></p>
<p><strong>Solution 1:</strong> To overcome this issue, we can use the <a target="_blank" href="https://github.com/Kotlin/kotlinx.collections.immutable">Kotlinx Immutable collections</a> instead of using the default collections. Here the same code is modified by using the <code>PersistentList</code> from the Immutable Collections library as a replacement of <code>List</code>. This makes <code>articles</code> as <strong>stable</strong>. Hence, when the <code>FavoriteButton</code> was toggled, the list of articles was not recomposed.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">StableListScreen</span><span class="hljs-params">(viewModel: <span class="hljs-type">ListViewModel</span> = viewModel()</span></span>) {
    <span class="hljs-keyword">var</span> favorite <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-literal">false</span>) }
    Column(
        modifier = Modifier.padding(<span class="hljs-number">16</span>.dp)
    ) {
        FavoriteButton(isFavorite = favorite, onToggle = { favorite = !favorite })
        Spacer(modifier = Modifier.height(<span class="hljs-number">16</span>.dp))
        StableList(viewModel.articles)
    }
}

<span class="hljs-meta">@Composable</span>
<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">StableList</span><span class="hljs-params">(
    articles: <span class="hljs-type">PersistentList</span>&lt;<span class="hljs-type">Article</span>&gt;, <span class="hljs-comment">// PersistentList = Stable, Article = Stable</span>
    modifier: <span class="hljs-type">Modifier</span> = Modifier <span class="hljs-comment">// Stable</span>
)</span></span> {
    LazyColumn(modifier = modifier) {
        items(articles) { article -&gt;
            Text(text = article.name)
        }
    }
}
</code></pre>
<p><strong>Solution 2:</strong> Though Solution 1 is a much more straightforward approach, one thing to consider here is the library is still in Alpha. So if we are skeptical about using this library in production code, we can use another approach: Using an Immutable wrapper around a standard List.</p>
<p>We should also follow this approach if the type of class used in the List cannot be made stable. Thus even if the <code>List</code> or <code>Article</code> is <strong>unstable</strong>, <code>ArticleList</code> will be <strong>stable</strong> because we are using the <code>@Immutable</code> annotation.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Immutable</span>
<span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ArticleList</span></span>(
    <span class="hljs-keyword">val</span> articles: List&lt;Article&gt; <span class="hljs-comment">// List = Unstable or Article = Unstable</span>
)
</code></pre>
<p>Then instead of using the <code>List</code> directly in the above code, we can use this <code>ArticleList</code>.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">StableList</span><span class="hljs-params">(
    articles: <span class="hljs-type">ArticleList</span>, <span class="hljs-comment">// ArticleList = Stable</span>
    modifier: <span class="hljs-type">Modifier</span> = Modifier <span class="hljs-comment">// Stable</span>
)</span></span> {
    LazyColumn(modifier = modifier) {
        items(articles) { article -&gt;
            Text(text = article.name)
        }
    }
}
</code></pre>
<h3 id="heading-2-remember-the-code-inside-the-clickable-modifier"><strong>2. Remember the code inside the clickable() Modifier</strong></h3>
<p>If we use the <code>clickable()</code> modifier on a composable, the lambda <code>onClick</code> of each item is reallocated every time the parent recomposes. This is because the object of the lambda is not auto-remembered.</p>
<p><strong>Consider this example to understand:</strong> Here on adding each item to the <code>LazyColumn</code> (on the press of the <code>Button</code>), the entire list including the previous items is recomposed. This is because we have the <code>clickable()</code> modifier on each item of the list. So the lambda <code>onClick</code> of each item is reallocated every time the <code>LazyColumn</code> recomposes. This is because the object of the lambda is <strong>not remembered</strong>. This might cause serious performance issues as the remaining items on the list have no reason to recompose unnecessarily.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ListWithNonRememberedClickableItems</span><span class="hljs-params">(viewModel: <span class="hljs-type">ListViewModel</span> = viewModel()</span></span>) {
    <span class="hljs-keyword">val</span> dynamicList <span class="hljs-keyword">by</span> viewModel.dynamicArticles.collectAsState()
    Column(
        modifier = Modifier.padding(<span class="hljs-number">16</span>.dp)
    ) {
        Button(
            onClick = { viewModel.addArticleToDynamicList() }
        ) {
            Text(text = <span class="hljs-string">"Add item"</span>)
        }
        Spacer(modifier = Modifier.height(<span class="hljs-number">32</span>.dp))
        ListWithNonRememberedClickableItems(dynamicList)
    }
}

<span class="hljs-meta">@Composable</span>
<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ListWithNonRememberedClickableItems</span><span class="hljs-params">(
    articles: <span class="hljs-type">List</span>&lt;<span class="hljs-type">Article</span>&gt;,
    modifier: <span class="hljs-type">Modifier</span> = Modifier
)</span></span> {
    <span class="hljs-keyword">val</span> context = LocalContext.current
    LazyColumn(modifier = modifier) {
        items(articles) { article -&gt;
            Text(
                modifier = Modifier.clickable { <span class="hljs-comment">// Not remembered</span>
                    Toast.makeText(context, <span class="hljs-string">"Clicked item: <span class="hljs-subst">${article.id}</span>"</span>, Toast.LENGTH_SHORT).show()
                },
                text = article.name
            )
        }
    }
}
</code></pre>
<p><strong>Overcome:</strong> To overcome this issue, we can wrap the <code>clickable()</code> Modifier inside a <code>remember</code> block. Thus the lambda object will be <strong>remembered</strong> and the won’t be reallocated every time the parent recomposes.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ListWithRememberedClickableItems</span><span class="hljs-params">(
    articles: <span class="hljs-type">List</span>&lt;<span class="hljs-type">Article</span>&gt;,
    modifier: <span class="hljs-type">Modifier</span> = Modifier
)</span></span> {
    <span class="hljs-keyword">val</span> context = LocalContext.current
    LazyColumn(modifier = modifier) {
        items(articles) { article -&gt;
            Text(
                modifier = Modifier.then(
                    remember {
                        Modifier.clickable { <span class="hljs-comment">// Remembered</span>
                            Toast.makeText(context, <span class="hljs-string">"Clicked item: <span class="hljs-subst">${article.id}</span>"</span>, Toast.LENGTH_SHORT).show()
                        }
                    }
                ),
                text = article.name
            )
        }
    }
}
</code></pre>
<p><strong>Consider another simple example:</strong> Here typing anything in the <code>TextField</code> will recompose the parent composable function. But the <code>Text</code> composable “Toggle me” has nothing to do with it. Still, it will recompose, as it is using the <code>clickable()</code> Modifier, which is <strong>not</strong> <strong>remembered</strong>.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ChildWithNonRememberedClickableModifier</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">var</span> text <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-string">""</span>) }
    <span class="hljs-keyword">var</span> isClicked <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-literal">false</span>) }
    Column(modifier = Modifier.padding(<span class="hljs-number">24</span>.dp)) {
        Text(
            modifier = Modifier,
            text = <span class="hljs-string">"Toggle state: <span class="hljs-variable">$isClicked</span>"</span>
        )
        Spacer(modifier = Modifier.height(<span class="hljs-number">8</span>.dp))
        Text(
            modifier = Modifier
                .padding(<span class="hljs-number">8</span>.dp)
                .clickable { isClicked = !isClicked },
            text = <span class="hljs-string">"Toggle me"</span>
        )
        Spacer(modifier = Modifier.height(<span class="hljs-number">16</span>.dp))
        TextField(value = text, onValueChange = { text = it })
    }
}
</code></pre>
<p><strong>Overcome:</strong> Similarly, to fix this issue, we can wrap the <code>clickable()</code> Modifier inside a <code>remember</code> block. Thus the lambda object will be <strong>remembered</strong> and the won’t be reallocated every time the parent recomposes.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ChildWithRememberedClickableModifier</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">var</span> text <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-string">""</span>) }
    <span class="hljs-keyword">var</span> isClicked <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-literal">false</span>) }
    Column(modifier = Modifier.padding(<span class="hljs-number">24</span>.dp)) {
        Text(
            modifier = Modifier,
            text = <span class="hljs-string">"Toggle state: <span class="hljs-variable">$isClicked</span>"</span>
        )
        Spacer(modifier = Modifier.height(<span class="hljs-number">8</span>.dp))
        Text(
            modifier = Modifier
                .padding(<span class="hljs-number">8</span>.dp)
                .then(
                    remember {
                        Modifier.clickable { isClicked = !isClicked }
                    }
                ),
            text = <span class="hljs-string">"Toggle me"</span>
        )
        Spacer(modifier = Modifier.height(<span class="hljs-number">16</span>.dp))
        TextField(value = text, onValueChange = { text = it })
    }
}
</code></pre>
<h3 id="heading-3-remember-lamdas-with-calls-on-an-unstable-object"><strong>3. Remember lamdas with calls on an unstable object</strong></h3>
<p>This is very well explained by <a target="_blank" href="https://medium.com/@bentrengrove">Ben Trengrove</a> in the “Unstable lambdas” section of the blog <a target="_blank" href="https://medium.com/androiddevelopers/jetpack-compose-strong-skipping-mode-explained-cbdb2aa4b900">Strong Skipping Mode Explained</a>. In short, as of now, the compose compiler does not auto-remember lambdas with unstable captures. And as discussed before if lambdas are <strong>not remembered</strong>, they will be reallocated every time the parent recomposes.</p>
<p><strong>Consider this example to understand</strong>: Here as <code>ListViewModel</code> is unstable, the lambda <code>onValueChnage</code> containing the call <code>viewModel.numberChanged(it)</code> is <strong>not auto-remembered</strong> by the compiler. Thus when the parent recomposes due to the change in the <code>TextField</code> input, the <code>NumberComposable</code> also recomposes, even when it has nothing to do with the change of TextField.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">CallOnUnstableObjectWithNonRememberedLambda</span><span class="hljs-params">(viewModel: <span class="hljs-type">ListViewModel</span> = viewModel()</span></span>) { <span class="hljs-comment">// ListViewModel is unstable</span>
    <span class="hljs-keyword">val</span> number <span class="hljs-keyword">by</span> viewModel.number.collectAsState()
    <span class="hljs-keyword">var</span> text <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-string">""</span>) }
    Column(modifier = Modifier.padding(<span class="hljs-number">16</span>.dp)) {
        NumberComposable(
            current = number,
            onValueChange = { viewModel.numberChanged(it) }
        )
        Spacer(modifier = Modifier.height(<span class="hljs-number">16</span>.dp))
        TextField(value = text, onValueChange = { text = it })
    }
}
</code></pre>
<p><strong>Overcome:</strong> To fix this problem, the <code>onValueChange</code> lambda content should be wrapped in a <code>remember</code> block.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">CallOnUnstableObjectWithRememberedLambda</span><span class="hljs-params">(viewModel: <span class="hljs-type">ListViewModel</span> = viewModel()</span></span>) { <span class="hljs-comment">// ListViewModel is unstable</span>
    <span class="hljs-keyword">val</span> number <span class="hljs-keyword">by</span> viewModel.number.collectAsState()
    <span class="hljs-keyword">var</span> text <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-string">""</span>) }
    Column(modifier = Modifier.padding(<span class="hljs-number">16</span>.dp)) {
        NumberComposable(
            current = number,
            onValueChange = remember { { viewModel.numberChanged(it) } }
        )
        Spacer(modifier = Modifier.height(<span class="hljs-number">16</span>.dp))
        TextField(value = text, onValueChange = { text = it })
    }
}
</code></pre>
<h3 id="heading-4-avoid-using-background-modifier-while-animating-color"><strong>4. Avoid using <em>background() Modifier while animating color</em></strong></h3>
<p>Let’s think about the 3 phases of rendering we discussed at the start of this blog: <strong>Composition</strong>, <strong>Layout</strong>, and <strong>Draw</strong>. The <code>background()</code> modifier takes a color argument which is quite useful at times. But it’s also worth highlighting that this modifier causes all the 3 phases of rendering to run, even though it could have skipped the Composition and Layout phase (as the content or position of the Box is not changing, just the color is changing). This means if we are changing the argument <code>color</code> very frequently then heavy computation is done because the composable might recompose that frequently.</p>
<p><strong>Consider this example to understand</strong>: Here the <code>background()</code> modifier is used on a <code>Box</code> composable, and the <code>color</code> is changed very frequently by using animation. As the color changes every 1 second, the <code>Box</code> recomposes every 1 second.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">CompositionInEveryPhase</span><span class="hljs-params">()</span></span> {
    Box(
        modifier = Modifier.fillMaxSize().padding(<span class="hljs-number">16</span>.dp)
    ) {
        <span class="hljs-keyword">var</span> isNeedColorChange <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-literal">false</span>) }
        <span class="hljs-keyword">val</span> startColor = Color.Blue
        <span class="hljs-keyword">val</span> endColor = Color.Green
        <span class="hljs-keyword">val</span> backgroundColor <span class="hljs-keyword">by</span> animateColorAsState(
            <span class="hljs-keyword">if</span> (isNeedColorChange) endColor <span class="hljs-keyword">else</span> startColor,
            animationSpec = tween(durationMillis = <span class="hljs-number">800</span>, delayMillis = <span class="hljs-number">100</span>, easing = LinearEasing),
            label = <span class="hljs-string">"Animate background color"</span>
        )
        LaunchedEffect(<span class="hljs-built_in">Unit</span>) {
            <span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
                delay(<span class="hljs-number">1000</span>)
                isNeedColorChange = !isNeedColorChange
            }
        }
        Box(
            modifier = Modifier
                .size(<span class="hljs-number">300</span>.dp)
                .align(Alignment.Center)
                .background(color = backgroundColor)
        )
    }
}
</code></pre>
<p><strong>Overcome:</strong> To fix this issue, we can use the <code>drawBehind{}</code> lambda modifier and <code>drawRect()</code> function to draw the background color, which will only run the <strong>Draw</strong> phase, just skipping the first 2 phases.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">CompositionOnlyInDrawPhase</span><span class="hljs-params">()</span></span> {
        <span class="hljs-comment">// Remaining code</span>
        Box(
            modifier = Modifier
                .size(<span class="hljs-number">300</span>.dp)
                .align(Alignment.Center)
                .drawBehind {
                    drawRect(color = backgroundColor)
                }
        )
    }
}
</code></pre>
<h3 id="heading-5-avoid-using-transform-modifiers-directly-while-animating-value"><strong>5. Avoid using transform modifiers directly while animating value</strong></h3>
<p>We often need to perform transform animations in Compose (rotation, scale, position). While using direct transform modifiers like <code>Modifier.rotate()</code> if we change the value of the argument: <code>degrees</code>, then the entire composable will recompose. This means if we are changing the argument <code>degrees</code> very frequently then heavy computation is done because the composable might recompose that frequently. This recomposition is unnecessary since the appearance of the composable is not changing, we are just rotating it.</p>
<p><strong>Consider this example to understand:</strong> Here we are using the rotate modifier directly on a <code>Box</code> composable, and changing the rotation <code>degrees</code> using an infinite animation. As a result, the <code>Box</code> recomposes whenever the value of the degree animates.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">TransformUsingRotateModifier</span><span class="hljs-params">()</span></span> {
    Box(modifier = Modifier.fillMaxSize()) {
        <span class="hljs-keyword">val</span> transition = rememberInfiniteTransition(label = <span class="hljs-string">"Infinite transition"</span>)
        <span class="hljs-keyword">val</span> rotationDegree <span class="hljs-keyword">by</span> transition.animateFloat(
            initialValue = <span class="hljs-number">0f</span>,
            targetValue = <span class="hljs-number">1f</span>,
            animationSpec = infiniteRepeatable(animation = tween(<span class="hljs-number">3000</span>)),
            label = <span class="hljs-string">"Infinite animation"</span>
        )
        Box(
            modifier = Modifier
                .align(Alignment.Center)
                .rotate(rotationDegree * <span class="hljs-number">360f</span>)
                .size(<span class="hljs-number">100</span>.dp)
                .background(Color.Gray)
        )
    }
}
</code></pre>
<p><strong>Overcome:</strong> To fix this performance caveat, we should use the <code>graphicsLayer{}</code> lambda modifier whenever possible. This Modifier only affects the draw phase, hence skipping the Composition and Layout phases, thus skipping recompositions. We should use it whenever possible while dealing with <strong>clipping</strong>, <strong>transform</strong>, <strong>rotation</strong>, or <strong>alpha</strong> changes.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">TransformUsingGraphicsLayerModifier</span><span class="hljs-params">()</span></span> {
        <span class="hljs-comment">// Remaining code</span>
        Box(
            modifier = Modifier
                .align(Alignment.Center)
                .graphicsLayer {
                    rotationZ = rotationRatio * <span class="hljs-number">360f</span>
                }
                .size(<span class="hljs-number">100</span>.dp)
                .background(Color.Gray)
        )
    }
}
</code></pre>
<hr />
<p>That’s all for this article!</p>
<p>All the examples used in this blog are available in this repo:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/pushpalroy/ComposePerformancePlayground">https://github.com/pushpalroy/ComposePerformancePlayground</a></div>
<p> </p>
<p>Use the “Layout Inspector” tool of Android Studio to run the examples and see the recompositions count, which will help a lot to understand.</p>
<blockquote>
<p><em>I hope this writing will help many developers avoid these common pitfalls and write Jetpack Compose code with best practices in mind.</em></p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Understanding Window Insets in Jetpack Compose]]></title><description><![CDATA[What are Insets?
Insets refer to the areas on a screen that are not fully usable for our app’s UI due to system UI elements like the status bar, navigation bar, display cutouts (often referred to as the notch or pinhole), and the IME keyboard.
By def...]]></description><link>https://blog.pushpalroy.com/understanding-window-insets-in-jetpack-compose</link><guid isPermaLink="true">https://blog.pushpalroy.com/understanding-window-insets-in-jetpack-compose</guid><category><![CDATA[Window Insets]]></category><category><![CDATA[Android]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[Kotlin]]></category><dc:creator><![CDATA[Pushpal Roy]]></dc:creator><pubDate>Sat, 02 Mar 2024 18:13:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709403036867/112bd646-efda-4c48-a2a9-6b6109d399d2.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-what-are-insets"><strong>What are Insets?</strong></h2>
<p>Insets refer to the areas on a screen that are not fully usable for our app’s UI due to system UI elements like the status bar, navigation bar, display cutouts (often referred to as the notch or pinhole), and the IME keyboard.</p>
<p>By default, our app’s UI is restricted to being laid out within the system UI, like the status bar and navigation bar. This ensures that system UI elements don’t obscure the app’s content.</p>
<p><strong>Then why should we worry about Insets at all?</strong></p>
<blockquote>
<p><em>It is recommended apps opt-in to display in these areas where system UI is also being displayed (going edge-to-edge), which results in a more seamless user experience and allows our app to take full advantage of the window space available to it.</em></p>
</blockquote>
<p>As modern smartphones embrace edge-to-edge screens and diverse aspect ratios, the management of insets has escalated in importance.</p>
<blockquote>
<p>“With great power comes great responsibility”</p>
</blockquote>
<p>In essence, we’re shifting from a system-controlled insets paradigm to one where developers actively engage in enabling edge-to-edge displays or drawing behind system UI elements, thus taking control of insets management!</p>
<h2 id="heading-how-to-get-started"><strong>How to get started?</strong></h2>
<h3 id="heading-initial-setup"><strong>Initial Setup</strong></h3>
<p>We have to get our app full control of the area where it will draw the content. Without this setup, our app may draw black or solid colors behind the system UI, or not animate synchronously with the software keyboard.</p>
<ul>
<li>Call the <a target="_blank" href="https://developer.android.com/reference/androidx/activity/ComponentActivity#(androidx.activity.ComponentActivity).enableEdgeToEdge(androidx.activity.SystemBarStyle,androidx.activity.SystemBarStyle)">enableEdgeToEdge</a> function in Activity <code>onCreate</code>.</li>
</ul>
<p><em>This call requests that our app display behind the system UI. The app will then be in control of how those insets are used to adjust the UI.</em></p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCreate</span><span class="hljs-params">(savedInstanceState: <span class="hljs-type">Bundle</span>?)</span></span> {
    <span class="hljs-keyword">super</span>.onCreate(savedInstanceState)

    enableEdgeToEdge()

    setContent {
        <span class="hljs-comment">// App content here</span>
    }
}
</code></pre>
<ul>
<li>Set <code>android:windowSoftInputMode="adjustResize"</code> in our Activity <code>AndroidManifest.xml</code>.</li>
</ul>
<p><em>This setting allows our app to receive the size of the software IME as insets, which we can use to pad and lay out content appropriately when the IME appears and disappears in our app.</em></p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">activity</span>
  <span class="hljs-attr">android:name</span>=<span class="hljs-string">".ui.MainActivity"</span>
  <span class="hljs-attr">android:label</span>=<span class="hljs-string">"@string/app_name"</span>
  <span class="hljs-attr">android:windowSoftInputMode</span>=<span class="hljs-string">"adjustResize"</span>
  <span class="hljs-attr">android:theme</span>=<span class="hljs-string">"@style/Theme.MyApplication"</span>
  <span class="hljs-attr">android:exported</span>=<span class="hljs-string">"true"</span>&gt;</span>
</code></pre>
<p>Let’s have a look at how our app looks like at this point with the below code:</p>
<pre><code class="lang-kotlin">setContent {
    Box(
        modifier = Modifier
             .fillMaxSize()
             .background(color = Color.DarkGray)
       )
}
</code></pre>
<p><img src="https://miro.medium.com/v2/resize:fit:309/1*iu8QqvcjL-QrpR3_T4ACsA.png" alt /></p>
<p>We can see that the colored <code>Box</code> has filled the entire screen and drawing even behind the system bars (top status bar and bottom navigation bar). This means, that now our code is capable of drawing behind the system UI and we can control these areas ourselves!</p>
<h3 id="heading-controlling-insets"><strong>Controlling Insets</strong></h3>
<p>Once our Activity is displaying behind the system UI and has taken control of handling all the Insets manually, we can use the Compose APIs to ensure that our app’s interactable content does not overlap with the system UI.</p>
<p><em>These APIs also synchronize our app’s layout with inset changes.</em></p>
<p>There are two primary ways to use the Inset types to adjust our Composable layouts: <strong>Padding Modifiers</strong> and <strong>Inset Size Modifiers</strong>.</p>
<p><strong>Padding Modifiers</strong></p>
<p>We can use <a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/package-summary#(androidx.compose.ui.Modifier).windowInsetsPadding(androidx.compose.foundation.layout.WindowInsets)"><code>Modifier.windowInsetsPadding(windowInsets: WindowInsets)</code></a> to apply Window insets as padding. It works very similar to <code>Modifier.padding</code>.</p>
<p>There are some built-in Window insets like <code>WindowInsets.systemBars</code>, <code>WindowInsets.statusBars</code>, <code>WindowInsets.navigationBars</code>, etc. which we can use to provide the desired padding.</p>
<p>For example in the previous code, if we want to leave out the status bar and navigation bar and then draw the <code>Grey</code> box we can do:</p>
<pre><code class="lang-kotlin">setContent {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.LightGray)
            .windowInsetsPadding(WindowInsets.systemBars)
    ) {
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(color = Color.DarkGray)
        )
    }
}
</code></pre>
<p>Modifier <code>windowInsetsPadding(WindowInsets.systemBars)</code> adds padding for the top status bar and bottom navigation bar, which are colored with <code>LightGray</code> for understanding. This will make our app look like this:</p>
<p><img src="https://miro.medium.com/v2/resize:fit:309/1*sFYFKwRaxCW-H49Y6VVf_A.png" alt /></p>
<p>We can also use <code>windowInsetsPadding(WindowInsets.statusBars)</code> or <code>windowInsetsPadding(WindowInsets.navigationBars)</code> to control these Insets separately.</p>
<p>There are also many built-in methods for the most common types of Insets. For example:</p>
<ol>
<li><p><code>safeDrawingPadding()</code>, equivalent to <code>windowInsetsPadding(WindowInsets.safeDrawing)</code></p>
</li>
<li><p><code>safeContentPadding()</code>, equivalent to <code>windowInsetsPadding(WindowInsets.safeContent)</code></p>
</li>
<li><p><code>safeGesturesPadding()</code>, equivalent to <code>windowInsetsPadding(WindowInsets.safeGestures)</code></p>
</li>
</ol>
<p><strong>Inset size modifiers</strong></p>
<p>These modifiers help in setting the size of a component to the exact size of the insets. These are useful for sizing a <code>Spacer</code> that takes up an Inset size while creating a screen.</p>
<p>For example, we can write the last code like this using Inset size modifiers to provide padding for the status bar and navigation bar. This will produce the same result:</p>
<pre><code class="lang-kotlin">setContent {
    Column {
        Spacer(
            modifier = Modifier
                .fillMaxWidth()
                .background(color = Color.LightGray)
                .windowInsetsTopHeight(WindowInsets.statusBars)
        )
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(color = Color.DarkGray)
                .weight(<span class="hljs-number">1f</span>)
        )
        Spacer(
            modifier = Modifier
                .fillMaxWidth()
                .background(color = Color.LightGray)
                .windowInsetsBottomHeight(WindowInsets.navigationBars)
        )
    }
}
</code></pre>
<p>Here instead of adding Insets padding to the <code>DarkGrey</code> <code>Box</code> like the last time, we added <code>LightGray</code> <code>Spacers</code> which take up the exact sizes of the status bar and navigation bar.</p>
<h3 id="heading-resize-component-padding-with-keyboard-ime"><strong>Resize component padding with Keyboard IME</strong></h3>
<p>There are times when we would want to apply dynamic padding to our UI component based on whether the keyboard IME is open or closed. A good use case is when we add an input field at the bottom of a list.</p>
<p>Consider this code:</p>
<pre><code class="lang-kotlin">setContent {
    Column(
        modifier = Modifier.fillMaxSize().systemBarsPadding()
    ) {
        LazyColumn(
            modifier = Modifier.weight(<span class="hljs-number">1f</span>),
            reverseLayout = <span class="hljs-literal">true</span>
        ) {
            items(<span class="hljs-number">100</span>) { index -&gt;
                Text(text = <span class="hljs-string">"Item <span class="hljs-variable">$index</span>"</span>, modifier = Modifier.padding(<span class="hljs-number">16</span>.dp).fillMaxWidth())
            }
        }

        <span class="hljs-keyword">var</span> textFieldValue <span class="hljs-keyword">by</span> remember { mutableStateOf(TextFieldValue()) }

        TextField(
            modifier = Modifier.fillMaxWidth(),
            value = textFieldValue,
            onValueChange = { textFieldValue = it },
            placeholder = {
                Text(text = <span class="hljs-string">"Type something here"</span>)
            }
        )
    }
}
</code></pre>
<p><strong>If we do not handle the padding for the</strong> <code>TextField</code><strong>:</strong></p>
<p><img src="https://miro.medium.com/v2/resize:fit:300/1*8HIx9O4S3Lk3mpUJRqI21g.gif" alt /></p>
<p>Here we can see that when the keyboard opens, the <code>TextField</code> stays at the bottom of the screen, which does not provide a very good user experience.</p>
<p><strong>After handling the</strong> <code>TextField</code> <strong>padding:</strong></p>
<p>We will just add the <code>imePadding()</code> modifier to the <code>TextField</code> above.</p>
<p>The code will now look like:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// Same code as above</span>

TextField(
    modifier = Modifier.fillMaxWidth().imePadding(), <span class="hljs-comment">// IME padding added</span>
    value = textFieldValue,
    onValueChange = { textFieldValue = it },
    placeholder = {
        Text(text = <span class="hljs-string">"Type something here"</span>)
    }
)
</code></pre>
<p>Now, the <code>TextField</code> padding will change and also animate with the changing state of the IME and thus will produce an effect that the input field is moving up along with the keyboard:</p>
<p><img src="https://miro.medium.com/v2/resize:fit:300/1*IQIdBM5ovhwMcHmJ0qCOZQ.gif" alt /></p>
<h3 id="heading-animate-keyboard-ime-while-scrolling"><strong>Animate keyboard IME while scrolling</strong></h3>
<p>There is another experimental API modifier <code>imeNestedScroll()</code> which when added to a scrolling container, animates and opens the keyboard when scrolling to the bottom of the container.</p>
<p>If we modify the above code, by adding this modifier to the <code>LazyColumn</code>:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// Same code as above</span>

LazyColumn(
    modifier = Modifier.weight(<span class="hljs-number">1f</span>).imeNestedScroll(), <span class="hljs-comment">// Modifier added</span>
    reverseLayout = <span class="hljs-literal">true</span>
) {

<span class="hljs-comment">// Same code as previous</span>
</code></pre>
<p>Will produce this experience:</p>
<p><img src="https://miro.medium.com/v2/resize:fit:300/1*dciSCS0k29yCh1Z4lAM85A.gif" alt /></p>
<h2 id="heading-insets-consumption"><strong>Insets Consumption</strong></h2>
<p>Now, a few questions might pop into our minds at this point.</p>
<p>Considering any inset padding modifier (say <code>safeDrawingPadding()</code>), do we need to apply it only once in the Composable hierarchy? What will happen if we apply it more than once? If we apply it to a parent and then again to a child below, will the padding be added twice?</p>
<blockquote>
<p>Well, here comes the concept of Insets consumption.</p>
</blockquote>
<p>The in-built inset padding modifiers automatically “consume” the portion of the insets that are applied as padding. While doing deeper down the composition tree, the nested inset padding modifiers and the inset size modifiers, which are applied to the child composables know that some portion of the insets has already been consumed (or applied or considered) by the outer modifiers, and hence skip applying those insets again, thus avoiding duplication of space.</p>
<p><strong>Let’s see an example to understand this:</strong></p>
<pre><code class="lang-kotlin">setContent {
    <span class="hljs-keyword">var</span> textFieldValue <span class="hljs-keyword">by</span> remember { mutableStateOf(TextFieldValue()) }
    LazyColumn(
        Modifier.windowInsetsPadding(WindowInsets.statusBars).imePadding()
    ) {
        items(count = <span class="hljs-number">30</span>) {
            Text(
                modifier = Modifier.fillMaxWidth().padding(<span class="hljs-number">16</span>.dp),
                text = <span class="hljs-string">"Item <span class="hljs-variable">$it</span>"</span>
            )
        }
        item {
            TextField(
                modifier = Modifier.fillMaxWidth().height(<span class="hljs-number">56</span>.dp),
                value = textFieldValue,
                onValueChange = { textFieldValue = it },
                placeholder = { Text(text = <span class="hljs-string">"Type something here"</span>) }
            )
        }
        item {
            Spacer(
                Modifier.windowInsetsBottomHeight(
                    WindowInsets.systemBars
                )
            )
        }
    }
}
</code></pre>
<p>This code displays a long list of items in a <code>LazyColumn</code>. At the bottom of the list, there is a <code>TextField</code> for user input, and at the end there is a <code>Spacer</code> which provides space for the bottom system navigation bar using a window insets size modifier. Also, an <code>imePadding</code> has been applied to the <code>LazyColumn</code>.</p>
<p>Here, when the keyboard is closed, the IME has no height, hence the <code>imePadding()</code> modifier applies no padding. Hence no insets are being consumed and the height of the <code>Spacer</code> at this point is the size of the bottom system bar. When the keyboard opens, the IME insets animate to match the size of the IME, and the <code>imePadding()</code> modifier begins to apply bottom padding to the <code>LazyColumn</code>. As a result, it also starts “consuming” that amount of insets.</p>
<p>Now at this point, an amount of the spacing for the bottom system bar has been already applied by the <code>imePadding()</code> modifier, therefore the height of the <code>Spacer</code> starts to decrease. At some point in time, when the IME padding size exceeds the size of the bottom system bar, the height of the <code>Spacer</code> becomes zero. When the keyboard closes the same mechanism happens in reverse.</p>
<p>This behavior is accomplished through communication between all <code>windowInsetsPadding</code> modifiers.</p>
<p>This is how the above code looks like in action:</p>
<p><img src="https://miro.medium.com/v2/resize:fit:300/1*8ZLXFWRuKcZXvPtRmjxPJQ.gif" alt /></p>
<p><strong>Let’s see another example:</strong></p>
<p>In this example, we would explore the modifier: <code>Modifier.consumedWindowInsets(insets: WindowInsets)</code>.</p>
<p>This modifier is used to consume insets in the same way as the <code>Modifier.windowInsetsPadding</code>, but it doesn't apply the consumed insets as padding.</p>
<p>Another modifier is the <code>Modifier.consumedWindowInsets(paddingValues: PaddingValues)</code>, which takes an arbitrary <code>PaddingValues</code> to consume.</p>
<p>This is useful for informing children when padding or spacing is provided by some other mechanism than the inset padding modifiers, such as an ordinary <code>Modifier.padding</code> or fixed-height spacers.</p>
<p>Consider this code:</p>
<pre><code class="lang-kotlin">setContent {
    Scaffold { innerPadding -&gt;
        <span class="hljs-comment">// innerPadding contains inset information to use and apply</span>
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(color = Color.LightGray)
                .padding(innerPadding)
        ) {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .background(color = Color.Red)
                    .windowInsetsPadding(WindowInsets.safeDrawing)
            ) {
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .background(color = Color.DarkGray)
                )
            }
        }
    }
}
</code></pre>
<p>This code will look like this:</p>
<p><img src="https://miro.medium.com/v2/resize:fit:309/1*NDhgll2l1nap6SrQztkcEw.png" alt /></p>
<p>We can see that there is some problem with this result. Though the <code>innerPadding</code> from the <code>Scaffold</code> lambda is applied, as a <code>padding</code> to the outer <code>Box</code>, the <code>windowInsetsPadding(WindowInsets.safeDrawing)</code> of the inner <code>Box</code> produces a duplicate padding (visible in Red color). That means, for some reason, Inset consumption did not happen here.</p>
<p>This is because, by default, a<code>Scaffold</code> provides insets as parameters <code>paddingValues</code> for us to consume and use. <code>Scaffold</code> does not apply the insets to content; this responsibility is ours.</p>
<p>Thus if we want to avoid this duplicate padding, we need to consume the padding ourselves using the <code>consumeWindowInsets(innerPadding)</code> modifier.</p>
<p>Considering this as the updated code:</p>
<pre><code class="lang-kotlin">setContent {
    Scaffold { innerPadding -&gt;
        <span class="hljs-comment">// innerPadding contains inset information to use and apply</span>
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(color = Color.LightGray)
                .padding(innerPadding)
                <span class="hljs-comment">// Consume this insets so that it's not applied again when using safeDrawing in the hierarchy below</span>
                .consumeWindowInsets(innerPadding)
        ) {
              <span class="hljs-comment">// Remaining code</span>
          }
        }
    }
}
</code></pre>
<p>will produce this:</p>
<p><img src="https://miro.medium.com/v2/resize:fit:309/1*sFYFKwRaxCW-H49Y6VVf_A.png" alt /></p>
<p>Thus, once the <code>innerPadding</code> is consumed by the outer <code>Box</code>, the <code>windowInsetsPadding(WindowInsets.safeDrawing)</code> of the inner <code>Box</code> does not apply any duplicate padding.</p>
<p>That’s it for this article!</p>
<p><em>I hope this writing will help many developers understand why we need Insets and how to use them.</em></p>
<p>You can get all the examples at this repo: <a target="_blank" href="https://github.com/pushpalroy/ComposeInsetsPlayground">https://github.com/pushpalroy/ComposeInsetsPlayground</a></p>
]]></content:encoded></item></channel></rss>