Внедрение зависимостей
Dependency inversion - инверсия зависимостей
Введение
Как мы уже разобрали в блоке многомодульность, нам нужно разбить проект на модули и обеспечить их минимальную связанность, чтобы скорость сборки не уменьшалась с ростом проекта (особенно на iOS
)
Так как модули не знают друг про друга, но приложение целостное и оно должно использовать:
- один общий источник данных
- общие строки локализации
- общие картинки
- и т.д.
Поэтому нам нужно обеспечить передачу некоторых общих компонентов и классов во все модули. Использовать один общий модуль для таких компонентов мы не можем, это также описывалось в блоке многомодульность.
В этом случае нам подойдет вариант с обратной зависимостью:
- модули не зависят от каких-то компонент
- необходимые модулю компоненты будут предоставляться извне
- модули не знают, кто именно передаст им компоненты и как они будут реализованы
Это называется Dependency inversion - инверсия зависимостей.
Пример
Допустим, мы делаем фичу авторизации.
Для авторизации нам нужно: отправить запрос на сервер с номером телефона и кодом авторизации.
За логику работы с сетью у нас отвечает общий для всех модулей репозиторий. Поэтому в фиче авторизации объявляем интерфейс с функцией signIn
:
interface AuthRepository {
suspend fun signIn(
phoneNumber: String,
authCode: String
)
}
А в конструкторе AuthViewModel
объявляем поле, типа этого интерфейса:
class AuthViewModel(
//...
repository: AuthRepository,
//...
)
Таким образом вьюмодель как бы объявляет: мне для работы нужен кто-то, кто реализует интерфейс AuthRepository
, потому что у него есть нужный мне метод signIn
. Мне вообще не важно, кто и как будет его реализовывать.
В классе общего репозитория реализуем интерфейс AuthRepository
и при создании фичи будем передавать объект общего репозитория.
DI на проектах
Вся логика приложения находится в общем коде. На платформах (iOS
и Android
) мы просто реализуем UI
и связываем его с логикой.
В общем коде вся логика сосредоточена во вьюмоделях разных фич, поэтому для каждого экрана от общего кода нужно получить нужную ему вьюмодель.
Однако, вьюмодель - это как правило большой и сложный класс, который нуждается в настройке.
Например, для создания стандартной вьюмодели ей необходимы:
- репозиторий, через который идет общение с источником данных
exceptionHandler
- объект, реализующий интерфейс ExceptionHandler и помогающий обрабатывать ошибки из общего кода (о нем вы узнаете позднее изmoko-errors
)
Наша цель - избавить платформу от сложности настройки вьюмоделей, чтобы не пришлось во фрагменте или вьюконтроллере получать все эти объекты, необходимые для создания вьюмодели.
Решение - по максимуму оставить логику настройки вьюмоделей в общем коде, используя Koin для предоставления зависимостей, чтобы со стороны платформы можно было получить готовую вьюмодель.
Уровень фичи
Нам потребуется модуль Koin для получения всех вьюмоделей одной фичи. Разбирать будем на примере фичи авторизации, а вьюмодель, которую мы хотим получить - вьюмодель экрана сброса пароля.
Начнем с вьюмодели, которую создадим в папке presentation фичи:
ResetPasswordViewModel.kt
:
class ResetPasswordViewModel(
private val repository: ResetPasswordRepository
) {
...
}
Рядом с ResetPasswordViewModel
в папке model создаем интерфейс репозитория. В конструктор ResetPasswordViewModel
принимает объект, который реализует этот интерфейс. В данном случае - кого-то, кто реализует метод для сброса пароля.
ResetPasswordRepository.kt
:
interface ResetPasswordRepository {
suspend fun resetPassword(
phoneNumber: String,
confirmCode: String
)
}
Класс репозитория фичи - AuthRepositoryImpl
, который будет реализовывать этот интерфейс разберем позднее.
Теперь сделаем featureAuthModule
- модуль Koin, с помощью которого будем предоставлять зависимости для вьюмоделей фичи авторизации и создавать вьюмодели.
featureAuthModule.kt
:
...
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<ResetPasswordViewModel>()
}
fun Koin.createAuthPhoneViewModel: AuthPhoneViewModel {
return get<AuthPhoneViewModel>()
}
fun Koin.createAuthCodeViewModel(phone: String): AuthCodeViewModel {
return get<AuthCodeViewModel> {
parametersOf(phone)
}
}
В этот модуль мы будем добавлять методы, аналогичные createResetPasswordViewModel
для создания других вьюмоделей, для них всех repository
будет одинаковым.
Теперь у нас есть доступ ко всем вьюмоделям фичи авторизации, чтобы создать какую-либо вьюмодель нужно просто вызвать нужную функцию, где требуется, с передачей аргумента (в примере выше это номер телефона).
Уровень mpp-library
Логика работы приложения с источником данных (сервер, БД и т.д.) выносятся в классы - репозитории, в данном случае сделаем репозиторий для фичи авторизации, имплементирующий интерфейс ResetPasswordRepository - AuthRepositoryImpl
AuthRepositoryImpl.kt
:
class AuthRepositoryImpl internal constructor(
private val keyValueStorage: KeyValueStorage,
private val dao: AppDao,
private val api: AuthApi
) : ResetPasswordRepository {
override fun resetPassword(
phoneNumber: String,
confirmCode: String
) {
// TODO
}
}
Этот класс реализует все интерфейсы вьюмоделей фичи авторизации для работы с источником данных. Для всех новых вьюмоделей других фичей мы будем объявлять свои интерфейсы, и реализовывать их в классе репозитория конкретной фичи, а затем прокидывать объект репозитория всем вьюмоделям.
Также нам потребуется модуль Koin для получения всех репозиториев, в том числе для фичи авторизации.
...
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
Теперь нужно собрать все модули фичей в FeatureModule.kt
:
internal val featuresModules = module {
includes(featureAuthModule)
includes(featureProfileModule)
...
}
Теперь нужно зарегистрировать все модули Koin:
internal fun registerKoinModules(
baseUrl: String
): List<Module> = listOf(
apiModule(baseUrl = baseUrl),
featuresModules,
repositoriesModule,
...
)
Также в commonMain подготовим функцию для инициализации Koin:
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)
}
}
Уровень платформы
Параметры startDI
- это то, что мы не можем создать из общего кода, а можем получить только с платформы.
iOS
Для удобства создаем Koin.swift
:
typealias KoinApplication = Koin_coreKoinApplication
typealias Koin = Koin_coreKoin
И KoinManager.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
инициализируем Koin.
func application(
_: UIApplication,
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
FirebaseApp.configure()
#if DEBUG
Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(false)
#endif
Koin.setup()
return true
}
Получение вьюмодели в контроллере выглядит вот так:
private var resetPasswordViewModel: ResetPasswordViewModel = Koin.shared
.createResetPasswordViewModel()
Android
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
...
val antilog: LogcatAntilog? = if (BuildConfig.DEBUG) {
LogcatAntilog()
} else {
null
}
startDI(
baseUrl = BuildConfig.BASE_URL,
antilog = antilog,
exceptionLogger = CrashlyticsLogger()
) {
if (BuildConfig.DEBUG) {
androidLogger()
}
androidContext(this@MainApplication)
}
}
}
Таким образом, чтобы начать работу с общим кодом, нужно только инициализировать Koin c помощью startDI
, передав ему несколько параметров, доступных только на платформе.
Практическое задание
- Используйте проект, готовый после раздела Многомодульность
- Добавьте модули Koin для фичей и его инициализацию