From dd1adad780184e847d602b96097d53a920600b72 Mon Sep 17 00:00:00 2001 From: DatLag Date: Tue, 30 Apr 2024 13:00:24 +0200 Subject: [PATCH] improved performance --- composeApp/build.gradle.kts | 1 + .../datlag/aniflow/common/ExtendComponent.kt | 6 +- .../initial/home/component/AiringCard.kt | 34 +++--- .../initial/home/component/MediumCard.kt | 25 ++-- .../datlag/aniflow/ui/theme/SchemeTheme.kt | 115 +++++++----------- 5 files changed, 85 insertions(+), 96 deletions(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 243af19..b4d2abe 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -108,6 +108,7 @@ kotlin { implementation(libs.kasechange) implementation(libs.oidc) + implementation(libs.kache) implementation("dev.datlag.sheets-compose-dialogs:rating:2.0.0-SNAPSHOT") implementation("dev.datlag.sheets-compose-dialogs:option:2.0.0-SNAPSHOT") diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendComponent.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendComponent.kt index fb1f445..ed8e498 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendComponent.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/common/ExtendComponent.kt @@ -1,8 +1,10 @@ package dev.datlag.aniflow.common import androidx.compose.runtime.* +import androidx.compose.ui.graphics.painter.Painter import com.arkivanov.essenty.lifecycle.Lifecycle import com.arkivanov.essenty.lifecycle.LifecycleOwner +import com.kmpalette.DominantColorState import dev.datlag.aniflow.LocalDI import dev.datlag.aniflow.ui.navigation.Component import dev.datlag.aniflow.ui.theme.SchemeTheme @@ -33,14 +35,14 @@ fun Component.onRender(content: @Composable () -> Unit) { } @Composable -fun Component.onRenderWithScheme(key: Any?, content: @Composable () -> Unit) { +fun Component.onRenderWithScheme(key: Any?, content: @Composable (DominantColorState) -> Unit) { onRender { SchemeTheme(key, content) } } @Composable -fun Component.onRenderApplyCommonScheme(key: Any?, content: @Composable () -> Unit) { +fun Component.onRenderApplyCommonScheme(key: Any?, content: @Composable (DominantColorState) -> Unit) { onRenderWithScheme(key, content) SchemeTheme.setCommon(key) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringCard.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringCard.kt index 08612ab..0f655ca 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/AiringCard.kt @@ -21,9 +21,11 @@ import dev.datlag.aniflow.anilist.model.Medium import dev.datlag.aniflow.common.preferred import dev.datlag.aniflow.settings.Settings import dev.datlag.aniflow.settings.model.AppSettings +import dev.datlag.aniflow.ui.theme.LocalDominantColorState import dev.datlag.aniflow.ui.theme.SchemeTheme import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch import org.kodein.di.instance import org.kodein.di.instanceOrNull @@ -34,6 +36,8 @@ fun AiringCard( modifier: Modifier = Modifier, onClick: (Medium) -> Unit ) { + val schemeState = LocalDominantColorState.current + airing.media?.let(::Medium)?.let { media -> Card( modifier = modifier, @@ -61,27 +65,27 @@ fun AiringCard( model = media.coverImage.medium, contentScale = ContentScale.Crop, onSuccess = { state -> - SchemeTheme.update( - key = media.id, - input = state.painter, - scope = scope - ) + if (schemeState != null) { + scope.launch { + schemeState.updateFrom(state.painter) + } + } } ), onSuccess = { state -> - SchemeTheme.update( - key = media.id, - input = state.painter, - scope = scope - ) + if (schemeState != null) { + scope.launch { + schemeState.updateFrom(state.painter) + } + } } ), onSuccess = { state -> - SchemeTheme.update( - key = media.id, - input = state.painter, - scope = scope - ) + if (schemeState != null) { + scope.launch { + schemeState.updateFrom(state.painter) + } + } } ) Column( diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/MediumCard.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/MediumCard.kt index c359a81..bee8fd4 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/MediumCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/navigation/screen/initial/home/component/MediumCard.kt @@ -31,6 +31,7 @@ import dev.datlag.aniflow.ui.theme.rememberSchemeThemeDominantColorState import dev.datlag.tooling.decompose.lifecycle.collectAsStateWithLifecycle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch @OptIn(ExperimentalStdlibApi::class) @Composable @@ -42,7 +43,7 @@ fun MediumCard( ) { SchemeTheme( key = medium.id - ) { + ) { schemeState -> Card( modifier = modifier, onClick = { @@ -63,9 +64,7 @@ fun MediumCard( defaultColor = color ?: MaterialTheme.colorScheme.primary, defaultOnColor = contentColorFor(color ?: MaterialTheme.colorScheme.primary) ) - var successPainter by remember { mutableStateOf(null) } - - SchemeTheme.update(medium.id, successPainter) + val scope = rememberCoroutineScope() AsyncImage( model = medium.coverImage.extraLarge, @@ -79,18 +78,28 @@ fun MediumCard( model = medium.coverImage.medium, contentScale = ContentScale.Crop, onSuccess = { state -> - successPainter = state.painter + scope.launch { + schemeState.updateFrom(state.painter) + } }, onError = { - successPainter = color?.let(::ColorPainter) + color?.let(::ColorPainter)?.let { painter -> + scope.launch { + schemeState.updateFrom(painter) + } + } } ), onSuccess = { state -> - successPainter = state.painter + scope.launch { + schemeState.updateFrom(state.painter) + } } ), onSuccess = { state -> - successPainter = state.painter + scope.launch { + schemeState.updateFrom(state.painter) + } } ) diff --git a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt index 4b8adb7..4659626 100644 --- a/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt +++ b/composeApp/src/commonMain/kotlin/dev/datlag/aniflow/ui/theme/SchemeTheme.kt @@ -15,7 +15,11 @@ import com.kmpalette.rememberPainterDominantColorState import com.materialkolor.AnimatedDynamicMaterialTheme import com.materialkolor.DynamicMaterialTheme import com.materialkolor.ktx.isDisliked +import com.mayakapps.kache.InMemoryKache +import com.mayakapps.kache.KacheStrategy import dev.datlag.aniflow.LocalDarkMode +import dev.datlag.tooling.async.scopeCatching +import dev.datlag.tooling.async.suspendCatching import dev.datlag.tooling.compose.ioDispatcher import dev.datlag.tooling.compose.launchIO import dev.datlag.tooling.compose.withIOContext @@ -29,79 +33,40 @@ import kotlin.coroutines.CoroutineContext data object SchemeTheme { internal val commonSchemeKey = MutableStateFlow(null) - internal val colorState = MutableStateFlow>>(emptyMap()) - internal val itemScheme = MutableStateFlow>(emptyMap()) + internal val kache = InMemoryKache>( + maxSize = 25L * 1024 * 1024 + ) { + strategy = KacheStrategy.LRU + } fun setCommon(key: Any?) { commonSchemeKey.update { key } } - - @Composable - fun update(key: Any?, input: Painter?) { - if (input == null) { - return - } - - LaunchedEffect(key, input) { - suspendUpdate(key, input) - } - } - - fun update(key: Any?, input: Painter?, scope: CoroutineScope) { - scope.launchIO { - suspendUpdate(key, input) - } - } - - fun update(key: Any?, color: Color?, scope: CoroutineScope) { - scope.launchIO { - suspendUpdate(key, color) - } - } - - suspend fun suspendUpdate(key: Any?, input: Painter?): Boolean { - if (key == null || input == null) { - return false - } - - withIOContext { - val useState = (colorState.firstOrNull() ?: colorState.value)[key] - useState?.updateFrom(input) - - itemScheme.getAndUpdate { - it.toMutableMap().apply { - put(key, useState?.color) - }.toMap() - } - } - return true - } - - suspend fun suspendUpdate(key: Any?, color: Color?) = suspendUpdate(key, color?.let { ColorPainter(it) }) } @Composable fun rememberSchemeThemeDominantColor( - key: Any? + key: Any?, + state: DominantColorState? = null, ): Color? { if (key == null) { return null } - val state = SchemeTheme.colorState.value[key] ?: rememberPainterDominantColorState( + val fallbackState = remember(state) { + state + } ?: remember(key) { + SchemeTheme.kache.getIfAvailable(key) + } ?: rememberPainterDominantColorState( coroutineContext = ioDispatcher() ) - SchemeTheme.colorState.update { - it.toMutableMap().apply { - put(key, state) - }.toMap() + val useState by produceState(fallbackState, key) { + value = withIOContext { + SchemeTheme.kache.getOrPut(key) { fallbackState } + } ?: fallbackState } - val color by remember(key) { - SchemeTheme.itemScheme.map { it[key] } - }.collectAsStateWithLifecycle(SchemeTheme.itemScheme.value[key]) - - return color + return remember(useState) { useState.color } } @Composable @@ -113,25 +78,25 @@ fun rememberSchemeThemeDominantColorState( isSwatchValid: (Palette.Swatch) -> Boolean = { true }, builder: Palette.Builder.() -> Unit = {}, ): DominantColorState { - val state by remember(key) { - SchemeTheme.colorState.map { it[key] } - }.collectAsStateWithLifecycle(SchemeTheme.colorState.value[key]) - return state ?: rememberPainterDominantColorState( + val fallbackState = remember(key) { + key?.let { SchemeTheme.kache.getIfAvailable(it) } + } ?: rememberPainterDominantColorState( defaultColor = defaultColor, defaultOnColor = defaultOnColor, coroutineContext = coroutineContext, builder = builder, isSwatchValid = isSwatchValid - ).also { - if (key != null) { - SchemeTheme.colorState.update { map -> - map.toMutableMap().apply { - put(key, it) - } - } + ) + val state by produceState(fallbackState, key) { + value = withIOContext { + key?.let { + SchemeTheme.kache.getOrPut(it) { fallbackState } + } ?: fallbackState } } + + return state } @Composable @@ -166,19 +131,27 @@ fun rememberSchemeThemeDominantColorState( ) } +val LocalDominantColorState = compositionLocalOf?>{ null } + @Composable -fun SchemeTheme(key: Any?, content: @Composable () -> Unit) { +fun SchemeTheme(key: Any?, content: @Composable (DominantColorState) -> Unit) { + val state = rememberSchemeThemeDominantColorState(key) + DynamicMaterialTheme( - seedColor = rememberSchemeThemeDominantColor(key) ?: MaterialTheme.colorScheme.primary, + seedColor = rememberSchemeThemeDominantColor(key, state) ?: MaterialTheme.colorScheme.primary, useDarkTheme = LocalDarkMode.current, animate = true ) { - content() + CompositionLocalProvider( + LocalDominantColorState provides state, + ) { + content(state) + } } } @Composable -fun CommonSchemeTheme(content: @Composable () -> Unit) { +fun CommonSchemeTheme(content: @Composable (DominantColorState) -> Unit) { val key by SchemeTheme.commonSchemeKey.collectAsStateWithLifecycle() SchemeTheme(key, content)