Skip to content

Commit 7e25d41

Browse files
authored
Merge pull request #161 from maiow/maiow-patch-3
Update learning, university
2 parents 57c533e + 34b07b1 commit 7e25d41

File tree

14 files changed

+656
-297
lines changed

14 files changed

+656
-297
lines changed
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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

Comments
 (0)