[Compose] Parse LottieComposition synchronously instead of using LottieTask (#1888)
Using LottieTask under the hood incurred several extra thread hops including a main thread post. Switching it to a result like this is both faster and also enables custom factories to be used. From my initial tests, this cut the parse time for the heart animation in the repo roughly in half.
Unfortunately, LaunchedTask takes a few ms to start. I tried with rememberCoroutineScope().launch and the initial delay was the same so I'm not sure if there is anything else that can be done here.
Fixes #1880
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCompositionSpec.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCompositionSpec.kt
index 9444a5b..ba38a5b 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCompositionSpec.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCompositionSpec.kt
@@ -1,5 +1,7 @@
package com.airbnb.lottie.compose
+import com.airbnb.lottie.LottieComposition
+
/**
* Specification for a [com.airbnb.lottie.LottieComposition]. Each subclass represents a different source.
* A [com.airbnb.lottie.LottieComposition] is the stateless parsed version of a Lottie json file and is
@@ -41,4 +43,9 @@
* Load an animation from its json string.
*/
inline class JsonString(val jsonString: String) : LottieCompositionSpec
+
+ /**
+ * Load an animation from a custom factory. This will be called on an IO thread pool.
+ */
+ inline class Custom(val factory: () -> LottieComposition) : LottieCompositionSpec
}
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/rememberLottieComposition.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/rememberLottieComposition.kt
index 42d3e3c..f4f4187 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/rememberLottieComposition.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/rememberLottieComposition.kt
@@ -13,18 +13,15 @@
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieImageAsset
-import com.airbnb.lottie.LottieTask
+import com.airbnb.lottie.LottieResult
import com.airbnb.lottie.model.Font
import com.airbnb.lottie.utils.Logger
import com.airbnb.lottie.utils.Utils
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.io.FileInputStream
import java.io.IOException
import java.util.zip.ZipInputStream
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
/**
* Use this with [rememberLottieComposition#cacheKey]'s cacheKey parameter to generate a default
@@ -81,6 +78,7 @@
): LottieCompositionResult {
val context = LocalContext.current
val result by remember(spec) { mutableStateOf(LottieCompositionResultImpl()) }
+
LaunchedEffect(spec) {
var exception: Throwable? = null
var failedCount = 0
@@ -115,58 +113,63 @@
fontFileExtension: String,
cacheKey: String?,
): LottieComposition {
- val task = when (spec) {
- is LottieCompositionSpec.RawRes -> {
- if (cacheKey == DefaultCacheKey) {
- LottieCompositionFactory.fromRawRes(context, spec.resId)
- } else {
- LottieCompositionFactory.fromRawRes(context, spec.resId, cacheKey)
- }
- }
- is LottieCompositionSpec.Url -> {
- if (cacheKey == DefaultCacheKey) {
- LottieCompositionFactory.fromUrl(context, spec.url)
- } else {
- LottieCompositionFactory.fromUrl(context, spec.url, cacheKey)
- }
- }
- is LottieCompositionSpec.File -> {
- val fis = withContext(Dispatchers.IO) {
- @Suppress("BlockingMethodInNonBlockingContext")
- FileInputStream(spec.fileName)
- }
- when {
- spec.fileName.endsWith("zip") -> LottieCompositionFactory.fromZipStream(
- ZipInputStream(fis),
- spec.fileName.takeIf { cacheKey != null },
- )
- else -> LottieCompositionFactory.fromJsonInputStream(fis, spec.fileName.takeIf { cacheKey != null })
- }
- }
- is LottieCompositionSpec.Asset -> {
- if (cacheKey == DefaultCacheKey) {
- LottieCompositionFactory.fromAsset(context, spec.assetName)
- } else {
- LottieCompositionFactory.fromAsset(context, spec.assetName, null)
- }
- }
- is LottieCompositionSpec.JsonString -> {
- val jsonStringCacheKey = if (cacheKey == DefaultCacheKey) spec.jsonString.hashCode().toString() else cacheKey
- LottieCompositionFactory.fromJsonString(spec.jsonString, jsonStringCacheKey)
- }
- }
+ val result = parseCompositionSync(context, spec, cacheKey)
+ result.exception?.let { throw it }
- val composition = task.await()
+ val composition = result.value!!
loadImagesFromAssets(context, composition, imageAssetsFolder)
loadFontsFromAssets(context, composition, fontAssetsFolder, fontFileExtension)
return composition
}
-private suspend fun <T> LottieTask<T>.await(): T = suspendCancellableCoroutine { cont ->
- addListener { c ->
- if (!cont.isCompleted) cont.resume(c)
- }.addFailureListener { e ->
- if (!cont.isCompleted) cont.resumeWithException(e)
+private fun parseCompositionSync(
+ context: Context,
+ spec: LottieCompositionSpec,
+ cacheKey: String?,
+): LottieResult<LottieComposition> {
+ return when (spec) {
+ is LottieCompositionSpec.RawRes -> {
+ if (cacheKey == DefaultCacheKey) {
+ LottieCompositionFactory.fromRawResSync(context, spec.resId)
+ } else {
+ LottieCompositionFactory.fromRawResSync(context, spec.resId, cacheKey)
+ }
+ }
+ is LottieCompositionSpec.Url -> {
+ if (cacheKey == DefaultCacheKey) {
+ LottieCompositionFactory.fromUrlSync(context, spec.url)
+ } else {
+ LottieCompositionFactory.fromUrlSync(context, spec.url, cacheKey)
+ }
+ }
+ is LottieCompositionSpec.File -> {
+ val fis = FileInputStream(spec.fileName)
+ when {
+ spec.fileName.endsWith("zip") -> LottieCompositionFactory.fromZipStreamSync(
+ ZipInputStream(fis),
+ spec.fileName.takeIf { cacheKey != null },
+ )
+ else -> LottieCompositionFactory.fromJsonInputStreamSync(fis, spec.fileName.takeIf { cacheKey != null })
+ }
+ }
+ is LottieCompositionSpec.Asset -> {
+ if (cacheKey == DefaultCacheKey) {
+ LottieCompositionFactory.fromAssetSync(context, spec.assetName)
+ } else {
+ LottieCompositionFactory.fromAssetSync(context, spec.assetName, null)
+ }
+ }
+ is LottieCompositionSpec.JsonString -> {
+ val jsonStringCacheKey = if (cacheKey == DefaultCacheKey) spec.jsonString.hashCode().toString() else cacheKey
+ LottieCompositionFactory.fromJsonStringSync(spec.jsonString, jsonStringCacheKey)
+ }
+ is LottieCompositionSpec.Custom -> {
+ try {
+ LottieResult(spec.factory())
+ } catch (e: Throwable) {
+ LottieResult<LottieComposition>(e)
+ }
+ }
}
}