From a87df479e9b1801c350430126499c564079aceee Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:26:46 +0300 Subject: [PATCH 01/21] Update di.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit устаревший подход предоставления зависимостей заменен на подход с использованием Koin, добавлен пример с кодом --- university/4-icerock-basics/di.md | 333 +++++++++++++++--------------- 1 file changed, 169 insertions(+), 164 deletions(-) diff --git a/university/4-icerock-basics/di.md b/university/4-icerock-basics/di.md index 32ac91da8..4c17b2026 100644 --- a/university/4-icerock-basics/di.md +++ b/university/4-icerock-basics/di.md @@ -13,7 +13,7 @@ sidebar_position: 5 - общие картинки - и т.д. -Поэтому, нам нужно обеспечить передачу некоторых общих компонентов и классов во все модули. Использовать один общий модуль для таких компонентов мы не можем, это также описывалось в блоке [многомодульность](multimodularity). +Поэтому нам нужно обеспечить передачу некоторых общих компонентов и классов во все модули. Использовать один общий модуль для таких компонентов мы не можем, это также описывалось в блоке [многомодульность](multimodularity). В этом случае нам подойдет вариант с обратной зависимостью: - модули не зависят от каких-то компонент - необходимые модулю компоненты будут предоставляться извне @@ -24,7 +24,7 @@ sidebar_position: 5 ### Пример Допустим, мы делаем фичу авторизации. Для авторизации нам нужно: отправить запрос на сервер с номером телефона и кодом авторизации. -За логику работы с сетью у нас отвечает общий для всех модулей репозиторий. Поэтому, в фиче авторизации объявляем интерфейс с функцией `signIn`: +За логику работы с сетью у нас отвечает общий для всех модулей репозиторий. Поэтому в фиче авторизации объявляем интерфейс с функцией `signIn`: ```kotlin interface AuthRepository { @@ -43,48 +43,39 @@ class AuthViewModel( ) ``` Таким образом вьюмодель как бы объявляет: мне для работы нужен кто-то, кто реализует интерфейс `AuthRepository`, потому что у него есть нужный мне метод `signIn`. Мне вообще не важно, кто и как будет его реализовывать. -В классе общего репозитория реализуем интерфейс `AuthRepository` и, при создании фичи будем передавать объект общего репозитория. +В классе общего репозитория реализуем интерфейс `AuthRepository` и при создании фичи будем передавать объект общего репозитория. ## DI на проектах Вся логика приложения находится в общем коде. На платформах (`iOS` и `Android`) мы просто реализуем `UI` и связываем его с логикой. -В общем коде вся логика сосредоточена во вьюмоделях разных фич, поэтому, для каждого экрана от общего кода нужно получить нужную ему вьюмодель. +В общем коде вся логика сосредоточена во вьюмоделях разных фич, поэтому для каждого экрана от общего кода нужно получить нужную ему вьюмодель. Однако, вьюмодель - это как правило большой и сложный класс, который нуждается в настройке. Например, для создания стандартной вьюмодели ей необходимы: -- строки локализации - строки, использующиеся в общем коде - репозиторий, через который идет общение с источником данных - `exceptionHandler` - объект, реализующий интерфейс [ExceptionHandler](https://github.com/icerockdev/moko-errors/blob/ece79111fb5a9451e6179ba8c5367213c117421b/errors/src/commonMain/kotlin/dev/icerock/moko/errors/handler/ExceptionHandler.kt) и помогающий обрабатывать ошибки из общего кода (о нем вы узнаете позднее из `moko-errors`) -- `eventsDispatcher` - объект, служащий для отправки событий(actions) от `viewModel` на `UI` (о нем вы узнаете уже в следующем разделе) Наша цель - избавить платформу от сложности настройки вьюмоделей, чтобы не пришлось во фрагменте или вьюконтроллере получать все эти объекты, необходимые для создания вьюмодели. -Решение - по максимуму оставить логику настройки вьюмоделей в общем коде, чтобы со стороны платформы можно было практически сразу получить готовую вьюмодель. +Решение - по максимуму оставить логику настройки вьюмоделей в общем коде, используя Koin для предоставления зависимостей, чтобы со стороны платформы можно было получить готовую вьюмодель. ### Уровень фичи -Первый уровень абстракции над вьюмоделями это фабрика фичи. Она позволяет получить все вьюмодели одной фичи. Разбирать будем на примере фичи авторизации, а вьюмодель, которую мы хотим получить - вьюмодель экрана сброса пароля. +Нам потребуется модуль Koin для получения всех вьюмоделей одной фичи. Разбирать будем на примере фичи авторизации, а вьюмодель, которую мы хотим получить - вьюмодель экрана сброса пароля. -Начнем с вьюмодели: +Начнем с вьюмодели, которую создадим в папке presentation фичи: `ResetPasswordViewModel.kt`: ```kotlin class ResetPasswordViewModel( - override val eventsDispatcher: EventsDispatcher, - val exceptionHandler: ExceptionHandler, - private val repository: ResetPasswordRepository, - private val strings: Strings + private val repository: ResetPasswordRepository ) { - interface Strings { - val resetDescription: StringDesc - } + ... } ``` -Вьюмодель объявляет интерфейс `Strings` - необходимые ей строки локализации. Далее мы разберем это подробнее. - -Рядом с `ResetPasswordViewModel` создаем интерфейс репозитория. Сделали мы это для того, чтобы не устанавливать связь фича-модуля на модуль со строками локализации. В конструктор `ResetPasswordViewModel` принимает объект, который реализует этот интерфейс. В данном случае - кого-то, кто реализует метод для сброса пароля. +Рядом с `ResetPasswordViewModel` в папке model создаем интерфейс репозитория. В конструктор `ResetPasswordViewModel` принимает объект, который реализует этот интерфейс. В данном случае - кого-то, кто реализует метод для сброса пароля. -`ResetPasswordRepository.kt` +`ResetPasswordRepository.kt`: ```kotlin interface ResetPasswordRepository { suspend fun resetPassword( @@ -93,45 +84,53 @@ interface ResetPasswordRepository { ) } ``` -Класс репозитория фичи - `AuthRepository`, который будет реализовывать этот интерфейс разберем позднее. +Класс репозитория фичи - `AuthRepositoryImpl`, который будет реализовывать этот интерфейс разберем позднее. -Теперь сделаем `AuthFactory` - класс, с помощью которого будем настраивать общие компоненты вьюмоделей фичи авторизации и создавать их. Класс фабрики также объявляется в модуле фичи. +Теперь сделаем `featureAuthModule` - модуль Koin, с помощью которого будем предоставлять зависимости для вьюмоделей фичи авторизации и создавать вьюмодели. -`AuthFactory.kt`: +`featureAuthModule.kt`: ```kotlin -class AuthFactory( - private val createExceptionHandler: () -> ExceptionHandler, - private val authRepository: AuthRepository, - private val strings: Strings -) { - fun createResetPasswordViewModel( - eventsDispatcher: EventsDispatcher - ) = ResetPasswordViewModel( - eventsDispatcher = eventsDispatcher, - exceptionHandler = createExceptionHandler(), - repository = authRepository, - strings = strings - ) +... +import org.koin.core.Koin +import org.koin.core.module.Module +import org.koin.core.module.dsl.factoryOf +import org.koin.core.parameter.parametersOf +import org.koin.dsl.module + +val featureAuthModule: Module = module { + factoryOf(::ResetPasswordViewModel) + factoryOf(::AuthPhoneViewModel) + factoryOf(::AuthCodeViewModel) +} +fun Koin.createResetPasswordViewModel(): ResetPasswordViewModel { + return get() +} - interface Strings : ResetPasswordViewModel.Strings +fun Koin.createAuthPhoneViewModel: AuthPhoneViewModel { + return get() } -``` -`interface Strings` фабрики реализует все интерфейсы `Strings` из других вьюмоделей. -В эту фабрику мы будем добавлять методы, аналогичные `createResetPasswordViewModel` для создания других вьюмоделей, для них всех `createExceptionHandler`, `repository` и `strings` будут одинаковыми. +fun Koin.createAuthCodeViewModel(phone: String): AuthCodeViewModel { + return get { + parametersOf(phone) + } +} +``` -Теперь у нас есть доступ ко всем вьюмоделям фичи авторизации - чтобы создать какую-либо вьюмодель нужно просто вызвать нужную функцию у фабрики и передать один единственный аргумент. +В этот модуль мы будем добавлять методы, аналогичные `createResetPasswordViewModel` для создания других вьюмоделей, для них всех `repository` будет одинаковым. -### Уроверь mpp-library +Теперь у нас есть доступ ко всем вьюмоделям фичи авторизации, чтобы создать какую-либо вьюмодель нужно просто вызвать нужную функцию, где требуется, с передачей аргумента (в примере выше это номер телефона). -Логика работы приложения с источником данных (сервер, БД и т.д.) выносятся в классы - репозитории, в данном случае сделаем репозиторий для фичи авторизации - `AuthRepository` +### Уровень mpp-library -`AuthRepository.kt`: +Логика работы приложения с источником данных (сервер, БД и т.д.) выносятся в классы - репозитории, в данном случае сделаем репозиторий для фичи авторизации, имплементирующий интерфейс ResetPasswordRepository - `AuthRepositoryImpl` + +`AuthRepositoryImpl.kt`: ```kotlin -internal class AuthRepository constructor( +class AuthRepositoryImpl internal constructor( private val keyValueStorage: KeyValueStorage, private val dao: AppDao, - private val coroutineScope: CoroutineScope + private val api: AuthApi ) : ResetPasswordRepository { override fun resetPassword( phoneNumber: String, @@ -143,170 +142,176 @@ internal class AuthRepository constructor( ``` Этот класс реализует все интерфейсы вьюмоделей фичи авторизации для работы с источником данных. Для всех новых вьюмоделей других фичей мы будем объявлять свои интерфейсы, и реализовывать их в классе репозитория конкретной фичи, а затем прокидывать объект репозитория всем вьюмоделям. -Второй уровень абстракции: фабрика фабрик - `SharedFactory`. В ней мы также создадим все фабрики, как до этого создавали вьюмодели в фабриках, настроим их, чтобы для работы с общим кодом нужно было создать только одну общую фабрику - `SharedFactory`. +Также нам потребуется модуль Koin для получения всех репозиториев, в том числе для фичи авторизации. -`SharedFactory.kt`: ```kotlin -class SharedFactory internal constructor( - settings: Settings, - antilogs: List, - databaseDriverFactory: DatabaseDriverFactory, - repositoryCoroutineScope: CoroutineScope -) { - // public constructor for platform side usage - constructor( - settings: Settings, - antilog: Antilog?, - databaseDriverFactory: DatabaseDriverFactory?, - mpiServiceConnector: MpiServiceConnector? - ) : this( - settings = settings, - antilogs = listOfNotNull( - antilog, - CrashReportingAntilog(CrashlyticsLogger()) - ), - databaseDriverFactory = databaseDriverFactory, - mpiServiceConnector = mpiServiceConnector, - repositoryCoroutineScope = CoroutineScope(Dispatchers.Main) - ) - - internal val authRepository: AuthRepository by lazy { - AuthRepository( - //TODO +... +import org.koin.core.module.Module +import org.koin.core.module.dsl.binds +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.bind +import org.koin.dsl.module + +internal val repositoriesModule: Module = module { + singleOf(::AuthRepositoryImpl) { + binds( + classes = listOf( + ResetPasswordRepository::class, + AuthCodeRepository::class, + ... + ) ) } + singleOf(::ProfileRepositoryImpl) bind ProfileRepository::class - val authFactory: AuthFactory by lazy { - AuthFactory( - createExceptionHandler = ::createExceptionHandler, - authRepository = authRepository, - strings = object : AuthFactory.Strings { - override val resetDescription: StringDesc = - MR.strings.reset_description.desc() - } - ) - } - - private fun createExceptionHandler(): ExceptionHandler = ExceptionHandler( - // TODO - ) +``` +Теперь нужно собрать все модули фичей в `FeatureModule.kt`: +```kotlin +internal val featuresModules = module { + includes(featureAuthModule) + includes(featureProfileModule) + ... } ``` -В `SharedFactory` мы создали оставшиеся необходимые фабрикам компоненты - `authRepository` и `createExceptionHandler`, а также установили все строки локализации, необходимые фиче. -Поскольку, вьюмодель у нас пока что-то одна, объект `strings` для `AuthFactory` содержит только строки `ResetPasswordViewModel`. Если бы вьюмоделей было больше - все необходимые им строки задавались бы здесь. -*** -Фиче может понадобиться гораздо больше строк локализации, чем одна, и самих фич в проекте может быть очень много. Если инициализировать строки локализации каждой в фабрики фичей именно в `SharedFactory`, то класс со временем сильно разрастется и ориентироваться в нем будет сложно. -Предлагаем вам использовать вспомогательные функции, расположенные рядом с `SharedFactory`, чтобы инициализировать фабрики строками именно там, а в `SharedFactory` вызывать эти функции. +Теперь нужно зарегистрировать все модули Koin: -`AuthFactoryInit.kt`: ```kotlin -internal fun AuthFactory( - createExceptionHandler: () -> ExceptionHandler, - authRepository: AuthRepositoryInterface -): AuthFactory { - return AuthFactory( - createExceptionHandler = createExceptionHandler, - authRepository = authRepository, - strings = object : AuthFactory.Strings { - override val resetDescription: StringDesc = - MR.strings.reset_description.desc() - } - ) -} +internal fun registerKoinModules( + baseUrl: String +): List = listOf( + apiModule(baseUrl = baseUrl), + featuresModules, + repositoriesModule, + ... +) ``` -Вызов в `SharedFactory`: + +Также в commonMain подготовим функцию для инициализации Koin: ```kotlin -val authFactory: AuthFactory by lazy { - AuthFactory( - createExceptionHandler = ::createExceptionHandler, - authRepository = authRepository - ) +fun startDI( + baseUrl: String, + antilog: Antilog?, + exceptionLogger: ExceptionLogger, + appDeclaration: KoinAppDeclaration? = null +): KoinApplication { + antilog?.also { Napier.base(antilog = it) } + Napier.base(CrashReportingAntilog(exceptionLogger)) + configureExceptionMappers() + + return startKoin { + modules( + registerKoinModules( + baseUrl = baseUrl + ) + ) + appDeclaration?.invoke(this) + } } ``` -*** -### Уроверь платформы +*** -Параметры `SharedFactory` - это то, что мы не можем создать из общего кода а можем получить только с платформы. +### Уровень платформы +Параметры `startDI` - это то, что мы не можем создать из общего кода, а можем получить только с платформы. ***iOS*** -Класс со статической переменной - фабрикой +Для удобства создаем `Koin.swift`: +```swift +typealias KoinApplication = Koin_coreKoinApplication +typealias Koin = Koin_coreKoin ``` -class AppComponent { - static var factory: SharedFactory! +И `KoinManager.swift`: + +```swift +fileprivate var koinInstance: Koin! + +extension Koin { + static var shared: Koin { + return koinInstance + } + + internal static func setup() { + guard koinInstance == nil else { + fatalError("koin already initialized!") + } + + let antilog: Antilog? + #if DEBUG + antilog = DebugAntilog(defaultTag: "debug") + #else + antilog = nil + #endif + + let koinApp: KoinApplication = KoinKt.startDI( + baseUrl: Environment.Keys.serverBaseUrl.value(), + antilog: antilog, + exceptionLogger: CrashlyticsExceptionLogger() + ) + + koinInstance = koinApp.koin + } } + ``` -В методе `application` класса `AppDelegate` инициализируем фабрику и прокидываем дальше в `AppCoordinator`. О нем вы узнаете уже в следующем разделе `Навигация между экранами`. +В методе `application` класса `AppDelegate` инициализируем Koin. ``` -func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { +func application( + _: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil +) -> Bool { FirebaseApp.configure() - MokoFirebaseCrashlytics.setup() - let antilog: Antilog? #if DEBUG - antilog = DebugAntilog(defaultTag: "debug") - #else - antilog = nil + Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(false) #endif - AppComponent.factory = SharedFactory( - settings: AppleSettings(delegate: UserDefaults.standard), - antilog: antilog, - databaseDriverFactory: SqlDatabaseDriverFactory(), - ) - - let window = UIWindow() - - coordinator = AppCoordinator( - window: window, - factory: AppComponent.factory - ) - coordinator.start() - - window.makeKeyAndVisible() - self.window = window + Koin.setup() return true } ``` -`AppCoordinator` прокидывает ее дальше, в дочерние координаторы, которые, в свою очередь, отправляют ее уже в контроллеры. -Получение вьюмоедли в контроллере выглядит вот так: +Получение вьюмодели в контроллере выглядит вот так: ``` -vc.resetPasswordViewModel = factory -.authFactory -.createResetPasswordViewModel(eventsDispatcher: EventsDispatcher(listener: vc)) +private var resetPasswordViewModel: ResetPasswordViewModel = Koin.shared + .createResetPasswordViewModel() ``` ***Android*** ```kotlin -val factory = SharedFactory( - AndroidSettings( - delegate = context.getSharedPreferences("app", MODE_PRIVATE) - ), - antilog = antilog, - databaseDriverFactory = SqlDatabaseDriverFactory(context) -) +class MainApplication : Application() { + override fun onCreate() { + super.onCreate() + ... + val antilog: LogcatAntilog? = if (BuildConfig.DEBUG) { + LogcatAntilog() + } else { + null + } -val resetPasswordViewModel = factory.authFactory.createResetPasswordViewModel( - eventsDispatcher = eventsDispatcherOnMain() -) + startDI( + baseUrl = BuildConfig.BASE_URL, + antilog = antilog, + exceptionLogger = CrashlyticsLogger() + ) { + if (BuildConfig.DEBUG) { + androidLogger() + } + androidContext(this@MainApplication) + } + } +} ``` -Наконец, как добавлять новые компоненты в фичи и вьюмодели, если вдруг что-то понадобилось: - - все что общее для вьюмоделей одной фичи - настраивается в фабрике - - все, что общее для всех фабрик - настраивается в `SharedFactory` - - -Таким образом, чтобы начать работу с общим кодом - нужно только создать объект `SharedFactory`, передав ему несколько параметров, доступных только на платформе. +Таким образом, чтобы начать работу с общим кодом, нужно только инициализировать Koin c помощью `startDI`, передав ему несколько параметров, доступных только на платформе. ## Практическое задание - Используйте проект, готовый после раздела [Многомодульность](./multimodularity#практическое-задание) -- Добавьте фабрики для создания фичей \ No newline at end of file +- Добавьте модули Koin для фичей и его инициализацию From e1d44ff835d3dd3c12eace06b608c5cb7f4bcf60 Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:04:47 +0300 Subject: [PATCH 02/21] Create di.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit создан раздел с di в KMP, начала его заполнение --- learning/kotlin-multiplatform/di.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 learning/kotlin-multiplatform/di.md diff --git a/learning/kotlin-multiplatform/di.md b/learning/kotlin-multiplatform/di.md new file mode 100644 index 000000000..085fc958c --- /dev/null +++ b/learning/kotlin-multiplatform/di.md @@ -0,0 +1,6 @@ +# Dependency Injection + +В настоящее время на наших проектах с KMP для Dependency injection мы используем [Koin](https://github.com/InsertKoinIO/koin).
+[Документация Koin](https://insert-koin.io).
Как именно он используется, можно посмотреть в шаблоне проектов и в [статье универа](https://kmm.icerock.dev/university/icerock-basics/di).
+Ранее использовался подход с фабриками, который еще можно встретить на старых проектах. Ниже вы найдете его описание. + From 65d837f59ce4b2fb70aa3a106980f6036144bc1e Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Mon, 28 Jul 2025 09:46:46 +0300 Subject: [PATCH 03/21] Update di.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit перенесено описание устаревшего подхода с SharedFactory из универа --- learning/kotlin-multiplatform/di.md | 260 ++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) diff --git a/learning/kotlin-multiplatform/di.md b/learning/kotlin-multiplatform/di.md index 085fc958c..a35451dc6 100644 --- a/learning/kotlin-multiplatform/di.md +++ b/learning/kotlin-multiplatform/di.md @@ -4,3 +4,263 @@ [Документация Koin](https://insert-koin.io).
Как именно он используется, можно посмотреть в шаблоне проектов и в [статье универа](https://kmm.icerock.dev/university/icerock-basics/di).
Ранее использовался подход с фабриками, который еще можно встретить на старых проектах. Ниже вы найдете его описание. +## Устаревший подход к DI на проектах с помощью SharedFactory + +Вся логика приложения находится в общем коде. На платформах (`iOS` и `Android`) мы просто реализуем `UI` и связываем его с логикой. +В общем коде вся логика сосредоточена во вьюмоделях разных фич, поэтому, для каждого экрана от общего кода нужно получить нужную ему вьюмодель. + +Однако, вьюмодель - это как правило большой и сложный класс, который нуждается в настройке. +Например, для создания стандартной вьюмодели ей необходимы: +- строки локализации - строки, использующиеся в общем коде +- репозиторий, через который идет общение с источником данных +- `exceptionHandler` - объект, реализующий интерфейс [ExceptionHandler](https://github.com/icerockdev/moko-errors/blob/ece79111fb5a9451e6179ba8c5367213c117421b/errors/src/commonMain/kotlin/dev/icerock/moko/errors/handler/ExceptionHandler.kt) и помогающий обрабатывать ошибки из общего кода (о нем вы узнаете позднее из `moko-errors`) +- `eventsDispatcher` - объект, служащий для отправки событий(actions) от `viewModel` на `UI` (о нем вы узнаете уже в следующем разделе) + +Наша цель - избавить платформу от сложности настройки вьюмоделей, чтобы не пришлось во фрагменте или вьюконтроллере получать все эти объекты, необходимые для создания вьюмодели. + +Решение - по максимуму оставить логику настройки вьюмоделей в общем коде, чтобы со стороны платформы можно было практически сразу получить готовую вьюмодель. + +### Уровень фичи + +Первый уровень абстракции над вьюмоделями это фабрика фичи. Она позволяет получить все вьюмодели одной фичи. Разбирать будем на примере фичи авторизации, а вьюмодель, которую мы хотим получить - вьюмодель экрана сброса пароля. + +Начнем с вьюмодели: + +`ResetPasswordViewModel.kt`: +```kotlin +class ResetPasswordViewModel( + override val eventsDispatcher: EventsDispatcher, + val exceptionHandler: ExceptionHandler, + private val repository: ResetPasswordRepository, + private val strings: Strings +) { + interface Strings { + val resetDescription: StringDesc + } +} +``` +Вьюмодель объявляет интерфейс `Strings` - необходимые ей строки локализации. Далее мы разберем это подробнее. + +Рядом с `ResetPasswordViewModel` создаем интерфейс репозитория. Сделали мы это для того, чтобы не устанавливать связь фича-модуля на модуль со строками локализации. В конструктор `ResetPasswordViewModel` принимает объект, который реализует этот интерфейс. В данном случае - кого-то, кто реализует метод для сброса пароля. + +`ResetPasswordRepository.kt` +```kotlin +interface ResetPasswordRepository { + suspend fun resetPassword( + phoneNumber: String, + confirmCode: String + ) +} +``` +Класс репозитория фичи - `AuthRepository`, который будет реализовывать этот интерфейс разберем позднее. + +Теперь сделаем `AuthFactory` - класс, с помощью которого будем настраивать общие компоненты вьюмоделей фичи авторизации и создавать их. Класс фабрики также объявляется в модуле фичи. + +`AuthFactory.kt`: +```kotlin +class AuthFactory( + private val createExceptionHandler: () -> ExceptionHandler, + private val authRepository: AuthRepository, + private val strings: Strings +) { + fun createResetPasswordViewModel( + eventsDispatcher: EventsDispatcher + ) = ResetPasswordViewModel( + eventsDispatcher = eventsDispatcher, + exceptionHandler = createExceptionHandler(), + repository = authRepository, + strings = strings + ) + + interface Strings : ResetPasswordViewModel.Strings +} +``` +`interface Strings` фабрики реализует все интерфейсы `Strings` из других вьюмоделей. + +В эту фабрику мы будем добавлять методы, аналогичные `createResetPasswordViewModel` для создания других вьюмоделей, для них всех `createExceptionHandler`, `repository` и `strings` будут одинаковыми. + +Теперь у нас есть доступ ко всем вьюмоделям фичи авторизации - чтобы создать какую-либо вьюмодель нужно просто вызвать нужную функцию у фабрики и передать один единственный аргумент. + +### Уроверь mpp-library + +Логика работы приложения с источником данных (сервер, БД и т.д.) выносятся в классы - репозитории, в данном случае сделаем репозиторий для фичи авторизации - `AuthRepository` + +`AuthRepository.kt`: +```kotlin +internal class AuthRepository constructor( + private val keyValueStorage: KeyValueStorage, + private val dao: AppDao, + private val coroutineScope: CoroutineScope +) : ResetPasswordRepository { + override fun resetPassword( + phoneNumber: String, + confirmCode: String + ) { + // TODO + } +} +``` +Этот класс реализует все интерфейсы вьюмоделей фичи авторизации для работы с источником данных. Для всех новых вьюмоделей других фичей мы будем объявлять свои интерфейсы, и реализовывать их в классе репозитория конкретной фичи, а затем прокидывать объект репозитория всем вьюмоделям. + +Второй уровень абстракции: фабрика фабрик - `SharedFactory`. В ней мы также создадим все фабрики, как до этого создавали вьюмодели в фабриках, настроим их, чтобы для работы с общим кодом нужно было создать только одну общую фабрику - `SharedFactory`. + +`SharedFactory.kt`: +```kotlin +class SharedFactory internal constructor( + settings: Settings, + antilogs: List, + databaseDriverFactory: DatabaseDriverFactory, + repositoryCoroutineScope: CoroutineScope +) { + // public constructor for platform side usage + constructor( + settings: Settings, + antilog: Antilog?, + databaseDriverFactory: DatabaseDriverFactory?, + mpiServiceConnector: MpiServiceConnector? + ) : this( + settings = settings, + antilogs = listOfNotNull( + antilog, + CrashReportingAntilog(CrashlyticsLogger()) + ), + databaseDriverFactory = databaseDriverFactory, + mpiServiceConnector = mpiServiceConnector, + repositoryCoroutineScope = CoroutineScope(Dispatchers.Main) + ) + + internal val authRepository: AuthRepository by lazy { + AuthRepository( + //TODO + ) + } + + val authFactory: AuthFactory by lazy { + AuthFactory( + createExceptionHandler = ::createExceptionHandler, + authRepository = authRepository, + strings = object : AuthFactory.Strings { + override val resetDescription: StringDesc = + MR.strings.reset_description.desc() + } + ) + } + + private fun createExceptionHandler(): ExceptionHandler = ExceptionHandler( + // TODO + ) +} +``` +В `SharedFactory` мы создали оставшиеся необходимые фабрикам компоненты - `authRepository` и `createExceptionHandler`, а также установили все строки локализации, необходимые фиче. +Поскольку, вьюмодель у нас пока что-то одна, объект `strings` для `AuthFactory` содержит только строки `ResetPasswordViewModel`. Если бы вьюмоделей было больше - все необходимые им строки задавались бы здесь. + +*** +Фиче может понадобиться гораздо больше строк локализации, чем одна, и самих фич в проекте может быть очень много. Если инициализировать строки локализации каждой в фабрики фичей именно в `SharedFactory`, то класс со временем сильно разрастется и ориентироваться в нем будет сложно. +Предлагаем вам использовать вспомогательные функции, расположенные рядом с `SharedFactory`, чтобы инициализировать фабрики строками именно там, а в `SharedFactory` вызывать эти функции. + +`AuthFactoryInit.kt`: +```kotlin +internal fun AuthFactory( + createExceptionHandler: () -> ExceptionHandler, + authRepository: AuthRepositoryInterface +): AuthFactory { + return AuthFactory( + createExceptionHandler = createExceptionHandler, + authRepository = authRepository, + strings = object : AuthFactory.Strings { + override val resetDescription: StringDesc = + MR.strings.reset_description.desc() + } + ) +} +``` +Вызов в `SharedFactory`: + +```kotlin +val authFactory: AuthFactory by lazy { + AuthFactory( + createExceptionHandler = ::createExceptionHandler, + authRepository = authRepository + ) +} +``` +*** + +### Уроверь платформы + +Параметры `SharedFactory` - это то, что мы не можем создать из общего кода а можем получить только с платформы. + + +***iOS*** + +Класс со статической переменной - фабрикой +``` +class AppComponent { + static var factory: SharedFactory! +} +``` + +В методе `application` класса `AppDelegate` инициализируем фабрику и прокидываем дальше в `AppCoordinator`. О нем вы узнаете уже в следующем разделе `Навигация между экранами`. +``` +func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + FirebaseApp.configure() + MokoFirebaseCrashlytics.setup() + + let antilog: Antilog? + #if DEBUG + antilog = DebugAntilog(defaultTag: "debug") + #else + antilog = nil + #endif + + AppComponent.factory = SharedFactory( + settings: AppleSettings(delegate: UserDefaults.standard), + antilog: antilog, + databaseDriverFactory: SqlDatabaseDriverFactory(), + ) + + let window = UIWindow() + + coordinator = AppCoordinator( + window: window, + factory: AppComponent.factory + ) + coordinator.start() + + window.makeKeyAndVisible() + self.window = window + + return true +} +``` + +`AppCoordinator` прокидывает ее дальше, в дочерние координаторы, которые, в свою очередь, отправляют ее уже в контроллеры. +Получение вьюмоедли в контроллере выглядит вот так: + +``` +vc.resetPasswordViewModel = factory +.authFactory +.createResetPasswordViewModel(eventsDispatcher: EventsDispatcher(listener: vc)) +``` + +***Android*** + +```kotlin +val factory = SharedFactory( + AndroidSettings( + delegate = context.getSharedPreferences("app", MODE_PRIVATE) + ), + antilog = antilog, + databaseDriverFactory = SqlDatabaseDriverFactory(context) +) + +val resetPasswordViewModel = factory.authFactory.createResetPasswordViewModel( + eventsDispatcher = eventsDispatcherOnMain() +) +``` + +Наконец, как добавлять новые компоненты в фичи и вьюмодели, если вдруг что-то понадобилось: + - все что общее для вьюмоделей одной фичи - настраивается в фабрике + - все, что общее для всех фабрик - настраивается в `SharedFactory` + +Таким образом, чтобы начать работу с общим кодом - нужно только создать объект `SharedFactory`, передав ему несколько параметров, доступных только на платформе. From c555145a4a0977b49c53493c57bec517d5b43877 Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:05:07 +0300 Subject: [PATCH 04/21] Update android-intro.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit livedata заменена на flows --- university/1-android-basics/android-intro.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/university/1-android-basics/android-intro.md b/university/1-android-basics/android-intro.md index d7627429d..cb254898b 100644 --- a/university/1-android-basics/android-intro.md +++ b/university/1-android-basics/android-intro.md @@ -19,7 +19,7 @@ sidebar_position: 0 - Верстка UI используя xml layout - Библиотеки AndroidX и Jetpack от Google - `RecyclerView` -- `LiveData` +- Kotlin flows, `StateFlow`, `SharedFlow` - `ViewModel` - Жизненный цикл `Application`, `Activity`, `Fragment`, `ViewModel` - `ViewBinding` @@ -29,4 +29,4 @@ sidebar_position: 0 :::info Для тех кому всё перечисленное уже знакомо, использовано на практике и есть уверенное понимание о чем речь - можно пропустить ознакомление с теоретическим блоком и сразу перейти к [практической задаче](practice). -::: \ No newline at end of file +::: From 9127740bf8e4e204733fb93233446d15144f51ac Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:37:12 +0300 Subject: [PATCH 05/21] Update app-logic.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit замена материалов в части ViewModel --- university/1-android-basics/app-logic.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/university/1-android-basics/app-logic.md b/university/1-android-basics/app-logic.md index 6d60774dd..c5d78fa16 100644 --- a/university/1-android-basics/app-logic.md +++ b/university/1-android-basics/app-logic.md @@ -19,12 +19,12 @@ sidebar_position: 5 ## ViewModel -Ознакомиться детальнее с ViewModel и LiveData помогут следующие материалы: +Ознакомиться детальнее с ViewModel и Kotlin flows помогут следующие материалы: - [Единый стейт экрана](../../learning/state) - статья о состояних и событиях -- [Android Kotlin Fundamentals: 5.1 ViewModel](https://developer.android.com/codelabs/kotlin-android-training-view-model) - не пропуская Summary и тест в Homework -- [Android Kotlin Fundamentals: LiveData and LiveData observers](https://developer.android.com/codelabs/kotlin-android-training-live-data) - не пропуская Summary и тест в Homework -- [Incorporate Lifecycle-Aware Components](https://developer.android.com/codelabs/android-lifecycles) - для закрепления связей жизненного цикла android компонентов и ViewModel, LiveData +- [Kotlin Flows on Android](https://developer.android.com/kotlin/flow) +- [StateFlow and SharedFlow](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow) +- [Android Basics Compose: 5. ViewModel and State in Compose](https://developer.android.com/codelabs/basic-android-kotlin-compose-viewmodel-and-state) - для закрепления связей жизненного цикла android компонентов и ViewModel, StateFlow. Не пропуская Conclusion и ссылки в нем ## Retrofit From 56815d9de93ea4521f0330a53e1a85441aa5d6ad Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Mon, 28 Jul 2025 10:50:00 +0300 Subject: [PATCH 06/21] Update practice.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit замена livedata на flow --- university/4-icerock-basics/practice.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/university/4-icerock-basics/practice.md b/university/4-icerock-basics/practice.md index 0c76e082d..e9991e0c1 100644 --- a/university/4-icerock-basics/practice.md +++ b/university/4-icerock-basics/practice.md @@ -26,7 +26,7 @@ sidebar_position: 16 3. Создать отдельные модули для: `common` кода, фичи авторизации, и фичи репозитория. 4. Сохранять токен авторизации в хранилище устройства: `SharedPreferences` для `Android` и `NSUserDefaults` для `iOS`. Работу с хранилищем делегировать классу `KeyValueStorage` 5. Использовать `multiplatform-settings` для работы с хранилищем устройства -6. Использовать `moko-mvvm` для внедрения всех ее возможностей, о которых вы узнали [статьи](../../learning/libraries/moko/moko-mvvm) +6. Использовать `moko-mvvm` для внедрения всех ее возможностей, о которых вы узнали из [статьи](../../learning/libraries/moko/moko-mvvm) 7. Использовать `moko-resources` для использования строк локализации приложения 8. Использовать `moko-units` для реализации списка репозиториев 9. Использовать `ExceptionMappersStorage` из `moko-errors` (не используйте `ExceptionHandler`) @@ -85,8 +85,8 @@ class KeyValueStorage { ### mpp-library-feature-auth ```kotlin class AuthViewModel { - val token: MutableLiveData - val state: LiveData + val token: MutableStateFlow + val state: StateFlow val actions: Flow fun onSignButtonPressed() { @@ -111,7 +111,7 @@ class AuthViewModel { ### mpp-library-feature-repo ```kotlin class RepositoryInfoViewModel { - val state: LiveData + val state: StateFlow sealed interface State { object Loading : State @@ -134,7 +134,7 @@ class RepositoryInfoViewModel { } class RepositoriesListViewModel { - val state: LiveData + val state: StateFlow sealed interface State { object Loading : State @@ -234,4 +234,4 @@ GitHubRepoRepository --> KeyValueStorage 7. [Настройке запросов в Ktor Client](https://ktor.io/docs/request.html) 8. [multiplatform-settings](https://github.com/russhwolf/multiplatform-settings) 9. [Android Дизайн](https://www.figma.com/file/Mh3ga5XAzyJNCY87NBp01G/Git_test) -10. [iOS Дизайн](https://www.figma.com/file/XmpoCqkdWTGb2NGdR2bgiQ/Git_test-iOS) \ No newline at end of file +10. [iOS Дизайн](https://www.figma.com/file/XmpoCqkdWTGb2NGdR2bgiQ/Git_test-iOS) From 63d0757f28cbe95a052767cf2e24d2bec4bb14c0 Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:00:28 +0300 Subject: [PATCH 07/21] Update practice.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit добавлен koin --- university/4-icerock-basics/practice.md | 30 +++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/university/4-icerock-basics/practice.md b/university/4-icerock-basics/practice.md index e9991e0c1..47e3e4694 100644 --- a/university/4-icerock-basics/practice.md +++ b/university/4-icerock-basics/practice.md @@ -30,19 +30,20 @@ sidebar_position: 16 7. Использовать `moko-resources` для использования строк локализации приложения 8. Использовать `moko-units` для реализации списка репозиториев 9. Использовать `ExceptionMappersStorage` из `moko-errors` (не используйте `ExceptionHandler`) -10. Вся логика должна находиться в `common` коде -11. Навигация на `iOS` должна быть реализована используя `AppCoordinator`, без `storyboards` -12. Логика хранения данных должна находиться в `common` коде -13. Логика работы с сетью должна находиться в `common` коде -14. Для работы с сетью использовать `Ktor Client` -15. Используйте доменные сущности, вместо сетевых -16. При перезапуске приложения авторизация должна сохраняться -17. Использовать локализацию для всех строк, показываемых пользователю -18. Использовать векторную графику везде, где это возможно -19. Обеспечить поддержку Android API 21 -20. Локализовать проект используя `sheets-localizations-generator` +10. Вся логика должна находиться в `common` коде +11. Используйте Koin для внедрения зависимостей +12. Навигация на `iOS` должна быть реализована используя `AppCoordinator`, без `storyboards` +13. Логика хранения данных должна находиться в `common` коде +14. Логика работы с сетью должна находиться в `common` коде +15. Для работы с сетью использовать `Ktor Client` +16. Используйте доменные сущности, вместо сетевых +17. При перезапуске приложения авторизация должна сохраняться +18. Использовать локализацию для всех строк, показываемых пользователю +19. Использовать векторную графику везде, где это возможно +20. Обеспечить поддержку Android API 21 +21. Локализовать проект используя `sheets-localizations-generator` - обеспечьте поддержку русского и английского языков -21. Обеспечить поддержку iOS 13.0 +22. Обеспечить поддержку iOS 13.0 ## Классы приложения @@ -233,5 +234,6 @@ GitHubRepoRepository --> KeyValueStorage 6. [Подключение Ktor Client](https://ktor.io/docs/gradle.html) 7. [Настройке запросов в Ktor Client](https://ktor.io/docs/request.html) 8. [multiplatform-settings](https://github.com/russhwolf/multiplatform-settings) -9. [Android Дизайн](https://www.figma.com/file/Mh3ga5XAzyJNCY87NBp01G/Git_test) -10. [iOS Дизайн](https://www.figma.com/file/XmpoCqkdWTGb2NGdR2bgiQ/Git_test-iOS) +9. [Koin](https://github.com/InsertKoinIO/koin) +10. [Android Дизайн](https://www.figma.com/file/Mh3ga5XAzyJNCY87NBp01G/Git_test) +11. [iOS Дизайн](https://www.figma.com/file/XmpoCqkdWTGb2NGdR2bgiQ/Git_test-iOS) From 10b3156591490c8eed88f23c4ef15240aadec6ba Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:19:50 +0300 Subject: [PATCH 08/21] Update testing.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit добавлена ссылка на тьюториал по тестированию пушей на iOS симуляторе --- university/10-push-notifications/testing.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/university/10-push-notifications/testing.md b/university/10-push-notifications/testing.md index f304d9943..a083109d3 100644 --- a/university/10-push-notifications/testing.md +++ b/university/10-push-notifications/testing.md @@ -58,9 +58,6 @@ sidebar_position: 2 Результат запроса: ![img.png](media/response-example.png) -:::info -Пуши на Android можно протестировать на эмуляторе. -Пуши на iOS протестировать на эмуляторе не получится, только на реальном девайсе. -::: -![img.jpg](media/response-example-phone.png) +Пуши на Android можно протестировать на эмуляторе без особых сложностей. +Чтобы разобраться, как тестировать push-уведомления на симуляторе iOS, прочитайте [тьюториал](https://sparrowcode.io/ru/tutorials/testing-push-notifications-ios-simulator). From b4b3a28a727cd76509d6a056c299e6249256ffdf Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:59:05 +0300 Subject: [PATCH 09/21] Update ktor-client.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit удален deprecated Input --- learning/libraries/ktor/ktor-client.md | 48 -------------------------- 1 file changed, 48 deletions(-) diff --git a/learning/libraries/ktor/ktor-client.md b/learning/libraries/ktor/ktor-client.md index 10d2db329..796b170d8 100644 --- a/learning/libraries/ktor/ktor-client.md +++ b/learning/libraries/ktor/ktor-client.md @@ -147,56 +147,8 @@ append("image", File("ktor_logo.png").readBytes(), Headers.build { ... ``` -### Передача файла как Input - -Этот подход подразумевает использование *kotlinx-io* `Input` ([ссылка на класс](https://ktor.kotlincn.net/kotlinx/io/io/input-output.html)). В таком случае используется другой подход формирования `formData`: -```kotlin -val data: List = formData { - appendInput( - key = "yourKey", - block = { input }, - headers = Headers.build { - append( - HttpHeaders.ContentType, - ContentType.Application.OctetStream.toString() - ) - append( - HttpHeaders.ContentDisposition, ContentDisposition.File - .withParameter(ContentDisposition.Parameters.FileName, fileName) - .toString() - ) - } - ) -} -``` -В примере представлен вариант добавления `headers`, по умолчанию *Empty*. Здесь можно конфигурировать хедеры под ваши нужды. - Касаемо использования formData, существует несколько подходов в формировании *ktor* HTTP клиента: -### Передача файла как Input в common коде - -Чтобы реализовать потоковую передачу файла в общем коде, используя ktor, необходимо получить объект [Input](https://api.ktor.io/older/1.6.8/ktor-io/io.ktor.utils.io.core/-input/index.html) на основе файла. Сделать это в общем коде можно используя expect/actual функции: - -***commonMain:*** -```kotlin -expect fun inputByFilepath(filePath: String): Input -``` -***androidMain:*** -```kotlin -actual fun inputByFilepath(filePath: String): Input{ - val file = File(filePath) - val inputStream = file.inputStream() - return inputStream.asInput() -} -``` -***iosMain:*** -```kotlin -actual fun inputByFilepath(filePath: String): Input { - val fileHandle = NSFileHandle.fileHandleForReadingAtPath(path = filePath) - return Input(fileHandle!!.fileDescriptor) -} -``` - ### submitFormWithBinaryData Для использования этого метода необходимо заранее сформировать `formData`. Код с таким методом выглядит следующим образом: From 8149ce052d8c0ed6e0dc77b9a4319d80311d09af Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:28:51 +0300 Subject: [PATCH 10/21] Update ktor-client.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit добавлен пример реализации загрузки файлов с multipart/formdata на android --- learning/libraries/ktor/ktor-client.md | 141 +++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/learning/libraries/ktor/ktor-client.md b/learning/libraries/ktor/ktor-client.md index 796b170d8..11a94b0cc 100644 --- a/learning/libraries/ktor/ktor-client.md +++ b/learning/libraries/ktor/ktor-client.md @@ -169,6 +169,147 @@ val result = httpClient.post { } ``` +### Пример реализации на Android + +Стек: Retrofit 2.11.0, KotlinX.io, Multipart-formdata для передачи файла. +Для передачи файла в Retrofit надо представить его в формате понятном ему - RequestBody. Создаем наследника данного класса: + +```kotlin +import kotlinx.io.Buffer +import kotlinx.io.readByteArray +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody +import okio.BufferedSink +import io.example.FileSource +import kotlin.math.min + +class FileSourceRequestBody( + private val fileSource: FileSource, + private val onUploadCallback: (Float) -> Unit, +) : RequestBody() { + + // Определяем тип контента для передачи в заголовке MediaType + override fun contentType(): MediaType? { + return when { + fileSource.fileName.endsWith(".png", ignoreCase = true) -> "image/png" + fileSource.fileName.endsWith(".jpg", ignoreCase = true) || + fileSource.fileName.endsWith(".jpeg", ignoreCase = true) -> "image/jpeg" + fileSource.fileName.endsWith(".bmp", ignoreCase = true) -> "image/bmp" + fileSource.fileName.endsWith(".gif", ignoreCase = true) -> "image/gif" + fileSource.fileName.endsWith(".webp", ignoreCase = true) -> "image/webp" + else -> "application/octet-stream" + }.toMediaTypeOrNull() + } + + override fun contentLength(): Long { + return fileSource.fileSize + } + + override fun writeTo(sink: BufferedSink) { + // Создаем буфер + val buffer = Buffer() + // Объем загруженный на сервер, для расчета прогресса загрузки + var totalBytesRead = 0L + + // Открываем поток, который будет закрыт автоматически по окончании работы с ним + fileSource.source.use { source -> + var readBytes: Long + + while (totalBytesRead != fileSource.fileSize) { + // Читаем файл по размеру буффера, либо по оставшемуся количеству от файла для загрузки + // Чтение происходит с удалением прочитанных байтов из source + readBytes = source.readAtMostTo( + sink = buffer, + byteCount = min( + a = DEFAULT_BUFFER_SIZE, + b = fileSource.fileSize - totalBytesRead + ) + ) + + totalBytesRead += readBytes + + // Записываем прочитанный объем в исходящий поток данных + sink.write( + source = buffer.readByteArray(), + offset = 0, + byteCount = readBytes.toInt() + ) + + // Вычисляем прогресс загрузки + calculateProgress(totalBytesRead, onUploadCallback) + } + + // Очищаем текущий поток + sink.flush() + } + } + + private fun calculateProgress( + totalBytesRead: Long, + onUploadCallback: (Float) -> Unit, + ) { + val progress: Float = (totalBytesRead / contentLength().toFloat()) + onUploadCallback(progress) + } + + companion object { + private const val DEFAULT_BUFFER_SIZE = 4096L + } +} +``` + +В качестве входящих параметров для класса нужно передать информацию о файле, вторым параметром передаем callback для отображения прогресса загрузки на ui. Структура FileSource: + +```kotlin +import kotlinx.io.RawSource + +data class FileSource( + val fileName: String, + val source: RawSource, + val fileSize: Long, +) +``` +Теперь когда готовы основные структуры для загрузки файла на сервер, давайте создадим интерфейс для нашего API: + +```kotlin +import okhttp3.MultipartBody +import retrofit2.Response +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part + +interface UploadApi { + @Multipart + @POST("/api/images") + suspend fun uploadImage( + @Part image: MultipartBody.Part, + ): Response +} +``` +Для обозначения, что в теле запроса содержится multi-part на него нужно повесить аннотацию '@Multipart', а для параметра содержащий его '@Part'. При вызове данного запроса в репозитории необходимо будет создать MultipartBody.Part, вызывом createFormData: + +```kotlin +... + suspend fun uploadImage( + fileSource: FileSource, + onUploadCallback: (Float) -> Unit, + ): ImageUploadResult { + return uploadApi.uploadImage( + image = MultipartBody.Part.createFormData( + name = "image", // имя Multipart файла указанное в api бекенда + filename = fileSource.fileName, // Имя файла передается в заголовке form-data + body = FileSourceRequestBody( + fileSource = fileSource, + onUploadCallback = onUploadCallback + ) + ) + ) + }.toDomain() + } +... +``` + ### application/octet-stream Данный подход используется довольно редко, но все же используется. Для данного типа запроса нет возможности передать несколько параметров или файлов, можно отправлять файл, притом только один. Для реализации подхода необходимо создать класс, унаследованный от `WriteChannelContent` ([ссылка на класс](https://www.mvndoc.com/c/io.ktor/ktor-http-iosarm64/io/ktor/http/content/OutgoingContent.WriteChannelContent.html)). Пример кода: From aeca3e66e88745382b51cf292ee9f82b0b7c9b13 Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Mon, 28 Jul 2025 13:54:56 +0300 Subject: [PATCH 11/21] Update entities.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit добавлен коммент, что Сетевые сущности должны содержать аннотацию с именем поля для всех полей класса --- university/4-icerock-basics/entities.md | 1 + 1 file changed, 1 insertion(+) diff --git a/university/4-icerock-basics/entities.md b/university/4-icerock-basics/entities.md index b920ad4ad..7c432ef57 100644 --- a/university/4-icerock-basics/entities.md +++ b/university/4-icerock-basics/entities.md @@ -35,6 +35,7 @@ data class StarShipResponse( val uniqueInfo: String? = null ) ``` +Сетевые сущности должны содержать аннотацию с именем поля для всех полей класса. Пример доменного класса с учетом того, что в приложении используется только следующая информация о корабле: - название From 334dacc9058de382cc4c92ab7b2f3ba1b849d135 Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Mon, 28 Jul 2025 15:37:10 +0300 Subject: [PATCH 12/21] Update moko-fields.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit добавление про отсутствие двусторонней связки для compose, пример реализации text field с onValueChanged --- learning/libraries/moko/moko-fields.md | 45 +++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/learning/libraries/moko/moko-fields.md b/learning/libraries/moko/moko-fields.md index c8a29ef67..e80fa79f7 100644 --- a/learning/libraries/moko/moko-fields.md +++ b/learning/libraries/moko/moko-fields.md @@ -15,16 +15,52 @@ sidebar_position: 6 ## FormField Потребность в библиотеке возникла из-за того, что для создания логики формы ввода в общем коде необходимы следующие элементы: -- `LiveData(String)` - текст поля -- `LiveData(Bool)` - валидно/невалидно поле -- `LiveData(String)` - текст ошибки валидации +- `LiveData/StateFlow(String)` - текст поля +- `LiveData/StateFlow(Bool)` - валидно/невалидно поле +- `LiveData/StateFlow(String)` - текст ошибки валидации А представьте, что у вас 7 или 8 таких полей, получится много однотипного кода, в котором легко будет запутаться и допустить ошибку. -Библиотека позволяет использовать специальный класс `FormField` для форм ввода, который включает в себя все эти три лайвдаты. +Библиотека позволяет использовать специальный класс `FormField` для форм ввода, который включает в себя все эти три лайвдаты/стейт флоу. Для создания `FormField` необходимо только установить тип и задать валидацию для этого значения. Тип поля не обязательно должен быть `String`, подойдет любой, который можно как-то установить: `int`, `bitmap`, `data` и тд. +Для Jetpack Compose и Compose Multiplatform двусторонная связка для передачи значения введенного текста поля в View Model не работает, необходимо реализовывать на UI TextField c onValueChange.
+В коде экрана: + +```kotlin +val (code, onCodeChange) = viewModel.code.data.collectAsMutableState() + +AuthCodeContent( + code = code, + onCodeChange = onCodeChange, + codeError = viewModel.code.error.collectAsState().value?.localized(), + ... +) +``` +В контенте экрана: +```kotlin +@Composable +fun AuthCodeContent( + code: String, + onCodeChange: (String) -> Unit, + codeError: String?, + ... +) { + ... + BasicTextField( + ... + value = code, + onValueChange = { newValue -> + onCodeChange(newValue) + }, + isError = codeError !=null + ) + ... +} + +``` + ## Валидация Разберем, как добавлять валидацию в `FormField`: - можно использовать [встроенные валидаторы](https://github.com/icerockdev/moko-fields/tree/c9c09069da717d4995ee6c96f8ec6ef7446af503/fields/src/commonMain/kotlin/dev/icerock/moko/validations) @@ -36,3 +72,4 @@ sidebar_position: 6 - поля можно объединить в список и валидировать их одновременно - валидация полей может быть завязана на других полях (пароль + повторите пароль) - у FormField есть поле `isValid` и `validationError` + From e932a381aaebc3507203285f89d4ed0ff430d1b3 Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:03:48 +0300 Subject: [PATCH 13/21] Update logging-and-errors.md up sidebar_position --- university/4-icerock-basics/logging-and-errors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/university/4-icerock-basics/logging-and-errors.md b/university/4-icerock-basics/logging-and-errors.md index eba682900..6555abbbf 100644 --- a/university/4-icerock-basics/logging-and-errors.md +++ b/university/4-icerock-basics/logging-and-errors.md @@ -1,5 +1,5 @@ --- -sidebar_position: 11 +sidebar_position: 10 --- # Логирование и обработка ошибок From ac2e5e52bc1a1cec1a3e162a6f262f1973a1d858 Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:04:53 +0300 Subject: [PATCH 14/21] Update android-just-like-ios.md up sidebar_position --- university/4-icerock-basics/android-just-like-ios.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/university/4-icerock-basics/android-just-like-ios.md b/university/4-icerock-basics/android-just-like-ios.md index ab8401740..0eaadb042 100644 --- a/university/4-icerock-basics/android-just-like-ios.md +++ b/university/4-icerock-basics/android-just-like-ios.md @@ -1,5 +1,5 @@ --- -sidebar_position: 12 +sidebar_position: 11 --- # Android ≈ iOS From 9ddd072511b632b220f6dd2dde404847f321c5c6 Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:05:25 +0300 Subject: [PATCH 15/21] Update repository.md up sidebar_position --- university/4-icerock-basics/repository.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/university/4-icerock-basics/repository.md b/university/4-icerock-basics/repository.md index da47259ff..d960b9b6a 100644 --- a/university/4-icerock-basics/repository.md +++ b/university/4-icerock-basics/repository.md @@ -1,5 +1,5 @@ --- -sidebar_position: 13 +sidebar_position: 12 --- # Реактивный источник данных From 356063f0f3e53fe96a8e8e664169b9060ab555a2 Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:05:50 +0300 Subject: [PATCH 16/21] Update mock-engine.md --- university/4-icerock-basics/mock-engine.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/university/4-icerock-basics/mock-engine.md b/university/4-icerock-basics/mock-engine.md index 1c6a08a1a..4c8214e23 100644 --- a/university/4-icerock-basics/mock-engine.md +++ b/university/4-icerock-basics/mock-engine.md @@ -1,5 +1,5 @@ --- -sidebar_position: 14 +sidebar_position: 13 --- # Мокирование данных From 7d85ca99b49915cbdd3884b46f121e6cc8297b6f Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:06:19 +0300 Subject: [PATCH 17/21] Update entities.md --- university/4-icerock-basics/entities.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/university/4-icerock-basics/entities.md b/university/4-icerock-basics/entities.md index 7c432ef57..1b24b644c 100644 --- a/university/4-icerock-basics/entities.md +++ b/university/4-icerock-basics/entities.md @@ -1,5 +1,5 @@ --- -sidebar_position: 15 +sidebar_position: 14 --- # Сущности приложения From d95a5dd0388866991215460e166e2c536d2aa9cf Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:06:48 +0300 Subject: [PATCH 18/21] Update practice.md --- university/4-icerock-basics/practice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/university/4-icerock-basics/practice.md b/university/4-icerock-basics/practice.md index 47e3e4694..9bea4529e 100644 --- a/university/4-icerock-basics/practice.md +++ b/university/4-icerock-basics/practice.md @@ -1,5 +1,5 @@ --- -sidebar_position: 16 +sidebar_position: 15 --- # Практическое задание From ce8c53d9d1bd6cf060d2638ced37d8484c51653d Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:07:59 +0300 Subject: [PATCH 19/21] Delete university/4-icerock-basics/ui-units.md --- university/4-icerock-basics/ui-units.md | 40 ------------------------- 1 file changed, 40 deletions(-) delete mode 100644 university/4-icerock-basics/ui-units.md diff --git a/university/4-icerock-basics/ui-units.md b/university/4-icerock-basics/ui-units.md deleted file mode 100644 index 0d4c95183..000000000 --- a/university/4-icerock-basics/ui-units.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -sidebar_position: 10 ---- -# Построение экранов вертикальными списками - -В мобильных приложениях наиболее распространен интерфейс с вертикальной прокруткой (scroll) экрана. Это связано с форм-фактором устройств и количеством контента, который нужно показывать пользователю. - -Для реализации таких экранов используются два подхода: -- `RecyclerView`/`UITableView` с переиспользованием элементов при прокрутке - этот подход используется в любой ленте или списке однотипных элементов; -- `ScrollView`/`UIScrollView` с ручной версткой содержимого, которое должно быть в scroll. Данный подход используется чаще всего на экранах с где контент не является набором однотипных элементов, но не влезает на экран устройства полностью. - -| `RecyclerView` / `UITableView` | `ScrollView` / `UIScrollView` | -| ----- | ----- | -| ![listview](media/listview.png) | ![scrollview](media/scrollview.png) | - -Первый вариант экранов, с однотипными списками элементов, достаточно очевидно как можно использовать с общей логикой - собираем список с данными в `ViewModel`, а на UI передаем эти данные в `Adapter`/`DataSource`. - -Для второго же предполагается много ручной работы по верстке UI под конкретное расположение элементов и выдачу данных из `ViewModel` отдельными полями. - -Мы используем подход по построению практически любого экрана с вертикальной прокруткой через первый вариант - `RecyclerView`/`UITableView`. Аналогичный подход используют Airbnb в своей библиотеке [epoxy](https://github.com/airbnb/epoxy). - -Это позволяет нам контролировать содержимое экрана из общей логики, частично управляя и расположением и видимостью элементов на UI. По сути экран бьется на строки - вертикальные блоки, которые мы назвали `Unit` (элемент). Разберем приведенный выше пример, вот как можно разбить на блоки: - -| | | -| ----- | ----- | -| ![units-1](media/scrollview-units-1.png) | ![units-2](media/scrollview-units-2.png) | - -## moko-units - -Библиотека [moko-units](https://github.com/icerockdev/moko-units) позволяет нам реализовать описанный выше подход с управлением составом отображаемых на UI блоков из общего кода. Со стороны каждой платформы интеграция очень простая и стандартная. Вся основная работа состоит в том, чтобы сверстать все блоки и прописать логику их создания из `sealed interface` находящегося в общем коде. - -Посмотрите видео материал с подробным пояснением причин обобщения реализации между платформами и принципами работы. - - - -## Практическое задание -- Используйте проект, готовый после раздела [Ресурсы на iOS](./ios-resources#практическое-задание) -- Подключите `moko-units` -- Реализуйте логику построения списков в общем коде (только для списка репозиториев) -- iOS и Android приложения должны работать From e2454230330a753a21690c6ba22e54ee29df2133 Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:26:00 +0300 Subject: [PATCH 20/21] Update android-intro.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit изменения view -> compose --- university/1-android-basics/android-intro.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/university/1-android-basics/android-intro.md b/university/1-android-basics/android-intro.md index cb254898b..96372e267 100644 --- a/university/1-android-basics/android-intro.md +++ b/university/1-android-basics/android-intro.md @@ -15,17 +15,14 @@ sidebar_position: 0 - `Service` - `BroadcastReceiver` - `ContentProvider` -- `Fragment` -- Верстка UI используя xml layout +- Верстка UI используя Compose - Библиотеки AndroidX и Jetpack от Google -- `RecyclerView` - Kotlin flows, `StateFlow`, `SharedFlow` - `ViewModel` -- Жизненный цикл `Application`, `Activity`, `Fragment`, `ViewModel` -- `ViewBinding` +- Жизненный цикл `Application`, `Activity`, `ViewModel` - Библиотека Android Navigation Component от Google - Библиотека Retrofit от Square -- Библиотека Hilt от Google +- Библиотека Koin :::info Для тех кому всё перечисленное уже знакомо, использовано на практике и есть уверенное понимание о чем речь - можно пропустить ознакомление с теоретическим блоком и сразу перейти к [практической задаче](practice). From d03742511308368df56f0acd3ebd2c566fd019c7 Mon Sep 17 00:00:00 2001 From: "@maiow" <113892176+maiow@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:45:26 +0300 Subject: [PATCH 21/21] Update logging-and-errors.md --- university/4-icerock-basics/logging-and-errors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/university/4-icerock-basics/logging-and-errors.md b/university/4-icerock-basics/logging-and-errors.md index 6555abbbf..364c44e51 100644 --- a/university/4-icerock-basics/logging-and-errors.md +++ b/university/4-icerock-basics/logging-and-errors.md @@ -68,7 +68,7 @@ sidebar_position: 10
## Практическое задание -- Используйте проект, готовый после раздела [Построение экранов вертикальными списками](./ui-units#практическое-задание) +- Используйте проект, готовый после раздела [Ресурсы на iOS](./ios-resources#практическое-задание) - Используйте `ExceptionMappersStorage` во вьюмоделях - Вместо нескольких `catch` разных ошибок, добавьте один `catch(e: Exception)` с её маппингом через `ExceptionMappersStorage` - Добавьте в регистрацию `ExceptionMappersStorage` все необходимые типы