|
| 1 | +# Dependency Injection |
| 2 | + |
| 3 | +В настоящее время на наших проектах с KMP для Dependency injection мы используем [Koin](https://github.com/InsertKoinIO/koin).<br/> |
| 4 | +[Документация Koin](https://insert-koin.io).<br/> Как именно он используется, можно посмотреть в шаблоне проектов и в [статье универа](https://kmm.icerock.dev/university/icerock-basics/di).<br/> |
| 5 | +Ранее использовался подход с фабриками, который еще можно встретить на старых проектах. Ниже вы найдете его описание. |
| 6 | + |
| 7 | +## Устаревший подход к DI на проектах с помощью SharedFactory |
| 8 | + |
| 9 | +Вся логика приложения находится в общем коде. На платформах (`iOS` и `Android`) мы просто реализуем `UI` и связываем его с логикой. |
| 10 | +В общем коде вся логика сосредоточена во вьюмоделях разных фич, поэтому, для каждого экрана от общего кода нужно получить нужную ему вьюмодель. |
| 11 | + |
| 12 | +Однако, вьюмодель - это как правило большой и сложный класс, который нуждается в настройке. |
| 13 | +Например, для создания стандартной вьюмодели ей необходимы: |
| 14 | +- строки локализации - строки, использующиеся в общем коде |
| 15 | +- репозиторий, через который идет общение с источником данных |
| 16 | +- `exceptionHandler` - объект, реализующий интерфейс [ExceptionHandler](https://github.com/icerockdev/moko-errors/blob/ece79111fb5a9451e6179ba8c5367213c117421b/errors/src/commonMain/kotlin/dev/icerock/moko/errors/handler/ExceptionHandler.kt) и помогающий обрабатывать ошибки из общего кода (о нем вы узнаете позднее из `moko-errors`) |
| 17 | +- `eventsDispatcher` - объект, служащий для отправки событий(actions) от `viewModel` на `UI` (о нем вы узнаете уже в следующем разделе) |
| 18 | + |
| 19 | +Наша цель - избавить платформу от сложности настройки вьюмоделей, чтобы не пришлось во фрагменте или вьюконтроллере получать все эти объекты, необходимые для создания вьюмодели. |
| 20 | + |
| 21 | +Решение - по максимуму оставить логику настройки вьюмоделей в общем коде, чтобы со стороны платформы можно было практически сразу получить готовую вьюмодель. |
| 22 | + |
| 23 | +### Уровень фичи |
| 24 | + |
| 25 | +Первый уровень абстракции над вьюмоделями это фабрика фичи. Она позволяет получить все вьюмодели одной фичи. Разбирать будем на примере фичи авторизации, а вьюмодель, которую мы хотим получить - вьюмодель экрана сброса пароля. |
| 26 | + |
| 27 | +Начнем с вьюмодели: |
| 28 | + |
| 29 | +`ResetPasswordViewModel.kt`: |
| 30 | +```kotlin |
| 31 | +class ResetPasswordViewModel( |
| 32 | + override val eventsDispatcher: EventsDispatcher<EventsListener>, |
| 33 | + val exceptionHandler: ExceptionHandler, |
| 34 | + private val repository: ResetPasswordRepository, |
| 35 | + private val strings: Strings |
| 36 | +) { |
| 37 | + interface Strings { |
| 38 | + val resetDescription: StringDesc |
| 39 | + } |
| 40 | +} |
| 41 | +``` |
| 42 | +Вьюмодель объявляет интерфейс `Strings` - необходимые ей строки локализации. Далее мы разберем это подробнее. |
| 43 | + |
| 44 | +Рядом с `ResetPasswordViewModel` создаем интерфейс репозитория. Сделали мы это для того, чтобы не устанавливать связь фича-модуля на модуль со строками локализации. В конструктор `ResetPasswordViewModel` принимает объект, который реализует этот интерфейс. В данном случае - кого-то, кто реализует метод для сброса пароля. |
| 45 | + |
| 46 | +`ResetPasswordRepository.kt` |
| 47 | +```kotlin |
| 48 | +interface ResetPasswordRepository { |
| 49 | + suspend fun resetPassword( |
| 50 | + phoneNumber: String, |
| 51 | + confirmCode: String |
| 52 | + ) |
| 53 | +} |
| 54 | +``` |
| 55 | +Класс репозитория фичи - `AuthRepository`, который будет реализовывать этот интерфейс разберем позднее. |
| 56 | + |
| 57 | +Теперь сделаем `AuthFactory` - класс, с помощью которого будем настраивать общие компоненты вьюмоделей фичи авторизации и создавать их. Класс фабрики также объявляется в модуле фичи. |
| 58 | + |
| 59 | +`AuthFactory.kt`: |
| 60 | +```kotlin |
| 61 | +class AuthFactory( |
| 62 | + private val createExceptionHandler: () -> ExceptionHandler, |
| 63 | + private val authRepository: AuthRepository, |
| 64 | + private val strings: Strings |
| 65 | +) { |
| 66 | + fun createResetPasswordViewModel( |
| 67 | + eventsDispatcher: EventsDispatcher<ResetPasswordViewModel.EventsListener> |
| 68 | + ) = ResetPasswordViewModel( |
| 69 | + eventsDispatcher = eventsDispatcher, |
| 70 | + exceptionHandler = createExceptionHandler(), |
| 71 | + repository = authRepository, |
| 72 | + strings = strings |
| 73 | + ) |
| 74 | + |
| 75 | + interface Strings : ResetPasswordViewModel.Strings |
| 76 | +} |
| 77 | +``` |
| 78 | +`interface Strings` фабрики реализует все интерфейсы `Strings` из других вьюмоделей. |
| 79 | + |
| 80 | +В эту фабрику мы будем добавлять методы, аналогичные `createResetPasswordViewModel` для создания других вьюмоделей, для них всех `createExceptionHandler`, `repository` и `strings` будут одинаковыми. |
| 81 | + |
| 82 | +Теперь у нас есть доступ ко всем вьюмоделям фичи авторизации - чтобы создать какую-либо вьюмодель нужно просто вызвать нужную функцию у фабрики и передать один единственный аргумент. |
| 83 | + |
| 84 | +### Уроверь mpp-library |
| 85 | + |
| 86 | +Логика работы приложения с источником данных (сервер, БД и т.д.) выносятся в классы - репозитории, в данном случае сделаем репозиторий для фичи авторизации - `AuthRepository` |
| 87 | + |
| 88 | +`AuthRepository.kt`: |
| 89 | +```kotlin |
| 90 | +internal class AuthRepository constructor( |
| 91 | + private val keyValueStorage: KeyValueStorage, |
| 92 | + private val dao: AppDao, |
| 93 | + private val coroutineScope: CoroutineScope |
| 94 | +) : ResetPasswordRepository { |
| 95 | + override fun resetPassword( |
| 96 | + phoneNumber: String, |
| 97 | + confirmCode: String |
| 98 | + ) { |
| 99 | + // TODO |
| 100 | + } |
| 101 | +} |
| 102 | +``` |
| 103 | +Этот класс реализует все интерфейсы вьюмоделей фичи авторизации для работы с источником данных. Для всех новых вьюмоделей других фичей мы будем объявлять свои интерфейсы, и реализовывать их в классе репозитория конкретной фичи, а затем прокидывать объект репозитория всем вьюмоделям. |
| 104 | + |
| 105 | +Второй уровень абстракции: фабрика фабрик - `SharedFactory`. В ней мы также создадим все фабрики, как до этого создавали вьюмодели в фабриках, настроим их, чтобы для работы с общим кодом нужно было создать только одну общую фабрику - `SharedFactory`. |
| 106 | + |
| 107 | +`SharedFactory.kt`: |
| 108 | +```kotlin |
| 109 | +class SharedFactory internal constructor( |
| 110 | + settings: Settings, |
| 111 | + antilogs: List<Antilog>, |
| 112 | + databaseDriverFactory: DatabaseDriverFactory, |
| 113 | + repositoryCoroutineScope: CoroutineScope |
| 114 | +) { |
| 115 | + // public constructor for platform side usage |
| 116 | + constructor( |
| 117 | + settings: Settings, |
| 118 | + antilog: Antilog?, |
| 119 | + databaseDriverFactory: DatabaseDriverFactory?, |
| 120 | + mpiServiceConnector: MpiServiceConnector? |
| 121 | + ) : this( |
| 122 | + settings = settings, |
| 123 | + antilogs = listOfNotNull( |
| 124 | + antilog, |
| 125 | + CrashReportingAntilog(CrashlyticsLogger()) |
| 126 | + ), |
| 127 | + databaseDriverFactory = databaseDriverFactory, |
| 128 | + mpiServiceConnector = mpiServiceConnector, |
| 129 | + repositoryCoroutineScope = CoroutineScope(Dispatchers.Main) |
| 130 | + ) |
| 131 | + |
| 132 | + internal val authRepository: AuthRepository by lazy { |
| 133 | + AuthRepository( |
| 134 | + //TODO |
| 135 | + ) |
| 136 | + } |
| 137 | + |
| 138 | + val authFactory: AuthFactory by lazy { |
| 139 | + AuthFactory( |
| 140 | + createExceptionHandler = ::createExceptionHandler, |
| 141 | + authRepository = authRepository, |
| 142 | + strings = object : AuthFactory.Strings { |
| 143 | + override val resetDescription: StringDesc = |
| 144 | + MR.strings.reset_description.desc() |
| 145 | + } |
| 146 | + ) |
| 147 | + } |
| 148 | + |
| 149 | + private fun createExceptionHandler(): ExceptionHandler = ExceptionHandler( |
| 150 | + // TODO |
| 151 | + ) |
| 152 | +} |
| 153 | +``` |
| 154 | +В `SharedFactory` мы создали оставшиеся необходимые фабрикам компоненты - `authRepository` и `createExceptionHandler`, а также установили все строки локализации, необходимые фиче. |
| 155 | +Поскольку, вьюмодель у нас пока что-то одна, объект `strings` для `AuthFactory` содержит только строки `ResetPasswordViewModel`. Если бы вьюмоделей было больше - все необходимые им строки задавались бы здесь. |
| 156 | + |
| 157 | +*** |
| 158 | +Фиче может понадобиться гораздо больше строк локализации, чем одна, и самих фич в проекте может быть очень много. Если инициализировать строки локализации каждой в фабрики фичей именно в `SharedFactory`, то класс со временем сильно разрастется и ориентироваться в нем будет сложно. |
| 159 | +Предлагаем вам использовать вспомогательные функции, расположенные рядом с `SharedFactory`, чтобы инициализировать фабрики строками именно там, а в `SharedFactory` вызывать эти функции. |
| 160 | + |
| 161 | +`AuthFactoryInit.kt`: |
| 162 | +```kotlin |
| 163 | +internal fun AuthFactory( |
| 164 | + createExceptionHandler: () -> ExceptionHandler, |
| 165 | + authRepository: AuthRepositoryInterface |
| 166 | +): AuthFactory { |
| 167 | + return AuthFactory( |
| 168 | + createExceptionHandler = createExceptionHandler, |
| 169 | + authRepository = authRepository, |
| 170 | + strings = object : AuthFactory.Strings { |
| 171 | + override val resetDescription: StringDesc = |
| 172 | + MR.strings.reset_description.desc() |
| 173 | + } |
| 174 | + ) |
| 175 | +} |
| 176 | +``` |
| 177 | +Вызов в `SharedFactory`: |
| 178 | + |
| 179 | +```kotlin |
| 180 | +val authFactory: AuthFactory by lazy { |
| 181 | + AuthFactory( |
| 182 | + createExceptionHandler = ::createExceptionHandler, |
| 183 | + authRepository = authRepository |
| 184 | + ) |
| 185 | +} |
| 186 | +``` |
| 187 | +*** |
| 188 | + |
| 189 | +### Уроверь платформы |
| 190 | + |
| 191 | +Параметры `SharedFactory` - это то, что мы не можем создать из общего кода а можем получить только с платформы. |
| 192 | + |
| 193 | + |
| 194 | +***iOS*** |
| 195 | + |
| 196 | +Класс со статической переменной - фабрикой |
| 197 | +``` |
| 198 | +class AppComponent { |
| 199 | + static var factory: SharedFactory! |
| 200 | +} |
| 201 | +``` |
| 202 | + |
| 203 | +В методе `application` класса `AppDelegate` инициализируем фабрику и прокидываем дальше в `AppCoordinator`. О нем вы узнаете уже в следующем разделе `Навигация между экранами`. |
| 204 | +``` |
| 205 | +func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { |
| 206 | + FirebaseApp.configure() |
| 207 | + MokoFirebaseCrashlytics.setup() |
| 208 | +
|
| 209 | + let antilog: Antilog? |
| 210 | + #if DEBUG |
| 211 | + antilog = DebugAntilog(defaultTag: "debug") |
| 212 | + #else |
| 213 | + antilog = nil |
| 214 | + #endif |
| 215 | +
|
| 216 | + AppComponent.factory = SharedFactory( |
| 217 | + settings: AppleSettings(delegate: UserDefaults.standard), |
| 218 | + antilog: antilog, |
| 219 | + databaseDriverFactory: SqlDatabaseDriverFactory(), |
| 220 | + ) |
| 221 | +
|
| 222 | + let window = UIWindow() |
| 223 | +
|
| 224 | + coordinator = AppCoordinator( |
| 225 | + window: window, |
| 226 | + factory: AppComponent.factory |
| 227 | + ) |
| 228 | + coordinator.start() |
| 229 | +
|
| 230 | + window.makeKeyAndVisible() |
| 231 | + self.window = window |
| 232 | +
|
| 233 | + return true |
| 234 | +} |
| 235 | +``` |
| 236 | + |
| 237 | +`AppCoordinator` прокидывает ее дальше, в дочерние координаторы, которые, в свою очередь, отправляют ее уже в контроллеры. |
| 238 | +Получение вьюмоедли в контроллере выглядит вот так: |
| 239 | + |
| 240 | +``` |
| 241 | +vc.resetPasswordViewModel = factory |
| 242 | +.authFactory |
| 243 | +.createResetPasswordViewModel(eventsDispatcher: EventsDispatcher<ResetPasswordViewModelEventsListener>(listener: vc)) |
| 244 | +``` |
| 245 | + |
| 246 | +***Android*** |
| 247 | + |
| 248 | +```kotlin |
| 249 | +val factory = SharedFactory( |
| 250 | + AndroidSettings( |
| 251 | + delegate = context.getSharedPreferences("app", MODE_PRIVATE) |
| 252 | + ), |
| 253 | + antilog = antilog, |
| 254 | + databaseDriverFactory = SqlDatabaseDriverFactory(context) |
| 255 | +) |
| 256 | + |
| 257 | +val resetPasswordViewModel = factory.authFactory.createResetPasswordViewModel( |
| 258 | + eventsDispatcher = eventsDispatcherOnMain() |
| 259 | +) |
| 260 | +``` |
| 261 | + |
| 262 | +Наконец, как добавлять новые компоненты в фичи и вьюмодели, если вдруг что-то понадобилось: |
| 263 | + - все что общее для вьюмоделей одной фичи - настраивается в фабрике |
| 264 | + - все, что общее для всех фабрик - настраивается в `SharedFactory` |
| 265 | + |
| 266 | +Таким образом, чтобы начать работу с общим кодом - нужно только создать объект `SharedFactory`, передав ему несколько параметров, доступных только на платформе. |
0 commit comments