├── assets └── demo.gif ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── kotlin-ir-plugin ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar │ │ └── kotlin │ │ └── ru │ │ └── ivk1800 │ │ ├── MyFirExtensionRegistrar.kt │ │ ├── MyIrGenerationExtension.kt │ │ ├── MyComponentRegistrar.kt │ │ ├── SourceCodeInjector.kt │ │ ├── HighlightedFirExtension.kt │ │ └── HighlightInjector.kt └── build.gradle.kts ├── composeApp ├── src │ ├── jsMain │ │ ├── resources │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── recomposition-visualization.png │ │ │ ├── manifest.json │ │ │ └── index.html │ │ └── kotlin │ │ │ ├── main.kt │ │ │ └── ru │ │ │ └── ivk1800 │ │ │ └── LocationManager.kt │ ├── commonMain │ │ ├── kotlin │ │ │ └── ru │ │ │ │ └── ivk1800 │ │ │ │ ├── Highlighted.kt │ │ │ │ ├── screen │ │ │ │ ├── main │ │ │ │ │ ├── MainScreenFactory.kt │ │ │ │ │ └── MainScreen.kt │ │ │ │ └── sample │ │ │ │ │ ├── SampleScreenFactory.kt │ │ │ │ │ └── SampleScreen.kt │ │ │ │ ├── utils │ │ │ │ ├── LocationManager.kt │ │ │ │ ├── NavTypeExt.kt │ │ │ │ └── icons.kt │ │ │ │ ├── app │ │ │ │ ├── LocalApplicationDependencies.kt │ │ │ │ └── ApplicationDependencies.kt │ │ │ │ ├── navigation │ │ │ │ ├── LocalNavHostController.kt │ │ │ │ └── Destinations.kt │ │ │ │ ├── HighlightManager.kt │ │ │ │ ├── sample │ │ │ │ ├── ReadStateGoodSample.kt │ │ │ │ ├── ReadStateBadSample.kt │ │ │ │ ├── LaunchEffectKeyBadSample.kt │ │ │ │ ├── WithoutDerivedStateOfSample.kt │ │ │ │ ├── LaunchEffectKeyGoodSample.kt │ │ │ │ ├── NotLambdaModifiersGoodSample.kt │ │ │ │ ├── LambdaModifiersGoodSample.kt │ │ │ │ ├── UnstableTypeBadSample.kt │ │ │ │ ├── DerivedStateOfSample.kt │ │ │ │ ├── ImmutableTypeGoodSample.kt │ │ │ │ ├── CompositionLocalSample.kt │ │ │ │ ├── StaticCompositionLocalSample.kt │ │ │ │ ├── AvoidUnnecessaryStateReadsGood1Sample.kt │ │ │ │ ├── AvoidUnnecessaryStateReadsBadSample.kt │ │ │ │ ├── AvoidUnnecessaryStateReadsGood2Sample.kt │ │ │ │ ├── LaunchEffectWithKeySample.kt │ │ │ │ ├── LaunchEffectWithoutKeySample.kt │ │ │ │ ├── WithoutRememberUpdatedStateSample.kt │ │ │ │ ├── LazyListWithoutKeyBadSample.kt │ │ │ │ ├── LazyListWithKeyGoodSample.kt │ │ │ │ ├── WithRememberUpdatedStateSample.kt │ │ │ │ └── MovableContentOfSample.kt │ │ │ │ ├── ui │ │ │ │ ├── AppBar.kt │ │ │ │ ├── InteractiveSample.kt │ │ │ │ ├── AppContent.kt │ │ │ │ └── App.kt │ │ │ │ ├── HighlightedText.kt │ │ │ │ ├── theme │ │ │ │ ├── Color.kt │ │ │ │ └── Theme.kt │ │ │ │ └── SampleDescriptor.kt │ │ └── composeResources │ │ │ ├── values-ru │ │ │ └── strings.xml │ │ │ └── values │ │ │ └── strings.xml │ └── jvmMain │ │ └── kotlin │ │ ├── ru │ │ └── ivk1800 │ │ │ └── LocationManager.kt │ │ └── main.kt ├── dependencies │ ├── jsCompileClasspath.txt │ └── jsRuntimeClasspath.txt └── build.gradle.kts ├── .gitignore ├── detekt.yml ├── gradle.properties ├── LICENSE ├── .github └── workflows │ └── deploy.yaml ├── settings.gradle.kts ├── README.MD ├── gradlew.bat └── gradlew /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivk1800/recomposition-visualization/HEAD/assets/demo.gif -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivk1800/recomposition-visualization/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /kotlin-ir-plugin/src/main/resources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar: -------------------------------------------------------------------------------- 1 | ru.ivk1800.MyComponentRegistrar -------------------------------------------------------------------------------- /composeApp/src/jsMain/resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivk1800/recomposition-visualization/HEAD/composeApp/src/jsMain/resources/favicon.ico -------------------------------------------------------------------------------- /composeApp/src/jsMain/resources/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivk1800/recomposition-visualization/HEAD/composeApp/src/jsMain/resources/favicon-16x16.png -------------------------------------------------------------------------------- /composeApp/src/jsMain/resources/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivk1800/recomposition-visualization/HEAD/composeApp/src/jsMain/resources/favicon-32x32.png -------------------------------------------------------------------------------- /composeApp/src/jsMain/resources/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivk1800/recomposition-visualization/HEAD/composeApp/src/jsMain/resources/apple-touch-icon.png -------------------------------------------------------------------------------- /kotlin-ir-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | } 4 | 5 | dependencies { 6 | compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable") 7 | } 8 | -------------------------------------------------------------------------------- /composeApp/src/jsMain/resources/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivk1800/recomposition-visualization/HEAD/composeApp/src/jsMain/resources/android-chrome-192x192.png -------------------------------------------------------------------------------- /composeApp/src/jsMain/resources/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivk1800/recomposition-visualization/HEAD/composeApp/src/jsMain/resources/android-chrome-512x512.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/Highlighted.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800 2 | 3 | @Target(AnnotationTarget.CLASS) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class Highlighted 6 | -------------------------------------------------------------------------------- /composeApp/src/jsMain/resources/recomposition-visualization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivk1800/recomposition-visualization/HEAD/composeApp/src/jsMain/resources/recomposition-visualization.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.iml 3 | .gradle 4 | .idea 5 | .kotlin 6 | .DS_Store 7 | build 8 | */build 9 | captures 10 | .externalNativeBuild 11 | .cxx 12 | local.properties 13 | xcuserdata/ 14 | Pods/ 15 | *.jks 16 | *.gpg 17 | *yarn.lock 18 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/screen/main/MainScreenFactory.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.screen.main 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | class MainScreenFactory { 6 | @Composable 7 | fun create() { 8 | MainScreen() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/utils/LocationManager.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800 2 | 3 | import io.ktor.http.Url 4 | 5 | expect val locationManager: LocationManager 6 | 7 | interface LocationManager { 8 | val href: Url 9 | 10 | fun setSampleId(value: Int?) 11 | } 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/app/LocalApplicationDependencies.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.app 2 | 3 | import androidx.compose.runtime.staticCompositionLocalOf 4 | 5 | val LocalApplicationDependencies = staticCompositionLocalOf { 6 | error("ApplicationDependencies not provided") 7 | } 8 | -------------------------------------------------------------------------------- /composeApp/src/jvmMain/kotlin/ru/ivk1800/LocationManager.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800 2 | 3 | import io.ktor.http.Url 4 | 5 | actual val locationManager: LocationManager = object : LocationManager { 6 | override val href: Url 7 | get() = Url("http://localhost:8080/") 8 | 9 | override fun setSampleId(value: Int?) = Unit 10 | } 11 | -------------------------------------------------------------------------------- /kotlin-ir-plugin/src/main/kotlin/ru/ivk1800/MyFirExtensionRegistrar.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800 2 | 3 | import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrar 4 | 5 | class MyFirExtensionRegistrar : FirExtensionRegistrar() { 6 | override fun ExtensionRegistrarContext.configurePlugin() { 7 | +::HighlightedFirExtension 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/navigation/LocalNavHostController.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.navigation 2 | 3 | import androidx.compose.runtime.staticCompositionLocalOf 4 | import androidx.navigation.NavHostController 5 | 6 | val LocalNavHostController = staticCompositionLocalOf { 7 | error("LocalNavHostController not provided") 8 | } 9 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/app/ApplicationDependencies.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.app 2 | 3 | import ru.ivk1800.screen.main.MainScreenFactory 4 | import ru.ivk1800.screen.sample.SampleScreenFactory 5 | 6 | class ApplicationDependencies { 7 | val mainScreenFactory = MainScreenFactory() 8 | val sampleScreenFactory = SampleScreenFactory() 9 | } 10 | -------------------------------------------------------------------------------- /detekt.yml: -------------------------------------------------------------------------------- 1 | style: 2 | NewLineAtEndOfFile: 3 | active: true 4 | UnusedImports: 5 | active: true 6 | WildcardImport: 7 | active: true 8 | 9 | formatting: 10 | active: true 11 | SpacingAroundColon: 12 | active: true 13 | NoConsecutiveBlankLines: 14 | active: true 15 | TrailingCommaOnCallSite: 16 | active: true 17 | TrailingCommaOnDeclarationSite: 18 | active: true 19 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/navigation/Destinations.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.navigation 2 | 3 | import kotlinx.serialization.Serializable 4 | import ru.ivk1800.SampleDescriptor 5 | 6 | @Serializable 7 | sealed interface AppDestination 8 | 9 | @Serializable 10 | data object MainDestination : AppDestination 11 | 12 | @Serializable 13 | data class SampleDestination(val sample: SampleDescriptor) : AppDestination 14 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/screen/sample/SampleScreenFactory.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.screen.sample 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.key 5 | import ru.ivk1800.SampleDescriptor 6 | 7 | class SampleScreenFactory { 8 | @Composable 9 | fun create(sample: SampleDescriptor) { 10 | key(sample) { 11 | SampleScreen(sample) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx4G 3 | org.gradle.caching=true 4 | org.gradle.configuration-cache=true 5 | org.gradle.daemon=true 6 | org.gradle.parallel=true 7 | 8 | #Kotlin 9 | kotlin.code.style=official 10 | kotlin.daemon.jvmargs=-Xmx4G 11 | kotlin.native.binary.gc=cms 12 | 13 | #Android 14 | android.useAndroidX=true 15 | android.nonTransitiveRClass=true 16 | 17 | #Compose 18 | org.jetbrains.compose.experimental.jscanvas.enabled=true 19 | -------------------------------------------------------------------------------- /composeApp/src/jsMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.ExperimentalComposeUiApi 2 | import androidx.compose.ui.window.ComposeViewport 3 | import kotlinx.browser.document 4 | import org.jetbrains.skiko.wasm.onWasmReady 5 | import ru.ivk1800.App 6 | 7 | @OptIn(ExperimentalComposeUiApi::class) 8 | fun main() { 9 | onWasmReady { 10 | val body = document.body ?: return@onWasmReady 11 | ComposeViewport(body) { 12 | App() 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /composeApp/src/jsMain/resources/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Recomposition visualization", 3 | "short_name": "Recomposition visualization", 4 | "icons": [ 5 | { 6 | "src": "./android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } -------------------------------------------------------------------------------- /composeApp/src/jvmMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.unit.dp 2 | import androidx.compose.ui.window.Window 3 | import androidx.compose.ui.window.application 4 | import androidx.compose.ui.window.rememberWindowState 5 | import ru.ivk1800.App 6 | import java.awt.Dimension 7 | 8 | fun main() = application { 9 | Window( 10 | title = "Recomposition visualization", 11 | state = rememberWindowState(width = 1400.dp, height = 900.dp), 12 | onCloseRequest = ::exitApplication, 13 | ) { 14 | window.minimumSize = Dimension(1400, 900) 15 | App() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /kotlin-ir-plugin/src/main/kotlin/ru/ivk1800/MyIrGenerationExtension.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800 2 | 3 | import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension 4 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 5 | import org.jetbrains.kotlin.ir.declarations.IrModuleFragment 6 | 7 | class MyIrGenerationExtension : IrGenerationExtension { 8 | override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) { 9 | moduleFragment.acceptChildren(HighlightInjector(pluginContext), HighlightInjector.Data()) 10 | moduleFragment.acceptChildren(SourceCodeInjector(pluginContext), SourceCodeInjector.Data()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /composeApp/src/jsMain/kotlin/ru/ivk1800/LocationManager.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800 2 | 3 | import io.ktor.http.Url 4 | import kotlinx.browser.window 5 | import org.w3c.dom.url.URL 6 | 7 | actual val locationManager: LocationManager = object : LocationManager { 8 | override val href: Url 9 | get() = Url(window.location.href) 10 | 11 | override fun setSampleId(value: Int?) { 12 | val url = URL(window.location.toString()) 13 | if (value == null) { 14 | url.searchParams.delete("sampleId"); 15 | } else { 16 | url.searchParams.set("sampleId", "$value") 17 | } 18 | window.history.replaceState(null, "", url.toString()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/HighlightManager.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.MutableSharedFlow 5 | 6 | object HighlightManager { 7 | private val _events = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) 8 | val events: Flow = _events 9 | 10 | fun dispatch(event: Event) { 11 | _events.tryEmit(event) 12 | } 13 | 14 | data class Event( 15 | val startOffset: Int, 16 | val endOffset: Int, 17 | ) 18 | } 19 | 20 | fun highlight( 21 | startOffset: Int, 22 | endOffset: Int, 23 | ) { 24 | HighlightManager.dispatch( 25 | HighlightManager.Event( 26 | startOffset = startOffset, 27 | endOffset = endOffset, 28 | ), 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /kotlin-ir-plugin/src/main/kotlin/ru/ivk1800/MyComponentRegistrar.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800 2 | 3 | import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension 4 | import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar 5 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 6 | import org.jetbrains.kotlin.config.CompilerConfiguration 7 | import org.jetbrains.kotlin.fir.extensions.FirExtensionRegistrarAdapter 8 | 9 | @OptIn(ExperimentalCompilerApi::class) 10 | class MyComponentRegistrar : CompilerPluginRegistrar() { 11 | override val supportsK2 = true 12 | 13 | override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { 14 | FirExtensionRegistrarAdapter.registerExtension(MyFirExtensionRegistrar()) 15 | IrGenerationExtension.registerExtension(MyIrGenerationExtension()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/utils/NavTypeExt.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.utils 2 | 3 | import androidx.navigation.NavType 4 | import androidx.savedstate.SavedState 5 | import androidx.savedstate.read 6 | import androidx.savedstate.write 7 | import kotlinx.serialization.json.Json 8 | 9 | inline fun navTypeOf( 10 | isNullableAllowed: Boolean = false, 11 | json: Json = Json, 12 | ) = object : NavType(isNullableAllowed = isNullableAllowed) { 13 | override fun get(bundle: SavedState, key: String): T? = 14 | bundle.read { getString(key).let(json::decodeFromString) } 15 | 16 | override fun parseValue(value: String): T = json.decodeFromString(value) 17 | 18 | override fun serializeAsValue(value: T): String = json.encodeToString(value) 19 | 20 | override fun put(bundle: SavedState, key: String, value: T) = 21 | bundle.write { putString(key, json.encodeToString(value)) } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 ivk1800 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy App to GitHub Pages 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: read 8 | pages: write 9 | id-token: write 10 | 11 | jobs: 12 | build-and-deploy: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: "✅ Checkout code" 17 | uses: actions/checkout@v4 18 | 19 | - name: "⚙️ Set up JDK 21" 20 | uses: actions/setup-java@v4 21 | with: 22 | distribution: 'temurin' 23 | java-version: '21' 24 | 25 | - name: "⚙️ Setup Gradle" 26 | uses: gradle/actions/setup-gradle@v3 27 | with: 28 | gradle-version: 9.0.0 29 | 30 | - name: "🛠️ Build App" 31 | run: gradle :composeApp:buildApp 32 | 33 | - name: "📄 Configure GitHub Pages" 34 | uses: actions/configure-pages@v5 35 | 36 | - name: "📤 Upload GitHub Pages artifact" 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | path: ./composeApp/build/dist/js/productionExecutable 40 | 41 | - name: "🚀 Deploy to GitHub Pages" 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/ReadStateGoodSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material3.Button 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.mutableIntStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import ru.ivk1800.Highlighted 14 | 15 | @Highlighted 16 | object ReadStateGoodSample { 17 | @Composable 18 | fun EntryPoint() { 19 | val countState = remember { mutableIntStateOf(0) } 20 | Column( 21 | verticalArrangement = Arrangement.Center, 22 | horizontalAlignment = Alignment.CenterHorizontally, 23 | modifier = Modifier.fillMaxSize(), 24 | ) { 25 | Button( 26 | onClick = { countState.value++ }, 27 | content = { Text("Increment ${countState.value}") }, 28 | ) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Recomposition-visualization" 2 | 3 | pluginManagement { 4 | repositories { 5 | google { 6 | content { 7 | includeGroupByRegex("com\\.android.*") 8 | includeGroupByRegex("com\\.google.*") 9 | includeGroupByRegex("androidx.*") 10 | includeGroupByRegex("android.*") 11 | } 12 | } 13 | gradlePluginPortal() 14 | mavenCentral() 15 | } 16 | } 17 | 18 | dependencyResolutionManagement { 19 | repositories { 20 | google { 21 | content { 22 | includeGroupByRegex("com\\.android.*") 23 | includeGroupByRegex("com\\.google.*") 24 | includeGroupByRegex("androidx.*") 25 | includeGroupByRegex("android.*") 26 | } 27 | } 28 | mavenCentral() 29 | } 30 | } 31 | plugins { 32 | //https://github.com/JetBrains/compose-hot-reload?tab=readme-ov-file#set-up-automatic-provisioning-of-the-jetbrains-runtime-jbr-via-gradle 33 | id("org.gradle.toolchains.foojay-resolver-convention").version("0.10.0") 34 | } 35 | 36 | include(":composeApp") 37 | include(":kotlin-ir-plugin") 38 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/ReadStateBadSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material3.Button 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.mutableIntStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import ru.ivk1800.Highlighted 14 | 15 | @Highlighted 16 | object ReadStateBadSample { 17 | @Composable 18 | fun EntryPoint() { 19 | val countState = remember { mutableIntStateOf(0) } 20 | Column( 21 | verticalArrangement = Arrangement.Center, 22 | horizontalAlignment = Alignment.CenterHorizontally, 23 | modifier = Modifier.fillMaxSize(), 24 | ) { 25 | val count = countState.value 26 | Button( 27 | onClick = { countState.value++ }, 28 | content = { Text("Increment $count") }, 29 | ) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/LaunchEffectKeyBadSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material3.Button 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.LaunchedEffect 10 | import androidx.compose.runtime.mutableIntStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import ru.ivk1800.Highlighted 15 | 16 | @Highlighted 17 | object LaunchEffectKeyBadSample { 18 | @Composable 19 | fun EntryPoint() { 20 | val count = remember { mutableIntStateOf(0) } 21 | Column( 22 | verticalArrangement = Arrangement.Center, 23 | horizontalAlignment = Alignment.CenterHorizontally, 24 | modifier = Modifier.fillMaxSize(), 25 | ) { 26 | Button( 27 | onClick = { count.value++ }, 28 | content = { Text("Increment") }, 29 | ) 30 | } 31 | 32 | LaunchedEffect(count.value) { 33 | println("Some log: ${count.value}") 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/ui/AppBar.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.app 2 | 3 | import androidx.compose.foundation.layout.RowScope 4 | import androidx.compose.material3.ExperimentalMaterial3Api 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.IconButton 7 | import androidx.compose.material3.TopAppBar 8 | import androidx.compose.runtime.Composable 9 | import ru.ivk1800.navigation.LocalNavHostController 10 | import ru.ivk1800.utils.ArrowBack 11 | 12 | @OptIn(ExperimentalMaterial3Api::class) 13 | @Composable 14 | fun AppBar( 15 | title: @Composable () -> Unit = {}, 16 | actions: @Composable RowScope.() -> Unit = {}, 17 | showNavigationIcon: Boolean = true, 18 | ) { 19 | TopAppBar( 20 | navigationIcon = if (showNavigationIcon) { 21 | { 22 | val navController = LocalNavHostController.current 23 | IconButton( 24 | onClick = { 25 | navController.popBackStack() 26 | }, 27 | ) { 28 | Icon( 29 | imageVector = ArrowBack, 30 | contentDescription = "Back", 31 | ) 32 | } 33 | } 34 | } else { 35 | {} 36 | }, 37 | title = title, 38 | actions = actions, 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/WithoutDerivedStateOfSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.rememberLazyListState 11 | import androidx.compose.material3.ListItem 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.unit.dp 17 | import ru.ivk1800.Highlighted 18 | 19 | @Highlighted 20 | object WithoutDerivedStateOfSample { 21 | @Composable 22 | fun EntryPoint() { 23 | Column(modifier = Modifier.fillMaxSize()) { 24 | val state = rememberLazyListState() 25 | LazyColumn(state = state, modifier = Modifier.height(200.dp)) { 26 | items(20) { index -> ListItem(headlineContent = { Text("Item: $index") }) } 27 | } 28 | val background = if (state.firstVisibleItemIndex > 0) Color.Red else Color.Green 29 | Box(modifier = Modifier.background(background).height(50.dp).fillMaxWidth()) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | 3 | kotlin = "2.2.20" # https://github.com/JetBrains/kotlin/releases 4 | dependency-guard = "0.5.0" 5 | compose = "1.9.0" # https://github.com/JetBrains/compose-multiplatform/releases 6 | hotReload = "1.0.0-beta07" # https://github.com/JetBrains/compose-hot-reload/releases 7 | ktor = "3.2.1" 8 | detekt = "1.23.7" 9 | buildconfig = "5.6.8" 10 | 11 | [libraries] 12 | ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } 13 | navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version = "2.9.0-beta01" } 14 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.8.1" } 15 | 16 | [plugins] 17 | multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 18 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 19 | compose = { id = "org.jetbrains.compose", version.ref = "compose" } 20 | hotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "hotReload" } 21 | detekt-gradle = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } 22 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 23 | dependencyGuard = { id = "com.dropbox.dependency-guard", version.ref = "dependency-guard" } 24 | buildconfig = { id = "com.github.gmazzo.buildconfig", version.ref = "buildconfig" } 25 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/LaunchEffectKeyGoodSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material3.Button 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.LaunchedEffect 10 | import androidx.compose.runtime.mutableIntStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.snapshotFlow 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import ru.ivk1800.Highlighted 16 | 17 | @Highlighted 18 | object LaunchEffectKeyGoodSample { 19 | @Composable 20 | fun EntryPoint() { 21 | val count = remember { mutableIntStateOf(0) } 22 | Column( 23 | verticalArrangement = Arrangement.Center, 24 | horizontalAlignment = Alignment.CenterHorizontally, 25 | modifier = Modifier.fillMaxSize(), 26 | ) { 27 | Button( 28 | onClick = { count.value++ }, 29 | content = { Text("Increment") }, 30 | ) 31 | } 32 | 33 | LaunchedEffect(count) { 34 | snapshotFlow { count.value } 35 | .collect { 36 | println("Some log: ${count.value}") 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/NotLambdaModifiersGoodSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.setValue 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.rotate 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.unit.dp 18 | import kotlinx.coroutines.delay 19 | import ru.ivk1800.Highlighted 20 | 21 | @Highlighted 22 | object NotLambdaModifiersGoodSample { 23 | @Composable 24 | fun EntryPoint() { 25 | Box(modifier = Modifier.fillMaxSize()) { 26 | var angle by remember { mutableStateOf(0F) } 27 | 28 | LaunchedEffect(Unit) { 29 | while (true) { 30 | delay(500) 31 | angle += 10 32 | } 33 | } 34 | Box( 35 | modifier = Modifier.size(100.dp) 36 | .rotate(angle) 37 | .align(Alignment.Center) 38 | .background(Color.Red), 39 | ) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/LambdaModifiersGoodSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.setValue 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.graphics.graphicsLayer 17 | import androidx.compose.ui.unit.dp 18 | import kotlinx.coroutines.delay 19 | import ru.ivk1800.Highlighted 20 | 21 | @Highlighted 22 | object LambdaModifiersGoodSample { 23 | @Composable 24 | fun EntryPoint() { 25 | Box(modifier = Modifier.fillMaxSize()) { 26 | var angle by remember { mutableStateOf(0F) } 27 | 28 | LaunchedEffect(Unit) { 29 | while (true) { 30 | delay(500) 31 | angle += 10 32 | } 33 | } 34 | Box( 35 | modifier = Modifier.size(100.dp) 36 | .graphicsLayer { 37 | rotationZ = angle 38 | } 39 | .align(Alignment.Center) 40 | .background(Color.Red), 41 | ) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/UnstableTypeBadSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material3.Button 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.mutableIntStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import ru.ivk1800.Highlighted 17 | 18 | @Highlighted 19 | object UnstableTypeBadSample { 20 | 21 | data class MyData( 22 | val items: List, 23 | ) 24 | 25 | @Composable 26 | fun EntryPoint() { 27 | val count = remember { mutableIntStateOf(0) } 28 | Column( 29 | verticalArrangement = Arrangement.Center, 30 | horizontalAlignment = Alignment.CenterHorizontally, 31 | modifier = Modifier.fillMaxSize(), 32 | ) { 33 | Button( 34 | onClick = { count.value++ }, 35 | content = { Text("Make recomposition") }, 36 | ) 37 | } 38 | count.value 39 | RedBox(data = MyData(listOf("test"))) 40 | } 41 | 42 | @Composable 43 | fun RedBox(data: MyData) { 44 | Box(Modifier.background(Color.Red)) { 45 | Text(text = "${data.hashCode()}") 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/DerivedStateOfSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.rememberLazyListState 11 | import androidx.compose.material3.ListItem 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.derivedStateOf 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.unit.dp 20 | import ru.ivk1800.Highlighted 21 | 22 | @Highlighted 23 | object DerivedStateOfSample { 24 | @Composable 25 | fun EntryPoint() { 26 | Column(modifier = Modifier.fillMaxSize()) { 27 | val state = rememberLazyListState() 28 | LazyColumn(state = state, modifier = Modifier.height(200.dp)) { 29 | items(20) { index -> ListItem(headlineContent = { Text("Item: $index") }) } 30 | } 31 | val background by remember { 32 | derivedStateOf { 33 | if (state.firstVisibleItemIndex > 0) Color.Red else Color.Green 34 | } 35 | } 36 | Box(modifier = Modifier.background(background).height(50.dp).fillMaxWidth()) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/ui/InteractiveSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800 2 | 3 | import androidx.compose.foundation.border 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxHeight 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.foundation.rememberScrollState 11 | import androidx.compose.foundation.verticalScroll 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.unit.dp 16 | 17 | @Composable 18 | fun InteractiveSample( 19 | sample: @Composable () -> Unit, 20 | code: @Composable () -> Unit, 21 | modifier: Modifier = Modifier, 22 | ) { 23 | Row( 24 | modifier = modifier.padding(16.dp), 25 | ) { 26 | Box( 27 | modifier = Modifier 28 | .weight(1F) 29 | .fillMaxHeight() 30 | .border(1.dp, MaterialTheme.colorScheme.outlineVariant), 31 | ) { 32 | sample.invoke() 33 | } 34 | Spacer(modifier = Modifier.width(16.dp)) 35 | Box( 36 | modifier = Modifier 37 | .weight(1F) 38 | .fillMaxHeight() 39 | .border(1.dp, MaterialTheme.colorScheme.outlineVariant), 40 | ) { 41 | val scrollState = rememberScrollState() 42 | 43 | Box(modifier = Modifier.verticalScroll(scrollState)) { 44 | code.invoke() 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/ImmutableTypeGoodSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material3.Button 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.Immutable 12 | import androidx.compose.runtime.mutableIntStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import ru.ivk1800.Highlighted 18 | 19 | @Highlighted 20 | object ImmutableTypeGoodSample { 21 | 22 | @Immutable 23 | data class MyData( 24 | val items: List, 25 | ) 26 | 27 | @Composable 28 | fun EntryPoint() { 29 | val count = remember { mutableIntStateOf(0) } 30 | Column( 31 | verticalArrangement = Arrangement.Center, 32 | horizontalAlignment = Alignment.CenterHorizontally, 33 | modifier = Modifier.fillMaxSize(), 34 | ) { 35 | Button( 36 | onClick = { count.value++ }, 37 | content = { Text("Make recomposition") }, 38 | ) 39 | } 40 | count.value 41 | RedBox(data = MyData(listOf("test"))) 42 | } 43 | 44 | @Composable 45 | fun RedBox(data: MyData) { 46 | Box(Modifier.background(Color.Red)) { 47 | Text(text = "${data.hashCode()}") 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/ui/AppContent.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.app 2 | 3 | import androidx.compose.animation.AnimatedContentTransitionScope 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.runtime.Composable 6 | import androidx.navigation.compose.NavHost 7 | import androidx.navigation.compose.composable 8 | import androidx.navigation.toRoute 9 | import ru.ivk1800.SampleDescriptor 10 | import ru.ivk1800.navigation.LocalNavHostController 11 | import ru.ivk1800.navigation.MainDestination 12 | import ru.ivk1800.navigation.SampleDestination 13 | import ru.ivk1800.utils.navTypeOf 14 | import kotlin.reflect.typeOf 15 | 16 | @Composable 17 | fun AppContent() { 18 | val navController = LocalNavHostController.current 19 | val applicationDependencies = LocalApplicationDependencies.current 20 | NavHost( 21 | navController = navController, 22 | startDestination = MainDestination, 23 | enterTransition = { 24 | slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start, tween(200)) 25 | }, 26 | exitTransition = { 27 | slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Start, tween(200)) 28 | }, 29 | popEnterTransition = { 30 | slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.End, tween(200)) 31 | }, 32 | popExitTransition = { 33 | slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End, tween(200)) 34 | }, 35 | 36 | ) { 37 | composable { 38 | applicationDependencies.mainScreenFactory.create() 39 | } 40 | composable( 41 | typeMap = mapOf(typeOf() to navTypeOf()), 42 | ) { 43 | val destination = it.toRoute() 44 | applicationDependencies.sampleScreenFactory.create(destination.sample) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/CompositionLocalSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material3.Button 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import androidx.compose.runtime.compositionLocalOf 13 | import androidx.compose.runtime.mutableIntStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import ru.ivk1800.Highlighted 19 | 20 | @Highlighted 21 | object CompositionLocalSample { 22 | @Composable 23 | fun EntryPoint() { 24 | val count = remember { mutableIntStateOf(0) } 25 | Column( 26 | verticalArrangement = Arrangement.Center, 27 | horizontalAlignment = Alignment.CenterHorizontally, 28 | modifier = Modifier.fillMaxSize(), 29 | ) { 30 | Button( 31 | onClick = { count.value++ }, 32 | content = { Text("Increment") }, 33 | ) 34 | CompositionLocalProvider(LocalCounter provides count.value) { 35 | RedBox() 36 | GreenBox() 37 | } 38 | } 39 | } 40 | 41 | @Composable 42 | fun RedBox() { 43 | Box(Modifier.background(Color.Red)) { 44 | Text(text = "Count: ${LocalCounter.current}") 45 | } 46 | } 47 | 48 | @Composable 49 | fun GreenBox() { 50 | Box(Modifier.background(Color.Green)) { 51 | Text(text = "Green") 52 | } 53 | } 54 | 55 | private val LocalCounter = compositionLocalOf { 0 } 56 | } 57 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/StaticCompositionLocalSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material3.Button 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.CompositionLocalProvider 12 | import androidx.compose.runtime.mutableIntStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.staticCompositionLocalOf 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import ru.ivk1800.Highlighted 19 | 20 | @Highlighted 21 | object StaticCompositionLocalSample { 22 | @Composable 23 | fun EntryPoint() { 24 | val count = remember { mutableIntStateOf(0) } 25 | Column( 26 | verticalArrangement = Arrangement.Center, 27 | horizontalAlignment = Alignment.CenterHorizontally, 28 | modifier = Modifier.fillMaxSize(), 29 | ) { 30 | Button( 31 | onClick = { count.value++ }, 32 | content = { Text("Increment") }, 33 | ) 34 | CompositionLocalProvider(LocalCounter provides count.value) { 35 | RedBox() 36 | GreenBox() 37 | } 38 | } 39 | } 40 | 41 | @Composable 42 | fun RedBox() { 43 | Box(Modifier.background(Color.Red)) { 44 | Text(text = "Count: ${LocalCounter.current}") 45 | } 46 | } 47 | 48 | @Composable 49 | fun GreenBox() { 50 | Box(Modifier.background(Color.Green)) { 51 | Text(text = "Green") 52 | } 53 | } 54 | 55 | private val LocalCounter = staticCompositionLocalOf { 0 } 56 | } 57 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/ui/App.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.CompositionLocalProvider 5 | import androidx.compose.runtime.LaunchedEffect 6 | import androidx.compose.runtime.remember 7 | import androidx.navigation.NavHostController 8 | import androidx.navigation.compose.rememberNavController 9 | import androidx.navigation.toRoute 10 | import org.jetbrains.compose.ui.tooling.preview.Preview 11 | import ru.ivk1800.app.AppContent 12 | import ru.ivk1800.app.ApplicationDependencies 13 | import ru.ivk1800.app.LocalApplicationDependencies 14 | import ru.ivk1800.navigation.LocalNavHostController 15 | import ru.ivk1800.navigation.SampleDestination 16 | import ru.ivk1800.theme.AppTheme 17 | 18 | @Preview 19 | @Composable 20 | internal fun App() = AppTheme { 21 | val navController: NavHostController = rememberNavController() 22 | 23 | CompositionLocalProvider( 24 | LocalApplicationDependencies provides remember { ApplicationDependencies() }, 25 | LocalNavHostController provides navController, 26 | ) { 27 | AppContent() 28 | } 29 | 30 | LaunchedEffect(navController) { 31 | val sampleId = locationManager.href.parameters["sampleId"]?.toIntOrNull() 32 | if (sampleId != null) { 33 | val sample = SampleDescriptor.entries.find { sample -> sample.id == sampleId } 34 | if (sample != null) { 35 | navController.navigate(SampleDestination(sample)) 36 | } 37 | } 38 | navController.currentBackStackEntryFlow 39 | .collect { entry -> 40 | // TODO Find best solution 41 | if (entry.destination.route?.contains("SampleDestination") == true) { 42 | val route = entry.toRoute() 43 | locationManager.setSampleId(route.sample.id) 44 | } else { 45 | locationManager.setSampleId(null) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/AvoidUnnecessaryStateReadsGood1Sample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.runtime.State 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.rotate 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.unit.dp 17 | import kotlinx.coroutines.delay 18 | import ru.ivk1800.Highlighted 19 | 20 | @Highlighted 21 | object AvoidUnnecessaryStateReadsGood1Sample { 22 | @Composable 23 | fun EntryPoint() { 24 | Box(modifier = Modifier.fillMaxSize()) { 25 | var angle = remember { mutableStateOf(0F) } 26 | 27 | LaunchedEffect(angle) { 28 | while (true) { 29 | delay(500) 30 | angle.value += 10 31 | } 32 | } 33 | Function1(angle = angle, modifier = Modifier.align(Alignment.Center)) 34 | } 35 | } 36 | 37 | @Composable 38 | fun Function1(angle: State, modifier: Modifier = Modifier) { 39 | Function2(angle, modifier) 40 | } 41 | 42 | @Composable 43 | fun Function2(angle: State, modifier: Modifier = Modifier) { 44 | Function3(angle, modifier) 45 | } 46 | 47 | @Composable 48 | fun Function3(angle: State, modifier: Modifier = Modifier) { 49 | RedBox(angle, modifier) 50 | } 51 | 52 | @Composable 53 | fun RedBox(angle: State, modifier: Modifier = Modifier) { 54 | Box( 55 | modifier = modifier.size(100.dp) 56 | .rotate(angle.value) 57 | .background(Color.Red), 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/AvoidUnnecessaryStateReadsBadSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.setValue 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.rotate 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.unit.dp 18 | import kotlinx.coroutines.delay 19 | import ru.ivk1800.Highlighted 20 | 21 | @Highlighted 22 | object AvoidUnnecessaryStateReadsBadSample { 23 | @Composable 24 | fun EntryPoint() { 25 | Box(modifier = Modifier.fillMaxSize()) { 26 | var angle by remember { mutableStateOf(0F) } 27 | 28 | LaunchedEffect(Unit) { 29 | while (true) { 30 | delay(500) 31 | angle += 10 32 | } 33 | } 34 | 35 | Function1(angle = angle, modifier = Modifier.align(Alignment.Center)) 36 | } 37 | } 38 | 39 | @Composable 40 | fun Function1(angle: Float, modifier: Modifier = Modifier) { 41 | Function2(angle, modifier) 42 | } 43 | 44 | @Composable 45 | fun Function2(angle: Float, modifier: Modifier = Modifier) { 46 | Function3(angle, modifier) 47 | } 48 | 49 | @Composable 50 | fun Function3(angle: Float, modifier: Modifier = Modifier) { 51 | RedBox(angle, modifier) 52 | } 53 | 54 | @Composable 55 | fun RedBox(angle: Float, modifier: Modifier = Modifier) { 56 | Box( 57 | modifier = modifier.size(100.dp) 58 | .rotate(angle) 59 | .background(Color.Red), 60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/AvoidUnnecessaryStateReadsGood2Sample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.setValue 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.rotate 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.unit.dp 18 | import kotlinx.coroutines.delay 19 | import ru.ivk1800.Highlighted 20 | 21 | @Highlighted 22 | object AvoidUnnecessaryStateReadsGood2Sample { 23 | @Composable 24 | fun EntryPoint() { 25 | Box(modifier = Modifier.fillMaxSize()) { 26 | var angle by remember { mutableStateOf(0F) } 27 | 28 | LaunchedEffect(Unit) { 29 | while (true) { 30 | delay(500) 31 | angle += 10 32 | } 33 | } 34 | Function1( 35 | angle = { angle }, 36 | modifier = Modifier.align(Alignment.Center), 37 | ) 38 | } 39 | } 40 | 41 | @Composable 42 | fun Function1(angle: () -> Float, modifier: Modifier = Modifier) { 43 | Function2(angle, modifier) 44 | } 45 | 46 | @Composable 47 | fun Function2(angle: () -> Float, modifier: Modifier = Modifier) { 48 | Function3(angle, modifier) 49 | } 50 | 51 | @Composable 52 | fun Function3(angle: () -> Float, modifier: Modifier = Modifier) { 53 | RedBox(angle, modifier) 54 | } 55 | 56 | @Composable 57 | fun RedBox(angle: () -> Float, modifier: Modifier = Modifier) { 58 | Box( 59 | modifier = modifier.size(100.dp) 60 | .rotate(angle.invoke()) 61 | .background(Color.Red), 62 | ) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Recomposition Visualization 2 | 3 | **Recomposition Visualization** is an application for visually understanding how Jetpack Compose works. It allows you to track recompositions and state changes in a clear way, similar to [**RxMarbles**](https://rxmarbles.com/) and [**FlowMarbles**](https://flowmarbles.com/) for data streams. 4 | 5 | The app is available [here](https://ivk1800.github.io/recomposition-visualization).😎 6 | 7 | ## ⚙️ Technologies 8 | 9 | * **Kotlin** 10 | * **Jetpack Compose Multiplatform** 11 | * **FIR & IR API** — used for code analysis and generation at the compiler level 12 | 13 | ## 🎬 Demo 14 | 15 | Demo of recompositions 16 | 17 | ## 🚀 How to Run 18 | 19 | ### Desktop 20 | ```bash 21 | ./gradlew :composeApp:run 22 | ``` 23 | 24 | ### JS Browser 25 | ```bash 26 | ./gradlew :composeApp:jsBrowserDevelopmentRun --continuous 27 | ``` 28 | 29 | ### JS Browser Distribution 30 | ```bash 31 | ./gradlew buildApp 32 | ``` 33 | 34 | ## 📝 Contribute Your Own Sample 35 | 36 | You can contribute your own Compose examples to the project via **Pull Requests**. To add a new sample, follow these steps: 37 | 38 | 1. **Create a sample class** 39 | In the package `ru.ivk1800.sample`, add a new class with the following structure: 40 | 41 | ```kotlin 42 | @Highlighted 43 | object MyAwesomeSample { 44 | @Composable 45 | fun EntryPoint() { 46 | // Your Compose code here 47 | } 48 | } 49 | ``` 50 | The class must be annotated with `@Highlighted`. It must contain a method `EntryPoint()` marked as `@Composable`. 51 | 52 | 2. Register the sample in SampleDescriptor. Add a new enum entry for your sample: 53 | ```kotlin 54 | MyAwesome( 55 | id = 16, 56 | entryPoint = { MyAwesomeSample.EntryPoint() }, 57 | sourceCode = { MyAwesomeSample.HighlightedSourceCode() }, 58 | title = Res.string.my_awesome_title, 59 | explanation = Res.string.my_awesome_explanation, 60 | good = null, 61 | ), 62 | ``` 63 | If your sample has two variants (“good” and “bad”), make sure to provide the good value. 64 | 65 | 3. Add string resources 66 | Add entries for the sample's title and explanation. 67 | 68 | 4. Submit a Pull Request 🏆. 69 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/LaunchEffectWithKeySample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.pager.HorizontalPager 10 | import androidx.compose.foundation.pager.rememberPagerState 11 | import androidx.compose.material3.Button 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.LaunchedEffect 16 | import androidx.compose.runtime.key 17 | import androidx.compose.runtime.mutableIntStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.unit.dp 23 | import kotlinx.coroutines.delay 24 | import ru.ivk1800.Highlighted 25 | 26 | @Highlighted 27 | object LaunchEffectWithKeySample { 28 | @Composable 29 | fun EntryPoint() { 30 | Column( 31 | modifier = Modifier.fillMaxSize(), 32 | verticalArrangement = Arrangement.Center, 33 | horizontalAlignment = Alignment.CenterHorizontally, 34 | ) { 35 | val keyState = remember { mutableIntStateOf(0) } 36 | val pagerState = key(keyState.value) { 37 | rememberPagerState(initialPageOffsetFraction = 0F, pageCount = { Int.MAX_VALUE }) 38 | } 39 | 40 | HorizontalPager( 41 | modifier = Modifier.size(200.dp), 42 | state = pagerState, 43 | ) { page -> 44 | Box( 45 | Modifier 46 | .fillMaxSize() 47 | .background(if (page % 2 == 0) Color.Red else Color.Green) 48 | ) { 49 | Text( 50 | modifier = Modifier.align(Alignment.Center), 51 | text = "$page", 52 | style = MaterialTheme.typography.headlineLarge, 53 | ) 54 | } 55 | } 56 | 57 | Button( 58 | onClick = { keyState.value += 1 }, 59 | content = { Text(text = "Reset") }, 60 | ) 61 | 62 | LaunchedEffect(pagerState) { 63 | while (true) { 64 | delay(1000) 65 | pagerState.animateScrollToPage(pagerState.currentPage + 1) 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/LaunchEffectWithoutKeySample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.pager.HorizontalPager 10 | import androidx.compose.foundation.pager.rememberPagerState 11 | import androidx.compose.material3.Button 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.LaunchedEffect 16 | import androidx.compose.runtime.key 17 | import androidx.compose.runtime.mutableIntStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.unit.dp 23 | import kotlinx.coroutines.delay 24 | import ru.ivk1800.Highlighted 25 | 26 | @Highlighted 27 | object LaunchEffectWithoutKeySample { 28 | @Composable 29 | fun EntryPoint() { 30 | Column( 31 | modifier = Modifier.fillMaxSize(), 32 | verticalArrangement = Arrangement.Center, 33 | horizontalAlignment = Alignment.CenterHorizontally, 34 | ) { 35 | val keyState = remember { mutableIntStateOf(0) } 36 | val pagerState = key(keyState.value) { 37 | rememberPagerState(initialPageOffsetFraction = 0F, pageCount = { Int.MAX_VALUE }) 38 | } 39 | 40 | HorizontalPager( 41 | modifier = Modifier.size(200.dp), 42 | state = pagerState, 43 | ) { page -> 44 | Box( 45 | Modifier 46 | .fillMaxSize() 47 | .background(if (page % 2 == 0) Color.Red else Color.Green) 48 | ) { 49 | Text( 50 | modifier = Modifier.align(Alignment.Center), 51 | text = "$page", 52 | style = MaterialTheme.typography.headlineLarge, 53 | ) 54 | } 55 | } 56 | 57 | Button( 58 | onClick = { keyState.value += 1 }, 59 | content = { Text(text = "Reset") }, 60 | ) 61 | 62 | LaunchedEffect(Unit) { 63 | while (true) { 64 | delay(1000) 65 | pagerState.animateScrollToPage(pagerState.currentPage + 1) 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /composeApp/src/jsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Recomposition visualization 11 | 12 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/WithoutRememberUpdatedStateSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.material3.Button 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.LaunchedEffect 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.setValue 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.graphics.Color 21 | import androidx.compose.ui.graphics.graphicsLayer 22 | import androidx.compose.ui.unit.dp 23 | import kotlinx.coroutines.delay 24 | import ru.ivk1800.Highlighted 25 | 26 | @Highlighted 27 | object WithoutRememberUpdatedStateSample { 28 | @Composable 29 | fun EntryPoint() { 30 | Column( 31 | modifier = Modifier.fillMaxSize(), 32 | verticalArrangement = Arrangement.Center, 33 | horizontalAlignment = Alignment.CenterHorizontally, 34 | ) { 35 | var angle by remember { mutableStateOf(0F) } 36 | 37 | Box(modifier = Modifier.size(100.dp).graphicsLayer { rotationZ = angle }.background(Color.Red)) 38 | Spacer(modifier = Modifier.size(100.dp)) 39 | 40 | var rightDirection = remember<(Float) -> Unit> { { newAngle -> angle = newAngle } } 41 | var leftDirection = remember<(Float) -> Unit> { { newAngle -> angle = -newAngle } } 42 | var currentDirection by remember { mutableStateOf(rightDirection) } 43 | 44 | AngleProducer(currentDirection) 45 | 46 | Button( 47 | onClick = { 48 | currentDirection = if (currentDirection == rightDirection) { 49 | leftDirection 50 | } else { 51 | rightDirection 52 | } 53 | }, 54 | content = { 55 | Text(text = "Change direction") 56 | }, 57 | ) 58 | } 59 | } 60 | 61 | @Composable 62 | private fun AngleProducer(onAngleChanged: (Float) -> Unit) { 63 | LaunchedEffect(Unit) { 64 | var angle = 0F 65 | while (true) { 66 | delay(250) 67 | angle += 10 68 | onAngleChanged.invoke(angle) 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/LazyListWithoutKeyBadSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.rememberLazyListState 11 | import androidx.compose.material3.Button 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.mutableStateListOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.unit.dp 19 | import ru.ivk1800.Highlighted 20 | import kotlin.time.Clock 21 | import kotlin.time.ExperimentalTime 22 | 23 | @Highlighted 24 | object LazyListWithoutKeyBadSample { 25 | 26 | data class Item(val id: Int) 27 | 28 | @Composable 29 | fun EntryPoint() { 30 | Column(modifier = Modifier.fillMaxSize()) { 31 | val state = rememberLazyListState() 32 | 33 | var items = remember { mutableStateListOf() } 34 | 35 | LazyColumn(state = state, modifier = Modifier.height(500.dp)) { 36 | items( 37 | count = items.size, 38 | ) { index -> 39 | val item = items[index] 40 | if (index % 2 == 0) { 41 | Even(item) 42 | } else { 43 | Odd(item) 44 | } 45 | } 46 | } 47 | Row { 48 | Button( 49 | onClick = { 50 | items.add(0, Item(items.size)) 51 | }, 52 | content = { Text("Add") }, 53 | ) 54 | Button( 55 | onClick = { items.clear() }, 56 | content = { Text("Reset") }, 57 | ) 58 | } 59 | } 60 | } 61 | 62 | @OptIn(ExperimentalTime::class) 63 | @Composable 64 | fun Odd(item: Item) { 65 | Box(Modifier.background(Color.Red)) { 66 | val time = remember { Clock.System.now().toEpochMilliseconds() } 67 | Text(text = "Item: ${item.id}, $time") 68 | } 69 | } 70 | 71 | @OptIn(ExperimentalTime::class) 72 | @Composable 73 | fun Even(item: Item) { 74 | Box(Modifier.background(Color.Yellow)) { 75 | val time = remember { Clock.System.now().toEpochMilliseconds() } 76 | Text(text = "Item: ${item.id}, $time") 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/LazyListWithKeyGoodSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.rememberLazyListState 11 | import androidx.compose.material3.Button 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.mutableStateListOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.unit.dp 19 | import ru.ivk1800.Highlighted 20 | import kotlin.time.Clock 21 | import kotlin.time.ExperimentalTime 22 | 23 | @Highlighted 24 | object LazyListWithKeyGoodSample { 25 | 26 | data class Item(val id: Int) 27 | 28 | @Composable 29 | fun EntryPoint() { 30 | Column(modifier = Modifier.fillMaxSize()) { 31 | val state = rememberLazyListState() 32 | 33 | var items = remember { mutableStateListOf() } 34 | 35 | LazyColumn(state = state, modifier = Modifier.height(500.dp)) { 36 | items( 37 | count = items.size, 38 | key = { index -> items[index].id }, 39 | ) { index -> 40 | val item = items[index] 41 | if (item.id % 2 == 0) { 42 | Even(item) 43 | } else { 44 | Odd(item) 45 | } 46 | } 47 | } 48 | Row { 49 | Button( 50 | onClick = { 51 | items.add(0, Item(items.size)) 52 | }, 53 | content = { Text("Add") }, 54 | ) 55 | Button( 56 | onClick = { items.clear() }, 57 | content = { Text("Reset") }, 58 | ) 59 | } 60 | } 61 | } 62 | 63 | @OptIn(ExperimentalTime::class) 64 | @Composable 65 | fun Odd(item: Item) { 66 | Box(Modifier.background(Color.Red)) { 67 | val time = remember { Clock.System.now().toEpochMilliseconds() } 68 | Text(text = "Item: ${item.id}, $time") 69 | } 70 | } 71 | 72 | @OptIn(ExperimentalTime::class) 73 | @Composable 74 | fun Even(item: Item) { 75 | Box(Modifier.background(Color.Yellow)) { 76 | val time = remember { Clock.System.now().toEpochMilliseconds() } 77 | Text(text = "Item: ${item.id}, $time") 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/WithRememberUpdatedStateSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.material3.Button 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.LaunchedEffect 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.runtime.rememberUpdatedState 18 | import androidx.compose.runtime.setValue 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.graphics.graphicsLayer 23 | import androidx.compose.ui.unit.dp 24 | import kotlinx.coroutines.delay 25 | import ru.ivk1800.Highlighted 26 | 27 | @Highlighted 28 | object WithRememberUpdatedStateSample { 29 | @Composable 30 | fun EntryPoint() { 31 | Column( 32 | modifier = Modifier.fillMaxSize(), 33 | verticalArrangement = Arrangement.Center, 34 | horizontalAlignment = Alignment.CenterHorizontally, 35 | ) { 36 | var angle by remember { mutableStateOf(0F) } 37 | 38 | Box(modifier = Modifier.size(100.dp).graphicsLayer { rotationZ = angle }.background(Color.Red)) 39 | Spacer(modifier = Modifier.size(100.dp)) 40 | 41 | var rightDirection = remember<(Float) -> Unit> { { newAngle -> angle = newAngle } } 42 | var leftDirection = remember<(Float) -> Unit> { { newAngle -> angle = -newAngle } } 43 | var currentDirection by remember { mutableStateOf(rightDirection) } 44 | 45 | AngleProducer(currentDirection) 46 | 47 | Button( 48 | onClick = { 49 | currentDirection = if (currentDirection == rightDirection) { 50 | leftDirection 51 | } else { 52 | rightDirection 53 | } 54 | }, 55 | content = { 56 | Text(text = "Change direction") 57 | }, 58 | ) 59 | } 60 | } 61 | 62 | @Composable 63 | private fun AngleProducer(onAngleChanged: (Float) -> Unit) { 64 | val currentOnAngleChanged = rememberUpdatedState(onAngleChanged) 65 | 66 | LaunchedEffect(currentOnAngleChanged) { 67 | var angle = 0F 68 | while (true) { 69 | delay(250) 70 | angle += 10 71 | currentOnAngleChanged.value.invoke(angle) 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/sample/MovableContentOfSample.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material3.Button 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.movableContentOf 15 | import androidx.compose.runtime.mutableIntStateOf 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.runtime.setValue 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.unit.dp 23 | import ru.ivk1800.Highlighted 24 | 25 | @Highlighted 26 | object MovableContentOfSample { 27 | @Composable 28 | fun EntryPoint() { 29 | var isHorizontal by remember { mutableStateOf(false) } 30 | Column( 31 | verticalArrangement = Arrangement.Center, 32 | horizontalAlignment = Alignment.CenterHorizontally, 33 | modifier = Modifier.fillMaxSize(), 34 | ) { 35 | Button( 36 | onClick = { isHorizontal = !isHorizontal }, 37 | content = { Text("Toggle") }, 38 | ) 39 | } 40 | 41 | val boxes = remember { 42 | movableContentOf { 43 | RedBox() 44 | GreenBox() 45 | } 46 | } 47 | 48 | if (isHorizontal) { 49 | Row { 50 | boxes() 51 | } 52 | } else { 53 | Column { 54 | boxes() 55 | } 56 | } 57 | } 58 | 59 | @Composable 60 | fun RedBox() { 61 | Box(Modifier.background(Color.Red)) { 62 | val count = remember { mutableIntStateOf(0) } 63 | Column( 64 | verticalArrangement = Arrangement.Center, 65 | horizontalAlignment = Alignment.CenterHorizontally, 66 | modifier = Modifier.padding(20.dp), 67 | ) { 68 | Button( 69 | onClick = { count.value++ }, 70 | content = { Text("${count.value}") }, 71 | ) 72 | } 73 | } 74 | } 75 | 76 | @Composable 77 | fun GreenBox() { 78 | Box(Modifier.background(Color.Green)) { 79 | val count = remember { mutableIntStateOf(0) } 80 | Column( 81 | verticalArrangement = Arrangement.Center, 82 | horizontalAlignment = Alignment.CenterHorizontally, 83 | modifier = Modifier.padding(20.dp), 84 | ) { 85 | Button( 86 | onClick = { count.value++ }, 87 | content = { Text("${count.value}") }, 88 | ) 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/screen/sample/SampleScreen.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.screen.sample 2 | 3 | import androidx.compose.foundation.layout.Spacer 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.layout.width 6 | import androidx.compose.material3.AlertDialog 7 | import androidx.compose.material3.OutlinedButton 8 | import androidx.compose.material3.Scaffold 9 | import androidx.compose.material3.Text 10 | import androidx.compose.material3.TextButton 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.unit.dp 18 | import org.jetbrains.compose.resources.stringResource 19 | import ru.ivk1800.InteractiveSample 20 | import ru.ivk1800.SampleDescriptor 21 | import ru.ivk1800.app.AppBar 22 | import ru.ivk1800.navigation.LocalNavHostController 23 | import ru.ivk1800.navigation.SampleDestination 24 | import ru.ivk1800.resources.Res 25 | import ru.ivk1800.resources.explanation 26 | import ru.ivk1800.resources.how_good 27 | 28 | @Composable 29 | fun SampleScreen(sample: SampleDescriptor) { 30 | var showExplanationDialog by remember { mutableStateOf(false) } 31 | val explanation = sample.explanation 32 | 33 | val navController = LocalNavHostController.current 34 | 35 | Scaffold( 36 | topBar = { 37 | AppBar( 38 | title = { 39 | Text(text = stringResource(sample.title)) 40 | }, 41 | actions = { 42 | if (sample.good != null) { 43 | OutlinedButton( 44 | onClick = { 45 | navController.navigate(SampleDestination(sample.good)) 46 | }, 47 | content = { 48 | Text(stringResource(Res.string.how_good)) 49 | }, 50 | ) 51 | } 52 | if (explanation != null) { 53 | Spacer(modifier = Modifier.width(8.dp)) 54 | OutlinedButton( 55 | onClick = { 56 | showExplanationDialog = true 57 | }, 58 | content = { 59 | Text(stringResource(Res.string.explanation)) 60 | }, 61 | ) 62 | } 63 | }, 64 | ) 65 | }, 66 | ) { 67 | InteractiveSample( 68 | modifier = Modifier.padding(it), 69 | sample = sample.entryPoint, 70 | code = sample.sourceCode, 71 | ) 72 | } 73 | 74 | if (showExplanationDialog && explanation != null) { 75 | AlertDialog( 76 | onDismissRequest = { showExplanationDialog = false }, 77 | confirmButton = { 78 | TextButton( 79 | onClick = { showExplanationDialog = false }, 80 | content = { Text("ОK") }, 81 | ) 82 | }, 83 | text = { Text(stringResource(explanation)) }, 84 | ) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /kotlin-ir-plugin/src/main/kotlin/ru/ivk1800/SourceCodeInjector.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800 2 | 3 | import org.jetbrains.kotlin.GeneratedDeclarationKey 4 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 5 | import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder 6 | import org.jetbrains.kotlin.ir.IrElement 7 | import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET 8 | import org.jetbrains.kotlin.ir.builders.irCall 9 | import org.jetbrains.kotlin.ir.builders.irString 10 | import org.jetbrains.kotlin.ir.declarations.IrClass 11 | import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin.GeneratedByPlugin 12 | import org.jetbrains.kotlin.ir.declarations.IrFile 13 | import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction 14 | import org.jetbrains.kotlin.ir.util.hasAnnotation 15 | import org.jetbrains.kotlin.ir.visitors.IrVisitor 16 | import org.jetbrains.kotlin.name.CallableId 17 | import org.jetbrains.kotlin.name.FqName 18 | import org.jetbrains.kotlin.name.Name 19 | import java.io.File 20 | 21 | class SourceCodeInjector( 22 | private val context: IrPluginContext, 23 | ) : IrVisitor() { 24 | 25 | private val highlightedAnnotationName = FqName("ru.ivk1800.Highlighted") 26 | 27 | class Data( 28 | var sourceCode: String? = null, 29 | ) 30 | 31 | fun interestedIn(key: GeneratedDeclarationKey?): Boolean = key == HighlightedFirExtension.Key 32 | 33 | override fun visitElement(element: IrElement, data: Data) { 34 | element.acceptChildren(this, data) 35 | } 36 | 37 | override fun visitClass(declaration: IrClass, data: Data) { 38 | if (declaration.annotations.hasAnnotation(highlightedAnnotationName)) { 39 | val parent = declaration.parent as IrFile 40 | val sourceRangeInfo = parent.fileEntry.getSourceRangeInfo( 41 | beginOffset = declaration.startOffset, 42 | endOffset = declaration.endOffset, 43 | ) 44 | 45 | val lines = File(sourceRangeInfo.filePath).readLines().joinToString("\n") 46 | 47 | data.sourceCode = lines 48 | super.visitClass(declaration, data = data) 49 | } 50 | } 51 | 52 | override fun visitSimpleFunction(declaration: IrSimpleFunction, data: Data) { 53 | val origin = declaration.origin 54 | if (origin !is GeneratedByPlugin || !interestedIn(origin.pluginKey)) { 55 | super.visitSimpleFunction(declaration, data) 56 | return 57 | } 58 | 59 | val sourceCode = data.sourceCode 60 | if (sourceCode != null) { 61 | val callableId = CallableId( 62 | packageName = FqName("ru.ivk1800"), 63 | callableName = Name.identifier("HighlightedText"), 64 | ) 65 | 66 | val highlightedTextSymbol = context.referenceFunctions(callableId).first() 67 | 68 | val builder = DeclarationIrBuilder(context, declaration.symbol) 69 | 70 | val irCall = builder.irCall(highlightedTextSymbol) 71 | irCall.arguments[0] = builder.irString(sourceCode) 72 | 73 | val body = context.irFactory.createBlockBody( 74 | startOffset = UNDEFINED_OFFSET, 75 | endOffset = UNDEFINED_OFFSET, 76 | ) 77 | 78 | body.statements += irCall 79 | declaration.body = body 80 | } 81 | 82 | super.visitSimpleFunction(declaration, data) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/HighlightedText.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.LaunchedEffect 8 | import androidx.compose.runtime.mutableStateListOf 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.snapshotFlow 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.drawBehind 14 | import androidx.compose.ui.geometry.Rect 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.text.TextLayoutResult 17 | import androidx.compose.ui.text.font.FontFamily 18 | import androidx.compose.ui.unit.dp 19 | import kotlinx.coroutines.ExperimentalCoroutinesApi 20 | import kotlinx.coroutines.delay 21 | import kotlinx.coroutines.flow.filterNotNull 22 | import kotlinx.coroutines.flow.flatMapLatest 23 | import kotlinx.coroutines.flow.mapNotNull 24 | import kotlinx.coroutines.launch 25 | import ru.ivk1800.theme.LocalThemeIsDark 26 | import kotlin.time.Duration.Companion.milliseconds 27 | 28 | @OptIn(ExperimentalCoroutinesApi::class) 29 | @Composable 30 | fun HighlightedText(text: String) { 31 | val layoutResult = remember { mutableStateOf(null) } 32 | 33 | val rectanglesState = remember { mutableStateListOf() } 34 | 35 | // TODO Move to theme 36 | val themeIsDark = LocalThemeIsDark.current 37 | val highlightColor = remember(themeIsDark.value) { 38 | if (themeIsDark.value) { 39 | Color.Yellow 40 | } else { 41 | Color.Red 42 | } 43 | .copy(alpha = 0.2f) 44 | } 45 | 46 | Box(modifier = Modifier.padding(16.dp)) { 47 | Text( 48 | text = text, 49 | onTextLayout = { layoutResult.value = it }, 50 | fontFamily = FontFamily.Monospace, 51 | modifier = Modifier 52 | .drawBehind { 53 | rectanglesState.forEach { rect -> 54 | drawRect( 55 | color = highlightColor, 56 | topLeft = rect.topLeft, 57 | size = rect.size, 58 | ) 59 | } 60 | }, 61 | ) 62 | 63 | LaunchedEffect(Unit) { 64 | snapshotFlow { layoutResult.value } 65 | .filterNotNull() 66 | .flatMapLatest { layoutResult -> 67 | HighlightManager.events 68 | .mapNotNull { event -> 69 | val start = event.startOffset 70 | val end = event.endOffset + 1 71 | 72 | if (end !in layoutResult.multiParagraph.intrinsics.annotatedString.text.indices) { 73 | return@mapNotNull null 74 | } 75 | 76 | if (start >= 0) { 77 | (start until end).map { index -> layoutResult.getBoundingBox(index) } 78 | } else { 79 | null 80 | } 81 | } 82 | } 83 | .collect { newRectangles -> 84 | launch { 85 | rectanglesState.addAll(newRectangles) 86 | delay(100.milliseconds) 87 | newRectangles.forEach(rectanglesState::remove) 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /kotlin-ir-plugin/src/main/kotlin/ru/ivk1800/HighlightedFirExtension.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800 2 | 3 | import org.jetbrains.kotlin.GeneratedDeclarationKey 4 | import org.jetbrains.kotlin.fir.FirSession 5 | import org.jetbrains.kotlin.fir.expressions.builder.buildAnnotation 6 | import org.jetbrains.kotlin.fir.expressions.impl.FirEmptyAnnotationArgumentMapping 7 | import org.jetbrains.kotlin.fir.extensions.FirDeclarationGenerationExtension 8 | import org.jetbrains.kotlin.fir.extensions.FirDeclarationPredicateRegistrar 9 | import org.jetbrains.kotlin.fir.extensions.MemberGenerationContext 10 | import org.jetbrains.kotlin.fir.extensions.predicate.LookupPredicate 11 | import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider 12 | import org.jetbrains.kotlin.fir.plugin.createMemberFunction 13 | import org.jetbrains.kotlin.fir.symbols.SymbolInternals 14 | import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol 15 | import org.jetbrains.kotlin.fir.symbols.impl.FirNamedFunctionSymbol 16 | import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol 17 | import org.jetbrains.kotlin.fir.types.ConeTypeProjection 18 | import org.jetbrains.kotlin.fir.types.builder.buildResolvedTypeRef 19 | import org.jetbrains.kotlin.fir.types.constructClassType 20 | import org.jetbrains.kotlin.fir.types.toLookupTag 21 | import org.jetbrains.kotlin.name.CallableId 22 | import org.jetbrains.kotlin.name.ClassId 23 | import org.jetbrains.kotlin.name.FqName 24 | import org.jetbrains.kotlin.name.Name 25 | 26 | class HighlightedFirExtension(session: FirSession) : FirDeclarationGenerationExtension(session) { 27 | 28 | companion object { 29 | private val HighlightedSourceCodeName = Name.identifier("HighlightedSourceCode") 30 | private val Predicate = LookupPredicate.create { annotated(FqName("ru.ivk1800.Highlighted")) } 31 | } 32 | 33 | private val matchedClasses by lazy { 34 | session.predicateBasedProvider.getSymbolsByPredicate(Predicate).filterIsInstance() 35 | } 36 | 37 | @OptIn(SymbolInternals::class) 38 | override fun generateFunctions( 39 | callableId: CallableId, 40 | context: MemberGenerationContext?, 41 | ): List { 42 | if (callableId.callableName != HighlightedSourceCodeName || context == null) return emptyList() 43 | 44 | val function = createMemberFunction( 45 | owner = context.owner, 46 | key = Key, 47 | name = callableId.callableName, 48 | returnType = session.builtinTypes.unitType.coneType, 49 | ) 50 | function.replaceAnnotations( 51 | listOf( 52 | buildAnnotation { 53 | annotationTypeRef = buildResolvedTypeRef { 54 | val constructClassType = 55 | ClassId(FqName("androidx.compose.runtime"), Name.identifier("Composable")).toLookupTag() 56 | .constructClassType(typeArguments = ConeTypeProjection.EMPTY_ARRAY) 57 | coneType = constructClassType 58 | } 59 | argumentMapping = FirEmptyAnnotationArgumentMapping 60 | }, 61 | ), 62 | ) 63 | return listOf(function.symbol) 64 | } 65 | 66 | override fun getCallableNamesForClass(classSymbol: FirClassSymbol<*>, context: MemberGenerationContext): Set { 67 | return when { 68 | classSymbol in matchedClasses -> setOf(HighlightedSourceCodeName) 69 | else -> emptySet() 70 | } 71 | } 72 | 73 | override fun FirDeclarationPredicateRegistrar.registerPredicates() { 74 | register(Predicate) 75 | } 76 | 77 | object Key : GeneratedDeclarationKey() { 78 | override fun toString(): String = "HighlightedSourceCodeKey" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | //generated by https://materialkolor.com 6 | //Color palette was taken here: https://coolors.co/palette/e63946-f1faee-a8dadc-457b9d-1d3557 7 | 8 | internal val Seed = Color(0xFF1D3557) 9 | 10 | internal val PrimaryLight = Color(0xFF485F84) 11 | internal val OnPrimaryLight = Color(0xFFFFFFFF) 12 | internal val PrimaryContainerLight = Color(0xFFD5E3FF) 13 | internal val OnPrimaryContainerLight = Color(0xFF30476A) 14 | internal val SecondaryLight = Color(0xFF2B6485) 15 | internal val OnSecondaryLight = Color(0xFFFFFFFF) 16 | internal val SecondaryContainerLight = Color(0xFFC7E7FF) 17 | internal val OnSecondaryContainerLight = Color(0xFF064C6B) 18 | internal val TertiaryLight = Color(0xFF356668) 19 | internal val OnTertiaryLight = Color(0xFFFFFFFF) 20 | internal val TertiaryContainerLight = Color(0xFFB9ECEE) 21 | internal val OnTertiaryContainerLight = Color(0xFF1A4E50) 22 | internal val ErrorLight = Color(0xFFBB152C) 23 | internal val OnErrorLight = Color(0xFFFFFFFF) 24 | internal val ErrorContainerLight = Color(0xFFFFDAD8) 25 | internal val OnErrorContainerLight = Color(0xFF410007) 26 | internal val BackgroundLight = Color(0xFFF9F9F9) 27 | internal val OnBackgroundLight = Color(0xFF1A1C1C) 28 | internal val SurfaceLight = Color(0xFFF9F9F9) 29 | internal val OnSurfaceLight = Color(0xFF1A1C1C) 30 | internal val SurfaceVariantLight = Color(0xFFDCE5D9) 31 | internal val OnSurfaceVariantLight = Color(0xFF404941) 32 | internal val OutlineLight = Color(0xFF717970) 33 | internal val OutlineVariantLight = Color(0xFFC0C9BE) 34 | internal val ScrimLight = Color(0xFF000000) 35 | internal val InverseSurfaceLight = Color(0xFF2F3131) 36 | internal val InverseOnSurfaceLight = Color(0xFFF0F1F1) 37 | internal val InversePrimaryLight = Color(0xFFB0C7F1) 38 | internal val SurfaceDimLight = Color(0xFFDADADA) 39 | internal val SurfaceBrightLight = Color(0xFFF9F9F9) 40 | internal val SurfaceContainerLowestLight = Color(0xFFFFFFFF) 41 | internal val SurfaceContainerLowLight = Color(0xFFF3F3F4) 42 | internal val SurfaceContainerLight = Color(0xFFEEEEEE) 43 | internal val SurfaceContainerHighLight = Color(0xFFE8E8E8) 44 | internal val SurfaceContainerHighestLight = Color(0xFFE2E2E2) 45 | 46 | internal val PrimaryDark = Color(0xFFB0C7F1) 47 | internal val OnPrimaryDark = Color(0xFF183153) 48 | internal val PrimaryContainerDark = Color(0xFF30476A) 49 | internal val OnPrimaryContainerDark = Color(0xFFD5E3FF) 50 | internal val SecondaryDark = Color(0xFF98CDF2) 51 | internal val OnSecondaryDark = Color(0xFF00344C) 52 | internal val SecondaryContainerDark = Color(0xFF064C6B) 53 | internal val OnSecondaryContainerDark = Color(0xFFC7E7FF) 54 | internal val TertiaryDark = Color(0xFF9ECFD1) 55 | internal val OnTertiaryDark = Color(0xFF003739) 56 | internal val TertiaryContainerDark = Color(0xFF1A4E50) 57 | internal val OnTertiaryContainerDark = Color(0xFFB9ECEE) 58 | internal val ErrorDark = Color(0xFFFFB3B1) 59 | internal val OnErrorDark = Color(0xFF680011) 60 | internal val ErrorContainerDark = Color(0xFF92001C) 61 | internal val OnErrorContainerDark = Color(0xFFFFDAD8) 62 | internal val BackgroundDark = Color(0xFF121414) 63 | internal val OnBackgroundDark = Color(0xFFE2E2E2) 64 | internal val SurfaceDark = Color(0xFF121414) 65 | internal val OnSurfaceDark = Color(0xFFE2E2E2) 66 | internal val SurfaceVariantDark = Color(0xFF404941) 67 | internal val OnSurfaceVariantDark = Color(0xFFC0C9BE) 68 | internal val OutlineDark = Color(0xFF8A9389) 69 | internal val OutlineVariantDark = Color(0xFF404941) 70 | internal val ScrimDark = Color(0xFF000000) 71 | internal val InverseSurfaceDark = Color(0xFFE2E2E2) 72 | internal val InverseOnSurfaceDark = Color(0xFF2F3131) 73 | internal val InversePrimaryDark = Color(0xFF485F84) 74 | internal val SurfaceDimDark = Color(0xFF121414) 75 | internal val SurfaceBrightDark = Color(0xFF37393A) 76 | internal val SurfaceContainerLowestDark = Color(0xFF0C0F0F) 77 | internal val SurfaceContainerLowDark = Color(0xFF1A1C1C) 78 | internal val SurfaceContainerDark = Color(0xFF1E2020) 79 | internal val SurfaceContainerHighDark = Color(0xFF282A2B) 80 | internal val SurfaceContainerHighestDark = Color(0xFF333535) 81 | -------------------------------------------------------------------------------- /kotlin-ir-plugin/src/main/kotlin/ru/ivk1800/HighlightInjector.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800 2 | 3 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 4 | import org.jetbrains.kotlin.ir.IrElement 5 | import org.jetbrains.kotlin.ir.IrFileEntry 6 | import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET 7 | import org.jetbrains.kotlin.ir.declarations.IrClass 8 | import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction 9 | import org.jetbrains.kotlin.ir.expressions.IrBlock 10 | import org.jetbrains.kotlin.ir.expressions.IrBlockBody 11 | import org.jetbrains.kotlin.ir.expressions.IrFunctionExpression 12 | import org.jetbrains.kotlin.ir.expressions.IrStatementContainer 13 | import org.jetbrains.kotlin.ir.expressions.IrWhileLoop 14 | import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl 15 | import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl 16 | import org.jetbrains.kotlin.ir.expressions.impl.fromSymbolOwner 17 | import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol 18 | import org.jetbrains.kotlin.ir.util.fileEntry 19 | import org.jetbrains.kotlin.ir.util.hasAnnotation 20 | import org.jetbrains.kotlin.ir.visitors.IrVisitor 21 | import org.jetbrains.kotlin.name.CallableId 22 | import org.jetbrains.kotlin.name.FqName 23 | import org.jetbrains.kotlin.name.Name 24 | 25 | class HighlightInjector( 26 | private val context: IrPluginContext, 27 | ) : IrVisitor() { 28 | 29 | private val highlightedAnnotation = FqName("ru.ivk1800.Highlighted") 30 | 31 | private val highlightSymbol: IrSimpleFunctionSymbol = 32 | context.referenceFunctions(CallableId(FqName("ru.ivk1800"), Name.identifier("highlight"))).first() 33 | 34 | class Data(var irClass: IrClass? = null) 35 | 36 | override fun visitElement(element: IrElement, data: Data) { 37 | element.acceptChildren(this, data) 38 | } 39 | 40 | override fun visitClass(declaration: IrClass, data: Data) { 41 | if (declaration.annotations.hasAnnotation(highlightedAnnotation)) { 42 | data.irClass = declaration 43 | super.visitClass(declaration, data) 44 | } 45 | } 46 | 47 | override fun visitSimpleFunction(declaration: IrSimpleFunction, data: Data) { 48 | val irClass = data.irClass ?: return 49 | val body = declaration.body as? IrStatementContainer ?: return 50 | injectHighlightCall(fileEntry = irClass.fileEntry, body = body) 51 | super.visitSimpleFunction(declaration, data) 52 | } 53 | 54 | override fun visitFunctionExpression(expression: IrFunctionExpression, data: Data) { 55 | val irClass = data.irClass ?: return 56 | val body = expression.function.body as? IrStatementContainer ?: return 57 | injectHighlightCall(fileEntry = irClass.fileEntry, body = body) 58 | super.visitFunctionExpression(expression, data) 59 | } 60 | 61 | override fun visitWhileLoop(loop: IrWhileLoop, data: Data) { 62 | val irClass = data.irClass ?: return 63 | val body = loop.body as? IrStatementContainer ?: return 64 | injectHighlightCall(fileEntry = irClass.fileEntry, body = body) 65 | super.visitWhileLoop(loop, data) 66 | } 67 | 68 | private fun injectHighlightCall(fileEntry: IrFileEntry, body: IrStatementContainer) { 69 | val bodySourceRangeInfo = fileEntry.getSourceRangeInfo( 70 | beginOffset = body.startOffset, 71 | endOffset = body.endOffset, 72 | ) 73 | 74 | val highlightCall = IrCallImpl.fromSymbolOwner( 75 | startOffset = UNDEFINED_OFFSET, 76 | endOffset = UNDEFINED_OFFSET, 77 | type = context.irBuiltIns.unitType, 78 | symbol = highlightSymbol, 79 | ) 80 | highlightCall.arguments[0] = IrConstImpl.int( 81 | startOffset = UNDEFINED_OFFSET, 82 | endOffset = UNDEFINED_OFFSET, 83 | type = context.irBuiltIns.intType, 84 | value = bodySourceRangeInfo.startOffset, 85 | ) 86 | highlightCall.arguments[1] = IrConstImpl.int( 87 | startOffset = UNDEFINED_OFFSET, 88 | endOffset = UNDEFINED_OFFSET, 89 | type = context.irBuiltIns.intType, 90 | value = bodySourceRangeInfo.endOffset, 91 | ) 92 | body.statements.add(0, highlightCall) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.Surface 6 | import androidx.compose.material3.darkColorScheme 7 | import androidx.compose.material3.lightColorScheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.CompositionLocalProvider 10 | import androidx.compose.runtime.compositionLocalOf 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | 15 | private val LightColorScheme = lightColorScheme( 16 | primary = PrimaryLight, 17 | onPrimary = OnPrimaryLight, 18 | primaryContainer = PrimaryContainerLight, 19 | onPrimaryContainer = OnPrimaryContainerLight, 20 | secondary = SecondaryLight, 21 | onSecondary = OnSecondaryLight, 22 | secondaryContainer = SecondaryContainerLight, 23 | onSecondaryContainer = OnSecondaryContainerLight, 24 | tertiary = TertiaryLight, 25 | onTertiary = OnTertiaryLight, 26 | tertiaryContainer = TertiaryContainerLight, 27 | onTertiaryContainer = OnTertiaryContainerLight, 28 | error = ErrorLight, 29 | onError = OnErrorLight, 30 | errorContainer = ErrorContainerLight, 31 | onErrorContainer = OnErrorContainerLight, 32 | background = BackgroundLight, 33 | onBackground = OnBackgroundLight, 34 | surface = SurfaceLight, 35 | onSurface = OnSurfaceLight, 36 | surfaceVariant = SurfaceVariantLight, 37 | onSurfaceVariant = OnSurfaceVariantLight, 38 | outline = OutlineLight, 39 | outlineVariant = OutlineVariantLight, 40 | scrim = ScrimLight, 41 | inverseSurface = InverseSurfaceLight, 42 | inverseOnSurface = InverseOnSurfaceLight, 43 | inversePrimary = InversePrimaryLight, 44 | surfaceDim = SurfaceDimLight, 45 | surfaceBright = SurfaceBrightLight, 46 | surfaceContainerLowest = SurfaceContainerLowestLight, 47 | surfaceContainerLow = SurfaceContainerLowLight, 48 | surfaceContainer = SurfaceContainerLight, 49 | surfaceContainerHigh = SurfaceContainerHighLight, 50 | surfaceContainerHighest = SurfaceContainerHighestLight, 51 | ) 52 | 53 | private val DarkColorScheme = darkColorScheme( 54 | primary = PrimaryDark, 55 | onPrimary = OnPrimaryDark, 56 | primaryContainer = PrimaryContainerDark, 57 | onPrimaryContainer = OnPrimaryContainerDark, 58 | secondary = SecondaryDark, 59 | onSecondary = OnSecondaryDark, 60 | secondaryContainer = SecondaryContainerDark, 61 | onSecondaryContainer = OnSecondaryContainerDark, 62 | tertiary = TertiaryDark, 63 | onTertiary = OnTertiaryDark, 64 | tertiaryContainer = TertiaryContainerDark, 65 | onTertiaryContainer = OnTertiaryContainerDark, 66 | error = ErrorDark, 67 | onError = OnErrorDark, 68 | errorContainer = ErrorContainerDark, 69 | onErrorContainer = OnErrorContainerDark, 70 | background = BackgroundDark, 71 | onBackground = OnBackgroundDark, 72 | surface = SurfaceDark, 73 | onSurface = OnSurfaceDark, 74 | surfaceVariant = SurfaceVariantDark, 75 | onSurfaceVariant = OnSurfaceVariantDark, 76 | outline = OutlineDark, 77 | outlineVariant = OutlineVariantDark, 78 | scrim = ScrimDark, 79 | inverseSurface = InverseSurfaceDark, 80 | inverseOnSurface = InverseOnSurfaceDark, 81 | inversePrimary = InversePrimaryDark, 82 | surfaceDim = SurfaceDimDark, 83 | surfaceBright = SurfaceBrightDark, 84 | surfaceContainerLowest = SurfaceContainerLowestDark, 85 | surfaceContainerLow = SurfaceContainerLowDark, 86 | surfaceContainer = SurfaceContainerDark, 87 | surfaceContainerHigh = SurfaceContainerHighDark, 88 | surfaceContainerHighest = SurfaceContainerHighestDark, 89 | ) 90 | 91 | internal val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) } 92 | 93 | @Composable 94 | internal fun AppTheme( 95 | content: @Composable () -> Unit, 96 | ) { 97 | val systemIsDark = isSystemInDarkTheme() 98 | val isDarkState = remember(systemIsDark) { mutableStateOf(systemIsDark) } 99 | CompositionLocalProvider( 100 | LocalThemeIsDark provides isDarkState, 101 | ) { 102 | val isDark by isDarkState 103 | MaterialTheme( 104 | colorScheme = if (isDark) DarkColorScheme else LightColorScheme, 105 | content = { Surface(content = content) }, 106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /composeApp/dependencies/jsCompileClasspath.txt: -------------------------------------------------------------------------------- 1 | androidx.annotation:annotation-js:1.9.1 2 | androidx.annotation:annotation:1.9.1 3 | androidx.collection:collection-js:1.5.0 4 | androidx.collection:collection:1.5.0 5 | androidx.compose.runtime:runtime-annotation-js:1.9.0 6 | androidx.compose.runtime:runtime-annotation:1.9.0 7 | androidx.compose.runtime:runtime-js:1.9.0 8 | androidx.compose.runtime:runtime:1.9.0 9 | androidx.lifecycle:lifecycle-common-js:2.9.2 10 | androidx.lifecycle:lifecycle-common:2.9.2 11 | androidx.savedstate:savedstate-js:1.3.1 12 | androidx.savedstate:savedstate:1.3.1 13 | io.ktor:ktor-client-core-js:3.2.1 14 | io.ktor:ktor-client-core:3.2.1 15 | io.ktor:ktor-events-js:3.2.1 16 | io.ktor:ktor-events:3.2.1 17 | io.ktor:ktor-http-cio-js:3.2.1 18 | io.ktor:ktor-http-cio:3.2.1 19 | io.ktor:ktor-http-js:3.2.1 20 | io.ktor:ktor-http:3.2.1 21 | io.ktor:ktor-io-js:3.2.1 22 | io.ktor:ktor-io:3.2.1 23 | io.ktor:ktor-serialization-js:3.2.1 24 | io.ktor:ktor-serialization:3.2.1 25 | io.ktor:ktor-sse-js:3.2.1 26 | io.ktor:ktor-sse:3.2.1 27 | io.ktor:ktor-utils-js:3.2.1 28 | io.ktor:ktor-utils:3.2.1 29 | io.ktor:ktor-websocket-serialization-js:3.2.1 30 | io.ktor:ktor-websocket-serialization:3.2.1 31 | io.ktor:ktor-websockets-js:3.2.1 32 | io.ktor:ktor-websockets:3.2.1 33 | org.jetbrains.androidx.lifecycle:lifecycle-common-js:2.9.4 34 | org.jetbrains.androidx.lifecycle:lifecycle-common:2.9.4 35 | org.jetbrains.androidx.lifecycle:lifecycle-runtime-js:2.9.0-beta01 36 | org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.9.0-beta01 37 | org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-js:2.9.0-beta01 38 | org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate-js:2.9.0-beta01 39 | org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.0-beta01 40 | org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.0-beta01 41 | org.jetbrains.androidx.navigation:navigation-common-js:2.9.0-beta01 42 | org.jetbrains.androidx.navigation:navigation-common:2.9.0-beta01 43 | org.jetbrains.androidx.navigation:navigation-compose-js:2.9.0-beta01 44 | org.jetbrains.androidx.navigation:navigation-compose:2.9.0-beta01 45 | org.jetbrains.androidx.navigation:navigation-runtime-js:2.9.0-beta01 46 | org.jetbrains.androidx.navigation:navigation-runtime:2.9.0-beta01 47 | org.jetbrains.androidx.savedstate:savedstate-compose-js:1.3.4 48 | org.jetbrains.androidx.savedstate:savedstate-compose:1.3.4 49 | org.jetbrains.androidx.savedstate:savedstate-js:1.3.4 50 | org.jetbrains.androidx.savedstate:savedstate:1.3.4 51 | org.jetbrains.compose.animation:animation-core-js:1.9.0 52 | org.jetbrains.compose.animation:animation-core:1.9.0 53 | org.jetbrains.compose.animation:animation-js:1.9.0 54 | org.jetbrains.compose.animation:animation:1.9.0 55 | org.jetbrains.compose.annotation-internal:annotation-js:1.8.0 56 | org.jetbrains.compose.annotation-internal:annotation:1.8.0 57 | org.jetbrains.compose.components:components-resources-js:1.9.0 58 | org.jetbrains.compose.components:components-resources:1.9.0 59 | org.jetbrains.compose.components:components-ui-tooling-preview-js:1.9.0 60 | org.jetbrains.compose.components:components-ui-tooling-preview:1.9.0 61 | org.jetbrains.compose.foundation:foundation-js:1.9.0 62 | org.jetbrains.compose.foundation:foundation-layout-js:1.9.0 63 | org.jetbrains.compose.foundation:foundation-layout:1.9.0 64 | org.jetbrains.compose.foundation:foundation:1.9.0 65 | org.jetbrains.compose.material3:material3-js:1.8.2 66 | org.jetbrains.compose.material3:material3:1.8.2 67 | org.jetbrains.compose.material:material-ripple-js:1.8.2 68 | org.jetbrains.compose.material:material-ripple:1.8.2 69 | org.jetbrains.compose.runtime:runtime-js:1.9.0 70 | org.jetbrains.compose.runtime:runtime-saveable-js:1.9.0 71 | org.jetbrains.compose.runtime:runtime-saveable:1.9.0 72 | org.jetbrains.compose.runtime:runtime:1.9.0 73 | org.jetbrains.compose.ui:ui-geometry-js:1.9.0 74 | org.jetbrains.compose.ui:ui-geometry:1.9.0 75 | org.jetbrains.compose.ui:ui-graphics-js:1.9.0 76 | org.jetbrains.compose.ui:ui-graphics:1.9.0 77 | org.jetbrains.compose.ui:ui-js:1.9.0 78 | org.jetbrains.compose.ui:ui-text-js:1.9.0 79 | org.jetbrains.compose.ui:ui-text:1.9.0 80 | org.jetbrains.compose.ui:ui-unit-js:1.9.0 81 | org.jetbrains.compose.ui:ui-unit:1.9.0 82 | org.jetbrains.compose.ui:ui-util-js:1.9.0 83 | org.jetbrains.compose.ui:ui-util:1.9.0 84 | org.jetbrains.compose.ui:ui:1.9.0 85 | org.jetbrains.kotlin:kotlin-dom-api-compat:2.2.20 86 | org.jetbrains.kotlin:kotlin-stdlib-js:2.2.20 87 | org.jetbrains.kotlin:kotlin-stdlib:2.2.20 88 | org.jetbrains.kotlinx:atomicfu-js:0.27.0 89 | org.jetbrains.kotlinx:atomicfu:0.27.0 90 | org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.10.2 91 | org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2 92 | org.jetbrains.kotlinx:kotlinx-io-bytestring-js:0.7.0 93 | org.jetbrains.kotlinx:kotlinx-io-bytestring:0.7.0 94 | org.jetbrains.kotlinx:kotlinx-io-core-js:0.7.0 95 | org.jetbrains.kotlinx:kotlinx-io-core:0.7.0 96 | org.jetbrains.kotlinx:kotlinx-serialization-core-js:1.8.1 97 | org.jetbrains.kotlinx:kotlinx-serialization-core:1.8.1 98 | org.jetbrains.kotlinx:kotlinx-serialization-json-js:1.8.1 99 | org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1 100 | org.jetbrains.skiko:skiko-js:0.9.22.2 101 | org.jetbrains.skiko:skiko:0.9.22.2 102 | -------------------------------------------------------------------------------- /composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.ExperimentalComposeLibrary 2 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 3 | import org.jetbrains.compose.reload.gradle.ComposeHotRun 4 | import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag 5 | 6 | plugins { 7 | alias(libs.plugins.multiplatform) 8 | alias(libs.plugins.compose.compiler) 9 | alias(libs.plugins.compose) 10 | alias(libs.plugins.hotReload) 11 | alias(libs.plugins.kotlin.serialization) 12 | alias(libs.plugins.dependencyGuard) 13 | alias(libs.plugins.buildconfig) 14 | } 15 | 16 | dependencyGuard { 17 | configuration("jsRuntimeClasspath") 18 | configuration("jsCompileClasspath") 19 | } 20 | 21 | buildConfig { 22 | buildConfigField("String", "KOTLIN_VERSION", "\"${libs.versions.kotlin.get()}\"") 23 | buildConfigField("String", "COMPOSE_VERSION", "\"${libs.versions.compose.get()}\"") 24 | } 25 | 26 | kotlin { 27 | jvm() 28 | 29 | js { 30 | browser() 31 | binaries.executable() 32 | } 33 | 34 | sourceSets { 35 | commonMain.dependencies { 36 | implementation(compose.runtime) 37 | implementation(compose.foundation) 38 | implementation(compose.material3) 39 | implementation(compose.components.resources) 40 | implementation(compose.components.uiToolingPreview) 41 | implementation(libs.ktor.client.core) 42 | implementation(libs.navigation.compose) 43 | implementation(libs.kotlinx.serialization.json) 44 | } 45 | 46 | commonTest.dependencies { 47 | implementation(kotlin("test")) 48 | @OptIn(ExperimentalComposeLibrary::class) 49 | implementation(compose.uiTest) 50 | } 51 | 52 | jvmMain.dependencies { 53 | implementation(compose.desktop.currentOs) 54 | } 55 | } 56 | } 57 | 58 | compose.resources { 59 | packageOfResClass = "ru.ivk1800.resources" 60 | } 61 | 62 | compose.desktop { 63 | application { 64 | mainClass = "MainKt" 65 | 66 | nativeDistributions { 67 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 68 | packageName = "Recomposition visualization" 69 | packageVersion = "1.0.0" 70 | 71 | linux { 72 | iconFile.set(project.file("desktopAppIcons/LinuxIcon.png")) 73 | } 74 | windows { 75 | iconFile.set(project.file("desktopAppIcons/WindowsIcon.ico")) 76 | } 77 | macOS { 78 | iconFile.set(project.file("desktopAppIcons/MacosIcon.icns")) 79 | bundleID = "ru.ivk1800.desktopApp" 80 | } 81 | } 82 | } 83 | } 84 | 85 | //https://github.com/JetBrains/compose-hot-reload 86 | composeCompiler { 87 | featureFlags.add(ComposeFeatureFlag.OptimizeNonSkippingGroups) 88 | } 89 | tasks.withType().configureEach { 90 | mainClass.set("MainKt") 91 | } 92 | 93 | dependencies { 94 | org.jetbrains.kotlin.gradle.plugin.PLUGIN_CLASSPATH_CONFIGURATION_NAME(project(":kotlin-ir-plugin")) 95 | } 96 | 97 | tasks.register("buildApp") { 98 | dependsOn("jsBrowserDistribution") 99 | 100 | val distDir = layout.buildDirectory.dir("dist/js/productionExecutable") 101 | val buildTimestamp: String by extra { System.currentTimeMillis().toString() } 102 | 103 | doLast { 104 | val dist = distDir.get().asFile 105 | if (!dist.exists()) { 106 | throw GradleException("Distribution directory not found: $distDir") 107 | } 108 | 109 | val composeResourcesDir = File(dist, "composeResources") 110 | if (!composeResourcesDir.exists()) { 111 | throw GradleException("composeResources not found in $dist") 112 | } 113 | val newComposeResourcesDirName = "composeResources-${buildTimestamp}" 114 | composeResourcesDir.renameTo(File(dist, newComposeResourcesDirName)) 115 | 116 | val targetDir = File(dist, buildTimestamp) 117 | targetDir.mkdirs() 118 | 119 | dist.listFiles() 120 | ?.filter { 121 | it.isFile && (it.name.startsWith("composeApp") || it.name.startsWith("skiko")) || 122 | it.name.endsWith(".wasm") 123 | } 124 | ?.forEach { file -> 125 | file.copyTo(File(targetDir, file.name), overwrite = true) 126 | file.delete() 127 | } 128 | 129 | val index = File(dist, "index.html") 130 | if (!index.exists()) { 131 | throw GradleException("index.html not found in $dist") 132 | } 133 | 134 | val updatedIndex = index.readText() 135 | .replace( 136 | """""".toRegex(), 137 | """""", 138 | ) 139 | .replace( 140 | """""".toRegex(), 141 | """""", 142 | ) 143 | 144 | index.writeText(updatedIndex) 145 | 146 | val composeAppFile = File(targetDir, "composeApp.js") 147 | if (!composeAppFile.exists()) { 148 | throw GradleException("composeApp.js not found in $dist") 149 | } 150 | 151 | val newComposeAppContent = composeAppFile.readText() 152 | .replace( 153 | "composeResources/ru.ivk1800.resources/", 154 | "$newComposeResourcesDirName/ru.ivk1800.resources/", 155 | ) 156 | composeAppFile.writeText(newComposeAppContent) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/screen/main/MainScreen.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.screen.main 2 | 3 | import Recomposition_visualization.composeApp.BuildConfig 4 | import androidx.compose.foundation.LocalScrollbarStyle 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.widthIn 13 | import androidx.compose.foundation.lazy.LazyColumn 14 | import androidx.compose.foundation.lazy.itemsIndexed 15 | import androidx.compose.material3.AlertDialog 16 | import androidx.compose.material3.HorizontalDivider 17 | import androidx.compose.material3.Icon 18 | import androidx.compose.material3.IconButton 19 | import androidx.compose.material3.ListItem 20 | import androidx.compose.material3.MaterialTheme 21 | import androidx.compose.material3.Scaffold 22 | import androidx.compose.material3.Text 23 | import androidx.compose.material3.TextButton 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.getValue 26 | import androidx.compose.runtime.mutableStateOf 27 | import androidx.compose.runtime.remember 28 | import androidx.compose.runtime.setValue 29 | import androidx.compose.ui.Alignment 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.platform.LocalUriHandler 32 | import androidx.compose.ui.text.style.TextAlign 33 | import androidx.compose.ui.unit.dp 34 | import org.jetbrains.compose.resources.stringResource 35 | import ru.ivk1800.SampleDescriptor 36 | import ru.ivk1800.navigation.LocalNavHostController 37 | import ru.ivk1800.navigation.SampleDestination 38 | import ru.ivk1800.resources.Res 39 | import ru.ivk1800.resources.app_title 40 | import ru.ivk1800.resources.build_info 41 | import ru.ivk1800.resources.compose_version 42 | import ru.ivk1800.resources.kotlin_version 43 | import ru.ivk1800.theme.LocalThemeIsDark 44 | import ru.ivk1800.utils.AlertCircle 45 | import ru.ivk1800.utils.DarkMode 46 | import ru.ivk1800.utils.Github 47 | 48 | @Composable 49 | fun MainScreen() { 50 | var showBuildInfoDialog by remember { mutableStateOf(false) } 51 | 52 | Scaffold( 53 | topBar = { 54 | Row( 55 | modifier = Modifier.fillMaxWidth(), 56 | horizontalArrangement = Arrangement.End, 57 | ) { 58 | val uriHandler = LocalUriHandler.current 59 | IconButton( 60 | onClick = { 61 | showBuildInfoDialog = true 62 | }, 63 | ) { 64 | Icon( 65 | imageVector = AlertCircle, 66 | contentDescription = "BuildInfo", 67 | ) 68 | } 69 | IconButton( 70 | onClick = { 71 | uriHandler.openUri("https://github.com/ivk1800/recomposition-visualization") 72 | }, 73 | ) { 74 | Icon( 75 | imageVector = Github, 76 | contentDescription = "Github", 77 | ) 78 | } 79 | val themeIsDark = LocalThemeIsDark.current 80 | IconButton( 81 | onClick = { 82 | themeIsDark.value = !themeIsDark.value 83 | }, 84 | ) { 85 | Icon( 86 | imageVector = DarkMode, 87 | contentDescription = "DarkMode", 88 | ) 89 | } 90 | } 91 | }, 92 | ) { 93 | Box(modifier = Modifier.fillMaxSize().padding(it)) { 94 | val navController = LocalNavHostController.current 95 | 96 | LazyColumn( 97 | modifier = Modifier.widthIn(max = 600.dp).align(Alignment.Center), 98 | ) { 99 | item { 100 | Text( 101 | modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp), 102 | text = stringResource(Res.string.app_title), 103 | style = MaterialTheme.typography.headlineLarge, 104 | textAlign = TextAlign.Center, 105 | ) 106 | } 107 | 108 | val items = SampleDescriptor.entries.toList() 109 | itemsIndexed(items = items) { index, sampleId -> 110 | ListItem( 111 | modifier = Modifier.clickable { 112 | navController.navigate(SampleDestination(sampleId)) 113 | }, 114 | headlineContent = { 115 | Text("${index + 1}. ${stringResource(sampleId.title)}") 116 | }, 117 | ) 118 | HorizontalDivider(modifier = Modifier.padding(end = LocalScrollbarStyle.current.thickness)) 119 | } 120 | } 121 | } 122 | } 123 | 124 | if (showBuildInfoDialog) { 125 | AlertDialog( 126 | onDismissRequest = { showBuildInfoDialog = false }, 127 | confirmButton = { 128 | TextButton( 129 | onClick = { showBuildInfoDialog = false }, 130 | content = { Text("OK") }, 131 | ) 132 | }, 133 | title = { Text(stringResource(Res.string.build_info)) }, 134 | text = { 135 | Text( 136 | buildString { 137 | append(stringResource(Res.string.kotlin_version, BuildConfig.KOTLIN_VERSION)) 138 | appendLine() 139 | append(stringResource(Res.string.compose_version, BuildConfig.COMPOSE_VERSION)) 140 | }, 141 | ) 142 | }, 143 | ) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /composeApp/dependencies/jsRuntimeClasspath.txt: -------------------------------------------------------------------------------- 1 | androidx.annotation:annotation-js:1.9.1 2 | androidx.annotation:annotation:1.9.1 3 | androidx.collection:collection-js:1.5.0 4 | androidx.collection:collection:1.5.0 5 | androidx.compose.runtime:runtime-annotation-js:1.9.0 6 | androidx.compose.runtime:runtime-annotation:1.9.0 7 | androidx.compose.runtime:runtime-js:1.9.0 8 | androidx.compose.runtime:runtime:1.9.0 9 | androidx.lifecycle:lifecycle-common-js:2.9.2 10 | androidx.lifecycle:lifecycle-common:2.9.2 11 | androidx.lifecycle:lifecycle-runtime-js:2.9.2 12 | androidx.lifecycle:lifecycle-runtime:2.9.2 13 | androidx.lifecycle:lifecycle-viewmodel-js:2.9.2 14 | androidx.lifecycle:lifecycle-viewmodel-savedstate-js:2.9.2 15 | androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.2 16 | androidx.lifecycle:lifecycle-viewmodel:2.9.2 17 | androidx.savedstate:savedstate-js:1.3.1 18 | androidx.savedstate:savedstate:1.3.1 19 | io.ktor:ktor-client-core-js:3.2.1 20 | io.ktor:ktor-client-core:3.2.1 21 | io.ktor:ktor-events-js:3.2.1 22 | io.ktor:ktor-events:3.2.1 23 | io.ktor:ktor-http-cio-js:3.2.1 24 | io.ktor:ktor-http-cio:3.2.1 25 | io.ktor:ktor-http-js:3.2.1 26 | io.ktor:ktor-http:3.2.1 27 | io.ktor:ktor-io-js:3.2.1 28 | io.ktor:ktor-io:3.2.1 29 | io.ktor:ktor-serialization-js:3.2.1 30 | io.ktor:ktor-serialization:3.2.1 31 | io.ktor:ktor-sse-js:3.2.1 32 | io.ktor:ktor-sse:3.2.1 33 | io.ktor:ktor-utils-js:3.2.1 34 | io.ktor:ktor-utils:3.2.1 35 | io.ktor:ktor-websocket-serialization-js:3.2.1 36 | io.ktor:ktor-websocket-serialization:3.2.1 37 | io.ktor:ktor-websockets-js:3.2.1 38 | io.ktor:ktor-websockets:3.2.1 39 | org.jetbrains.androidx.lifecycle:lifecycle-common-js:2.9.4 40 | org.jetbrains.androidx.lifecycle:lifecycle-common:2.9.4 41 | org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose-js:2.9.4 42 | org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.9.4 43 | org.jetbrains.androidx.lifecycle:lifecycle-runtime-js:2.9.4 44 | org.jetbrains.androidx.lifecycle:lifecycle-runtime:2.9.4 45 | org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose-js:2.9.0-beta01 46 | org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.9.0-beta01 47 | org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-js:2.9.4 48 | org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate-js:2.9.4 49 | org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.9.4 50 | org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.9.4 51 | org.jetbrains.androidx.navigation:navigation-common-js:2.9.0-beta01 52 | org.jetbrains.androidx.navigation:navigation-common:2.9.0-beta01 53 | org.jetbrains.androidx.navigation:navigation-compose-js:2.9.0-beta01 54 | org.jetbrains.androidx.navigation:navigation-compose:2.9.0-beta01 55 | org.jetbrains.androidx.navigation:navigation-runtime-js:2.9.0-beta01 56 | org.jetbrains.androidx.navigation:navigation-runtime:2.9.0-beta01 57 | org.jetbrains.androidx.savedstate:savedstate-compose-js:1.3.4 58 | org.jetbrains.androidx.savedstate:savedstate-compose:1.3.4 59 | org.jetbrains.androidx.savedstate:savedstate-js:1.3.4 60 | org.jetbrains.androidx.savedstate:savedstate:1.3.4 61 | org.jetbrains.compose.animation:animation-core-js:1.9.0 62 | org.jetbrains.compose.animation:animation-core:1.9.0 63 | org.jetbrains.compose.animation:animation-js:1.9.0 64 | org.jetbrains.compose.animation:animation:1.9.0 65 | org.jetbrains.compose.annotation-internal:annotation-js:1.9.0 66 | org.jetbrains.compose.annotation-internal:annotation:1.9.0 67 | org.jetbrains.compose.collection-internal:collection-js:1.9.0 68 | org.jetbrains.compose.collection-internal:collection:1.9.0 69 | org.jetbrains.compose.components:components-resources-js:1.9.0 70 | org.jetbrains.compose.components:components-resources:1.9.0 71 | org.jetbrains.compose.components:components-ui-tooling-preview-js:1.9.0 72 | org.jetbrains.compose.components:components-ui-tooling-preview:1.9.0 73 | org.jetbrains.compose.foundation:foundation-js:1.9.0 74 | org.jetbrains.compose.foundation:foundation-layout-js:1.9.0 75 | org.jetbrains.compose.foundation:foundation-layout:1.9.0 76 | org.jetbrains.compose.foundation:foundation:1.9.0 77 | org.jetbrains.compose.material3:material3-js:1.8.2 78 | org.jetbrains.compose.material3:material3:1.8.2 79 | org.jetbrains.compose.material:material-ripple-js:1.8.2 80 | org.jetbrains.compose.material:material-ripple:1.8.2 81 | org.jetbrains.compose.runtime:runtime-js:1.9.0 82 | org.jetbrains.compose.runtime:runtime-saveable-js:1.9.0 83 | org.jetbrains.compose.runtime:runtime-saveable:1.9.0 84 | org.jetbrains.compose.runtime:runtime:1.9.0 85 | org.jetbrains.compose.ui:ui-backhandler-js:1.9.0 86 | org.jetbrains.compose.ui:ui-backhandler:1.9.0 87 | org.jetbrains.compose.ui:ui-geometry-js:1.9.0 88 | org.jetbrains.compose.ui:ui-geometry:1.9.0 89 | org.jetbrains.compose.ui:ui-graphics-js:1.9.0 90 | org.jetbrains.compose.ui:ui-graphics:1.9.0 91 | org.jetbrains.compose.ui:ui-js:1.9.0 92 | org.jetbrains.compose.ui:ui-text-js:1.9.0 93 | org.jetbrains.compose.ui:ui-text:1.9.0 94 | org.jetbrains.compose.ui:ui-unit-js:1.9.0 95 | org.jetbrains.compose.ui:ui-unit:1.9.0 96 | org.jetbrains.compose.ui:ui-util-js:1.9.0 97 | org.jetbrains.compose.ui:ui-util:1.9.0 98 | org.jetbrains.compose.ui:ui:1.9.0 99 | org.jetbrains.kotlin:kotlin-dom-api-compat:2.2.20 100 | org.jetbrains.kotlin:kotlin-stdlib-js:2.2.20 101 | org.jetbrains.kotlin:kotlin-stdlib:2.2.20 102 | org.jetbrains.kotlin:kotlinx-atomicfu-runtime:2.1.21 103 | org.jetbrains.kotlinx:atomicfu-js:0.27.0 104 | org.jetbrains.kotlinx:atomicfu:0.27.0 105 | org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.10.2 106 | org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2 107 | org.jetbrains.kotlinx:kotlinx-datetime-js:0.6.0 108 | org.jetbrains.kotlinx:kotlinx-datetime:0.6.0 109 | org.jetbrains.kotlinx:kotlinx-io-bytestring-js:0.7.0 110 | org.jetbrains.kotlinx:kotlinx-io-bytestring:0.7.0 111 | org.jetbrains.kotlinx:kotlinx-io-core-js:0.7.0 112 | org.jetbrains.kotlinx:kotlinx-io-core:0.7.0 113 | org.jetbrains.kotlinx:kotlinx-serialization-core-js:1.8.1 114 | org.jetbrains.kotlinx:kotlinx-serialization-core:1.8.1 115 | org.jetbrains.kotlinx:kotlinx-serialization-json-js:1.8.1 116 | org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1 117 | org.jetbrains.skiko:skiko-js:0.9.22.2 118 | org.jetbrains.skiko:skiko:0.9.22.2 119 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/utils/icons.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800.utils 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.PathFillType 5 | import androidx.compose.ui.graphics.PathFillType.Companion.EvenOdd 6 | import androidx.compose.ui.graphics.PathFillType.Companion.NonZero 7 | import androidx.compose.ui.graphics.SolidColor 8 | import androidx.compose.ui.graphics.StrokeCap 9 | import androidx.compose.ui.graphics.StrokeCap.Companion.Butt 10 | import androidx.compose.ui.graphics.StrokeCap.Companion.Round 11 | import androidx.compose.ui.graphics.StrokeJoin 12 | import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter 13 | import androidx.compose.ui.graphics.vector.DefaultFillType 14 | import androidx.compose.ui.graphics.vector.ImageVector 15 | import androidx.compose.ui.graphics.vector.PathBuilder 16 | import androidx.compose.ui.graphics.vector.path 17 | import androidx.compose.ui.unit.dp 18 | 19 | @PublishedApi 20 | internal const val MaterialIconDimension = 24f 21 | 22 | inline fun materialIcon( 23 | name: String, 24 | autoMirror: Boolean = false, 25 | block: ImageVector.Builder.() -> ImageVector.Builder, 26 | ): ImageVector = ImageVector.Builder( 27 | name = name, 28 | defaultWidth = MaterialIconDimension.dp, 29 | defaultHeight = MaterialIconDimension.dp, 30 | viewportWidth = MaterialIconDimension, 31 | viewportHeight = MaterialIconDimension, 32 | autoMirror = autoMirror, 33 | ).block().build() 34 | 35 | inline fun ImageVector.Builder.materialPath( 36 | fillAlpha: Float = 1f, 37 | strokeAlpha: Float = 1f, 38 | pathFillType: PathFillType = DefaultFillType, 39 | pathBuilder: PathBuilder.() -> Unit, 40 | ) = 41 | path( 42 | fill = SolidColor(Color.Black), 43 | fillAlpha = fillAlpha, 44 | stroke = null, 45 | strokeAlpha = strokeAlpha, 46 | strokeLineWidth = 1f, 47 | strokeLineCap = StrokeCap.Butt, 48 | strokeLineJoin = StrokeJoin.Bevel, 49 | strokeLineMiter = 1f, 50 | pathFillType = pathFillType, 51 | pathBuilder = pathBuilder, 52 | ) 53 | 54 | val ArrowBack: ImageVector 55 | get() { 56 | if (_arrowBack != null) { 57 | return _arrowBack!! 58 | } 59 | _arrowBack = materialIcon(name = "AutoMirrored.Filled.ArrowBack", autoMirror = true) { 60 | materialPath { 61 | moveTo(20.0f, 11.0f) 62 | horizontalLineTo(7.83f) 63 | lineToRelative(5.59f, -5.59f) 64 | lineTo(12.0f, 4.0f) 65 | lineToRelative(-8.0f, 8.0f) 66 | lineToRelative(8.0f, 8.0f) 67 | lineToRelative(1.41f, -1.41f) 68 | lineTo(7.83f, 13.0f) 69 | horizontalLineTo(20.0f) 70 | verticalLineToRelative(-2.0f) 71 | close() 72 | } 73 | } 74 | return _arrowBack!! 75 | } 76 | 77 | private var _arrowBack: ImageVector? = null 78 | 79 | val Github: ImageVector 80 | get() { 81 | if (_github != null) { 82 | return _github!! 83 | } 84 | _github = materialIcon(name = "Github", autoMirror = false) { 85 | materialPath { 86 | moveTo(12.0f, 0.297f) 87 | curveToRelative(-6.63f, 0.0f, -12.0f, 5.373f, -12.0f, 12.0f) 88 | curveToRelative(0.0f, 5.303f, 3.438f, 9.8f, 8.205f, 11.385f) 89 | curveToRelative(0.6f, 0.113f, 0.82f, -0.258f, 0.82f, -0.577f) 90 | curveToRelative(0.0f, -0.285f, -0.01f, -1.04f, -0.015f, -2.04f) 91 | curveToRelative(-3.338f, 0.724f, -4.042f, -1.61f, -4.042f, -1.61f) 92 | curveTo(4.422f, 18.07f, 3.633f, 17.7f, 3.633f, 17.7f) 93 | curveToRelative(-1.087f, -0.744f, 0.084f, -0.729f, 0.084f, -0.729f) 94 | curveToRelative(1.205f, 0.084f, 1.838f, 1.236f, 1.838f, 1.236f) 95 | curveToRelative(1.07f, 1.835f, 2.809f, 1.305f, 3.495f, 0.998f) 96 | curveToRelative(0.108f, -0.776f, 0.417f, -1.305f, 0.76f, -1.605f) 97 | curveToRelative(-2.665f, -0.3f, -5.466f, -1.332f, -5.466f, -5.93f) 98 | curveToRelative(0.0f, -1.31f, 0.465f, -2.38f, 1.235f, -3.22f) 99 | curveToRelative(-0.135f, -0.303f, -0.54f, -1.523f, 0.105f, -3.176f) 100 | curveToRelative(0.0f, 0.0f, 1.005f, -0.322f, 3.3f, 1.23f) 101 | curveToRelative(0.96f, -0.267f, 1.98f, -0.399f, 3.0f, -0.405f) 102 | curveToRelative(1.02f, 0.006f, 2.04f, 0.138f, 3.0f, 0.405f) 103 | curveToRelative(2.28f, -1.552f, 3.285f, -1.23f, 3.285f, -1.23f) 104 | curveToRelative(0.645f, 1.653f, 0.24f, 2.873f, 0.12f, 3.176f) 105 | curveToRelative(0.765f, 0.84f, 1.23f, 1.91f, 1.23f, 3.22f) 106 | curveToRelative(0.0f, 4.61f, -2.805f, 5.625f, -5.475f, 5.92f) 107 | curveToRelative(0.42f, 0.36f, 0.81f, 1.096f, 0.81f, 2.22f) 108 | curveToRelative(0.0f, 1.606f, -0.015f, 2.896f, -0.015f, 3.286f) 109 | curveToRelative(0.0f, 0.315f, 0.21f, 0.69f, 0.825f, 0.57f) 110 | curveTo(20.565f, 22.092f, 24.0f, 17.592f, 24.0f, 12.297f) 111 | curveToRelative(0.0f, -6.627f, -5.373f, -12.0f, -12.0f, -12.0f) 112 | } 113 | } 114 | return _github!! 115 | } 116 | 117 | private var _github: ImageVector? = null 118 | 119 | val DarkMode: ImageVector 120 | get() { 121 | if (_darkMode != null) { 122 | return _darkMode!! 123 | } 124 | _darkMode = materialIcon(name = "DarkMode", autoMirror = false) { 125 | materialPath { 126 | path( 127 | fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, 128 | strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, 129 | pathFillType = NonZero, 130 | ) { 131 | moveTo(12.0f, 16.0f) 132 | curveTo(14.209f, 16.0f, 16.0f, 14.209f, 16.0f, 12.0f) 133 | curveTo(16.0f, 9.791f, 14.209f, 8.0f, 12.0f, 8.0f) 134 | verticalLineTo(16.0f) 135 | close() 136 | } 137 | path( 138 | fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, 139 | strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, 140 | pathFillType = EvenOdd, 141 | ) { 142 | moveTo(12.0f, 2.0f) 143 | curveTo(6.477f, 2.0f, 2.0f, 6.477f, 2.0f, 12.0f) 144 | curveTo(2.0f, 17.523f, 6.477f, 22.0f, 12.0f, 22.0f) 145 | curveTo(17.523f, 22.0f, 22.0f, 17.523f, 22.0f, 12.0f) 146 | curveTo(22.0f, 6.477f, 17.523f, 2.0f, 12.0f, 2.0f) 147 | close() 148 | moveTo(12.0f, 4.0f) 149 | verticalLineTo(8.0f) 150 | curveTo(9.791f, 8.0f, 8.0f, 9.791f, 8.0f, 12.0f) 151 | curveTo(8.0f, 14.209f, 9.791f, 16.0f, 12.0f, 16.0f) 152 | verticalLineTo(20.0f) 153 | curveTo(16.418f, 20.0f, 20.0f, 16.418f, 20.0f, 12.0f) 154 | curveTo(20.0f, 7.582f, 16.418f, 4.0f, 12.0f, 4.0f) 155 | close() 156 | } 157 | } 158 | } 159 | return _darkMode!! 160 | } 161 | 162 | private var _darkMode: ImageVector? = null 163 | 164 | val AlertCircle: ImageVector 165 | get() { 166 | if (_alertCircle != null) { 167 | return _alertCircle!! 168 | } 169 | _alertCircle = materialIcon(name = "Infosys", autoMirror = false) { 170 | path( 171 | fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, 172 | strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, 173 | pathFillType = NonZero, 174 | ) { 175 | path( 176 | fill = SolidColor(Color(0x00000000)), stroke = SolidColor(Color(0xFF000000)), 177 | strokeLineWidth = 2.0f, strokeLineCap = Round, strokeLineJoin = 178 | StrokeJoin.Companion.Round, strokeLineMiter = 4.0f, pathFillType = NonZero, 179 | ) { 180 | moveTo(12.0f, 12.0f) 181 | moveToRelative(-9.0f, 0.0f) 182 | arcToRelative(9.0f, 9.0f, 0.0f, true, true, 18.0f, 0.0f) 183 | arcToRelative(9.0f, 9.0f, 0.0f, true, true, -18.0f, 0.0f) 184 | } 185 | path( 186 | fill = SolidColor(Color(0x00000000)), stroke = SolidColor(Color(0xFF000000)), 187 | strokeLineWidth = 2.0f, strokeLineCap = Round, strokeLineJoin = 188 | StrokeJoin.Companion.Round, strokeLineMiter = 4.0f, pathFillType = NonZero, 189 | ) { 190 | moveTo(12.0f, 8.0f) 191 | lineTo(12.0f, 12.0f) 192 | } 193 | path( 194 | fill = SolidColor(Color(0x00000000)), stroke = SolidColor(Color(0xFF000000)), 195 | strokeLineWidth = 2.0f, strokeLineCap = Round, strokeLineJoin = 196 | StrokeJoin.Companion.Round, strokeLineMiter = 4.0f, pathFillType = NonZero, 197 | ) { 198 | moveTo(12.0f, 16.0f) 199 | lineTo(12.01f, 16.0f) 200 | } 201 | } 202 | } 203 | return _alertCircle!! 204 | } 205 | 206 | private var _alertCircle: ImageVector? = null 207 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/values-ru/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Здесь angle пробрасывается через цепочку Function1 -> Function2 -> Function3 -> RedBox. 3 | Каждая функция принимает angle как параметр и будет рекомпозирована при каждом изменении значения. Function1, Function2 и Function3 сами по себе ничего не делают с angle, кроме как просто передают его дальше. Но поскольку angle является параметром, Compose считает их зависящими от состояния и выполняет рекомпозицию. 4 | Теперь Function1, Function2 и Function3 не зависят от конкретного значения angle, а получают объект State<Float>. 5 | Поскольку State это стабильный тип, Compose видит, что ссылка на объект не меняется при обновлении angle.value. 6 | Рекомпозиция происходит только там, где реально читается angle.value, то есть в RedBox. 7 | Таким образом, лишние рекомпозиций промежуточных функций исчезают. 8 | Теперь Function1, Function2 и Function3 получают лямбду () -> Float, а не конкретное значение angle. 9 | Поскольку сама лямбда стабильна и не меняется при обновлении состояния, Compose видит, что ссылка на объект не меняется, и рекомпозиция происходит только там, где вызывается angle.invoke() то есть в RedBox. 10 | Таким образом, лишние рекомпозиций промежуточных функций исчезают. 11 | В этом коде проблема в том, что модификаторы (.rotate(angle) и др.) строятся внутри @Composable функции и напрямую зависят от изменяемого состояния (angle). 12 | Когда angle меняется, Compose считает, что весь Modifier-цепочка изменилась и пересоздаёт её полностью. Это приводит к лишним рекомпозициям. 13 | В этом коде модификатор graphicsLayer принимает лямбду. Она является стабильной и не пересоздаётся при изменении состояния, поэтому сам модификатор остаётся тем же объектом. При изменении angle Compose просто повторно вызывает лямбду уже на фазе отрисовки, обновляя свойство rotationZ. 14 | Таким образом, вместо пересоздания всей цепочки модификаторов при каждом изменении состояния, если использовать .rotate(angle), меняется только параметр и происходит отрисока, что делает обновление более эффективным. 15 | Переменная count считывается внутри Column { ... }, то есть при каждой отрисовки Column будет заново читать countState.value. Это приводит к тому, что при изменении countState происходит рекомпозиция всей колонки, даже если значение используется только в одном месте — внутри content. 16 | Иначе говоря: чтение состояния вынесено слишком "высоко". Из-за этого больше компонентов подписаны на изменения, чем реально нужно. 17 | Состояние countState читается ровно там, где оно нужно — внутри content. Благодаря этому на изменения значения будет реагировать только content, а не вся Column. 18 | То есть область подписки на состояние сузилась, и теперь при обновлении countState перерисовывается только та часть UI, которая действительно зависит от него. 19 | MyData — это data class без аннотации @Stable. В Compose такие классы считаются нестабильными (unstable), даже если внутри у них "простые" поля. Из-за этого каждый раз при recomposition (триггерится изменением count.value) RedBox будет пересоздавать MyData(listOf("test")). 20 | Более того, List<String> сам по себе нестабилен (нет гарантий, что он не поменяется). Поэтому MyData ещё и наследует эту нестабильность. 21 | В итоге Compose не может оптимизировать проверку и всегда будет перерисовывать RedBox, даже если данные фактически не изменились. 22 | @Immutable явно сообщает Compose, что экземпляр MyData не изменяемый: его поля не меняются после создания, значит, при одинаковых аргументах объект можно считать «эквивалентным». Благодаря этому компилятор и runtime Compose могут пропускать лишние рекомпозиции: если новый MyData равен старому, RedBox не будет перерисован. Даже несмотря на то, что List<String> сам по себе нестабилен, аннотация @Immutable говорит: «раз объект immutable и мы не меняем список внутри, считай это безопасным». Это снимает лишние recomposition-триггеры. 23 | Теперь RedBox действительно будет перерисовываться только тогда, когда данные реально изменятся. 24 | movableContentOf создаёт Composable, который может перемещаться между разными местами дерева композиции, сохраняя своё состояние. При перемещении контента из Column в Row (или наоборот) не происходит лишних рекомпозиций для внутреннего содержимого RedBox и GreenBox. Их состояние сохраняется, и Compose не пересоздаёт эти компоненты. 25 | В Compose ключ определяет когда эффект должен перезапуститься. Каждый раз, когда count.value меняется, Compose считает, что если ключ изменился, полностью пересоздаёт LaunchedEffect Даже если внутри эффект делает что-то простое (как здесь — println), эффект запускается заново, что в реальных приложениях может быть дорогостоящим. 26 | LaunchedEffect предназначен для запуска действий один раз или при реальных изменениях зависимостей, а не каждый раз, когда изменяется простое значение состояния. Здесь мы логируем значение — это не требует перезапуска эффекта, достаточно просто читать значение внутри блока. 27 | Использовать простое состояние в качестве ключа без необходимости плохая практика, потому что это приводит к лишним recomposition и перезапуску эффектов. 28 | В качестве ключа используется сам объект count, а не его значение count.value. LaunchedEffect запускается один раз при создании EntryPoint и не пересоздаётся при каждом изменении значения. Для отслеживания изменений значения используется snapshotFlow { count.value }, который создаёт поток изменений состояния. Внутри collect выполняется логирование только тогда, когда count.value действительно изменяется. Compose не пересоздаёт весь эффект и не тратит лишние ресурсы на повторные запуски. 29 | Создаётся LazyColumn с items, для каждого элемента используется ключ key = { index -> items[index].id }. Это гарантирует, что Compose сопоставляет Composable с конкретным элементом по id, а не по позиции в списке. Внутри LazyColumn элементы делятся на Even и Odd, и каждый использует remember для сохранения времени создания (time). 30 | Если бы ключи не использовались, при добавлении нового элемента в начало списка Compose не смог бы понять, что старые элементы просто сдвинулись вниз. Все элементы пересоздавались бы заново, time обновлялся бы у всех элементов, хотя фактически они не изменились. Именно поэтому использование ключа items[index].id критично для оптимизации. 31 | Благодаря этому только новые или изменённые элементы рекомпозируются. Остальные элементы это никак не затрагивает, что экономит ресурсы при больших списках. 32 | В этом примере LazyColumn рендерит элементы списка без ключей, сопоставляя Composable по позиции (index). При добавлении нового элемента в начало все элементы сдвигаются вниз, и Compose считает, что на каждой позиции теперь новый элемент. В результате все Composable пересоздаются. Это четко видно, что при добавлении нового элемента рекомопозируются Odd и Even, даже если данные элементов фактически не изменились. 33 | В этом примере используется derivedStateOf, чтобы вычислять цвет фона на основе позиции скролла LazyColumn. Вместо того чтобы пересчитывать цвет при каждой мелкой прокрутке и вызывать ненужные рекомпозиции, Compose обновляет состояние только тогда, когда реально изменяется условие (firstVisibleItemIndex > 0). Это уменьшает количество лишних пересчётов и делает код эффективнее. 34 | Так как напрямую используется state.firstVisibleItemIndex в условии, то каждое малейшее движение списка (например, скролл на пару пикселей, даже без смены элемента) приводит к новой рекомпозиции всего Box, хотя по факту цвет остаётся тем же самым. Это значит, что мы нагружаем систему лишними вычислениями и перерисовками, которые вообще не дают пользователю никакого видимого эффекта. 35 | По сути, здесь Compose реагирует на любое изменение LazyListState, а не на логическое условие «элемент стал больше нуля или нет». Поэтому вместо оптимизированных апдейтов у нас идёт поток бессмысленных рекомпозиций. 36 | Проблема в том, что LaunchedEffect(Unit) захватывает лямбду onAngleChanged один раз при старте. Когда в композиции меняется направление, наружная ссылка обновляется, но корутина продолжает вызывать старую версию функции. Из-за этого новые изменения не применяются. 37 | Здесь проблема с устаревшей лямбдой решена: rememberUpdatedState хранит актуальную ссылку на onAngleChanged, и корутина внутри LaunchedEffect всегда вызывает свежую функцию, даже если направление меняется. rememberUpdatedState — это обёртка над значением, которая всегда хранит последнюю актуальную версию, но при этом не перезапускает LaunchedEffect или другие сайд-эффекты. 38 | LaunchedEffect(Unit) захватывает pagerState, который был создан при первой композиции. 39 | Когда происходит нажатие на Reset (меняется keyState -> создаётся новый pagerState), корутина продолжает работать со старым состоянием. Новый pagerState не анимируется. 40 | В этом варианте проблема решена: LaunchedEffect привязан к pagerState, поэтому при его пересоздании старая корутина отменяется и запускается новая. В итоге после нажатия Reset анимация продолжается уже с актуальным состоянием. 41 | 42 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/ru/ivk1800/SampleDescriptor.kt: -------------------------------------------------------------------------------- 1 | package ru.ivk1800 2 | 3 | import androidx.compose.runtime.Composable 4 | import kotlinx.serialization.Serializable 5 | import org.jetbrains.compose.resources.StringResource 6 | import ru.ivk1800.resources.Res 7 | import ru.ivk1800.resources.avoid_unnecessary_state_reads_1_good_explanation 8 | import ru.ivk1800.resources.avoid_unnecessary_state_reads_1_good_title 9 | import ru.ivk1800.resources.avoid_unnecessary_state_reads_2_good_explanation 10 | import ru.ivk1800.resources.avoid_unnecessary_state_reads_2_good_title 11 | import ru.ivk1800.resources.avoid_unnecessary_state_reads_bad_explanation 12 | import ru.ivk1800.resources.avoid_unnecessary_state_reads_bad_title 13 | import ru.ivk1800.resources.composition_local_explanation 14 | import ru.ivk1800.resources.composition_local_title 15 | import ru.ivk1800.resources.derived_state_of_bad_explanation 16 | import ru.ivk1800.resources.derived_state_of_bad_title 17 | import ru.ivk1800.resources.derived_state_of_explanation 18 | import ru.ivk1800.resources.derived_state_of_title 19 | import ru.ivk1800.resources.immutable_type_explanation 20 | import ru.ivk1800.resources.immutable_type_title 21 | import ru.ivk1800.resources.lambda_modifiers_good_explanation 22 | import ru.ivk1800.resources.lambda_modifiers_good_title 23 | import ru.ivk1800.resources.launch_effect_key_bad_explanation 24 | import ru.ivk1800.resources.launch_effect_key_bad_title 25 | import ru.ivk1800.resources.launch_effect_key_good_explanation 26 | import ru.ivk1800.resources.launch_effect_key_good_title 27 | import ru.ivk1800.resources.launch_effect_with_key_explanation 28 | import ru.ivk1800.resources.launch_effect_with_key_title 29 | import ru.ivk1800.resources.launch_effect_without_key_explanation 30 | import ru.ivk1800.resources.launch_effect_without_key_title 31 | import ru.ivk1800.resources.lazy_list_with_key_good 32 | import ru.ivk1800.resources.lazy_list_with_key_good_explanation 33 | import ru.ivk1800.resources.lazy_list_without_key_bad_explanation 34 | import ru.ivk1800.resources.lazy_list_without_key_bad_title 35 | import ru.ivk1800.resources.movable_content_of_explanation 36 | import ru.ivk1800.resources.movable_content_of_title 37 | import ru.ivk1800.resources.not_lambda_modifiers_bad_explanation 38 | import ru.ivk1800.resources.not_lambda_modifiers_bad_title 39 | import ru.ivk1800.resources.read_state_bad_explanation 40 | import ru.ivk1800.resources.read_state_bad_title 41 | import ru.ivk1800.resources.read_state_good_explanation 42 | import ru.ivk1800.resources.read_state_good_title 43 | import ru.ivk1800.resources.static_composition_local_explanation 44 | import ru.ivk1800.resources.static_composition_local_title 45 | import ru.ivk1800.resources.unstable_type_explanation 46 | import ru.ivk1800.resources.unstable_type_title 47 | import ru.ivk1800.resources.with_remember_updated_state_explanation 48 | import ru.ivk1800.resources.with_remember_updated_state_title 49 | import ru.ivk1800.resources.without_remember_updated_state_explanation 50 | import ru.ivk1800.resources.without_remember_updated_state_title 51 | import ru.ivk1800.sample.AvoidUnnecessaryStateReadsBadSample 52 | import ru.ivk1800.sample.AvoidUnnecessaryStateReadsGood1Sample 53 | import ru.ivk1800.sample.AvoidUnnecessaryStateReadsGood2Sample 54 | import ru.ivk1800.sample.CompositionLocalSample 55 | import ru.ivk1800.sample.DerivedStateOfSample 56 | import ru.ivk1800.sample.ImmutableTypeGoodSample 57 | import ru.ivk1800.sample.LambdaModifiersGoodSample 58 | import ru.ivk1800.sample.LaunchEffectKeyBadSample 59 | import ru.ivk1800.sample.LaunchEffectKeyGoodSample 60 | import ru.ivk1800.sample.LaunchEffectWithKeySample 61 | import ru.ivk1800.sample.LaunchEffectWithoutKeySample 62 | import ru.ivk1800.sample.LazyListWithKeyGoodSample 63 | import ru.ivk1800.sample.LazyListWithoutKeyBadSample 64 | import ru.ivk1800.sample.MovableContentOfSample 65 | import ru.ivk1800.sample.NotLambdaModifiersGoodSample 66 | import ru.ivk1800.sample.ReadStateBadSample 67 | import ru.ivk1800.sample.ReadStateGoodSample 68 | import ru.ivk1800.sample.StaticCompositionLocalSample 69 | import ru.ivk1800.sample.UnstableTypeBadSample 70 | import ru.ivk1800.sample.WithRememberUpdatedStateSample 71 | import ru.ivk1800.sample.WithoutDerivedStateOfSample 72 | import ru.ivk1800.sample.WithoutRememberUpdatedStateSample 73 | 74 | @Serializable 75 | enum class SampleDescriptor( 76 | val id: Int, 77 | val entryPoint: @Composable () -> Unit, 78 | val sourceCode: @Composable () -> Unit, 79 | val title: StringResource, 80 | val explanation: StringResource?, 81 | val good: SampleDescriptor?, 82 | ) { 83 | CompositionLocal( 84 | id = 1, 85 | entryPoint = { CompositionLocalSample.EntryPoint() }, 86 | sourceCode = { CompositionLocalSample.HighlightedSourceCode() }, 87 | title = Res.string.composition_local_title, 88 | explanation = Res.string.composition_local_explanation, 89 | good = null, 90 | ), 91 | StaticCompositionLocal( 92 | id = 2, 93 | entryPoint = { StaticCompositionLocalSample.EntryPoint() }, 94 | sourceCode = { StaticCompositionLocalSample.HighlightedSourceCode() }, 95 | title = Res.string.static_composition_local_title, 96 | explanation = Res.string.static_composition_local_explanation, 97 | good = CompositionLocal, 98 | ), 99 | LambdaModifiersGood( 100 | id = 3, 101 | entryPoint = { LambdaModifiersGoodSample.EntryPoint() }, 102 | sourceCode = { LambdaModifiersGoodSample.HighlightedSourceCode() }, 103 | title = Res.string.lambda_modifiers_good_title, 104 | explanation = Res.string.lambda_modifiers_good_explanation, 105 | good = null, 106 | ), 107 | NotLambdaModifiersBad( 108 | id = 4, 109 | entryPoint = { NotLambdaModifiersGoodSample.EntryPoint() }, 110 | sourceCode = { NotLambdaModifiersGoodSample.HighlightedSourceCode() }, 111 | title = Res.string.not_lambda_modifiers_bad_title, 112 | explanation = Res.string.not_lambda_modifiers_bad_explanation, 113 | good = LambdaModifiersGood, 114 | ), 115 | AvoidUnnecessaryStateReadsGood2( 116 | id = 5, 117 | entryPoint = { AvoidUnnecessaryStateReadsGood2Sample.EntryPoint() }, 118 | sourceCode = { AvoidUnnecessaryStateReadsGood2Sample.HighlightedSourceCode() }, 119 | title = Res.string.avoid_unnecessary_state_reads_2_good_title, 120 | explanation = Res.string.avoid_unnecessary_state_reads_2_good_explanation, 121 | good = null, 122 | ), 123 | AvoidUnnecessaryStateReadsGood1( 124 | id = 6, 125 | entryPoint = { AvoidUnnecessaryStateReadsGood1Sample.EntryPoint() }, 126 | sourceCode = { AvoidUnnecessaryStateReadsGood1Sample.HighlightedSourceCode() }, 127 | title = Res.string.avoid_unnecessary_state_reads_1_good_title, 128 | explanation = Res.string.avoid_unnecessary_state_reads_1_good_explanation, 129 | good = null, 130 | ), 131 | AvoidUnnecessaryStateReadsBad( 132 | id = 7, 133 | entryPoint = { AvoidUnnecessaryStateReadsBadSample.EntryPoint() }, 134 | sourceCode = { AvoidUnnecessaryStateReadsBadSample.HighlightedSourceCode() }, 135 | title = Res.string.avoid_unnecessary_state_reads_bad_title, 136 | explanation = Res.string.avoid_unnecessary_state_reads_bad_explanation, 137 | good = AvoidUnnecessaryStateReadsGood1, 138 | ), 139 | DerivedStateOf( 140 | id = 8, 141 | entryPoint = { DerivedStateOfSample.EntryPoint() }, 142 | sourceCode = { DerivedStateOfSample.HighlightedSourceCode() }, 143 | title = Res.string.derived_state_of_title, 144 | explanation = Res.string.derived_state_of_explanation, 145 | good = null, 146 | ), 147 | WithoutDerivedStateOf( 148 | id = 16, 149 | entryPoint = { WithoutDerivedStateOfSample.EntryPoint() }, 150 | sourceCode = { WithoutDerivedStateOfSample.HighlightedSourceCode() }, 151 | title = Res.string.derived_state_of_bad_title, 152 | explanation = Res.string.derived_state_of_bad_explanation, 153 | good = DerivedStateOf, 154 | ), 155 | ReadStateGood( 156 | id = 9, 157 | entryPoint = { ReadStateGoodSample.EntryPoint() }, 158 | sourceCode = { ReadStateGoodSample.HighlightedSourceCode() }, 159 | title = Res.string.read_state_good_title, 160 | explanation = Res.string.read_state_good_explanation, 161 | good = null, 162 | ), 163 | ReadStateBad( 164 | id = 10, 165 | entryPoint = { ReadStateBadSample.EntryPoint() }, 166 | sourceCode = { ReadStateBadSample.HighlightedSourceCode() }, 167 | title = Res.string.read_state_bad_title, 168 | explanation = Res.string.read_state_bad_explanation, 169 | good = ReadStateGood, 170 | ), 171 | StableType( 172 | id = 12, 173 | entryPoint = { ImmutableTypeGoodSample.EntryPoint() }, 174 | sourceCode = { ImmutableTypeGoodSample.HighlightedSourceCode() }, 175 | title = Res.string.immutable_type_title, 176 | explanation = Res.string.immutable_type_explanation, 177 | good = null, 178 | ), 179 | UnstableType( 180 | id = 11, 181 | entryPoint = { UnstableTypeBadSample.EntryPoint() }, 182 | sourceCode = { UnstableTypeBadSample.HighlightedSourceCode() }, 183 | title = Res.string.unstable_type_title, 184 | explanation = Res.string.unstable_type_explanation, 185 | good = StableType, 186 | ), 187 | MovableContentOf( 188 | id = 12, 189 | entryPoint = { MovableContentOfSample.EntryPoint() }, 190 | sourceCode = { MovableContentOfSample.HighlightedSourceCode() }, 191 | title = Res.string.movable_content_of_title, 192 | explanation = Res.string.movable_content_of_explanation, 193 | good = null, 194 | ), 195 | LaunchEffectKeyGood( 196 | id = 14, 197 | entryPoint = { LaunchEffectKeyGoodSample.EntryPoint() }, 198 | sourceCode = { LaunchEffectKeyGoodSample.HighlightedSourceCode() }, 199 | title = Res.string.launch_effect_key_good_title, 200 | explanation = Res.string.launch_effect_key_good_explanation, 201 | good = null, 202 | ), 203 | LaunchEffectKeyBad( 204 | id = 13, 205 | entryPoint = { LaunchEffectKeyBadSample.EntryPoint() }, 206 | sourceCode = { LaunchEffectKeyBadSample.HighlightedSourceCode() }, 207 | title = Res.string.launch_effect_key_bad_title, 208 | explanation = Res.string.launch_effect_key_bad_explanation, 209 | good = LaunchEffectKeyGood, 210 | ), 211 | LazyListWithKey( 212 | id = 14, 213 | entryPoint = { LazyListWithKeyGoodSample.EntryPoint() }, 214 | sourceCode = { LazyListWithKeyGoodSample.HighlightedSourceCode() }, 215 | title = Res.string.lazy_list_with_key_good, 216 | explanation = Res.string.lazy_list_with_key_good_explanation, 217 | good = null, 218 | ), 219 | LazyListWithoutKey( 220 | id = 15, 221 | entryPoint = { LazyListWithoutKeyBadSample.EntryPoint() }, 222 | sourceCode = { LazyListWithoutKeyBadSample.HighlightedSourceCode() }, 223 | title = Res.string.lazy_list_without_key_bad_title, 224 | explanation = Res.string.lazy_list_without_key_bad_explanation, 225 | good = LazyListWithKey, 226 | ), 227 | WithRememberUpdatedState( 228 | id = 17, 229 | entryPoint = { WithRememberUpdatedStateSample.EntryPoint() }, 230 | sourceCode = { WithRememberUpdatedStateSample.HighlightedSourceCode() }, 231 | title = Res.string.with_remember_updated_state_title, 232 | explanation = Res.string.with_remember_updated_state_explanation, 233 | good = null, 234 | ), 235 | WithoutRememberUpdatedState( 236 | id = 18, 237 | entryPoint = { WithoutRememberUpdatedStateSample.EntryPoint() }, 238 | sourceCode = { WithoutRememberUpdatedStateSample.HighlightedSourceCode() }, 239 | title = Res.string.without_remember_updated_state_title, 240 | explanation = Res.string.without_remember_updated_state_explanation, 241 | good = WithRememberUpdatedState, 242 | ), 243 | LaunchEffectWithKey( 244 | id = 19, 245 | entryPoint = { LaunchEffectWithKeySample.EntryPoint() }, 246 | sourceCode = { LaunchEffectWithKeySample.HighlightedSourceCode() }, 247 | title = Res.string.launch_effect_with_key_title, 248 | explanation = Res.string.launch_effect_with_key_explanation, 249 | good = null, 250 | ), 251 | LaunchEffectWithoutKey( 252 | id = 20, 253 | entryPoint = { LaunchEffectWithoutKeySample.EntryPoint() }, 254 | sourceCode = { LaunchEffectWithoutKeySample.HighlightedSourceCode() }, 255 | title = Res.string.launch_effect_without_key_title, 256 | explanation = Res.string.launch_effect_without_key_explanation, 257 | good = LaunchEffectWithKey, 258 | ), 259 | } 260 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Recomposition visualization 3 | Explanation 4 | What's best 5 | Build info 6 | Kotlin: %1$s 7 | Compose: %1$s 8 | 9 | Use composition local for frequently changing state variables (Good). 10 | Do not use static composition local for frequently changing state variables (Bad). 11 | Use lambda-based modifiers for frequently changing state variables (Good). 12 | Do not use value based modifiers for frequently changing state variables (Bad). 13 | Unnecessary state reads (Bad). 14 | Get rid of intermediate recompositions using State (Good). 15 | Get rid of intermediate recompositions using Lambda (Good). 16 | Read frequently changing value leads to recomposition (Bad). 17 | Use derivedStateOf to limit recompositions. 18 | Read the state in the nearest scope as possible (Good). 19 | Do not read the state in the parent scope if it is not necessary. (Bad). 20 | Use immutable type to reduce recompositions (Good). 21 | Unstable type leads to recomposition (Bad). 22 | Use movableContentOf to reduce recompositions. 23 | Do not use frequently changing value as LaunchEffect key (Bad). 24 | Use snapshotFlow frequently changing value (Good). 25 | Default key in Lazy(Column/Row) leads to recomposition (Bad). 26 | Use custom key in Lazy(Column/Row) for reduce recompositions (Good). 27 | Long-lived lambda referenced parameters or values. (Bad). 28 | Use rememberUpdatedState when parameters or values referenced by a long-lived lambda. (Good). 29 | LaunchedEffect without key referenced parameters or values (Bad). 30 | Use keys in LaunchedEffect (Good). 31 | 32 | compositionLocalOf: Changing the value provided during recomposition invalidates only the content that reads its current value. Please note that the GreenBox is not recomposed. 33 | staticCompositionLocalOf: Unlike compositionLocalOf, reads of a staticCompositionLocalOf are not tracked by Compose. Changing the value causes the entirety of the content lambda where the CompositionLocal is provided to be recomposed, instead of just the places where the current value is read in the Composition. Please note that the GreenBox is being recomposed. 34 | Here, angle is passed through the chain Function1 -> Function2 -> Function3 -> RedBox. 35 | Each function takes angle as a parameter and will be recomposed every time the value changes. Function1, Function2, and Function3 themselves don’t do anything with angle except passing it further. But since angle is a parameter, Compose considers them dependent on the state and recomposes them. 36 | Now Function1, Function2, and Function3 don’t depend on the specific value of angle, but instead receive a State<Float> object. 37 | Since State is a stable type, Compose sees that the reference to the object does not change when angle.value updates. 38 | Recomposition happens only where angle.value is actually read — in RedBox. 39 | Thus, unnecessary recompositions of intermediate functions disappear. 40 | Now Function1, Function2, and Function3 receive a lambda () -> Float instead of the actual angle value. 41 | Since the lambda itself is stable and does not change when the state updates, Compose sees that the reference to the object stays the same, and recomposition happens only where angle.invoke() is called — in RedBox. 42 | Thus, unnecessary recompositions of intermediate functions disappear. 43 | In this code, the problem is that modifiers (.rotate(angle), etc.) are built inside a @Composable function and directly depend on the mutable state (angle). 44 | When angle changes, Compose considers the entire Modifier chain changed and rebuilds it completely. This causes unnecessary recompositions. 45 | In this code, the graphicsLayer modifier takes a lambda. It is stable and does not get recreated when the state changes, so the modifier itself remains the same object. When angle changes, Compose simply re-invokes the lambda during the drawing phase, updating the rotationZ property. 46 | Thus, instead of rebuilding the entire modifier chain on every state change (as with .rotate(angle)), only the parameter changes and the UI is redrawn, which makes the update more efficient. 47 | The count variable is read inside Column { ... }, which means Column re-reads countState.value on every recomposition. This causes the entire Column to recompose whenever countState changes, even though the value is only actually used in one place — inside content. 48 | In other words: the state read is placed too “high”. As a result, more components subscribe to changes than really need to. 49 | The countState is read exactly where it is needed — inside content. Thanks to this, only content reacts to changes, not the entire Column. 50 | That means the subscription area has been narrowed, and now only the part of the UI that truly depends on countState will recompose when it changes. 51 | MyData is a data class without the @Stable annotation. In Compose, such classes are considered unstable, even if they contain only “simple” fields. Because of this, every recomposition (triggered by count.value changes) will recreate MyData(listOf("test")) in RedBox. 52 | Moreover, List<String> itself is unstable (there are no guarantees it won’t change). Therefore, MyData also inherits this instability. 53 | As a result, Compose cannot optimize the check and will always recompose RedBox, even if the data hasn’t actually changed. 54 | @Immutable explicitly tells Compose that a MyData instance is immutable: its fields do not change after creation, so with the same arguments, the object can be considered “equivalent.” Thanks to this, the Compose compiler and runtime can skip unnecessary recompositions: if the new MyData equals the old one, RedBox won’t be redrawn. Even though List<String> itself is unstable, the @Immutable annotation tells Compose: “this object is immutable and we don’t mutate the list inside, so treat it as safe.” This removes unnecessary recomposition triggers. 55 | Now RedBox will actually recompose only when the data really changes. 56 | movableContentOf creates a Composable that can be moved between different places in the composition tree while preserving its state. When moving content from Column to Row (or vice versa), unnecessary recompositions of the inner RedBox and GreenBox do not occur. Their state is preserved, and Compose does not recreate these components. 57 | In Compose, the key defines when an effect should restart. Every time count.value changes, Compose sees the key as changed and completely recreates the LaunchedEffect. Even if the effect only does something simple (like println), it still restarts, which in real apps can be costly. 58 | LaunchedEffect is meant for launching actions once or when dependencies truly change, not every time a simple state changes. Here we are just logging the value — this doesn’t require restarting the effect, it’s enough to simply read the value inside the block. 59 | Using a state value as a key unnecessarily is a bad practice, because it causes extra recompositions and restarts of effects. 60 | Here the key is the count object itself, not its value count.value. LaunchedEffect runs once at EntryPoint creation and is not recreated on every value change. To track count.value updates, snapshotFlow { count.value } is used, which produces a flow of state changes. Inside collect, logging happens only when count.value actually changes. Compose does not recreate the whole effect or waste resources on repeated launches. 61 | A LazyColumn is created with items, and each element uses a key = { index -> items[index].id }. This ensures that Compose matches Composables with specific items by id, not by position in the list. Inside the LazyColumn, elements are divided into Even and Odd, each using remember to save creation time (time). 62 | Without keys, when a new item is inserted at the start, Compose cannot tell that old items just shifted down. All elements would be recreated, and time would be refreshed for all of them, even though they didn’t really change. That’s why using items[index].id as a key is critical for optimization. 63 | Thanks to this, only new or changed elements recompose. The others remain unaffected, which saves resources on large lists. 64 | In this example, LazyColumn renders items without keys, matching Composables by position (index). When a new element is added at the start, all items shift down, and Compose thinks there is a new item at each position. As a result, all Composables are recreated. You can clearly see that when adding a new element, Odd and Even both recompose, even though the actual data hasn’t changed. 65 | In this example, derivedStateOf is used to calculate the background color based on the scroll position of the LazyColumn. Instead of recalculating the color on every tiny scroll and causing unnecessary recompositions, Compose updates the state only when the actual condition changes (firstVisibleItemIndex > 0). This reduces redundant computations and makes the code more efficient. 66 | Here, state.firstVisibleItemIndex is used directly in the condition, so every tiny list movement (like scrolling by a couple of pixels, even without switching items) triggers a new recomposition of the entire Box, even if the color stays the same. This means we overload the system with useless computations and redraws, which give the user no visible effect. 67 | In short, Compose reacts to any LazyListState change, not to the logical condition “index > 0 or not.” So instead of optimized updates, we get a stream of meaningless recompositions. 68 | The problem is that LaunchedEffect(Unit) captures the onAngleChanged lambda once at startup. When the direction changes in the composition, the external reference updates, but the coroutine continues to call the old version of the function. Because of this, new changes are not applied. 69 | Here the stale lambda problem is solved: rememberUpdatedState holds the actual reference to onAngleChanged, and the coroutine inside LaunchedEffect always calls the latest function, even if the direction changes. rememberUpdatedState is a wrapper around a value that always keeps the latest version, but does not restart LaunchedEffect or other side effects. 70 | LaunchedEffect(Unit) captures pagerState, which was created during the first composition. 71 | When Reset is pressed (keyState changes -> a new pagerState is created), the coroutine continues to work with the old state. The new pagerState is not animated. 72 | In this version the problem is solved: LaunchedEffect is tied to pagerState, so when it is recreated the old coroutine is canceled and a new one is launched. As a result, after pressing Reset the animation continues with the actual state. 73 | 74 | --------------------------------------------------------------------------------