From af0befe6d0c0fdcdee60a65113f83c86b0c87473 Mon Sep 17 00:00:00 2001
From: Kuzmin <«vkuzmin@icerockdev.com»>
Date: Fri, 15 May 2026 16:32:10 +0300
Subject: [PATCH 01/15] edit moko-resources
---
learning/libraries/moko/moko-resources.md | 153 +++++++++++++++++++++-
1 file changed, 150 insertions(+), 3 deletions(-)
diff --git a/learning/libraries/moko/moko-resources.md b/learning/libraries/moko/moko-resources.md
index e246258bf..adf2f225d 100644
--- a/learning/libraries/moko/moko-resources.md
+++ b/learning/libraries/moko/moko-resources.md
@@ -4,9 +4,156 @@ sidebar_position: 2
# moko-resources
-## Почему StringDesc не Parcelable
+Библиотека [moko-resources](https://github.com/icerockdev/moko-resources) — это решение для доступа к ресурсам (строки, изображения, цвета, шрифты, файлы) из общего Kotlin Multiplatform кода. Поддерживает Android, iOS, macOS, JVM, JS/Wasm и Compose Multiplatform.
-StringDesc не может быть Parcelable так как у нас есть ResourceFormattedStringDesc
+## Возможности
+
+- **Строки и плюралы** с поддержкой локализации — общий код для всех платформ
+- **StringDesc** — lifecycle-safe контейнер для строк, позволяющий отложить получение строки до момента, когда доступен platform context
+- **Цвета** с поддержкой light/dark темы
+- **Изображения** (SVG, PNG, JPG) с light/dark режимом
+- **Шрифты** (TTF, OTF)
+- **Файлы** (raw/assets) для Android
+- **Compose Multiplatform** — все ресурсы доступны как `painterResource`, `stringResource`, `colorResource` и т.д.
+
+## Подключение
+
+```kotlin
+// root build.gradle.kts
+buildscript {
+ dependencies {
+ classpath("dev.icerock.moko:resources-generator:0.26.4")
+ }
+}
+```
+
+```kotlin
+// shared/build.gradle.kts
+plugins {
+ kotlin("multiplatform")
+ id("dev.icerock.mobile.multiplatform-resources")
+}
+
+dependencies {
+ commonMainApi("dev.icerock.moko:resources:0.26.4")
+ // для Compose Multiplatform:
+ commonMainApi("dev.icerock.moko:resources-compose:0.26.4")
+ commonTestImplementation("dev.icerock.moko:resources-test:0.26.4")
+}
+
+multiplatformResources {
+ resourcesPackage.set("com.example.app") // обязательный package
+ resourcesClassName.set("SharedRes") // опционально, по умолчанию MR
+ resourcesVisibility.set(MRVisibility.Internal)
+}
+```
+
+## Использование
+
+Вот несколько примеров, подробнее смотрите в [moko-resources](https://github.com/icerockdev/moko-resources)
+
+### Строки (strings.xml)
+
+```xml
+
+
+ Hello
+ Test data %d
+ second string %2$s first decimal %1$d
+
+```
+
+```kotlin
+// commonMain — получение StringDesc
+val hello: StringDesc = MR.strings.hello.desc()
+val formatted: StringDesc = MR.strings.format.format(9)
+val positional: StringDesc = MR.strings.positional.format(9, "str")
+```
+
+```kotlin
+// Android — преобразование в String
+val text: String = hello.toString(context = this)
+
+// iOS
+let text: String = hello.localized()
+```
+
+### Изображения
+
+PNG/JPG имена файлов должны содержать суффикс плотности (`@1x`, `@2x`, `@3x`, `@4x`).
+
+```kotlin
+val image: ImageResource = MR.images.home_black_18
+val vector: ImageResource = MR.images.car_black // SVG
+
+// Android
+imageView.setImageResource(image.drawableResId)
+
+// iOS
+imageView.image = image.toUIImage()
+
+// Compose
+Image(painter = painterResource(MR.images.car_black), contentDescription = null)
+```
+
+Поддержка Dark Mode: добавьте `-dark` к имени файла:
+
+- `car.svg`
+- `car-dark.svg`
+
+### Файлы и ассеты
+
+```kotlin
+// Чтение текстового файла
+val text: String = MR.files.test_txt.readText() // common
+val text: String = MR.files.test_txt.getText(context) // Android
+
+// Compose — реактивное чтение
+val content: String? by MR.files.test_txt.readTextAsState()
+```
+
+## Compose Multiplatform
+
+Если подключён модуль `resources-compose`, все ресурсы доступны напрямую в `commonMain`:
+
+```kotlin
+// Строки
+Text(text = stringResource(MR.strings.hello_world))
+
+// Плюралы
+Text(text = pluralStringResource(MR.plurals.chars_count, counter, counter))
+
+// Цвета
+Text(color = colorResource(MR.colors.textColor), text = "Hello")
+
+// Изображения
+Image(painter = painterResource(MR.images.moko_logo), contentDescription = null)
+
+// Шрифты
+Text(fontFamily = fontFamilyResource(MR.fonts.cormorant_italic), text = "Hello")
+
+// Файлы
+val fileContent: String? by MR.files.some_file_txt.readTextAsState()
+```
+
+## Multi-module проекты
+
+Если ресурсы лежат в отдельном модуле, плагин нужно применить и в модуле с ресурсами, и в модуле, который собирается в framework.
+
+Для вложенного модуля можно задать кастомное имя класса:
+
+```kotlin
+multiplatformResources {
+ resourcesPackage.set("com.example.nested")
+ resourcesClassName.set("NestedMR")
+}
+```
+
+---
+
+## Почему StringDesc не Parcelable
+
+StringDesc не может быть Parcelable так как у нас есть ResourceFormattedStringDesc:
```kotlin
actual data class ResourceFormattedStringDesc actual constructor(
@@ -23,4 +170,4 @@ actual data class ResourceFormattedStringDesc actual constructor(
}
```
-Так как его аргументы типа Any, значит класс ResourceFormattedStringDesc не может быть parcelable, а значит и StringDesc не может быть Parcelable
\ No newline at end of file
+Так как его аргументы типа `Any`, класс `ResourceFormattedStringDesc` не может быть Parcelable, а значит и `StringDesc` не может быть Parcelable.
From 1b9e8205a312af7bf7c39b98836c2af081bc04be Mon Sep 17 00:00:00 2001
From: Kuzmin <«vkuzmin@icerockdev.com»>
Date: Fri, 15 May 2026 16:53:25 +0300
Subject: [PATCH 02/15] edit moko-mvvm
---
learning/libraries/moko/moko-mvvm.md | 129 ++++++++++++++++++++++++++-
1 file changed, 127 insertions(+), 2 deletions(-)
diff --git a/learning/libraries/moko/moko-mvvm.md b/learning/libraries/moko/moko-mvvm.md
index a4c3320e0..8f5d9228a 100644
--- a/learning/libraries/moko/moko-mvvm.md
+++ b/learning/libraries/moko/moko-mvvm.md
@@ -4,9 +4,134 @@ sidebar_position: 3
# moko-mvvm
-Библиотека [moko-mvvm](https://github.com/icerockdev/moko-mvvm) позволяет реализовывать приложения с паттерном
-Model - View - ViewModel с ViewModel в общем коде.
+Библиотека [moko-mvvm](https://github.com/icerockdev/moko-mvvm) предоставляет архитектурные компоненты Model-View-ViewModel для Kotlin Multiplatform. ViewModel работает в общем коде, на Android обеспечивается lifecycle-aware поведение через интеграцию с `androidx.lifecycle`.
+
+## Возможности
+
+- **ViewModel** — хранение и управление UI-данными с `viewModelScope` (CoroutineScope, отменяемый в `onCleared`)
+- **LiveData / Flow** — реактивные обёртки с операторами (`map`, `merge`, `combine`, `all`)
+- **EventsDispatcher** — отправка одноразовых событий из ViewModel во View с контролем lifecycle
+- **CFlow / CStateFlow / CMutableStateFlow** — обёртки над корутинными Flow, совместимые со Swift
+- **ResourceState** — sealed class для состояний: `Loading`, `Success`, `Empty`, `Failed`
+- **Compose Multiplatform / SwiftUI** — готовая интеграция
+
+## Требования
+
+- Android API 16+
+- iOS 11.0+
+- Gradle 6.8+
+
+## Подключение
+
+```kotlin
+// shared/build.gradle.kts
+dependencies {
+ commonMainApi("dev.icerock.moko:mvvm-core:0.16.1")
+ commonMainApi("dev.icerock.moko:mvvm-flow:0.16.1")
+ commonMainApi("dev.icerock.moko:mvvm-state:0.16.1")
+
+ // Compose Multiplatform
+ commonMainApi("dev.icerock.moko:mvvm-compose:0.16.1")
+ commonMainApi("dev.icerock.moko:mvvm-flow-compose:0.16.1")
+
+ commonTestImplementation("dev.icerock.moko:mvvm-test:0.16.1")
+}
+```
+
+Экспорт в iOS framework:
+
+```kotlin
+kotlin {
+ targets.withType(KotlinNativeTarget::class.java).all {
+ binaries.withType(Framework::class.java).all {
+ export("dev.icerock.moko:mvvm-core:0.16.1")
+ export("dev.icerock.moko:mvvm-flow:0.16.1")
+ }
+ }
+}
+```
+
+## Simple ViewModel — счётчик на CStateFlow
+
+```kotlin
+// commonMain
+class SimpleViewModel : ViewModel() {
+ private val _counter = MutableStateFlow(0)
+ val counter: CStateFlow =
+ _counter.map { it.toString() }
+ .stateIn(viewModelScope, SharingStarted.Lazily, "0")
+ .cStateFlow()
+
+ fun onCounterButtonPressed() {
+ _counter.value++
+ }
+}
+```
+
+**Compose:**
+
+```kotlin
+@Composable
+fun CounterScreen(
+ viewModel: SimpleViewModel = getViewModel(
+ factory = createViewModelFactory { SimpleViewModel() }
+ )
+) {
+ val counter: String by viewModel.counter.collectAsState()
+
+ Column {
+ Text(text = counter)
+ Button(onClick = { viewModel.onCounterButtonPressed() }) {
+ Text("Press me")
+ }
+ }
+}
+```
+
+**SwiftUI:**
+
+```swift
+struct CounterView: View {
+ @ObservedObject var viewModel: SimpleViewModel = SimpleViewModel()
+
+ var body: some View {
+ VStack {
+ Text(viewModel.counter.state(\.counter))
+ Button("Press me") { viewModel.onCounterButtonPressed() }
+ }
+ }
+}
+```
+
+## ResourceState
+
+```kotlin
+sealed class ResourceState {
+ class Loading : ResourceState()
+ data class Success(val data: T) : ResourceState()
+ class Empty : ResourceState()
+ data class Failed(val error: E) : ResourceState()
+}
+```
+
+```kotlin
+val state: CStateFlow, StringDesc>> = ...
+
+// Compose
+when (val s = state.collectAsState().value) {
+ is ResourceState.Loading -> LoadingState()
+ is ResourceState.Success -> BookList(s.data)
+ is ResourceState.Empty -> EmptyState()
+ is ResourceState.Failed -> ErrorState(s.error)
+}
+```
+
+## Дополнительный материал
+
+- [Референс библиотеки](https://github.com/icerockdev/moko-mvvm)
+- [Семплы (DataBinding)](https://github.com/icerockdev/moko-mvvm/tree/master/sample)
+- [Семплы (Compose + SwiftUI)](https://github.com/icerockdev/moko-mvvm/tree/master/sample-declarative-ui)
From 86ff193e6d95c0daecf55763e769638fd13e50d0 Mon Sep 17 00:00:00 2001
From: Kuzmin <«vkuzmin@icerockdev.com»>
Date: Fri, 15 May 2026 16:56:39 +0300
Subject: [PATCH 03/15] move moko-units
---
learning/{libraries/moko => legacy}/moko-units.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
rename learning/{libraries/moko => legacy}/moko-units.md (99%)
diff --git a/learning/libraries/moko/moko-units.md b/learning/legacy/moko-units.md
similarity index 99%
rename from learning/libraries/moko/moko-units.md
rename to learning/legacy/moko-units.md
index 264155958..f18e88590 100644
--- a/learning/libraries/moko/moko-units.md
+++ b/learning/legacy/moko-units.md
@@ -1,5 +1,5 @@
---
-sidebar_position: 4
+sidebar_position: 6
---
# moko-units
From 017698b4d85d62bf9f839f0dbd333f3adfacd840 Mon Sep 17 00:00:00 2001
From: Kuzmin <«vkuzmin@icerockdev.com»>
Date: Mon, 18 May 2026 10:24:37 +0300
Subject: [PATCH 04/15] fix detekt
---
learning/libraries/moko/moko-mvvm.md | 2 +-
university/6-lists/moko-units.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/learning/libraries/moko/moko-mvvm.md b/learning/libraries/moko/moko-mvvm.md
index 8f5d9228a..43b68522e 100644
--- a/learning/libraries/moko/moko-mvvm.md
+++ b/learning/libraries/moko/moko-mvvm.md
@@ -10,7 +10,7 @@ sidebar_position: 3
- **ViewModel** — хранение и управление UI-данными с `viewModelScope` (CoroutineScope, отменяемый в `onCleared`)
- **LiveData / Flow** — реактивные обёртки с операторами (`map`, `merge`, `combine`, `all`)
-- **EventsDispatcher** — отправка одноразовых событий из ViewModel во View с контролем lifecycle
+- **EventsDispatcher** — отправка одноразовых событий из ViewModel во View с контролем lifecycle (устаревший подход)
- **CFlow / CStateFlow / CMutableStateFlow** — обёртки над корутинными Flow, совместимые со Swift
- **ResourceState** — sealed class для состояний: `Loading`, `Success`, `Empty`, `Failed`
- **Compose Multiplatform / SwiftUI** — готовая интеграция
diff --git a/university/6-lists/moko-units.md b/university/6-lists/moko-units.md
index eacf3b6e1..57179e431 100644
--- a/university/6-lists/moko-units.md
+++ b/university/6-lists/moko-units.md
@@ -4,4 +4,4 @@ sidebar_position: 2
# moko-units
-Ознакомьтесь со всеми материалами со страницы [moko-units в базе знаний](/learning/libraries/moko/moko-units/).
+Ознакомьтесь со всеми материалами со страницы [moko-units в базе знаний](/learning/legacy/moko-units/).
From 5be562e8900436b5bc986d7778f6c84b5c2b4136 Mon Sep 17 00:00:00 2001
From: Kuzmin <«vkuzmin@icerockdev.com»>
Date: Mon, 18 May 2026 10:54:35 +0300
Subject: [PATCH 05/15] update moko-paging
---
learning/libraries/moko/moko-paging.md | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/learning/libraries/moko/moko-paging.md b/learning/libraries/moko/moko-paging.md
index 3ebdd2004..91b08c5d2 100644
--- a/learning/libraries/moko/moko-paging.md
+++ b/learning/libraries/moko/moko-paging.md
@@ -59,6 +59,19 @@ val pagination = Pagination(
comparator = { a, b -> a.id.compareTo(b.id) }
```
+Для удобства в библиотеке есть готовая реализация — `IdEntity` и `IdComparator`.
+Достаточно наследовать класс данных от `IdEntity` и использовать `IdComparator`:
+
+```kotlin
+data class Data(override val id: Long, ...) : IdEntity
+
+val pagination = Pagination(
+ ...
+ comparator = IdComparator(),
+ ...
+)
+```
+
Лямбды `nextPageListener` и `refreshListener` удобно использовать для показа ошибок при дозагрузке/обновлении данных:
```kotlin
@@ -83,6 +96,7 @@ pagination.refresh()
val isEmpty: LiveData = pagination.state.isEmptyState()
val isLoading: LiveData = pagination.state.isLoadingState()
val isRefreshing: LiveData = pagination.refreshLoading
+val isNextPageLoading: LiveData = pagination.nextPageLoading
val isErrorVisible: LiveData = pagination.state.isErrorState()
val error: LiveData = pagination.state.error().map { errorMapper(it) }
@@ -113,6 +127,7 @@ private val pagination = Pagination(
val isEmpty: LiveData = pagination.state.isEmptyState()
val isLoading: LiveData = pagination.state.isLoadingState()
val isRefreshing: LiveData = pagination.refreshLoading
+val isNextPageLoading: LiveData = pagination.nextPageLoading
val isErrorVisible: LiveData = pagination.state.isErrorState()
val error: LiveData = pagination.state.error().map { errorMapper(it) }
From ab0bc93c03b36904fbe7c8bccbc7d751bf70a10d Mon Sep 17 00:00:00 2001
From: Kuzmin <«vkuzmin@icerockdev.com»>
Date: Mon, 18 May 2026 10:54:50 +0300
Subject: [PATCH 06/15] update moko-fields
---
learning/libraries/moko/moko-fields.md | 58 +++++++++++++++++++++++++-
1 file changed, 56 insertions(+), 2 deletions(-)
diff --git a/learning/libraries/moko/moko-fields.md b/learning/libraries/moko/moko-fields.md
index e80fa79f7..afd0176c0 100644
--- a/learning/libraries/moko/moko-fields.md
+++ b/learning/libraries/moko/moko-fields.md
@@ -12,6 +12,14 @@ sidebar_position: 6
+## Состав библиотеки
+
+Библиотека состоит из нескольких модулей:
+- `fields-core` — базовые классы и логика валидации
+- `fields-livedata` — интеграция с `LiveData` из moko-mvvm
+- `fields-flow` — интеграция с `Flow` из kotlinx.coroutines
+- `fields-material` — Android View компоненты с Material Design
+
## FormField
Потребность в библиотеке возникла из-за того, что для создания логики формы ввода в общем коде необходимы следующие элементы:
@@ -63,13 +71,59 @@ fun AuthCodeContent(
## Валидация
Разберем, как добавлять валидацию в `FormField`:
-- можно использовать [встроенные валидаторы](https://github.com/icerockdev/moko-fields/tree/c9c09069da717d4995ee6c96f8ec6ef7446af503/fields/src/commonMain/kotlin/dev/icerock/moko/validations)
+- можно использовать [встроенные валидаторы](https://github.com/icerockdev/moko-fields/tree/master/fields-core/src/commonMain/kotlin/dev/icerock/moko/fields/core/validations)
- можно создать полностью свою валидацию
Как можно настроить валидацию:
- валидацию можно вызвать в любой момент. Зачем это?
- при первом вводе юзера - валидация не должна проверяться, пока он не закончит ввод до конца, и не нажмет кнопку, к которой будет привязана валидация, чтобы, пока он еще не ввел все, что задумал, у него не светились ошибки.
-- поля можно объединить в список и валидировать их одновременно
+- поля можно объединить в список и валидировать их одновременно:
+ ```kotlin
+ private val fields = listOf(emailField, passwordField)
+
+ fun onSubmit() {
+ if (!fields.validate()) return
+ // данные валидны
+ }
+ ```
- валидация полей может быть завязана на других полях (пароль + повторите пароль)
- у FormField есть поле `isValid` и `validationError`
+## Использование с Flow
+
+Для работы с корутинами используйте модуль `fields-flow`.
+`FormField` создаётся с указанием `CoroutineScope`:
+
+```kotlin
+val emailField: FormField = FormField(
+ scope = viewModelScope,
+ initialValue = "",
+ validation = flowBlock { email ->
+ ValidationResult.of(email) {
+ notBlank("Email не может быть пустым".desc())
+ matchRegex("Неверный формат".desc(), EMAIL_REGEX)
+ }
+ }
+)
+```
+
+Валидация строится через `ValidationResult` — цепочкой:
+
+```kotlin
+ValidationResult.of(value)
+ .notBlank("Поле не может быть пустым".desc())
+ .matchRegex("Неверный формат".desc(), SOME_REGEX)
+ .validate()
+```
+
+Или через DSL, как в примере выше. В UI данные доступны через `StateFlow`:
+
+```kotlin
+val email: String by viewModel.emailField.data.collectAsState()
+
+TextField(
+ value = email,
+ onValueChange = { viewModel.emailField.data.value = it }
+)
+```
+
From c9e850548681814c582bdd680879371a03c1ee3a Mon Sep 17 00:00:00 2001
From: Kuzmin <«vkuzmin@icerockdev.com»>
Date: Mon, 18 May 2026 12:08:00 +0300
Subject: [PATCH 07/15] update moko-errors
---
learning/libraries/moko/moko-errors.md | 162 ++++++++++++++++++++++++-
1 file changed, 161 insertions(+), 1 deletion(-)
diff --git a/learning/libraries/moko/moko-errors.md b/learning/libraries/moko/moko-errors.md
index c611d6e82..2db3715a2 100644
--- a/learning/libraries/moko/moko-errors.md
+++ b/learning/libraries/moko/moko-errors.md
@@ -4,7 +4,167 @@ sidebar_position: 7
# moko-errors
-Библиотека [moko-errors](https://github.com/icerockdev/moko-errors) позволяет обрабатывать и показывать ошибки на экране из общего кода.
+Библиотека [moko-errors](https://github.com/icerockdev/moko-errors) позволяет обрабатывать и показывать ошибки на экране из общего кода.
+
+## ExceptionMappersStorage
+
+`ExceptionMappersStorage` — синглтон, хранящий набор конвертеров исключений в классы ошибок, требуемые `ErrorPresenter`.
+
+Регистрация простого маппера:
+
+```kotlin
+ExceptionMappersStorage
+ .register {
+ "Был передан недопустимый аргумент!".desc()
+ }
+ .register {
+ it.code
+ }
+```
+
+Регистрация маппера с условием:
+
+```kotlin
+ExceptionMappersStorage.condition(
+ condition = { it is CustomException && it.code == 10 },
+ mapper = { "Произошла кастомная ошибка!".desc() }
+)
+```
+
+Для каждого типа ошибки (кроме `StringDesc`, у которого уже есть значение по умолчанию) необходимо задать fallback-значение через `setFallbackValue`:
+
+```kotlin
+ExceptionMappersStorage
+ .setFallbackValue(520)
+
+val throwableToIntMapper: (Throwable) -> Int =
+ ExceptionMappersStorage.throwableMapper()
+```
+
+Регистрацию можно выстраивать в цепочку:
+
+```kotlin
+ExceptionMappersStorage
+ .condition(
+ condition = { it is CustomException && it.code == 10 },
+ mapper = { "Произошла кастомная ошибка!".desc() }
+ )
+ .register {
+ "Был передан недопустимый аргумент!".desc()
+ }
+ .register {
+ it.code
+ }
+ .setFallbackValue(520)
+```
+
+## ExceptionHandler
+
+`ExceptionHandler` реализует безопасное выполнение кода и автоматическое отображение ошибок через `ErrorPresenter`.
+
+Объявление в ViewModel:
+
+```kotlin
+class SimpleViewModel(
+ val exceptionHandler: ExceptionHandler
+) : ViewModel()
+```
+
+Привязка на платформе.
+
+На Android в Activity или Fragment:
+
+```kotlin
+viewModel.exceptionHandler.bind(
+ lifecycleOwner = this,
+ activity = this
+)
+```
+
+На iOS в ViewController:
+
+```swift
+viewModel.exceptionHandler.bind(viewController: self)
+```
+
+Создание экземпляра `ExceptionHandler`:
+
+```kotlin
+val exceptionHandler = ExceptionHandler(
+ errorPresenter = errorPresenter,
+ exceptionMapper = ExceptionMappersStorage.throwableMapper(),
+ onCatch = { println("Поймано исключение: $it") }
+)
+```
+
+Использование в ViewModel:
+
+```kotlin
+fun onSendRequest() {
+ viewModelScope.launch {
+ exceptionHandler.handle {
+ serverRequest()
+ }.finally {
+ // код после выполнения
+ }.execute()
+ }
+}
+```
+
+Можно добавлять кастомные `catch`-обработчики:
+
+```kotlin
+fun onSendRequest() {
+ viewModelScope.launch {
+ exceptionHandler.handle {
+ serverRequest()
+ }.catch {
+ // кастомная обработка
+ false // true — отменяет показ ошибки; false — позволяет ErrorPresenter показать ошибку
+ }.execute()
+ }
+}
+```
+
+## ErrorPresenter
+
+Набор реализаций `ErrorPresenter`, определяющих способ отображения ошибки на платформе:
+
+- `AlertErrorPresenter` — показывает ошибку в alert-диалоге;
+- `ToastErrorPresenter` — показывает ошибку в toast на Android (на iOS — alert-диалог);
+- `SnackBarErrorPresenter` — показывает ошибку в snackbar на Android (на iOS — alert-диалог);
+- `SelectorErrorPresenter` — выбирает презентер по условию.
+
+Создание презентеров в общем коде:
+
+```kotlin
+val alertErrorPresenter = AlertErrorPresenter(
+ alertTitle = "Ошибка".desc(),
+ positiveButtonText = "OK".desc()
+)
+val toastErrorPresenter = ToastErrorPresenter(
+ duration = ToastDuration.LONG
+)
+```
+
+`SelectorErrorPresenter` позволяет выбрать способ показа в зависимости от типа исключения:
+
+```kotlin
+val selectorErrorPresenter = SelectorErrorPresenter { throwable ->
+ when (throwable) {
+ is CustomException -> alertErrorPresenter
+ else -> toastErrorPresenter
+ }
+}
+
+val exceptionHandler = ExceptionHandler(
+ errorPresenter = selectorErrorPresenter,
+ exceptionMapper = ExceptionMappersStorage.throwableMapper()
+)
+```
+
+## Дополнительные материалы
+
From e59193bda49935965ddb83d4164d7e8487befb67 Mon Sep 17 00:00:00 2001
From: Kuzmin <«vkuzmin@icerockdev.com»>
Date: Mon, 18 May 2026 13:39:42 +0300
Subject: [PATCH 08/15] update moko-socket-io
---
learning/libraries/moko/moko-socket-io.md | 73 ++++++++++++++++++++++-
1 file changed, 70 insertions(+), 3 deletions(-)
diff --git a/learning/libraries/moko/moko-socket-io.md b/learning/libraries/moko/moko-socket-io.md
index 03ad63c16..ce178314f 100644
--- a/learning/libraries/moko/moko-socket-io.md
+++ b/learning/libraries/moko/moko-socket-io.md
@@ -4,10 +4,77 @@ sidebar_position: 8
# moko-socket-io
-Библиотека [moko-socket-io](https://github.com/icerockdev/moko-socket-io)
+Библиотека [moko-socket-io](https://github.com/icerockdev/moko-socket-io) предоставляет реализацию Socket.IO для Kotlin Multiplatform, используя нативные клиенты [socket.io-client-java](https://github.com/socketio/socket.io-client-java) и [socket.io-client-swift](https://github.com/socketio/socket.io-client-swift).
-СОкеты
+## Создание сокета
-[дока ктора](https://ktor.io/docs/servers-raw-sockets.html#client_create_socket)
+Для подключения к серверу создаётся экземпляр `Socket` с указанием endpoint и конфигурации:
+```kotlin
+val socket = Socket(
+ endpoint = "wss://my-super-server:8080",
+ config = SocketOptions(
+ queryParams = mapOf("token" to "MySuperToken"),
+ transport = SocketOptions.Transport.WEBSOCKET
+ )
+) {
+ on(SocketEvent.Connect) {
+ println("connect")
+ }
+ on(SocketEvent.Connecting) {
+ println("connecting")
+ }
+
+ on(SocketEvent.Disconnect) {
+ println("disconnect")
+ }
+
+ on(SocketEvent.Error) {
+ println("error $it")
+ }
+
+ on(SocketEvent.Reconnect) {
+ println("reconnect")
+ }
+
+ on(SocketEvent.ReconnectAttempt) {
+ println("reconnect attempt $it")
+ }
+
+ on(SocketEvent.Ping) {
+ println("ping")
+ }
+
+ on(SocketEvent.Pong) {
+ println("pong")
+ }
+
+ on("employee.connected") { data ->
+ val serializer = DeliveryCar.serializer()
+ val json = JSON.nonstrict
+ val deliveryCar: DeliveryCar = json.parse(serializer, data)
+ }
+}
+```
+
+## SocketOptions
+
+Основные параметры конфигурации:
+
+- `queryParams` — query-параметры, передаваемые при подключении (например, токен авторизации);
+- `transport` — тип транспорта: `WEBSOCKET` или `POLLING`.
+
+## Обработка событий
+
+Библиотека поддерживает как встроенные события (`SocketEvent.Connect`, `Disconnect`, `Error` и т.д.), так и кастомные события сервера. Для подписки используется метод `on` внутри блока создания сокета. В кастомных событиях вторым аргументом приходит строка `data`, которую можно распарсить в модель с помощью `kotlinx.serialization`.
+
+## Подключение для iOS
+
+Для работы на iOS необходима интеграция через CocoaPods:
+
+```ruby
+pod 'mokoSocketIo', :git => 'https://github.com/icerockdev/moko-socket-io.git', :tag => 'release/0.6.0'
+```
+
+В `build.gradle.kts` проекта настройте pod-зависимость в соответствии с используемым плагином (JetBrains Native CocoaPods или IceRock CocoaPods).
From c1b7355d118f8266c63449e5473e24fca88bece9 Mon Sep 17 00:00:00 2001
From: Kuzmin <«vkuzmin@icerockdev.com»>
Date: Mon, 18 May 2026 13:44:44 +0300
Subject: [PATCH 09/15] update moko-network
---
learning/libraries/moko/moko-network.md | 166 +++++++++++++-----------
1 file changed, 90 insertions(+), 76 deletions(-)
diff --git a/learning/libraries/moko/moko-network.md b/learning/libraries/moko/moko-network.md
index 18c7f0224..509c1307d 100644
--- a/learning/libraries/moko/moko-network.md
+++ b/learning/libraries/moko/moko-network.md
@@ -4,63 +4,87 @@ sidebar_position: 9
# moko-network
-## moko-network
-Библиотека [moko-network](https://github.com/icerockdev/moko-network) - позволяет генерировать сущности и API классы из OpenAPI (Swagger) файлов.
+Библиотека [moko-network](https://github.com/icerockdev/moko-network) предоставляет компоненты для работы с сетью в Kotlin Multiplatform: генерацию сущностей и API-классов из OpenAPI (Swagger) спецификаций, а также набор Ktor-плагинов для типовых задач.
-
-
-
+## Состав библиотеки
-## moko-network-errors
-Библиотека `moko-network` содержит внутри себя модуль `moko-network-errors` - интеграцию с `moko-errors`, чтобы удобно обрабатывать ошибки сети.
-Для начала, ознакомьтесь с этим модулем по [README](https://github.com/icerockdev/moko-network#moko-network-errors).
-
-Для использования этого модуля, проделайте следующие действия:
-1. Подключить его в project.build.gradle: `commonMainApi("dev.icerock.moko:network-errors:$mokoNetworkVersion")`
-2. Вызвать метод для регистрации основных ошибок сети: `ExceptionMappersStorage.registerAllNetworkMappers()`, его реализацию можете посмотреть [тут](https://github.com/icerockdev/moko-network/blob/0f8459ff2d51c6b7cade0cadd6d11066b7a55d60/network-errors/src/commonMain/kotlin/dev/icerock/moko/network/errors/NetworkExceptionMappers.kt#L27).
- - Если вы хотите изменить текст при основных ошибках сети - переопределите параметр `errorsTexts` метода `registerAllNetworkMappers`.
- - Если вы хотите добавить обработку другого класса ошибок, используйте `ExceptionMappersStorage.register` - [метод](https://github.com/icerockdev/moko-errors/blob/8bfccf376ccbbcf1b87a7522d08df182143f0cf3/errors/src/commonMain/kotlin/dev/icerock/moko/errors/mappers/ExceptionMappersStorage.kt#L31) из `moko-errors`.
-3. Используйте `exceptionHandler` для автоматической обработки ошибок сети:
- ```kotlin
- viewModelScope.launch {
- exceptionHandler.handle {
- api.mareTestRequest()
- // ...
- }.execute()
- }
- ```
-
-## Features
-Библиотека `moko-network` содержит в себе фичи - классы, реализующие интерфейс `HttpClientFeature` из Ktor.
-В версии `Ktor 2.0.0` интерфейс `HttpClientFeature` [переименовали](https://ktor.io/docs/migrating-2.html#feature-plugin-client) в `HttpClientPlugin`.
-
-`Ktor` содержит уже готовые плагины, вот, например, для чего их можно использовать:
-- [Cache](https://github.com/ktorio/ktor/tree/main/ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/cache) - включить кеширование для каждого запроса, чтобы они отрабатывали быстрее
-- [DefaultRequest](https://ktor.io/docs/default-request.html) добавлять для всех запросов какие-нибудь хидеры по умолчанию
-- [Logging](https://ktor.io/docs/client-logging.html) - плагин для логгирования запросов
-- [BodyProgress](https://github.com/ktorio/ktor/blob/main/ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/BodyProgress.kt) - плагин для получения `observable` прогресса загрузки и скачивания
-- [HttpTimeout](https://github.com/ktorio/ktor/blob/main/ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/HttpTimeout.kt) - плагин для настройки таймаутов
-
-С полным списком плагинов, доступных в Ktor вы можете ознакомиться по [ссылке](https://github.com/ktorio/ktor/tree/main/ktor-client/ktor-client-core/common/src/io/ktor/client/plugins).
-
-Также, примеры использования стандартных плагинов можно посмотреть в статье [Kotlin Multiplatform Mobile: Intercepting Network Request and Response](https://yusufabd.medium.com/kotlin-multiplatform-mobile-intercepting-network-request-and-response-6805a79b4699).
-
-Плагин выполняет свою задачу, как правило, для каждого запроса или ответа сервера. Чтобы понять, что делает каждый плагин, смотрите реализацию метода
-- `handle`, для плагинов из Ktor
-- `install` в `companion object`, для плагинов из `moko-network`
-
-### Подключение
-[Пример](https://github.com/icerockdev/moko-network/blob/0f8459ff2d51c6b7cade0cadd6d11066b7a55d60/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/TestViewModel.kt#L40) создания `httpClient`, в котором происходит подключение и настройка плагинов.
-
-Теперь рассмотрим те [плагины](https://github.com/icerockdev/moko-network/tree/master/network/src/commonMain/kotlin/dev/icerock/moko/network/features), которые есть в `moko-network`.
+Библиотека состоит из нескольких модулей:
+- `network` — базовый модуль с Ktor-плагинами (`ExceptionFeature`, `TokenFeature`, `RefreshTokenFeature`, `LanguageFeature`);
+- `network-generator` — Gradle-плагин для генерации кода из OpenAPI;
+- `network-errors` — интеграция с `moko-errors` для обработки сетевых ошибок;
+- `network-engine` — предварительно сконфигурированный `HttpClientEngine`;
+- `network-bignum` — сериализатор для `BigInteger`/`BigDecimal`.
+
+## OpenAPI code generation
+
+Для генерации кода подключите плагин и укажите путь к OpenAPI спецификации.
+
+### Подключение плагина
+
+```groovy
+buildscript {
+ dependencies {
+ classpath "dev.icerock.moko:network-generator:0.23.0"
+ }
+}
+
+apply plugin: "dev.icerock.mobile.multiplatform-network-generator"
+```
+
+### Конфигурация спецификации
+
+```groovy
+mokoNetwork {
+ spec("pets") {
+ inputSpec = file("src/swagger.json")
+ }
+ spec("news") {
+ inputSpec = file("src/newsApi.yaml")
+ packageName = "news"
+ isInternal = false
+ isOpen = true
+ enumFallbackNull = false
+ }
+}
+```
+
+После запуска Gradle-задачи `openApiGenerate` сгенерированные классы появятся в `build/generated/moko-network`.
+
+### Использование в коде
+
+```kotlin
+import dev.icerock.moko.network.generated.models.*
+import dev.icerock.moko.network.generated.apis.*
+
+class TestViewModel : ViewModel() {
+ private val petApi = PetApi(
+ basePath = "https://petstore.swagger.io/v2/",
+ httpClient = ktorHttpClient,
+ json = kotlinxJsonParser
+ )
+
+ fun apiRequest() {
+ viewModelScope.launch {
+ val pet = petApi.findPetsByStatus(listOf("available"))
+ }
+ }
+}
+```
+
+Флаг `enumFallbackNull = true` включает генерацию обёртки `Safeable` для enum-свойств — при неожиданном значении вернётся `null`.
+
+## Features (Ktor-плагины)
+
+Плагины `moko-network` реализуют интерфейс `HttpClientFeature` (Ktor 2.x — `HttpClientPlugin`).
### ExceptionFeature
-Эта фича просто кидает ошибку, если `status` ответа сервера неудачный.
+
+Проверяет статус ответа сервера. Если статус неуспешный — генерирует исключение на основе тела ответа.
+
```kotlin
override fun install(feature: ExceptionFeature, scope: HttpClient) {
scope.responsePipeline.intercept(HttpResponsePipeline.Receive) { (_, body) ->
if (body !is ByteReadChannel) return@intercept
-
val response = context.response
if (!response.status.isSuccess()) {
val packet = body.readRemaining()
@@ -77,7 +101,9 @@ override fun install(feature: ExceptionFeature, scope: HttpClient) {
```
### LanguageFeature
-Эта фича позволяет добавить язык к каждому запросу, чтобы уведомить сервер, на каком языке мы хотим получить ответ.
+
+Добавляет заголовок с языком к каждому запросу, чтобы сервер мог вернуть ответ на нужном языке.
+
```kotlin
override fun install(feature: LanguageFeature, scope: HttpClient) {
scope.requestPipeline.intercept(HttpRequestPipeline.State) {
@@ -89,8 +115,9 @@ override fun install(feature: LanguageFeature, scope: HttpClient) {
```
### TokenFeature
-Эта фича к каждому запросу добавляет токен, например для авторизации, по ключу, которое вы укажите в `tokenHeaderName`, при настройке фичи. (обычно - authorization)
-Для использования фичи необходимо реализовать метод получения токена - `getToken()`.
+
+Добавляет токен авторизации к каждому запросу. Для использования необходимо реализовать `getToken()`.
+
```kotlin
override fun install(feature: TokenFeature, scope: HttpClient) {
scope.requestPipeline.intercept(HttpRequestPipeline.State) {
@@ -103,44 +130,31 @@ override fun install(feature: TokenFeature, scope: HttpClient) {
```
### RefreshTokenFeature
-Бывают ситуации, когда у токена есть время жизни, по истечении которого токен становится недействителен. В этом случае необходимо как-то его обновить.
-За правила обновления и сохранения нового токена отвечает метод `feature.updateTokenHandler.invoke()`.
-Этот блок кода отвечает за создание реквеста, подставления туда текущего использующегося токена и выполнения запроса.
-```kotlin
-val requestBuilder = HttpRequestBuilder().takeFrom(context.request)
-val result: HttpResponse = context.client!!.request(requestBuilder)
-proceedWith(result)
-```
+Обрабатывает ответ `401 Unauthorized`: обновляет токен через `updateTokenHandler` и повторяет запрос. Если несколько запросов получили `401` одновременно — проверяется актуальность токена через `isCredentialsActual`, и лишние обновления не выполняются.
-Что может произойти?
-Отправили запрос - получили ответ - 401 ошибка авторизации. После этого мы обновили токен и повторили запрос - все ок.
-
-Но, может получиться так, что мы успели отправить несколько запросов с неправильным токеном, и каждому из них придет ответ - ошибка авторизации.
-
-Первое, что нам нужно сделать в этом случае - проверить, отличается ли тот токен, который мы отправили от того, который находится у нас в хранилище. За эту проверку отвечает метод `feature.isCredentialsActual()`.
-Если токены отличаются, значит какой-то запрос до нас его уже обновил, и нам нужно просто повторно отправить наш запрос, но уже с новым токеном.
```kotlin
if (!feature.isCredentialsActual(context.request)) {
- refreshTokenHttpFeatureMutex.unlock()
val requestBuilder = HttpRequestBuilder().takeFrom(context.request)
val result: HttpResponse = context.client!!.request(requestBuilder)
proceedWith(result)
return@intercept
}
-```
-В случае, если мы получили ошибку 401, но токен который мы отправили не отличается от того, который находится у нас в хранилище - просто обновляем токен методом `feature.updateTokenHandler.invoke()`.
-Если обновление токена прошло успешно - повторяем запрос с новым токеном. Если обновить не удалось - отправляем результат дальше, чтобы показать проблему юзеру.
-```kotlin
+
if (feature.updateTokenHandler.invoke()) {
- // Если обновление токена прошел успешно, пробуем повторить запрос
- refreshTokenHttpFeatureMutex.unlock()
val requestBuilder = HttpRequestBuilder().takeFrom(context.request)
val result: HttpResponse = context.client!!.request(requestBuilder)
proceedWith(result)
} else {
- // Если не удалось обновить токен -
- refreshTokenHttpFeatureMutex.unlock()
proceedWith(subject)
}
```
+
+## moko-network-errors
+
+Модуль `network-errors` предоставляет встроенные мапперы для преобразования исключений moko-network в `StringDesc` (тексты на русском и английском).
+
+## Дополнительные материалы
+
+
+
From 02f39a2b270637014a78220fb4691ce323703247 Mon Sep 17 00:00:00 2001
From: Kuzmin <«vkuzmin@icerockdev.com»>
Date: Mon, 18 May 2026 14:55:41 +0300
Subject: [PATCH 10/15] update moko-kswift
---
learning/{libraries/moko => legacy}/moko-kswift.md | 4 +++-
learning/legacy/state.md | 2 +-
university/4-icerock-basics/mvvm.md | 2 +-
3 files changed, 5 insertions(+), 3 deletions(-)
rename learning/{libraries/moko => legacy}/moko-kswift.md (98%)
diff --git a/learning/libraries/moko/moko-kswift.md b/learning/legacy/moko-kswift.md
similarity index 98%
rename from learning/libraries/moko/moko-kswift.md
rename to learning/legacy/moko-kswift.md
index e0c07b43a..b9e4417b3 100644
--- a/learning/libraries/moko/moko-kswift.md
+++ b/learning/legacy/moko-kswift.md
@@ -1,9 +1,11 @@
---
-sidebar_position: 10
+sidebar_position: 7
---
# moko-kswift
+Плагин устарел, используйте [https://skie.touchlab.co](https://skie.touchlab.co)
+
## moko-kswift
[moko-kswift](https://github.com/icerockdev/moko-kswift) - этот плагин, позволяет автоматически генерировать Swift-friendly API из общего кода:
diff --git a/learning/legacy/state.md b/learning/legacy/state.md
index daa7e8128..17c1dad1c 100644
--- a/learning/legacy/state.md
+++ b/learning/legacy/state.md
@@ -224,7 +224,7 @@ viewModel.state.observe(viewLifecycleOwner) { state ->
### Обработка на iOS
#### moko-kswift
-За счет [moko-kswift](/learning/libraries/moko/moko-kswift) у нас есть возможность использовать `sealed interface` для `State` и `Actions` из общего кода в виде `enum` в Swift, чтобы можно было обрабатывать объекты в `switch` без ветки `default`.
+За счет [moko-kswift](/learning/legacy/moko-kswift) у нас есть возможность использовать `sealed interface` для `State` и `Actions` из общего кода в виде `enum` в Swift, чтобы можно было обрабатывать объекты в `switch` без ветки `default`.
Это очень полезно для обработки `Actions`, потому что при появлении нового `Action` в общем коде iOS приложение не скомпилируется из-за того, что не все объекты `enum` будут обработаны.
diff --git a/university/4-icerock-basics/mvvm.md b/university/4-icerock-basics/mvvm.md
index 94c508a92..25bedd16d 100644
--- a/university/4-icerock-basics/mvvm.md
+++ b/university/4-icerock-basics/mvvm.md
@@ -321,7 +321,7 @@ viewModel.actions.subscribe { [weak self] action in
Теперь разберемся со второй проблемой - преобразованием `sealed interface` к `enum` в Swift.
Используя плагин [moko-kswift](https://github.com/icerockdev/moko-kswift), мы можем получать автоматически генерируемые Swift `enum`, соответствующие `sealed-interface`-ам общего кода, а после работать с ними в `switch`.
-Для более полного понимания проблемы и её решения, изучите [страницу](../../learning/libraries/moko/moko-kswift) плагина в базе знаний.
+Для более полного понимания проблемы и её решения, изучите [страницу](../../learning/legacy/moko-kswift) плагина в базе знаний.
## Удобное public api общего кода
From e6304789d8606b8d3f92c4007fd91fb923569f14 Mon Sep 17 00:00:00 2001
From: Kuzmin <«vkuzmin@icerockdev.com»>
Date: Mon, 18 May 2026 15:04:15 +0300
Subject: [PATCH 11/15] update moko-permissions
---
learning/libraries/moko/moko-permissions.md | 97 ++++++++++++++++++---
1 file changed, 83 insertions(+), 14 deletions(-)
diff --git a/learning/libraries/moko/moko-permissions.md b/learning/libraries/moko/moko-permissions.md
index 91677438b..e2bd5f107 100644
--- a/learning/libraries/moko/moko-permissions.md
+++ b/learning/libraries/moko/moko-permissions.md
@@ -4,22 +4,91 @@ sidebar_position: 10
# moko-permissions
-Библиотека [moko-permissions](https://github.com/icerockdev/moko-permissions) позволяет получать runtime permissions в общем коде.
+Библиотека [moko-permissions](https://github.com/icerockdev/moko-permissions) позволяет запрашивать runtime-разрешения в общем коде для iOS и Android.
-
-
-
+## Состав библиотеки
+
+Библиотека состоит из базового модуля и набора модулей для конкретных разрешений:
+
+- `permissions` — базовый модуль с `PermissionsController` и `Permission`;
+- `permissions-camera` — разрешение `Permission.CAMERA`;
+- `permissions-contacts` — `Permission.CONTACTS`;
+- `permissions-gallery` — `Permission.GALLERY`;
+- `permissions-location` — `Permission.LOCATION`, `COARSE_LOCATION`, `BACKGROUND_LOCATION`;
+- `permissions-microphone` — `Permission.RECORD_AUDIO`;
+- `permissions-motion` — `Permission.MOTION`;
+- `permissions-notifications` — `Permission.REMOTE_NOTIFICATION`;
+- `permissions-storage` — `Permission.STORAGE`, `WRITE_STORAGE`;
+- `permissions-bluetooth` — `Permission.BLUETOOTH_LE`, `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, `BLUETOOTH_ADVERTISE`;
+- `permissions-compose` — интеграция с Compose Multiplatform;
+- `permissions-test` — вспомогательные утилиты для тестирования.
+
+## Использование
+
+### Общий код
+
+```kotlin
+class ViewModel(val permissionsController: PermissionsController) : ViewModel() {
+ fun onPhotoPressed() {
+ viewModelScope.launch {
+ try {
+ permissionsController.providePermission(Permission.GALLERY)
+ // разрешение получено
+ } catch (deniedAlways: DeniedAlwaysException) {
+ // разрешение запрещено навсегда — только настройки
+ } catch (denied: DeniedException) {
+ // разрешение запрещено в текущей сессии
+ }
+ }
+ }
+}
+```
+
+### Android
+
+```kotlin
+val viewModel = ViewModel(PermissionsController())
+viewModel.permissionsController.bind(activity)
+```
-## Denied and DeniedAlways
-Результаты запроса разрешения на Android могут быть следующие:
-- Granted - разрешение получено
-- Denied - для текущей сессии разрешение не получено, можно запросить повторно в следующей сессии
- - в случае непредоставления разрешения в следующий раз, после denied - оно перейдет в состояние DeniedAlways
-- DeniedAlways - для всего приложения разрешение не предоставлено, системным запросом разрешение больше не получить.
- - сразу можно установить, если поставить флаг: "Больше не показывать"
+### iOS
-Также, на iOS, в отличие от Android состояния разрешения бывают только granted и always denied. Из always denied переход в granted также возможен только в настройках приложения.
+```swift
+let viewModel = ViewModel(permissionsController: PermissionsController())
+```
-Из случая DeniedAlways установить разрешение в Granted можно только в настройках приложения. Поэтому, нужно направлять юзера туда и уточнять, что именно ему нужно сделать.
+### Compose Multiplatform
-Чтобы получше разобраться, в каких случаях результат запроса будет Denied, а в каких DeniedAlways, предлагаем вам запустить sample библиотеки и протестировать самостоятельно.
+```kotlin
+@Composable
+fun Sample() {
+ val factory: PermissionsControllerFactory = rememberPermissionsControllerFactory()
+ val controller: PermissionsController = remember(factory) {
+ factory.createPermissionsController()
+ }
+ val coroutineScope = rememberCoroutineScope()
+
+ Button(onClick = {
+ coroutineScope.launch {
+ controller.providePermission(Permission.REMOTE_NOTIFICATION)
+ }
+ }) {
+ Text("give permissions")
+ }
+}
+```
+
+## Denied и DeniedAlways
+
+Результат запроса разрешения может быть:
+
+- `Granted` — разрешение получено;
+- `Denied` — в текущей сессии не получено, можно запросить повторно. При повторном отказе переходит в `DeniedAlways`;
+- `DeniedAlways` — разрешение запрещено для приложения, системный диалог больше не показывается. Переход в `Granted` возможен только через настройки приложения.
+
+На iOS состояния только `granted` и `always denied` (аналог `DeniedAlways`).
+
+## Дополнительные материалы
+
+
+
From fcee0ac2c01fc7d51fb72b391d404481c4f42420 Mon Sep 17 00:00:00 2001
From: Kuzmin <«vkuzmin@icerockdev.com»>
Date: Mon, 18 May 2026 15:10:49 +0300
Subject: [PATCH 12/15] update moko-geo
---
learning/libraries/moko/moko-geo.md | 71 ++++++++++++++++++++++++++++-
1 file changed, 70 insertions(+), 1 deletion(-)
diff --git a/learning/libraries/moko/moko-geo.md b/learning/libraries/moko/moko-geo.md
index 14b2039fe..9bf8dfa56 100644
--- a/learning/libraries/moko/moko-geo.md
+++ b/learning/libraries/moko/moko-geo.md
@@ -4,4 +4,73 @@ sidebar_position: 11
# moko-geo
-Библиотека [moko-geo](https://github.com/icerockdev/moko-geo) - для работы с геолокацией из общего кода.
+Библиотека [moko-geo](https://github.com/icerockdev/moko-geo) предоставляет возможность отслеживать геолокацию пользователя из общего кода.
+
+## Состав библиотеки
+
+- `geo` — базовый модуль с `LocationTracker`;
+- `geo-compose` — интеграция с Compose Multiplatform.
+
+## LocationTracker
+
+Основной класс `LocationTracker` использует `PermissionsController` из `moko-permissions` для запроса разрешений и позволяет управлять процессом отслеживания.
+
+### Общий код
+
+```kotlin
+class TrackerViewModel(
+ val locationTracker: LocationTracker
+) : ViewModel() {
+
+ init {
+ viewModelScope.launch {
+ locationTracker.getLocationsFlow()
+ .distinctUntilChanged()
+ .collect { location ->
+ println("new location: $location")
+ }
+ }
+ }
+
+ fun onStartPressed() {
+ viewModelScope.launch { locationTracker.startTracking() }
+ }
+
+ fun onStopPressed() {
+ locationTracker.stopTracking()
+ }
+}
+```
+
+### Android
+
+```kotlin
+val locationTracker = LocationTracker(
+ permissionsController = PermissionsController(applicationContext)
+)
+val viewModel = TrackerViewModel(locationTracker)
+
+viewModel.locationTracker.bind(lifecycle, this, supportFragmentManager)
+```
+
+### Compose
+
+```kotlin
+val locationTracker = LocationTracker(
+ permissionsController = PermissionsController(applicationContext)
+)
+val viewModel = TrackerViewModel(locationTracker)
+
+BindLocationTrackerEffect(locationTracker = locationTracker)
+```
+
+### iOS
+
+```swift
+let viewModel = TrackerViewModel(
+ locationTracker: LocationTracker(
+ permissionsController: PermissionsController(),
+ accuracy: kCLLocationAccuracyBest
+ )
+)
+```
From 1b07d5e771b7c48a741542e5cf98297d1ba504de Mon Sep 17 00:00:00 2001
From: Kuzmin <«vkuzmin@icerockdev.com»>
Date: Mon, 18 May 2026 15:13:32 +0300
Subject: [PATCH 13/15] update moko-media
---
learning/libraries/moko/moko-media.md | 77 +++++++++++++++++++++++++--
1 file changed, 72 insertions(+), 5 deletions(-)
diff --git a/learning/libraries/moko/moko-media.md b/learning/libraries/moko/moko-media.md
index c3180fe59..9c050cc28 100644
--- a/learning/libraries/moko/moko-media.md
+++ b/learning/libraries/moko/moko-media.md
@@ -4,9 +4,76 @@ sidebar_position: 12
# moko-media
-Библиотека [moko-media](https://github.com/icerockdev/moko-media) - для работы с медиа файлами, устройства из общего кода.
+Библиотека [moko-media](https://github.com/icerockdev/moko-media) позволяет работать с медиафайлами в общем коде: получать фото из галереи или с камеры, управлять видеоплеером.
-Библиотека поддерживает следующий функционал:
-- получение фото из галереи или от камеры
-- воспроизведение аудио и видео файлов
-- выбор файлов на устройстве
+## Состав библиотеки
+
+- `media` — базовый модуль с `MediaPickerController`;
+- `media-compose` — интеграция с Compose Multiplatform;
+- `media-test` — утилиты для тестирования.
+
+## MediaPickerController
+
+Основной класс для получения изображений. Использует `PermissionsController` из `moko-permissions`.
+
+### Общий код
+
+```kotlin
+class ViewModel(val mediaController: MediaPickerController) : ViewModel() {
+ fun onSelectPhotoPressed() {
+ viewModelScope.launch {
+ try {
+ val bitmap = mediaController.pickImage(MediaSource.CAMERA)
+ // фото получено
+ } catch (_: CanceledException) {
+ // пользователь отменил
+ } catch (error: Throwable) {
+ // нет разрешения или ошибка чтения
+ }
+ }
+ }
+}
+```
+
+### Android
+
+```kotlin
+val permissionsController = PermissionsController()
+val mediaController = MediaPickerController(permissionsController)
+val viewModel = ViewModel(mediaController)
+
+viewModel.mediaController.bind(lifecycle, supportFragmentManager)
+```
+
+### iOS
+
+```swift
+let permissionsController = PermissionsController()
+let mediaController = MediaPickerController(
+ permissionsController: permissionsController,
+ viewController: self
+)
+let viewModel = ViewModel(mediaController: mediaController)
+```
+
+### Compose Multiplatform
+
+```kotlin
+@Composable
+fun Sample() {
+ val factory = rememberMediaPickerControllerFactory()
+ val picker = remember(factory) { factory.createMediaPickerController() }
+ val coroutineScope = rememberCoroutineScope()
+
+ BindMediaPickerEffect(picker)
+
+ Button(onClick = {
+ coroutineScope.launch {
+ val result = picker.pickImage(MediaSource.GALLERY)
+ // используем результат
+ }
+ }) {
+ Text("Выбрать фото")
+ }
+}
+```
From 46cda8b8b5435a34ca0fa36e54270eab7177f889 Mon Sep 17 00:00:00 2001
From: Kuzmin <«vkuzmin@icerockdev.com»>
Date: Mon, 18 May 2026 17:35:10 +0300
Subject: [PATCH 14/15] update Compose Multiplatform
---
learning/compose-multiplatform/intro.md | 37 +++++
learning/compose-multiplatform/links.md | 7 +-
learning/compose-multiplatform/moko-mvvm.md | 76 +++++++++++
.../platform-integration.md | 113 ++++++++++++++++
learning/compose-multiplatform/setup.md | 128 ++++++++++++++++++
5 files changed, 359 insertions(+), 2 deletions(-)
create mode 100644 learning/compose-multiplatform/intro.md
create mode 100644 learning/compose-multiplatform/moko-mvvm.md
create mode 100644 learning/compose-multiplatform/platform-integration.md
create mode 100644 learning/compose-multiplatform/setup.md
diff --git a/learning/compose-multiplatform/intro.md b/learning/compose-multiplatform/intro.md
new file mode 100644
index 000000000..7b97a2389
--- /dev/null
+++ b/learning/compose-multiplatform/intro.md
@@ -0,0 +1,37 @@
+---
+sidebar_position: 1
+---
+
+# Compose Multiplatform — обзор
+
+**Compose Multiplatform** (CMP) — фреймворк от JetBrains для декларативного создания пользовательских интерфейсов на Kotlin, работающий на нескольких платформах.
+
+## Поддерживаемые платформы
+
+- **Android** — полноценная поддержка, идентична Jetpack Compose;
+- **iOS** — рендеринг через Skia, UI выводится через `ComposeUIViewController`;
+- **Desktop (JVM)** — нативные окна через compose-desktop;
+- **Web** — экспериментальная поддержка через Canvas.
+
+## Преимущества
+
+- Единый код UI для всех платформ;
+- Декларативный реактивный подход (как Jetpack Compose);
+- Доступ к платформенным API через `AndroidView` / `UIKitView`;
+- Полная совместимость с существующими Kotlin Multiplatform проектами;
+- Богатая экосистема: Material3, навигация, анимации.
+
+## Требования
+
+- Kotlin 1.9.0+
+- Gradle 6.8+
+- Android API 21+
+- iOS 12.0+
+
+## Дополнительная информация
+
+- [Официальный сайт](https://www.jetbrains.com/lp/compose-multiplatform/)
+- [GitHub репозиторий](https://github.com/JetBrains/compose-multiplatform)
+- [Документация JetBrains](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-getting-started.html)
+- [Compose Multiplatform Resources](https://jetbrains.github.io/compose-multiplatform/)
+- [Compose Multiplatform — статься на kotlinlang.org](https://kotlinlang.org/docs/compose-multiplatform.html)
diff --git a/learning/compose-multiplatform/links.md b/learning/compose-multiplatform/links.md
index f0fba381a..cb404e55f 100644
--- a/learning/compose-multiplatform/links.md
+++ b/learning/compose-multiplatform/links.md
@@ -1,5 +1,8 @@
# Ссылки
-- https://johnoreilly.dev/posts/swiftui-component-compose-ios/ - как встроить Compose Multiplatform в SwiftUI ScrollView
-- https://gist.github.com/Alex009/6b150b5027a04f9d4d1cf2072b713793 - как сделать QR сканер в Compose Multiplatform
+- https://www.jetbrains.com/lp/compose-multiplatform/ — официальный сайт Compose Multiplatform
+- https://github.com/JetBrains/compose-multiplatform — GitHub репозиторий
+- https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-getting-started.html — официальное руководство по началу работы
+- https://johnoreilly.dev/posts/swiftui-component-compose-ios/ — как встроить Compose Multiplatform в SwiftUI ScrollView
+- https://gist.github.com/Alex009/6b150b5027a04f9d4d1cf2072b713793 — как сделать QR сканер в Compose Multiplatform
diff --git a/learning/compose-multiplatform/moko-mvvm.md b/learning/compose-multiplatform/moko-mvvm.md
new file mode 100644
index 000000000..677c2a827
--- /dev/null
+++ b/learning/compose-multiplatform/moko-mvvm.md
@@ -0,0 +1,76 @@
+---
+sidebar_position: 3
+---
+
+# Compose и moko-mvvm
+
+Библиотеки из экосистемы MOKO предоставляют готовые решения для совместной работы с Compose Multiplatform.
+
+## StateFlow и collectAsState
+
+Данные из `ViewModel` (StateFlow) преобразуются в Compose-состояние через `collectAsState()`:
+
+```kotlin
+@Composable
+fun UserScreen(viewModel: UserViewModel) {
+ val user by viewModel.user.collectAsState()
+
+ Text("Hello, ${user.name}")
+}
+```
+
+## moko-mvvm с Compose
+
+`ViewModel` из moko-mvvm не требует специальных адаптеров — достаточно `collectAsState()` для `StateFlow` и `collectAsMutableState()` для двустороннего биндинга:
+
+```kotlin
+val (code, onCodeChange) = viewModel.code.data.collectAsMutableState()
+
+AuthCodeContent(
+ code = code,
+ onCodeChange = onCodeChange,
+ codeError = viewModel.code.error.collectAsState().value?.localized()
+)
+```
+
+## BindEffect
+
+Для привязки контроллеров к жизненному циклу Compose используйте `BindEffect`:
+
+```kotlin
+@Composable
+fun Sample(viewModel: PermissionsViewModel) {
+ BindEffect(viewModel.permissionsController)
+ // ...
+}
+```
+
+Поддерживается для:
+- `PermissionsController` из `moko-permissions`;
+- `MediaPickerController` из `moko-media`;
+- `LocationTracker` из `moko-geo`;
+
+## remember-фабрики
+
+Для создания экземпляров контроллеров внутри `@Composable` используются фабрики:
+
+```kotlin
+@Composable
+fun Sample() {
+ val factory = rememberMediaPickerControllerFactory()
+ val picker = remember(factory) { factory.createMediaPickerController() }
+
+ BindMediaPickerEffect(picker)
+}
+```
+
+Доступные фабрики:
+- `rememberPermissionsControllerFactory()` — из `moko-permissions-compose`;
+- `rememberMediaPickerControllerFactory()` — из `moko-media-compose`;
+- `PermissionsControllerFactory` — для кастомной конфигурации.
+
+## Дополнительная информация
+
+- [moko-mvvm](https://github.com/icerockdev/moko-mvvm)
+- [moko-permissions](https://github.com/icerockdev/moko-permissions)
+- [moko-media](https://github.com/icerockdev/moko-media)
diff --git a/learning/compose-multiplatform/platform-integration.md b/learning/compose-multiplatform/platform-integration.md
new file mode 100644
index 000000000..cef5e72ba
--- /dev/null
+++ b/learning/compose-multiplatform/platform-integration.md
@@ -0,0 +1,113 @@
+---
+sidebar_position: 4
+---
+
+# Compose и интеграция с платформой
+
+Compose Multiplatform предоставляет механизмы для встраивания платформенных компонентов в Compose UI и наоборот — встраивания Compose в существующие UIKit/SwiftUI проекты.
+
+## AndroidView
+
+Для использования Android View внутри Compose:
+
+```kotlin
+@Composable
+fun CameraPreview(modifier: Modifier = Modifier) {
+ AndroidView(
+ factory = { context ->
+ PreviewView(context).apply {
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ scaleType = PreviewView.ScaleType.FILL_CENTER
+ }
+ },
+ modifier = modifier
+ )
+}
+```
+
+## UIKitView (iOS)
+
+Для встраивания UIKit компонентов в Compose на iOS:
+
+```kotlin
+@Composable
+fun NativeMap(modifier: Modifier = Modifier) {
+ UIKitView(
+ factory = { MKMapView() },
+ modifier = modifier
+ )
+}
+```
+
+## ComposeUIViewController (iOS)
+
+Для встраивания Compose UI в существующий UIKit/SwiftUI проект используется `ComposeUIViewController`:
+
+```kotlin
+fun MainViewController() = ComposeUIViewController {
+ MaterialTheme {
+ Greeting("iOS")
+ }
+}
+```
+
+В SwiftUI оборачивается через `UIViewControllerRepresentable`:
+
+```swift
+struct ComposeView: UIViewControllerRepresentable {
+ func makeUIViewController(context: Context) -> UIViewController {
+ MainViewControllerKt.MainViewController()
+ }
+
+ func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
+}
+```
+
+## Compose внутри SwiftUI ScrollView
+
+При встраивании Compose компонента в SwiftUI `ScrollView` необходимо явно указывать высоту, так как `UIViewControllerRepresentable` не определяет свой размер автоматически.
+
+Используйте `onGloballyPositioned` в Compose для измерения и колбэк высоты в SwiftUI:
+
+```kotlin
+// Kotlin
+fun ViewController(onHeightChanged: (Int) -> Unit) = ComposeUIViewController {
+ Column(modifier = Modifier.onGloballyPositioned { coordinates ->
+ onHeightChanged(coordinates.size.height)
+ }) {
+ // содержимое
+ }
+}
+```
+
+```swift
+// SwiftUI
+struct ComponentView: UIViewControllerRepresentable {
+ @Binding var measuredHeight: CGFloat
+
+ func makeUIViewController(context: Context) -> UIViewController {
+ ViewControllerKt.ViewController { height in
+ let scale = UIScreen.main.scale
+ measuredHeight = CGFloat(height) / scale
+ }
+ }
+
+ func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
+}
+
+// В SwiftUI ScrollView
+ScrollView {
+ ComponentView(measuredHeight: $measuredHeight)
+ .frame(height: measuredHeight)
+}
+```
+
+## Дополнительная информация
+
+- [Compose iOS Integration](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-ios.html)
+- [AndroidView](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/AndroidView)
+- [UIKitView](https://jetbrains.github.io/compose-multiplatform/kotlin/multiplatform/ios/UIKitView)
+- [SwiftUI Integration Guide](https://johnoreilly.dev/posts/swiftui-component-compose-ios/) — John O'Reilly
diff --git a/learning/compose-multiplatform/setup.md b/learning/compose-multiplatform/setup.md
new file mode 100644
index 000000000..a74617b85
--- /dev/null
+++ b/learning/compose-multiplatform/setup.md
@@ -0,0 +1,128 @@
+---
+sidebar_position: 2
+---
+
+# Подключение и настройка
+
+## Плагин Compose Multiplatform
+
+Для подключения Compose Multiplatform используется официальный Gradle-плагин `org.jetbrains.compose`.
+
+```kotlin
+plugins {
+ kotlin("multiplatform")
+ id("org.jetbrains.compose")
+ id("org.jetbrains.kotlin.plugin.compose") // Kotlin Compose compiler plugin
+}
+```
+
+Версия плагина задаётся в `gradle.properties` или `build.gradle.kts`:
+
+```kotlin
+// build.gradle.kts
+plugins {
+ kotlin("multiplatform") version "2.0.21"
+ id("org.jetbrains.compose") version "1.7.3"
+ id("org.jetbrains.kotlin.plugin.compose") version "2.0.21"
+}
+```
+
+## Подключение модулей
+
+```kotlin
+kotlin {
+ androidTarget()
+ listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { iosTarget ->
+ iosTarget.binaries.framework {
+ baseName = "shared"
+ isStatic = true
+ }
+ }
+
+ sourceSets {
+ commonMain.dependencies {
+ implementation(compose.runtime)
+ implementation(compose.foundation)
+ implementation(compose.material3)
+ implementation(compose.ui)
+ }
+
+ androidMain.dependencies {
+ implementation(compose.preview)
+ implementation("androidx.activity:activity-compose:1.9.3")
+ }
+ }
+}
+```
+
+## Android
+
+В Android Activity используйте `setContent`:
+
+```kotlin
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ MaterialTheme {
+ Greeting("Android")
+ }
+ }
+ }
+}
+```
+
+## iOS
+
+На iOS Compose UI выводится через `ComposeUIViewController`:
+
+```kotlin
+// commonMain
+fun MainViewController() = ComposeUIViewController {
+ MaterialTheme {
+ Greeting("iOS")
+ }
+}
+```
+
+```swift
+// iOS App
+import SwiftUI
+import shared
+
+struct ComposeView: UIViewControllerRepresentable {
+ func makeUIViewController(context: Context) -> UIViewController {
+ MainViewControllerKt.MainViewController()
+ }
+
+ func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
+}
+
+@main
+struct iOSApp: App {
+ var body: some Scene {
+ WindowGroup {
+ ComposeView().ignoresSafeArea()
+ }
+ }
+}
+```
+
+## Desktop
+
+```kotlin
+// jvmMain
+fun main() = application {
+ Window(title = "Compose Desktop", state = rememberWindowState()) {
+ MaterialTheme {
+ Greeting("Desktop")
+ }
+ }
+}
+```
+
+## Дополнительная информация
+
+- [Getting Started — официальное руководство](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-getting-started.html)
+- [Compose Multiplatform — Tutorials](https://github.com/JetBrains/compose-multiplatform/tree/master/tutorials)
+- [Настройка iOS в Compose Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-ios.html)
From 447d7139668940520f5797951b82b7c5c3b49baa Mon Sep 17 00:00:00 2001
From: Kuzmin <«vkuzmin@icerockdev.com»>
Date: Mon, 18 May 2026 17:35:48 +0300
Subject: [PATCH 15/15] update kotlin study materials
---
learning/kotlin/study-materials.md | 20 +++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/learning/kotlin/study-materials.md b/learning/kotlin/study-materials.md
index b162873cd..60ab2ed66 100644
--- a/learning/kotlin/study-materials.md
+++ b/learning/kotlin/study-materials.md
@@ -6,20 +6,20 @@ sidebar_position: 1
## Официальная документация
- [Документация на английском](https://kotlinlang.org/docs/home.html)
-- [Документация на русском](https://kotlinlang.ru/) Фанатский перевод официальной документации котлина на русский язык
+- [Документация на русском](https://kotlinlang.ru/) — фанатский перевод официальной документации
## Бесплатные курсы
### Kotlin
-- [Kotlin Basics - JetBrains Academy](https://hyperskill.org/tracks/18)
-- [Kotlin Bootcamp for Programmers - Google](https://www.udacity.com/course/kotlin-bootcamp-for-programmers--ud9011)
+- [Kotlin Core — JetBrains Academy](https://hyperskill.org/courses/18-kotlin-core)
+- [Kotlin Bootcamp for Programmers — Google](https://www.udacity.com/course/kotlin-bootcamp-for-programmers--ud9011)
## Платные курсы
### Kotlin
-- [Kotlin Developer - JetBrains Academy](https://hyperskill.org/tracks/3)
+- [Kotlin Developer — JetBrains Academy](https://hyperskill.org/tracks/3)
## Видео курсы
@@ -27,6 +27,12 @@ sidebar_position: 1
## Книги
-- [Atomic Kotlin](https://www.atomickotlin.com/atomickotlin/) - подойдет для новичков в программировании. Медленные и подробные пояснения
-- [Kotlin в действии](https://www.ozon.ru/product/kotlin-v-deystvii-143582448/) - для людей с Java опытом
-- [Effective Kotlin](https://kt.academy/book/effectivekotlin) - для людей уже с опытом в Котлине. Книга посвящена лучшим практикам использования языка
+- [Kotlin in Action, Second Edition](https://www.manning.com/books/kotlin-in-action-second-edition) — для людей с Java опытом. Второе издание (2024) от разработчиков языка, охватывает Kotlin 2.0, корутины и flows
+- [Effective Kotlin](https://kt.academy/book/effectivekotlin) — для людей уже с опытом в Kotlin. Лучшие практики и идиомы языка
+- [Kotlin Essentials](https://kt.academy/book/kotlinessentials) — для начинающих, базовый курс от Marcin Moskała
+
+## Дополнительные ресурсы
+
+- [Kotlin Multiplatform — официальная документация](https://kotlinlang.org/docs/multiplatform.html)
+- [Kotlin 2.0 — What's New](https://kotlinlang.org/docs/whatsnew20.html)
+- [Google's Android Basics in Kotlin](https://developer.android.com/courses/android-basics-kotlin/course)