{"id":356671,"date":"2024-05-20T23:34:36","date_gmt":"2024-05-20T23:34:36","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=356671"},"modified":"-0001-11-30T00:00:00","modified_gmt":"2024-05-20T23:34:36","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=356671","title":{"rendered":"<span>Android Ktorfit+Compose<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<h2>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h2>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/edb\/fbe\/29d\/edbfbe29d5a73edca804a034f9656b49.png\" alt=\"Data+Domain\" title=\"Data+Domain\" width=\"1370\" height=\"906\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/edb\/fbe\/29d\/edbfbe29d5a73edca804a034f9656b49.png\"\/><\/p>\n<div><figcaption>Data+Domain<\/figcaption><\/div>\n<\/figure>\n<p>\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u0438\u0442 \u0438\u0437 \u043e\u0434\u043d\u043e\u0439 Activity \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0441\u043e\u0437\u0434\u0430\u0435\u0442\u0441\u044f GetFalconInfoUseCase.<\/p>\n<pre><code class=\"kotlin\">@AndroidEntryPoint class MainActivity : ComponentActivity() {      @Inject     lateinit var getFalconInfoUseCase: GetFalconInfoUseCase<\/code><\/pre>\n<p>\u0414\u0430\u043b\u0435\u0435 \u044d\u0442\u043e\u0442 UseCase \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u0442\u0441\u044f \u0432 \u044d\u043a\u0440\u0430\u043d \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u044f \u0441\u043f\u0438\u0441\u043a\u0430 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/side-effects#producestate\" rel=\"noopener noreferrer nofollow\">producestate<\/a> \u0434\u043b\u044f \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f non-Compose state into Compose state. \u0415\u0449\u0435 \u043f\u0440\u043e &#171;\u041f\u043e\u0431\u043e\u0447\u043d\u044b\u0435 \u044d\u0444\u0444\u0435\u043a\u0442\u044b&#187; \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u044e\u0449\u0438\u0435 \u043f\u0440\u0438 \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u0438 \u043a\u043e\u043c\u043f\u043e\u0437\u0438\u0446\u0438\u0438 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0442\u0443\u0442 <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/runtime\/package-summary#SideEffect(kotlin.Function0)\" rel=\"noopener noreferrer nofollow\">SideEffect<\/a>. <a href=\"https:\/\/habr.com\/ru\/companies\/joydev\/articles\/743048\/\" rel=\"noopener noreferrer nofollow\">https:\/\/habr.com\/ru\/companies\/joydev\/articles\/743048\/<\/a><\/p>\n<details class=\"spoiler\">\n<summary>FalconInfoItemsScreen<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"kotlin\">@Composable fun FalconInfoItemsScreen(getFalconInfoUseCase: GetFalconInfoUseCase) {         produceState&lt;ResponseResult&lt;List&lt;FalconInfo>>>(initialValue = ResponseResult.Loading, getFalconInfoUseCase) {             try {                 getFalconInfoUseCase.getFalconInfo().collect {                     value = it                 }             } catch (e: DataSourceException) {                 value = ResponseResult.Error(e)             }         }.let {             when (it.value) {             is ResponseResult.Loading -> ShowStatusScreen(\"Loading...\")              is ResponseResult.Success -> {                 it.value.onSuccess { rockets ->                     FalconInfoListView(rockets)                 }             }              is ResponseResult.Error -> it.value.onError { error ->                 ShowStatusScreen(error.getError(LocalContext.current))             }          }     } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0417\u0430\u043f\u0440\u043e\u0441\u0430 \u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f <em>rememberCoroutineScope \u0438 mutableStateOf<\/em><\/p>\n<p><strong>mutableStateOf<\/strong>\u00a0&#8212; \u0421\u043e\u0437\u0434\u0430\u0435\u0442 State \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e compose \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f<\/p>\n<p><strong>remember<\/strong>\u00a0&#8212; \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u044d\u0442\u043e\u0442 State \u043f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u043c \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438<\/p>\n<details class=\"spoiler\">\n<summary>\u0414\u043b\u044f \u0442\u0438\u043f\u0430 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u044f @Stable \u0435\u0441\u043b\u0438 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043d\u0435 \u0443\u0434\u043e\u0432\u043b\u0435\u0442\u0432\u043e\u0440\u044f\u0435\u0442 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f\u043c \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0430<\/summary>\n<div class=\"spoiler__content\">\n<h4>Skipping if the inputs haven&#8217;t changed<\/h4>\n<p>If a composable is already in the Composition, it can skip recomposition if all the inputs are stable and haven&#8217;t changed.<\/p>\n<p>A stable type must comply with the following contract:<\/p>\n<ul>\n<li>\n<p>The result of <code>equals<\/code> for two instances will <em>forever<\/em> be the same for the same two instances.<\/p>\n<\/li>\n<li>\n<p>If a public property of the type changes, Composition will be notified.<\/p>\n<\/li>\n<li>\n<p>All public property types are also stable.<\/p>\n<\/li>\n<\/ul>\n<p>There are some important common types that fall into this contract that the compose compiler will treat as stable, even though they are not explicitly marked as stable by using the <code>@Stable<\/code> annotation:<\/p>\n<ul>\n<li>\n<p>All primitive value types: <code>Boolean<\/code>, <code>Int<\/code>, <code>Long<\/code>, <code>Float<\/code>, <code>Char<\/code>, etc.<\/p>\n<\/li>\n<li>\n<p>Strings<\/p>\n<\/li>\n<li>\n<p>All Function types (lambdas)<\/p>\n<\/li>\n<\/ul>\n<p>All of these types are able to follow the contract of stable because they are immutable. Since immutable types never change, they never have to notify Composition of the change, so it is much easier to follow this contract.<\/p>\n<p><strong>Note:<\/strong> All deeply immutable types can safely be considered stable types.<\/p>\n<p>One notable type that is stable but <em>is<\/em> mutable is Compose\u2019s <code>MutableState<\/code> type. If a value is held in a <code>MutableState<\/code>, the state object overall is considered to be stable as Compose will be notified of any changes to the <code>.value<\/code> property of <code>State<\/code>.<\/p>\n<p>When all types passed as parameters to a composable are stable, the parameter values are compared for equality based on the composable position in the UI tree. Recomposition is skipped if all the values are unchanged since the previous call.<\/p>\n<p><strong>Key Point:<\/strong> Compose skips the recomposition of a composable if all the inputs are stable and haven&#8217;t changed. The comparison uses the <code>equals<\/code> method.<\/p>\n<p>Compose considers a type stable only if it can prove it. For example, an interface is generally treated as not stable, and types with mutable public properties whose implementation could be immutable are not stable either.<\/p>\n<p>If Compose is not able to infer that a type is stable, but you want to force Compose to treat it as stable, mark it with the <code>@Stable<\/code> annotation.<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 \u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u044d\u043a\u0440\u0430\u043d\u0430<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"kotlin\">fun FalconInfoItemsScreen(getFalconInfoUseCase: GetFalconInfoUseCase) {      val scope = rememberCoroutineScope()     var result: ResponseResult&lt;List&lt;FalconInfo>> by remember { mutableStateOf(ResponseResult.Loading) }      LaunchedEffect(key1 = true) {         scope.launch {             try {                 getFalconInfoUseCase.getFalconInfo().collect {                     result = it                 }             } catch (e: DataSourceException) {                 result = ResponseResult.Error(e)             }         }     }      when (result) {         is ResponseResult.Loading -> ShowStatusScreen(stringResource(R.string.loading))          is ResponseResult.Success -> {                 result.onSuccess {                     FalconInfoListView(it)                     Timber.d(\"size = ${it.size}\")                 }         }          is ResponseResult.Error -> result.onError { error ->             ShowStatusScreen(error.getError(LocalContext.current))         }     } <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<h2>\u041d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044f<\/h2>\n<p>\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0437\u0430\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0432 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 \u043f\u0430\u0440\u044b <em>route<\/em> \u043a\u0430\u043a \u0441\u0442\u0440\u043e\u043a\u043e\u0432\u043e\u0439 \u043a\u043e\u043d\u0441\u0442\u0430\u043d\u0442\u044b \u0438 compose \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 \u043e\u0442\u0440\u0438\u0441\u043e\u0432\u043a\u0438. \u0422\u0430\u043a\u0436\u0435 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u043c \u043f\u0435\u0440\u0432\u044b\u0439 \u044d\u043a\u0440\u0430\u043d <em>startDestination<\/em>.<\/p>\n<pre><code class=\"kotlin\">val navController = rememberNavController()     NavHost(navController = navController, startDestination = LOGIN_SCREEN) {         composable(HOME_SCREEN) { HomeScreen() }         composable(ROCKETS_SCREEN) { FalconInfoItemsScreen(getFalconInfoUseCase) }         composable(LOGIN_SCREEN) { LoginScreen() }     }<\/code><\/pre>\n<p>\u041d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044f \u043f\u043e \u044d\u043a\u0440\u0430\u043d\u0430\u043c \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0442\u0430\u0431\u0430<\/p>\n<pre><code class=\"kotlin\">          when (selectedTabIndex) {               HOME_TAB -> navigate(navController, Constants.HOME_SCREEN)               ROCKETS_TAB -> navigate(navController, Constants.ROCKETS_SCREEN)               LOGIN_TAB -> navigate(navController, Constants.LOGIN_SCREEN)           }<\/code><\/pre>\n<p>\u0425\u0430\u043a \u0434\u043b\u044f \u0432\u044b\u0442\u0430\u043b\u043a\u0438\u0432\u0430\u043d\u0438\u044f \u044d\u043a\u0440\u0430\u043d\u0430, \u043a\u0430\u043a \u0444\u0440\u0430\u0433\u043c\u0435\u043d\u0442\u0430 \u0438\u0437 BackStack \u043f\u0440\u0438 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0435, \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0435 \u044d\u043a\u0440\u0430\u043d\u044b \u0432 \u0441\u0442\u0435\u043a\u0435 \u0438 \u0441\u0440\u0430\u0437\u0443 \u0432\u044b\u0439\u0442\u0438 \u0438\u0437 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043f\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044e Back.<\/p>\n<pre><code class=\"kotlin\">fun navigate(navController: NavController, route: String) {     navController.navigate(route) {         popUpTo(0)     } }<\/code><\/pre>\n<details class=\"spoiler\">\n<summary>Refactoring<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"kotlin\">sealed class Screen(     val tabIndex: Int,     val route: String,     @StringRes val textResId: Int,     val selectedItem: ImageVector,     val unselectedItem: ImageVector,     val popUp: Boolean = true ) {     data object Home : Screen(HOME_TAB, HOME_SCREEN, R.string.home,         Icons.Filled.Home, Icons.Outlined.Home)     data object Rockets : Screen(ROCKETS_TAB, ROCKETS_SCREEN, R.string.rockets,         Icons.Filled.Rocket, Icons.Outlined.Rocket)     data object Login : Screen(LOGIN_TAB, LOGIN_SCREEN, R.string.login,         Icons.Filled.AccountCircle, Icons.Outlined.AccountCircle)      companion object {         const val HOME_SCREEN = \"homeScreen\"         const val ROCKETS_SCREEN = \"rocketsScreen\"         const val LOGIN_SCREEN = \"loginScreen\"          const val HOME_TAB = 0         const val ROCKETS_TAB = 1         const val LOGIN_TAB = 2     } }    @Composable private fun initTabItems(): List&lt;TabItem> {     val items = listOf(Screen.Home, Screen.Rockets, Screen.Login)      return items.map {         TabItem(title = stringResource(it.textResId),             unselectedItem = it.unselectedItem,             selectedItem = it.selectedItem)     } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<h2>Ktorfit<\/h2>\n<p>\u0422\u0440\u0430\u0434\u0438\u0446\u0438\u043e\u043d\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 Retrofit \u0437\u0430\u043c\u0435\u043d\u0435\u043d \u043d\u0430 Ktorfit. \u041f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0434\u0430\u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0434\u0432\u0443\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u043e\u0445\u043e\u0436\u0438 \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a\u0436\u0435.  \u0412 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0442\u0430\u043a \u0438 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u043e &#8212; inspired by Retrofit. \u0410\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u044b \u0431\u0435\u0437 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439. \u0412 \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f <em><a class=\"mention\" href=\"\/users\/get.\">@GET.<\/a><\/em><\/p>\n<blockquote>\n<p>Ktorfit is a HTTP client\/Kotlin Symbol Processor for Kotlin Multiplatform ( Android, iOS, Js, Jvm, Linux) using\u00a0<a href=\"https:\/\/github.com\/google\/ksp\" rel=\"noopener noreferrer nofollow\">KSP<\/a>\u00a0and\u00a0<a href=\"https:\/\/ktor.io\/docs\/getting-started-ktor-client.html\" rel=\"noopener noreferrer nofollow\">Ktor clients<\/a>\u00a0inspired by\u00a0<a href=\"https:\/\/square.github.io\/retrofit\/\" rel=\"noopener noreferrer nofollow\">Retrofit<\/a>.<\/p>\n<\/blockquote>\n<pre><code class=\"kotlin\">    @Singleton     @Provides     fun provideKtorfit(): Ktorfit {         val ktorfit = ktorfit {             baseUrl(Constants.BASE_URL)             httpClient(HttpClient {                 \/\/ install(HttpCache)                 install(ContentNegotiation)                 {                     json(                         Json {                             prettyPrint = true                             isLenient = true                             ignoreUnknownKeys = true                         }                     )                 }             })             converterFactories(                 FlowConverterFactory(),                 CallConverterFactory(),             )          }         return ktorfit     }      @Singleton     @Provides     fun provideApiServices(ktorfitClient: Ktorfit): ApiService {         return ktorfitClient.create&lt;ApiService>()     } <\/code><\/pre>\n<p>\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u043f\u0440\u043e\u0435\u043a\u0442 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043f\u043e\u0445\u043e\u0436\u0435\u0439 \u043d\u0430 Retrofit. HttpClient \u0438 json serializer \u043f\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0438\u0437 Ktor \u043f\u0430\u043a\u0435\u0442\u0430. \u041f\u043e\u0434 \u043a\u0430\u043f\u043e\u0442\u043e\u043c \u0442\u0430\u043c <em>package kotlinx.serialization.json<\/em>. <a href=\"https:\/\/foso.github.io\/Ktorfit\/converters\/converters\/\" rel=\"noopener noreferrer nofollow\">Converters<\/a> \u0433\u0438\u0431\u043a\u043e \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u043d\u0438\u0436\u0435 \u0441\u043f\u0438\u0441\u043e\u043a \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0445 \u043f\u0430\u0440\u0441\u0435\u0440\u043e\u0432.<\/p>\n<details class=\"spoiler\">\n<summary>Provides a list of standard subtypes of an application content type<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"kotlin\">public val Any: ContentType = ContentType(\"application\", \"*\") public val Atom: ContentType = ContentType(\"application\", \"atom+xml\") public val Cbor: ContentType = ContentType(\"application\", \"cbor\") public val Json: ContentType = ContentType(\"application\", \"json\") public val HalJson: ContentType = ContentType(\"application\", \"hal+json\") public val JavaScript: ContentType = ContentType(\"application\", \"javascript\") public val OctetStream: ContentType = ContentType(\"application\", \"octet-stream\") public val Rss: ContentType = ContentType(\"application\", \"rss+xml\") public val Xml: ContentType = ContentType(\"application\", \"xml\") public val Xml_Dtd: ContentType = ContentType(\"application\", \"xml-dtd\") public val Zip: ContentType = ContentType(\"application\", \"zip\") public val GZip: ContentType = ContentType(\"application\", \"gzip\") <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u041d\u0430 GitHub Ktorfit \u0435\u0441\u0442\u044c \u0434\u0432\u0430 \u043f\u0440\u0438\u043c\u0435\u0440\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f <a href=\"https:\/\/github.com\/Foso\/Ktorfit\/tree\/master\/example\/AndroidOnlyExample\" rel=\"noopener noreferrer nofollow\">AndroidOnlyExample<\/a> \u0438 <a href=\"https:\/\/github.com\/Foso\/Ktorfit\/tree\/master\/example\/MultiplatformExample\" rel=\"noopener noreferrer nofollow\">MultiplatformExample<\/a>. \u0412 \u043f\u0440\u043e\u0435\u043a\u0442 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u043f\u0435\u0440\u0432\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442. \u0412 \u0431\u0443\u0434\u0443\u0449\u0435\u043c \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043d\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 KMM (<em>Kotlin Multiplatform Mobile<\/em>).<\/p>\n<h2>\u041a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 Ktorfit \u0432 Room<\/h2>\n<p>\u041b\u043e\u0433\u0438\u043a\u0430 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a &#8212; \u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442\u0441\u044f \u043f\u0443\u0441\u0442\u0430\u044f \u043b\u0438 \u0431\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445. \u0415\u0441\u043b\u0438 \u043d\u0435\u0442 \u0442\u043e \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437\u0432\u043b\u0435\u043a\u0430\u044e\u0442\u0441\u044f \u0438\u0437 \u043d\u0435\u0435. \u0415\u0441\u043b\u0438 \u043f\u0443\u0441\u0442\u0430\u044f \u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442\u0441\u044f \u0437\u0430\u043f\u0440\u043e\u0441 \u0432 \u0441\u0435\u0442\u044c \u0438 \u043f\u043e\u0441\u043b\u0435 \u043f\u0435\u0440\u0435\u043c\u0430\u043f\u043b\u0438\u0432\u0430\u043d\u0438\u044f <em>responseToDomain()<\/em> \u043e\u0442\u0434\u0430\u0435\u0442\u0441\u044f \u043d\u0430 \u0443\u0440\u043e\u0432\u0435\u043d\u044c Domain \u0438 \u0441\u043b\u0435\u0434\u043e\u043c \u0438\u0434\u0435\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435 <em>insertAllRocketsInfo()<\/em> \u0432 \u0431\u0430\u0437\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u043f\u043e\u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432. \u041a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 HTTP \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f <s>install(HttpCache)<\/s>. \u0420\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0442\u0430\u043a\u043e\u0435 \u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0441 \u0411\u0430\u0437\u043e\u0439 \u0434\u0430\u043d\u043d\u044b\u0445 \u0437\u043d\u0430\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0431\u044b\u0441\u0442\u0440\u0435\u0435, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043d\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f Json \u043f\u0430\u0440\u0441\u0438\u043d\u0433.<\/p>\n<pre><code class=\"kotlin\">class FalconRepositoryImpl @Inject constructor(     private val remoteDataSource: RemoteDataSource, private val localDataSource: LocalDataSource ) : FalconRepository {     override fun getFalconInfo(): Flow&lt;ResponseResult&lt;List&lt;FalconInfo>>> {         return flow  {             if (localDataSource.getAllRocketsInfo().isNotEmpty()) {                 emit(ResponseResult.Success(                     DataMapper.entryToDomain(localDataSource.getAllRocketsInfo())))             } else {                 remoteDataSource.getFalconInfo().run {                     when (this) {                         is ResponseResult.Success -> {                             emit(ResponseResult.Success(DataMapper.responseToDomain(response)))                             localDataSource                                 .insertAllRocketsInfo(                                     DataMapper.responseToEntry(response))                         }                         is ResponseResult.Error -> {                             emit(ResponseResult.Error(exception))                         }                         else -> {}                     }                 }             }         }.flowOn(Dispatchers.IO).onFlowStarts()     } } <\/code><\/pre>\n<p>onFlowStart() \u044d\u0442\u043e extension \u0434\u043b\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f ResponseResult.Loading \u043f\u0435\u0440\u0435\u0434 \u0442\u0435\u043c \u043a\u0430\u043a \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0441\u044f Flow.<\/p>\n<pre><code class=\"kotlin\">\/** extension function for Flow Class to emit loading state before the flow starts *\/ fun &lt;T> Flow&lt;ResponseResult&lt;T>>.onFlowStarts() = onStart { emit(ResponseResult.Loading) }.catch { e: Throwable ->     e.printStackTrace()     emit(ResponseResult.Error(DataSourceException.Unexpected(R.string.error_client_unexpected_message))) }<\/code><\/pre>\n<h2>\u041a\u0430\u0440\u0442\u043e\u0447\u043a\u0438<\/h2>\n<p>\u0414\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/components\/card\" rel=\"noopener noreferrer nofollow\">Card<\/a>. \u041f\u0440\u0435\u0434\u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440 \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u043a\u0430\u043a \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043e\u0434\u0438\u043d \u044d\u043b\u0435\u043c\u0435\u043d\u0442, \u043e\u043d \u0440\u0430\u0441\u0442\u044f\u043d\u0443\u0442 \u043f\u043e \u0432\u044b\u0441\u043e\u0442\u0435 \u0438 \u0448\u0438\u0440\u0438\u043d\u0435 \u0438 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u043d\u0438\u043c\u0430\u0442\u044c \u0432\u0441\u0435 \u0432\u044b\u0434\u0435\u043b\u0435\u043d\u043d\u043e\u0435 \u0435\u0439 \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u043e \u0432 <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/foundation\/lazy\/grid\/package-summary#LazyVerticalGrid(androidx.compose.foundation.lazy.grid.GridCells,androidx.compose.ui.Modifier,androidx.compose.foundation.lazy.grid.LazyGridState,androidx.compose.foundation.layout.PaddingValues,kotlin.Boolean,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.Arrangement.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,kotlin.Function1)\" rel=\"noopener noreferrer nofollow\">LazyVerticalGrid<\/a>.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/7a4\/f35\/6b7\/7a4f356b7278af3739fe3aeda393af82.png\" alt=\" \u041e\u0434\u043d\u0430 Card. \u041f\u0440\u0435\u0434\u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\" title=\" \u041e\u0434\u043d\u0430 Card. \u041f\u0440\u0435\u0434\u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\" width=\"1500\" height=\"848\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/7a4\/f35\/6b7\/7a4f356b7278af3739fe3aeda393af82.png\"\/><\/p>\n<div><figcaption> \u041e\u0434\u043d\u0430 Card. \u041f\u0440\u0435\u0434\u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440<\/figcaption><\/div>\n<\/figure>\n<p>\u0415\u0441\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0434\u0440\u0443\u0433\u0438\u0445 \u0442\u0438\u043f\u043e\u0432 \u043a\u0430\u0440\u0442\u043e\u0447\u0435\u043a ElevatedCard \u0438 OutlinedCard. \u041f\u0435\u0440\u0432\u0430\u044f \u0437\u0430\u0434\u0430\u0435\u0442 \u0442\u0435\u043d\u044c, \u0432\u0442\u043e\u0440\u0430\u044f \u043e\u0447\u0435\u0440\u0447\u0435\u043d\u043d\u0430\u044f \u043a\u0430\u043a \u044d\u0442\u043e \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0437 \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0430.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/b38\/6c0\/fe6\/b386c0fe6a76599fa77d147597d28e99.png\" alt=\"ElevatedCard\" title=\"ElevatedCard\" width=\"534\" height=\"231\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/b38\/6c0\/fe6\/b386c0fe6a76599fa77d147597d28e99.png\"\/><\/p>\n<div><figcaption>ElevatedCard<\/figcaption><\/div>\n<\/figure>\n<p><a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/material3\/package-summary\" rel=\"noopener noreferrer nofollow\">material3 package-summary<\/a> \u043f\u043e\u043b\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0439  <a href=\"https:\/\/m3.material.io\/\" rel=\"noopener noreferrer nofollow\">Material Design 3<\/a>.<\/p>\n<h2>LazyVerticalGrid<\/h2>\n<details class=\"spoiler\">\n<summary>\u041a\u043e\u0434 \u0432 \u0441\u043f\u043e\u0439\u043b\u0435\u0440\u0435<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"kotlin\">@Composable fun FalconInfoCard(falconInfo: FalconInfo) {         Card(modifier = Modifier             .fillMaxSize()             .padding(4.dp),             colors = CardDefaults.cardColors(                 containerColor = MaterialTheme.colorScheme.primary             ),                 shape = RoundedCornerShape(30.dp),) {              Row(                 modifier = Modifier                     .padding(4.dp)                     .fillMaxWidth(),                     horizontalArrangement = Arrangement.Center,             ) {                   Text(                       maxLines = 1,                       text = falconInfo.name,                       style = MaterialTheme.typography.titleSmall                   )              }              Row(modifier = Modifier                 .fillMaxWidth(),                 horizontalArrangement = Arrangement.Center) {                 AsyncImage(                     placeholder = rememberVectorPainter(Icons.Filled.Rocket),                     model = falconInfo.links?.patch?.small,                     contentDescription = null,                     contentScale = ContentScale.FillWidth,                     modifier = Modifier                         .size(120.dp)                         .clip(CircleShape)                 )             }              Spacer(modifier = Modifier                 .height(1.dp))              Row(                 modifier = Modifier                     .wrapContentHeight()                     .padding(8.dp)             ) {                 falconInfo.details?.let {                     Text(                         maxLines = 2,                         overflow = TextOverflow.Ellipsis,                         text = it,                         style = MaterialTheme.typography.bodyMedium                     )                  }             }         } } <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>LazyVerticalGrid \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043e\u0442\u0440\u0438\u0441\u043e\u0432\u044b\u0432\u0430\u0442\u044c \u0442\u0430\u0431\u043b\u0438\u0446\u044b. \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 columns \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043a\u043e\u043b\u043e\u043d\u043e\u043a. \u0412 \u043c\u043e\u0435\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0438\u0445 \u0434\u0432\u0435 <em>columns = GridCells.Fixed(2).<\/em> \u0426\u0432\u0435\u0442 \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0438 \u0437\u0430\u0434\u0430\u0435\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 containerColor. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0446\u0432\u0435\u0442\u0430 \u0438\u0437 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u0432\u0430\u0448\u0435\u0439 MaterialTheme, \u0430 \u043d\u0435 \u043a\u043e\u043d\u0441\u0442\u0430\u043d\u0442\u044b \u0442\u0430\u043a\u0438\u0435 \u043a\u0430\u043a Const.Blue \u0438 \u0442.\u0434. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 primary \u0446\u0432\u0435\u0442 \u0434\u043b\u044f \u043a\u0430\u0440\u0442\u043e\u0447\u043a\u0438 \u0437\u0430\u0434\u0430\u0435\u0442\u0441\u044f \u0442\u0430\u043a &#8212;  <em>containerColor = MaterialTheme.colorScheme.primary<\/em>.<\/p>\n<h2>\u0411\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 Room<\/h2>\n<p>\u0414\u043b\u044f \u043f\u0440\u043e\u0441\u0442\u043e\u0442\u044b \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u0432\u0435\u0440\u0441\u0438\u0438 \u0431\u0430\u0437\u044b \u043e\u043d\u0430 \u043e\u0447\u0438\u0449\u0430\u0435\u0442\u0441\u044f. \u041c\u0435\u0442\u043e\u0434 <em>cleanRockets() \u0442\u0430\u043a \u0436\u0435 \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043b\u044f \u0441\u0431\u0440\u043e\u0441\u0430 \u043a\u044d\u0448\u0430, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0447\u0435\u0440\u0435\u0437 WorkManager \u0447\u0435\u0440\u0435\u0437 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u044b \u0438\u043b\u0438 \u043f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u043f\u043e \u0441\u0432\u043e\u0435\u0439 \u043a\u043d\u043e\u043f\u043a\u0435 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445<\/em>.<\/p>\n<pre><code>            override fun onDestructiveMigration(db: SupportSQLiteDatabase) {                 super.onDestructiveMigration(db)                 INSTANCE?.let {                     scope.launch {                         it.dao().cleanRockets()                     }                 }             }  ...      @Query(\"DELETE FROM FalconInfo\")     fun cleanRockets() <\/code><\/pre>\n<p>\u041a\u043b\u0430\u0441\u0441-\u0422\u0430\u0431\u043b\u0438\u0446\u0430 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0411\u0430\u0437\u044b \u0414\u0430\u043d\u043d\u044b\u0445 \u043f\u043e\u043c\u0435\u0447\u0430\u0435\u043c <a class=\"mention\" href=\"\/users\/Entity\">@Entity<\/a>\u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u0435\u0439.<\/p>\n<pre><code class=\"kotlin\">@Entity(tableName = \"FalconInfo\")<\/code><\/pre>\n<p>\u0411\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438\u043c\u0435\u0435\u0442 \u0432\u043b\u043e\u0436\u0435\u043d\u043d\u0443\u044e \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0432\u043b\u043e\u0436\u0435\u043d\u043d\u044b\u0435 \u0442\u0430\u0431\u043b\u0438\u0446\u044b \u043d\u0430\u0434\u043e \u0442\u0430\u043a\u0436\u0435 \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c <a class=\"mention\" href=\"\/users\/embedded\">@Embedded<\/a>\u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u0435\u0439. Room \u0442\u0430\u043a\u0436\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043a\u0430\u0441\u043a\u0430\u0434\u043d\u043e\u0435 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435. \u042d\u0442\u043e\u0442 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.<\/p>\n<pre><code class=\"kotlin\">@Embedded(\"links\")  var linksEntry              : LinksEntry?              = LinksEntry(),<\/code><\/pre>\n<p>\u041f\u0440\u043e\u0441\u0442\u043e\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u0432\u0441\u0435\u0445 \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u0438\u0437 \u0411\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a.<\/p>\n<pre><code class=\"kotlin\">    @Query(\"SELECT * FROM FalconInfo\")     fun getAllRocketsInfo(): List&lt;FalconInfoEntity><\/code><\/pre>\n<h2>\u00a0\u041e\u043f\u0442\u0438\u043c\u0438\u0437\u0430\u0446\u0438\u0438<\/h2>\n<p>\u041a\u043e\u0433\u0434\u0430 \u043d\u043e\u0432\u044b\u0439 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432 \u043a\u043e\u043d\u0435\u0446 \u0441\u043f\u0438\u0441\u043a\u0430 \u0440\u0435\u043a\u043e\u043c\u043f\u043e\u0437\u0438\u0446\u0438\u0438 \u0441\u0446\u0435\u043d\u044b \u043d\u0435 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442. \u041d\u043e \u0435\u0441\u043b\u0438 \u043d\u043e\u0432\u044b\u0439 \u044d\u043b\u0435\u043c\u0435\u043d\u0442 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432 \u0441\u0435\u0440\u0435\u0434\u0438\u043d\u0443, \u043d\u0430\u0447\u0430\u043b\u043e, \u0443\u0434\u0430\u043b\u0438\u043b\u0441\u044f \u0438\u043b\u0438 \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u043e \u043f\u0435\u0440\u0435\u0443\u043f\u043e\u0440\u044f\u0434\u043e\u0447\u0438\u0432\u0430\u043d\u0438\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0441\u043f\u0438\u0441\u043a\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0439\u0434\u0435\u0442 \u043f\u0435\u0440\u0435\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0430 \u0432\u0441\u0435\u0445 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432<\/p>\n<p>\u0418\u0437 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438:<\/p>\n<p><a href=\"https:\/\/developer.android.com\/jetpack\/compose\/lifecycle\" rel=\"noopener noreferrer nofollow\">https:\/\/developer.android.com\/jetpack\/compose\/lifecycle<\/a><\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/8d7\/02a\/285\/8d702a28510f31b26900aa4fe96733b8.png\" alt=\"\u0420\u0430\u0437\u043d\u044b\u043c\u0438 \u0446\u0432\u0435\u0442\u0430\u043c\u0438 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u044b \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0441\u0442\u0440\u043e\u0435\u043d\u044b \" title=\"\u0420\u0430\u0437\u043d\u044b\u043c\u0438 \u0446\u0432\u0435\u0442\u0430\u043c\u0438 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u044b \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0441\u0442\u0440\u043e\u0435\u043d\u044b \" width=\"1999\" height=\"648\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/8d7\/02a\/285\/8d702a28510f31b26900aa4fe96733b8.png\"\/><\/p>\n<div><figcaption>\u0420\u0430\u0437\u043d\u044b\u043c\u0438 \u0446\u0432\u0435\u0442\u0430\u043c\u0438 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u044b \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0441\u0442\u0440\u043e\u0435\u043d\u044b <\/figcaption><\/div>\n<\/figure>\n<p><em>\u0415\u0441\u043b\u0438 \u043c\u044b \u0438\u0437\u043c\u0435\u043d\u0438\u043c \u043f\u043e\u0440\u044f\u0434\u043e\u043a \u0441\u043f\u0438\u0441\u043a\u0430 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432, \u0432 \u0438\u0434\u0435\u0430\u043b\u0435 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u043e\u0440\u044f\u0434\u043e\u043a \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u043e\u0432 \u0432 \u0434\u0435\u0440\u0435\u0432\u0435 \u043a\u043e\u043c\u043f\u043e\u0437\u0438\u0446\u0438\u0438 \u0432\u043c\u0435\u0441\u0442\u043e \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u0435\u0440\u0435\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u044b\u0432\u0430\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 MovieOverview \u0441 \u0434\u0440\u0443\u0433\u0438\u043c \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u043e\u043c \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 \u0441\u043f\u0438\u0441\u043a\u0430<\/em>.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/518\/92f\/d79\/51892fd79568468af0570b892aca8d7f.png\" alt=\"\u041f\u0435\u0440\u0435\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0430 \u043f\u043e\u0441\u043b\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430. \u041f\u0435\u0440\u0435\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\" title=\"\u041f\u0435\u0440\u0435\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0430 \u043f\u043e\u0441\u043b\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430. \u041f\u0435\u0440\u0435\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\" width=\"1999\" height=\"639\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/518\/92f\/d79\/51892fd79568468af0570b892aca8d7f.png\"\/><\/p>\n<div><figcaption>\u041f\u0435\u0440\u0435\u043a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u043a\u0430 \u043f\u043e\u0441\u043b\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430. \u041f\u0435\u0440\u0435\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u044d\u043b\u0435\u043c\u0435\u043d\u0442<\/figcaption><\/div>\n<\/figure>\n<p><em>Compose \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043d\u0430\u043c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0438\u0442\u044c \u0441\u0440\u0435\u0434\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f, \u043a\u0430\u043a\u0438\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043c\u044b \u0445\u043e\u0442\u0438\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043b\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0434\u0430\u043d\u043d\u043e\u0439 \u0447\u0430\u0441\u0442\u0438 \u0434\u0435\u0440\u0435\u0432\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a\u043b\u044e\u0447\u0430<\/em><\/p>\n<p>\u041e\u043f\u0442\u0438\u043c\u0438\u0437\u0430\u0446\u0438\u044f \u0441\u043f\u0438\u0441\u043a\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0432\u044b\u0434\u043b\u044f\u0434\u0438\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c:<\/p>\n<pre><code class=\"kotlin\">        columns = GridCells.Fixed(2), content = {             items(falconInfoEntries, key = {falconInfo -> falconInfo.id}) { falconInfo ->                 FalconInfoCard(falconInfo)             }         })<\/code><\/pre>\n<details class=\"spoiler\">\n<summary>Links<\/summary>\n<div class=\"spoiler__content\">\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/joydev\/articles\/743048\/\" rel=\"noopener noreferrer nofollow\">https:\/\/habr.com\/ru\/companies\/joydev\/articles\/743048\/<\/a><\/p>\n<p><a href=\"https:\/\/habr.com\/ru\/companies\/ozontech\/articles\/742854\/\" rel=\"noopener noreferrer nofollow\">https:\/\/habr.com\/ru\/companies\/ozontech\/articles\/742854\/<\/a><\/p>\n<\/div>\n<\/details>\n<h2>\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 \u043f\u0440\u043e\u0435\u043a\u0442\u0430<\/h2>\n<p>\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 \u0432\u0441\u0435\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u043d\u0430 <a href=\"https:\/\/github.com\/app-z\/NoArchitecture-Kotlin-Compose\/tree\/architecture\" rel=\"noopener noreferrer nofollow\">GitHub<\/a> \u0432 \u0432\u0435\u0442\u043a\u0435 architecture.<\/p>\n<p>\u041f\u0435\u0440\u0432\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u043e\u043f\u0438\u0441\u0430\u043d\u0430 \u0432 \u0441\u0442\u0430\u0442\u044c\u0435 <a href=\"https:\/\/habr.com\/ru\/articles\/763980\/\" rel=\"noopener noreferrer nofollow\">https:\/\/habr.com\/ru\/articles\/763980\/<\/a><\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><\/p>\n<div class=\"tm-article-poll-container\"><!--[--><\/p>\n<div class=\"tm-article-poll tm-article-poll_variant-bordered\">\n<div class=\"tm-notice tm-notice_positive tm-article-poll__notice\"><!----><\/p>\n<div class=\"tm-notice__inner\"><!----><\/p>\n<div class=\"tm-notice__content\" data-test-id=\"notice-content\"><!--[--><span>\u0422\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u043c\u043e\u0433\u0443\u0442 \u0443\u0447\u0430\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c \u0432 \u043e\u043f\u0440\u043e\u0441\u0435. <a rel=\"nofollow\" href=\"\/kek\/v1\/auth\/habrahabr\/?back=\/ru\/articles\/765416\/&#038;hl=ru\">\u0412\u043e\u0439\u0434\u0438\u0442\u0435<\/a>, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430.<\/span><!--]--><\/div>\n<\/div>\n<\/div>\n<p><!--[--><\/p>\n<div class=\"tm-article-poll__header\">\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 \u043b\u0438 \u0432\u044b Ktorfit \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0430\u0445?<\/div>\n<div class=\"tm-article-poll__answers\"><!--[--><\/p>\n<div class=\"tm-article-poll__answer\">\n<div class=\"tm-article-poll__answer-data\"><span class=\"tm-article-poll__answer-percent\">16.67% <\/span><span class=\"tm-article-poll__answer-label\">\u0414\u0430<\/span><span class=\"tm-article-poll__answer-votes\">3<\/span><\/div>\n<div class=\"tm-article-poll__answer-bar\">\n<div class=\"tm-article-poll__answer-progress\" style=\"width: 16.67%\"><\/div>\n<\/div>\n<\/div>\n<div class=\"tm-article-poll__answer\">\n<div class=\"tm-article-poll__answer-data\"><span class=\"tm-article-poll__answer-percent tm-article-poll__answer-percent_winning\">83.33% <\/span><span class=\"tm-article-poll__answer-label\">\u041d\u0435\u0442<\/span><span class=\"tm-article-poll__answer-votes\">15<\/span><\/div>\n<div class=\"tm-article-poll__answer-bar\">\n<div class=\"tm-article-poll__answer-progress tm-article-poll__answer-progress_winning\" style=\"width: 83.33%\"><\/div>\n<\/div>\n<\/div>\n<p><!--]--><\/div>\n<div class=\"tm-article-poll__stats\"> \u041f\u0440\u043e\u0433\u043e\u043b\u043e\u0441\u043e\u0432\u0430\u043b\u0438 18 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439.   \u0412\u043e\u0437\u0434\u0435\u0440\u0436\u0430\u0432\u0448\u0438\u0445\u0441\u044f \u043d\u0435\u0442. <\/div>\n<p><!--]--><\/div>\n<div class=\"tm-article-poll tm-article-poll_variant-bordered\">\n<div class=\"tm-notice tm-notice_positive tm-article-poll__notice\"><!----><\/p>\n<div class=\"tm-notice__inner\"><!----><\/p>\n<div class=\"tm-notice__content\" data-test-id=\"notice-content\"><!--[--><span>\u0422\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u043c\u043e\u0433\u0443\u0442 \u0443\u0447\u0430\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c \u0432 \u043e\u043f\u0440\u043e\u0441\u0435. <a rel=\"nofollow\" href=\"\/kek\/v1\/auth\/habrahabr\/?back=\/ru\/articles\/765416\/&#038;hl=ru\">\u0412\u043e\u0439\u0434\u0438\u0442\u0435<\/a>, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430.<\/span><!--]--><\/div>\n<\/div>\n<\/div>\n<p><!--[--><\/p>\n<div class=\"tm-article-poll__header\">\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 \u043b\u0438 \u0432\u044b Compose \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0430\u0445?<\/div>\n<div class=\"tm-article-poll__answers\"><!--[--><\/p>\n<div class=\"tm-article-poll__answer\">\n<div class=\"tm-article-poll__answer-data\"><span class=\"tm-article-poll__answer-percent tm-article-poll__answer-percent_winning\">53.33% <\/span><span class=\"tm-article-poll__answer-label\">\u0414\u0430<\/span><span class=\"tm-article-poll__answer-votes\">8<\/span><\/div>\n<div class=\"tm-article-poll__answer-bar\">\n<div class=\"tm-article-poll__answer-progress tm-article-poll__answer-progress_winning\" style=\"width: 53.33%\"><\/div>\n<\/div>\n<\/div>\n<div class=\"tm-article-poll__answer\">\n<div class=\"tm-article-poll__answer-data\"><span class=\"tm-article-poll__answer-percent\">46.67% <\/span><span class=\"tm-article-poll__answer-label\">\u041d\u0435\u0442<\/span><span class=\"tm-article-poll__answer-votes\">7<\/span><\/div>\n<div class=\"tm-article-poll__answer-bar\">\n<div class=\"tm-article-poll__answer-progress\" style=\"width: 46.67%\"><\/div>\n<\/div>\n<\/div>\n<p><!--]--><\/div>\n<div class=\"tm-article-poll__stats\"> \u041f\u0440\u043e\u0433\u043e\u043b\u043e\u0441\u043e\u0432\u0430\u043b\u0438 15 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439.   \u0412\u043e\u0437\u0434\u0435\u0440\u0436\u0430\u043b\u0441\u044f 1 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c. <\/div>\n<p><!--]--><\/div>\n<p><!--]--><\/div>\n<p> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/765416\/\"> https:\/\/habr.com\/ru\/articles\/765416\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<h2>\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f<\/h2>\n<figure class=\"full-width\">\n<div><figcaption>Data+Domain<\/figcaption><\/div>\n<\/figure>\n<p>\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u0438\u0442 \u0438\u0437 \u043e\u0434\u043d\u043e\u0439 Activity \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0441\u043e\u0437\u0434\u0430\u0435\u0442\u0441\u044f GetFalconInfoUseCase.<\/p>\n<pre><code class=\"kotlin\">@AndroidEntryPoint class MainActivity : ComponentActivity() {      @Inject     lateinit var getFalconInfoUseCase: GetFalconInfoUseCase<\/code><\/pre>\n<p>\u0414\u0430\u043b\u0435\u0435 \u044d\u0442\u043e\u0442 UseCase \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u0442\u0441\u044f \u0432 \u044d\u043a\u0440\u0430\u043d \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u044f \u0441\u043f\u0438\u0441\u043a\u0430 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f <a href=\"https:\/\/developer.android.com\/jetpack\/compose\/side-effects#producestate\" rel=\"noopener noreferrer nofollow\">producestate<\/a> \u0434\u043b\u044f \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f non-Compose state into Compose state. \u0415\u0449\u0435 \u043f\u0440\u043e &#171;\u041f\u043e\u0431\u043e\u0447\u043d\u044b\u0435 \u044d\u0444\u0444\u0435\u043a\u0442\u044b&#187; \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u044e\u0449\u0438\u0435 \u043f\u0440\u0438 \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u0438 \u043a\u043e\u043c\u043f\u043e\u0437\u0438\u0446\u0438\u0438 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0442\u0443\u0442 <a href=\"https:\/\/developer.android.com\/reference\/kotlin\/androidx\/compose\/runtime\/package-summary#SideEffect(kotlin.Function0)\" rel=\"noopener noreferrer nofollow\">SideEffect<\/a>. <a href=\"https:\/\/habr.com\/ru\/companies\/joydev\/articles\/743048\/\" rel=\"noopener noreferrer nofollow\">https:\/\/habr.com\/ru\/companies\/joydev\/articles\/743048\/<\/a><\/p>\n<details class=\"spoiler\">\n<summary>FalconInfoItemsScreen<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"kotlin\">@Composable fun FalconInfoItemsScreen(getFalconInfoUseCase: GetFalconInfoUseCase) {         produceState&lt;ResponseResult&lt;List&lt;FalconInfo>>>(initialValue = ResponseResult.Loading, getFalconInfoUseCase) {             try {                 getFalconInfoUseCase.getFalconInfo().collect {                     value = it                 }             } catch (e: DataSourceException) {                 value = ResponseResult.Error(e)             }         }.let {             when (it.value) {             is ResponseResult.Loading -> ShowStatusScreen(\"Loading...\")              is ResponseResult.Success -> {                 it.value.onSuccess { rockets ->                     FalconInfoListView(rockets)                 }             }              is ResponseResult.Error -> it.value.onError { error ->                 ShowStatusScreen(error.getError(LocalContext.current))             }          }     } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0417\u0430\u043f\u0440\u043e\u0441\u0430 \u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f <em>rememberCoroutineScope \u0438 mutableStateOf<\/em><\/p>\n<p><strong>mutableStateOf<\/strong>\u00a0&#8212; \u0421\u043e\u0437\u0434\u0430\u0435\u0442 State \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e compose \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f<\/p>\n<p><strong>remember<\/strong>\u00a0&#8212; \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u044d\u0442\u043e\u0442 State \u043f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u043c \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438<\/p>\n<details class=\"spoiler\">\n<summary>\u0414\u043b\u044f \u0442\u0438\u043f\u0430 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0430\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u044f @Stable \u0435\u0441\u043b\u0438 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430 \u043d\u0435 \u0443\u0434\u043e\u0432\u043b\u0435\u0442\u0432\u043e\u0440\u044f\u0435\u0442 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f\u043c \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u0430<\/summary>\n<div class=\"spoiler__content\">\n<h4>Skipping if the inputs haven&#8217;t changed<\/h4>\n<p>If a composable is already in the Composition, it can skip recomposition if all the inputs are stable and haven&#8217;t changed.<\/p>\n<p>A stable type must comply with the following contract:<\/p>\n<ul>\n<li>\n<p>The result of <code>equals<\/code> for two instances will <em>forever<\/em> be the same for the same two instances.<\/p>\n<\/li>\n<li>\n<p>If a public property of the type changes, Composition will be notified.<\/p>\n<\/li>\n<li>\n<p>All public property types are also stable.<\/p>\n<\/li>\n<\/ul>\n<p>There are some important common types that fall into this contract that the compose compiler will treat as stable, even though they are not explicitly marked as stable by using the <code>@Stable<\/code> annotation:<\/p>\n<ul>\n<li>\n<p>All primitive value types: <code>Boolean<\/code>, <code>Int<\/code>, <code>Long<\/code>, <code>Float<\/code>, <code>Char<\/code>, etc.<\/p>\n<\/li>\n<li>\n<p>Strings<\/p>\n<\/li>\n<li>\n<p>All Function types (lambdas)<\/p>\n<\/li>\n<\/ul>\n<p>All of these types are able to follow the contract of stable because they are immutable. Since immutable types never change, they never have to notify Composition of the change, so it is much easier to follow this contract.<\/p>\n<p><strong>Note:<\/strong> All deeply immutable types can safely be considered stable types.<\/p>\n<p>One notable type that is stable but <em>is<\/em> mutable is Compose\u2019s <code>MutableState<\/code> type. If a value is held in a <code>MutableState<\/code>, the state object overall is considered to be stable as Compose will be notified of any changes to the <code>.value<\/code> property of <code>State<\/code>.<\/p>\n<p>When all types passed as parameters to a composable are stable, the parameter values are compared for equality based on the composable position in the UI tree. Recomposition is skipped if all the values are unchanged since the previous call.<\/p>\n<p><strong>Key Point:<\/strong> Compose skips the recomposition of a composable if all the inputs are stable and haven&#8217;t changed. The comparison uses the <code>equals<\/code> method.<\/p>\n<p>Compose considers a type stable only if it can prove it. For example, an interface is generally treated as not stable, and types with mutable public properties whose implementation could be immutable are not stable either.<\/p>\n<p>If Compose is not able to infer that a type is stable, but you want to force Compose to treat it as stable, mark it with the <code>@Stable<\/code> annotation.<\/p>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 \u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u044d\u043a\u0440\u0430\u043d\u0430<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"kotlin\">fun FalconInfoItemsScreen(getFalconInfoUseCase: GetFalconInfoUseCase) {      val scope = rememberCoroutineScope()     var result: ResponseResult&lt;List&lt;FalconInfo>> by remember { mutableStateOf(ResponseResult.Loading) }      LaunchedEffect(key1 = true) {         scope.launch {             try {                 getFalconInfoUseCase.getFalconInfo().collect {                     result = it                 }             } catch (e: DataSourceException) {                 result = ResponseResult.Error(e)             }         }     }      when (result) {         is ResponseResult.Loading -> ShowStatusScreen(stringResource(R.string.loading))          is ResponseResult.Success -> {                 result.onSuccess {                     FalconInfoListView(it)                     Timber.d(\"size = ${it.size}\")                 }         }          is ResponseResult.Error -> result.onError { error ->             ShowStatusScreen(error.getError(LocalContext.current))         }     } <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<h2>\u041d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044f<\/h2>\n<p>\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0437\u0430\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0432 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 \u043f\u0430\u0440\u044b <em>route<\/em> \u043a\u0430\u043a \u0441\u0442\u0440\u043e\u043a\u043e\u0432\u043e\u0439 \u043a\u043e\u043d\u0441\u0442\u0430\u043d\u0442\u044b \u0438 compose \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0430 \u043e\u0442\u0440\u0438\u0441\u043e\u0432\u043a\u0438. \u0422\u0430\u043a\u0436\u0435 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u043c \u043f\u0435\u0440\u0432\u044b\u0439 \u044d\u043a\u0440\u0430\u043d <em>startDestination<\/em>.<\/p>\n<pre><code class=\"kotlin\">val navController = rememberNavController()     NavHost(navController = navController, startDestination = LOGIN_SCREEN) {         composable(HOME_SCREEN) { HomeScreen() }         composable(ROCKETS_SCREEN) { FalconInfoItemsScreen(getFalconInfoUseCase) }         composable(LOGIN_SCREEN) { LoginScreen() }     }<\/code><\/pre>\n<p>\u041d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044f \u043f\u043e \u044d\u043a\u0440\u0430\u043d\u0430\u043c \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0442\u0430\u0431\u0430<\/p>\n<pre><code class=\"kotlin\">          when (selectedTabIndex) {               HOME_TAB -> navigate(navController, Constants.HOME_SCREEN)               ROCKETS_TAB -> navigate(navController, Constants.ROCKETS_SCREEN)               LOGIN_TAB -> navigate(navController, Constants.LOGIN_SCREEN)           }<\/code><\/pre>\n<p>\u0425\u0430\u043a \u0434\u043b\u044f \u0432\u044b\u0442\u0430\u043b\u043a\u0438\u0432\u0430\u043d\u0438\u044f \u044d\u043a\u0440\u0430\u043d\u0430, \u043a\u0430\u043a \u0444\u0440\u0430\u0433\u043c\u0435\u043d\u0442\u0430 \u0438\u0437 BackStack \u043f\u0440\u0438 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0435, \u0447\u0442\u043e\u0431\u044b \u043d\u0435 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0435 \u044d\u043a\u0440\u0430\u043d\u044b \u0432 \u0441\u0442\u0435\u043a\u0435 \u0438 \u0441\u0440\u0430\u0437\u0443 \u0432\u044b\u0439\u0442\u0438 \u0438\u0437 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043f\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044e Back.<\/p>\n<pre><code class=\"kotlin\">fun navigate(navController: NavController, route: String) {     navController.navigate(route) {         popUpTo(0)     } }<\/code><\/pre>\n<details class=\"spoiler\">\n<summary>Refactoring<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"kotlin\">sealed class Screen(     val tabIndex: Int,     val route: String,     @StringRes val textResId: Int,     val selectedItem: ImageVector,     val unselectedItem: ImageVector,     val popUp: Boolean = true ) {     data object Home : Screen(HOME_TAB, HOME_SCREEN, R.string.home,         Icons.Filled.Home, Icons.Outlined.Home)     data object Rockets : Screen(ROCKETS_TAB, ROCKETS_SCREEN, R.string.rockets,         Icons.Filled.Rocket, Icons.Outlined.Rocket)     data object Login : Screen(LOGIN_TAB, LOGIN_SCREEN, R.string.login,         Icons.Filled.AccountCircle, Icons.Outlined.AccountCircle)      companion object {         const val HOME_SCREEN = \"homeScreen\"         const val ROCKETS_SCREEN = \"rocketsScreen\"         const val LOGIN_SCREEN = \"loginScreen\"          const val HOME_TAB = 0         const val ROCKETS_TAB = 1         const val LOGIN_TAB = 2     } }    @Composable private fun initTabItems(): List&lt;TabItem> {     val items = listOf(Screen.Home, Screen.Rockets, Screen.Login)      return items.map {         TabItem(title = stringResource(it.textResId),             unselectedItem = it.unselectedItem,             selectedItem = it.selectedItem)     } }<\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<h2>Ktorfit<\/h2>\n<p>\u0422\u0440\u0430\u0434\u0438\u0446\u0438\u043e\u043d\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 Retrofit \u0437\u0430\u043c\u0435\u043d\u0435\u043d \u043d\u0430 Ktorfit. \u041f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0434\u0430\u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0434\u0432\u0443\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u043e\u0445\u043e\u0436\u0438 \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a\u0436\u0435.  \u0412 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0442\u0430\u043a \u0438 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u043e &#8212; inspired by Retrofit. \u0410\u043d\u043d\u043e\u0442\u0430\u0446\u0438\u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u044b \u0431\u0435\u0437 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439. \u0412 \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f <em><a class=\"mention\" href=\"\/users\/get.\">@GET.<\/a><\/em><\/p>\n<blockquote>\n<p>Ktorfit is a HTTP client\/Kotlin Symbol Processor for Kotlin Multiplatform ( Android, iOS, Js, Jvm, Linux) using\u00a0<a href=\"https:\/\/github.com\/google\/ksp\" rel=\"noopener noreferrer nofollow\">KSP<\/a>\u00a0and\u00a0<a href=\"https:\/\/ktor.io\/docs\/getting-started-ktor-client.html\" rel=\"noopener noreferrer nofollow\">Ktor clients<\/a>\u00a0inspired by\u00a0<a href=\"https:\/\/square.github.io\/retrofit\/\" rel=\"noopener noreferrer nofollow\">Retrofit<\/a>.<\/p>\n<\/blockquote>\n<pre><code class=\"kotlin\">    @Singleton     @Provides     fun provideKtorfit(): Ktorfit {         val ktorfit = ktorfit {             baseUrl(Constants.BASE_URL)             httpClient(HttpClient {                 \/\/ install(HttpCache)                 install(ContentNegotiation)                 {                     json(                         Json {                             prettyPrint = true                             isLenient = true                             ignoreUnknownKeys = true                         }                     )                 }             })             converterFactories(                 FlowConverterFactory(),                 CallConverterFactory(),             )          }         return ktorfit     }      @Singleton     @Provides     fun provideApiServices(ktorfitClient: Ktorfit): ApiService {         return ktorfitClient.create&lt;ApiService>()     } <\/code><\/pre>\n<p>\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u043f\u0440\u043e\u0435\u043a\u0442 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043f\u043e\u0445\u043e\u0436\u0435\u0439 \u043d\u0430 Retrofit. HttpClient \u0438 json serializer \u043f\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0438\u0437 Ktor \u043f\u0430\u043a\u0435\u0442\u0430. \u041f\u043e\u0434 \u043a\u0430\u043f\u043e\u0442\u043e\u043c \u0442\u0430\u043c <em>package kotlinx.serialization.json<\/em>. <a href=\"https:\/\/foso.github.io\/Ktorfit\/converters\/converters\/\" rel=\"noopener noreferrer nofollow\">Converters<\/a> \u0433\u0438\u0431\u043a\u043e \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u043d\u0438\u0436\u0435 \u0441\u043f\u0438\u0441\u043e\u043a \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0445 \u043f\u0430\u0440\u0441\u0435\u0440\u043e\u0432.<\/p>\n<details class=\"spoiler\">\n<summary>Provides a list of standard subtypes of an application content type<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"kotlin\">public val Any: ContentType = ContentType(\"application\", \"*\") public val Atom: ContentType = ContentType(\"application\", \"atom+xml\") public val Cbor: ContentType = ContentType(\"application\", \"cbor\") public val Json: ContentType = ContentType(\"application\", \"json\") public val HalJson: ContentType = ContentType(\"application\", \"hal+json\") public val JavaScript: ContentType = ContentType(\"application\", \"javascript\") public val OctetStream: ContentType = ContentType(\"application\", \"octet-stream\") public val Rss: ContentType = ContentType(\"application\", \"rss+xml\") public val Xml: ContentType = ContentType(\"application\", \"xml\") public val Xml_Dtd: ContentType = ContentType(\"application\", \"xml-dtd\") public val Zip: ContentType = ContentType(\"application\", \"zip\") public val GZip: ContentType = ContentType(\"application\", \"gzip\") <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u041d\u0430 GitHub Ktorfit \u0435\u0441\u0442\u044c \u0434\u0432\u0430 \u043f\u0440\u0438\u043c\u0435\u0440\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f <a href=\"https:\/\/github.com\/Foso\/Ktorfit\/tree\/master\/example\/AndroidOnlyExample\" rel=\"noopener noreferrer nofollow\">AndroidOnlyExample<\/a> \u0438 <a href=\"https:\/\/github.com\/Foso\/Ktorfit\/tree\/master\/example\/MultiplatformExample\" rel=\"noopener noreferrer nofollow\">MultiplatformExample<\/a>. \u0412 \u043f\u0440\u043e\u0435\u043a\u0442 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u043f\u0435\u0440\u0432\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442. \u0412 \u0431\u0443\u0434\u0443\u0449\u0435\u043c \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043d\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 KMM (<em>Kotlin Multiplatform Mobile<\/em>).<\/p>\n<h2>\u041a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 Ktorfit \u0432 Room<\/h2>\n<p>\u041b\u043e\u0433\u0438\u043a\u0430 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a &#8212; \u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442\u0441\u044f \u043f\u0443\u0441\u0442\u0430\u044f \u043b\u0438 \u0431\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445. \u0415\u0441\u043b\u0438 \u043d\u0435\u0442 \u0442\u043e \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437\u0432\u043b\u0435\u043a\u0430\u044e\u0442\u0441\u044f \u0438\u0437 \u043d\u0435\u0435. \u0415\u0441\u043b\u0438 \u043f\u0443\u0441\u0442\u0430\u044f \u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442\u0441\u044f \u0437\u0430\u043f\u0440\u043e\u0441 \u0432 \u0441\u0435\u0442\u044c \u0438 \u043f\u043e\u0441\u043b\u0435 \u043f\u0435\u0440\u0435\u043c\u0430\u043f\u043b\u0438\u0432\u0430\u043d\u0438\u044f <em>responseToDomain()<\/em> \u043e\u0442\u0434\u0430\u0435\u0442\u0441\u044f \u043d\u0430 <\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-356671","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/356671","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=356671"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/356671\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=356671"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=356671"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=356671"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}