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/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() +) +``` + +## Дополнительные материалы +

diff --git a/learning/libraries/moko/moko-fields.md b/learning/libraries/moko/moko-fields.md index e80fa79f7..dfdd1246c 100644 --- a/learning/libraries/moko/moko-fields.md +++ b/learning/libraries/moko/moko-fields.md @@ -10,7 +10,15 @@ sidebar_position: 6
-
+ + +## Состав библиотеки + +Библиотека состоит из нескольких модулей: +- `fields-core` — базовые классы и логика валидации +- `fields-livedata` — интеграция с `LiveData` из moko-mvvm +- `fields-flow` — интеграция с `Flow` из kotlinx.coroutines +- `fields-material` — Android View компоненты с Material Design ## FormField @@ -38,7 +46,9 @@ AuthCodeContent( ... ) ``` + В контенте экрана: + ```kotlin @Composable fun AuthCodeContent( @@ -58,18 +68,66 @@ 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 } +) +``` 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 + ) +) +``` 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("Выбрать фото") + } +} +``` 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` (тексты на русском и английском). + +## Дополнительные материалы + + +
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) } 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`). + +## Дополнительные материалы + + +
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). 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 общего кода