Repository: airbnb/lottie-android
Issue: Compose: LottieAnimation recomposes on every frame degrading performance

Issue Repro Compose Fork : [https://github.com/MSDarwish2000/lottie-android-performance](Fork)

**Describe the bug**
`LottieAnimation` recomposes itself on every frame. This is completely unnecessary in most cases as the only actual change happens during the draw phase, not composition or layout phases. This happens as a result of passing `progress` to `LottieAnimation`. Even when using the function without progress, this happens internally reducing the scope of recomposition but not preventing it.

**Steps To Reproduce**
Steps to reproduce the behavior:

1. This can be reproduced with any Lottie Compose sample using Layout Inspector in Android Studio Dolphin
2. This can be benchmarked using the code in the issue above or any similar code

**Sample benchmark result**

- _While running minified release variant_
 Drawing time: 1174.3ms - Recomposition time: 979.4ms - percentage: 45.48%
- _While running debug variant_
 Drawing time: 1369.3ms - Recomposition time: 1219ms - percentage: 47.1%
- _While debugging_
 Drawing time: 2398.3ms - Recomposition time: 1837.2ms - percentage: 43.38%

_Note:_ The percentage of recomposition time may increase if the parent composable had more code which is very likely 

Changed files:
sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/TransitionsExamplesPage.kt
sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/AnimatableExamplesPage.kt
issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt
sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ViewPagerExample.kt
sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/PlayerPage.kt
lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt
sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/BasicUsageExamplesPage.kt

diff --git a/issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt b/issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt
index 81acbedf..aeecf2fb 100755
--- a/issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt
+++ b/issue-repro-compose/src/main/java/com/airbnb/lottie/issues/compose/ComposeIssueReproActivity.kt
@@ -22,6 +22,6 @@ class ComposeIssueReproActivity : AppCompatActivity() {
     fun Content() {
         val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
         val progress by animateLottieCompositionAsState(composition)
-        LottieAnimation(composition, progress)
+        LottieAnimation(composition, { progress })
     }
 }
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt
index f22be003..fefea855 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt
@@ -32,12 +32,12 @@ import kotlin.math.roundToInt
  *
  * @param composition The composition that will be rendered. To generate a [LottieComposition], you can use
  *                    [rememberLottieComposition].
- * @param progress The progress (between 0 and 1) that should be rendered. If you want to render a specific
- *                 frame, you can use [LottieComposition.getFrameForProgress]. In most cases, you will want
- *                 to use one of the overloaded LottieAnimation composables that drives the animation for you.
- *                 The overloads that have isPlaying as a parameter instead of progress will drive the
- *                 animation automatically. You may want to use this version if you want to drive the animation
- *                 from your own Animatable or via events such as download progress or a gesture.
+ * @param progressProvider A provider for the progress (between 0 and 1) that should be rendered. If you want to render a
+ *                         specific frame, you can use [LottieComposition.getFrameForProgress]. In most cases, you will want
+ *                         to use one of the overloaded LottieAnimation composables that drives the animation for you.
+ *                         The overloads that have isPlaying as a parameter instead of progress will drive the
+ *                         animation automatically. You may want to use this version if you want to drive the animation
+ *                         from your own Animatable or via events such as download progress or a gesture.
  * @param outlineMasksAndMattes Enable this to debug slow animations by outlining masks and mattes.
  *                              The performance overhead of the masks and mattes will be proportional to the
  *                              surface area of all of the masks/mattes combined.
@@ -69,7 +69,7 @@ import kotlin.math.roundToInt
 @Composable
 fun LottieAnimation(
     composition: LottieComposition?,
-    @FloatRange(from = 0.0, to = 1.0) progress: Float,
+    @FloatRange(from = 0.0, to = 1.0) progressProvider: () -> Float,
     modifier: Modifier = Modifier,
     outlineMasksAndMattes: Boolean = false,
     applyOpacityToLayers: Boolean = false,
@@ -114,16 +114,52 @@ fun LottieAnimation(
             drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers
             drawable.maintainOriginalImageBounds = maintainOriginalImageBounds
             drawable.clipToCompositionBounds = clipToCompositionBounds
-            drawable.progress = progress
+            drawable.progress = progressProvider()
             drawable.setBounds(0, 0, composition.bounds.width(), composition.bounds.height())
             drawable.draw(canvas.nativeCanvas, matrix)
         }
     }
 }
 
+/**
+ * This is like [LottieAnimation] except that it takes a raw progress parameter instead of taking a progress provider.
+ *
+ * @see LottieAnimation
+ */
+@Composable
+fun LottieAnimation(
+    composition: LottieComposition?,
+    @FloatRange(from = 0.0, to = 1.0) progress: Float,
+    modifier: Modifier = Modifier,
+    outlineMasksAndMattes: Boolean = false,
+    applyOpacityToLayers: Boolean = false,
+    enableMergePaths: Boolean = false,
+    renderMode: RenderMode = RenderMode.AUTOMATIC,
+    maintainOriginalImageBounds: Boolean = false,
+    dynamicProperties: LottieDynamicProperties? = null,
+    alignment: Alignment = Alignment.Center,
+    contentScale: ContentScale = ContentScale.Fit,
+    clipToCompositionBounds: Boolean = true,
+) {
+    LottieAnimation(
+        composition,
+        { progress },
+        modifier,
+        outlineMasksAndMattes,
+        applyOpacityToLayers,
+        enableMergePaths,
+        renderMode,
+        maintainOriginalImageBounds,
+        dynamicProperties,
+        alignment,
+        contentScale,
+        clipToCompositionBounds,
+    )
+}
+
 /**
  * This is like [LottieAnimation] except that it handles driving the animation via [animateLottieCompositionAsState]
- * instead of taking a raw progress parameter.
+ * instead of taking a progress provider.
  *
  * @see LottieAnimation
  * @see animateLottieCompositionAsState
@@ -157,7 +193,7 @@ fun LottieAnimation(
     )
     LottieAnimation(
         composition,
-        progress,
+        { progress },
         modifier,
         outlineMasksAndMattes,
         applyOpacityToLayers,
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/AnimatableExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/AnimatableExamplesPage.kt
index 562b837a..f52b15dd 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/AnimatableExamplesPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/AnimatableExamplesPage.kt
@@ -64,7 +64,7 @@ private fun Example1() {
             iterations = LottieConstants.IterateForever,
         )
     }
-    LottieAnimation(anim.composition, anim.progress)
+    LottieAnimation(anim.composition, { anim.progress })
 }
 
 @Composable
@@ -84,7 +84,7 @@ private fun Example2() {
         }
     }
     Box {
-        LottieAnimation(anim.composition, anim.progress)
+        LottieAnimation(anim.composition, { anim.progress })
         Slider(
             value = sliderGestureProgress ?: anim.progress,
             onValueChange = { sliderGestureProgress = it },
@@ -110,7 +110,7 @@ private fun Example3() {
         )
     }
     Box {
-        LottieAnimation(composition, anim.progress)
+        LottieAnimation(composition, { anim.progress })
         Slider(
             value = speed,
             onValueChange = { speed = it },
@@ -144,7 +144,7 @@ private fun Example4() {
     }
     LottieAnimation(
         composition,
-        animatable.progress,
+        { animatable.progress },
         modifier = Modifier
             .clickable { nonce++ }
     )
@@ -162,7 +162,7 @@ private fun Example5() {
     }
     LottieAnimation(
         composition,
-        animatable.progress,
+        { animatable.progress },
         modifier = Modifier
             .clickable { shouldPlay = !shouldPlay }
     )
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/BasicUsageExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/BasicUsageExamplesPage.kt
index d592c18f..f82e2ba9 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/BasicUsageExamplesPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/BasicUsageExamplesPage.kt
@@ -140,7 +140,7 @@ private fun Example6() {
     )
     LottieAnimation(
         composition,
-        progress,
+        { progress },
     )
 }
 
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/TransitionsExamplesPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/TransitionsExamplesPage.kt
index e6eec4e3..0dbcd85d 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/TransitionsExamplesPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/TransitionsExamplesPage.kt
@@ -90,7 +90,7 @@ fun SingleCompositionTransition(section: TransitionSection) {
             } while (s == TransitionSection.LoopMiddle)
         }
     }
-    LottieAnimation(composition, animatable.progress)
+    LottieAnimation(composition, { animatable.progress })
 }
 
 @Composable
@@ -113,5 +113,5 @@ fun SplitCompositionTransition(section: TransitionSection) {
         )
     }
 
-    LottieAnimation(animatable.composition, animatable.progress)
+    LottieAnimation(animatable.composition, { animatable.progress })
 }
\\ No newline at end of file
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ViewPagerExample.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ViewPagerExample.kt
index 26a51469..79945bef 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ViewPagerExample.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/examples/ViewPagerExample.kt
@@ -60,7 +60,7 @@ private fun WalkthroughAnimation(pagerState: PagerState) {
     val progress by derivedStateOf { (pagerState.currentPage + pagerState.currentPageOffset) / (pagerState.pageCount - 1f) }
     LottieAnimation(
         composition,
-        progress,
+        { progress },
         modifier = Modifier
             .fillMaxSize()
     )
diff --git a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/PlayerPage.kt b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/PlayerPage.kt
index e050069d..3a640f27 100644
--- a/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/PlayerPage.kt
+++ b/sample-compose/src/main/java/com/airbnb/lottie/sample/compose/player/PlayerPage.kt
@@ -255,7 +255,7 @@ fun PlayerPageContent(
         ) {
             PlayerPageLottieAnimation(
                 composition,
-                state.animatable.progress,
+                { state.animatable.progress },
                 modifier = Modifier
                     // TODO: figure out how maxWidth can play nice with the aspectRatio modifier inside of LottieAnimation.
                     .fillMaxWidth()
@@ -291,12 +291,12 @@ fun PlayerPageContent(
 @Composable
 private fun PlayerPageLottieAnimation(
     composition: LottieComposition?,
-    progress: Float,
+    progressProvider: () -> Float,
     modifier: Modifier = Modifier,
 ) {
     LottieAnimation(
         composition,
-        progress,
+        progressProvider,
         modifier = modifier,
     )
 }