Если оставить в стороне одноклеточные организмы, почти все в мире зависит от других существ, чтобы функционировать. Будь то что-то в природе или что-то, созданное человечеством, обычно требуется несколько вещей, чтобы создать работающий экземпляр чего-либо.
Представьте себе конвейер на автомобильном заводе. Они не создают двигатели и колеса на конвейере. Производители автомобилей передают многие детали на аутсорсинг другим компаниям. В конце концов, они привозят их все на сборочную линию, вводят каждую деталь в незавершенное производство, и появляется новая блестящая машина. Автомобиль зависит от других объектов. То же самое относится и к миру программного обеспечения.
Если бы вы моделировали Car
класс в качестве одной из его зависимостей, это был бы файл Engine
. Объект автомобиля не должен отвечать за создание двигателя. Вы должны внедрить двигатель извне в сборочную линию — или в программную номенклатуру, конструктор или инициализатор.
Преимущества внедрения зависимостей
Внедрение зависимостей или DI имеет много преимуществ.
- Ремонтопригодность : DI делает ваш код ремонтопригодным. Если ваши классы слабо связаны, вам будет легче выявлять ошибки и решать возможную проблему быстрее, чем если бы вы использовали сложный класс, не придерживающийся принципа единой ответственности.
- Повторное использование : Возвращаясь к примеру с автомобильным заводом, вы можете повторно использовать одну и ту же модель колес для многих автомобилей, которые производит завод. Слабосвязанный код позволит вам повторно использовать многие части вашего кода по-разному.
- Простота рефакторинга : в жизни вашего приложения может наступить момент, когда вам нужно будет внести изменения в кодовую базу. Чем менее связаны ваши занятия, тем проще будет процесс. Представьте, что вам нужно поменять двигатель, если вы хотите иметь новые фары!
- Тестируемость : все возвращается к слабосвязанному коду. Если каждый объект автономен, вы можете проверить его функциональность независимо от других. Никто не хотел бы машину, двигатель которой не работал бы, когда сломался стеклоочиститель! Таким образом, каждая команда, ответственная за каждый модуль, будет тестировать свой продукт и передавать его другим командам.
- Простота работы в командах : как неявно упоминалось в других пунктах, DI сделает продукт доступным для производства разными командами. Это также делает код более читабельным и понятным, поскольку он прост и не содержит ненужных дополнений.
Автоматический DI против ручного DI
Теперь, когда вы находитесь на одной волне с теми, кто предпочитает использовать внедрение зависимостей в своих приложениях, вам нужно фактически предоставить зависимости там, где это необходимо.
Откройте стартовый проект в Android Studio. Затем откройте RemindersViewModel.kt из каталога презентаций в commonMain . Вы возьмете на себя ответственность за создание repository
экземпляра снаружи RemindersViewModel
.
Удалите repository
определение и передайте его через конструктор следующим образом:
class RemindersViewModel( private val repository: RemindersRepository ) : BaseViewModel() {// ... }
Создайте проект, перейдя в меню « Сборка » и нажав «Создать проект» . Вы сразу увидите проблемы с компиляцией как в файлах RemindersView.kt на Android, так и на настольных компьютерах. Та же ошибка возникает и для RemindersView.swift , которую Android Studio не может отловить.

Рис. 9.1 — Не передано значение для репозитория
Примечание . Одной из реализаций, которая не будет отображаться в приведенном выше выводе, но ее все же необходимо обновить, является
viewModel
определение в RemindersViewModelTest.kt . ПройтиRemindersRepository()
вRemindersViewModel()
класс.
Вы должны перейти к каждому из этих файлов и предоставить экземпляр RemindersRepository
. Что делать, если repository
есть свои собственные зависимости? А что, если у этих зависимостей тоже есть свои зависимости? Это кроличья нора, в которую лучше не попасть!
Вы можете предоставить все зависимости самостоятельно, и никто не может вам в этом помешать. Неофициально iOS-разработчики обычно делают все это и пишут все шаблоны самостоятельно, поскольку не существует популярной библиотеки или методологии, с которыми все согласны.
Однако в мире Android некоторые библиотеки, решающие эту проблему, автоматизируют процесс создания и предоставления зависимостей. Они делятся на две категории:
- Статические решения, которые генерируют граф зависимостей во время компиляции.
- Решения, которые подключают зависимости во время выполнения.
Самая известная библиотека для первой категории — Dagger . Недавно Google представил Hilt , который они построили на базе Dagger. Google рекомендует Hilt как часть предложений по архитектуре приложений.
Загвоздка в том, что ни Dagger, ни Hilt не доступны для KMP. Таким образом, подход, который вы можете выбрать, — это делать DI вручную или использовать самую известную библиотеку второй категории: Koin .
Многие называют такие библиотеки, как Koin, которые разрешают зависимости во время выполнения , Service Locators . Те, кто предпочитает статические библиотеки DI, будут серьезно возражать, если вы назовете Koin библиотекой DI. Впрочем, здесь вы вольны называть это как угодно.
Настройка Коина
Настройка Koin аналогична настройке других мультиплатформенных библиотек в предыдущих главах — общая часть и некоторые специальные библиотеки для использования на каждой платформе.
Откройте build.gradle.kts для проекта и добавьте константу верхнего уровня для версии Koin. На момент написания последней версией Koin была 3.1.5.
val koinVersion by extra("3.1.5")
Затем откройте build.gradle.kts для общего модуля.
Добавьте зависимость для исходного набора commonMain следующим образом:
implementation("io.insert-koin:koin-core:${rootProject.extra["koinVersion"]}")
Пока вы здесь, добавьте также тестовую зависимость в commonTest . Он понадобится вам позже в этой главе.
implementation("io.insert-koin:koin-test:${rootProject.ext["koinVersion"]}")
Затем откройте build.gradle.kts для androidApp и добавьте эти две зависимости. Второй необходим, потому что приложение использует Jetpack Compose.
implementation("io.insert-koin:koin-android:${rootProject.ext["koinVersion"]}") implementation("io.insert-koin:koin-androidx-compose:${rootProject.ext["koinVersion"]}")
И последнее, но не менее важное: откройте build.gradle.kts для desktopApp и добавьте эту зависимость в jvmMain
:
implementation("io.insert-koin:koin-core:${rootProject.ext["koinVersion"]}")
Обязательно синхронизируйте Gradle после добавления всех этих зависимостей.
Объявление зависимостей вашего приложения для Koin
Koin использует специальный доменный язык Kotlin или DSL , чтобы вы могли описать свое приложение и его график зависимостей.
Есть три шага, чтобы начать использовать Koin:
- Объявите свои модули . Модули — это объекты, которые Koin позже внедряет в различные части вашего приложения по мере необходимости. У вас может быть столько модулей, сколько вы хотите.
- Запуск Koin : один вызов
startKoin
функции, передающий модули в вашем приложении, сделает экземпляр Koin готовым для выполнения работы по внедрению в вашем приложении. - Выполнение инъекции : использование некоторых специальных ключевых слов, предоставленных Koin, позволяет вам вводить экземпляры объектов по желанию.
Внутри общего модуля в каталоге commonMain создайте одноуровневый файл для Platform.kt с именем KoinCommon.kt . Вы собираетесь написать коды установки Koin там.
Во-первых, создайте объект, в котором вы можете хранить ссылки на модули.
object Modules { val repositories = module { factory { RemindersRepository() } } }
Определите модуль, используя module
блок. A factory
— это определение, которое будет давать вам новый экземпляр каждый раз, когда вы запрашиваете этот тип объекта. Если вы хотите иметь один экземпляр или синглтон на протяжении всего жизненного цикла вашего приложения, используйте single
ключевое слово. Он больше всего подходит для таких вещей, как базы данных и сетевые менеджеры.
Во-вторых, добавьте константу для модуля ViewModel внутри Modules
объекта.
val viewModels = module { factory { RemindersViewModel(get()) } }
Новичок в городе — это get()
функция. Это общая функция, которая разрешает зависимость компонента. Когда вы используете эту функцию, Koin просматривает предоставленное вами объявление и находит соответствующий вызов. Как вы помните, в своем конструкторе RemindersViewModel
требуется экземпляр a RemindersRepository
, и вы только что определили его как модуль.
Итак, Коин готов к работе! Имейте в виду, что Koin разрешает эту зависимость во время выполнения. Следовательно, если вы используете get()
без соответствующего объявления, ваше приложение, скорее всего, выйдет из строя.
Наконец, ниже создайте глобальную функцию Modules
, которую вы будете вызывать с каждой платформы.
fun initKoin( appModule: Module = module { }, repositoriesModule: Module = Modules.repositories, viewModelsModule: Module = Modules.viewModels, ): KoinApplication = startKoin { modules( appModule, repositoriesModule, viewModelsModule, ) }
Эта функция принимает три параметра.
Первый — это appModule
. Вы можете использовать это в последующих главах для внедрения зависимостей на уровне приложения. Поскольку эти зависимости исходят от каждой платформы, вы даете возможность передавать их извне.
Второй и третий параметры предназначены для репозиториев и моделей представления со значениями по умолчанию. Позже вы увидите, что вам нужно пройти их в определенных сценариях.
Возвращаемый тип initKoin
— это экземпляр KoinApplication
. Вы получаете экземпляр этого типа, вызывая startKoin
, передавая все определенные вами модули. Это отправная точка Koin и клей, который держит все вместе.
Использование Koin на каждой платформе
Теперь, когда у вас есть все части, пришло время использовать Koin по-настоящему.
Андроид
Откройте OrganizeApp.kt в модуле androidApp . Добавьте onCreate
функцию ниже внутри класса и запустите там Koin.
override fun onCreate() { super.onCreate() initKoin( viewModelsModule = module { viewModel { RemindersViewModel(get()) } } ) }
Вызовите initKoin
метод, который вы определили ранее. Вы используете viewModel
блок из org.koin.androidx.viewmodel.dsl.viewModel
пакета, чтобы объявить модель представления Android. Разница между моделями представления Android и другими заключается в том, что они будут учитывать изменения конфигурации Android, такие как ротация устройства. Для этого требуется особая инициализация, которую Koin для Android делает за вас.
Откройте RemindersView.kt в модуле androidApp и замените RemindersView
определение функции следующим образом:
@Composable fun RemindersView( viewModel: RemindersViewModel = getViewModel(), onAboutButtonClick: () -> Unit, ) { // ... }
Вызов getViewModel()
функции расширения, которую предоставляет Koin, выполняет весь процесс создания и внедрения.
Создайте и запустите приложение для Android. Приложение должно вести себя так, как вы привыкли, но на этот раз с DI.

Рис. 9.2 — Android-приложение после интеграции Koin.
iOS
Koin — это библиотека Kotlin. Происходит много мостов, если вы хотите использовать его с файлами и классами Swift и Objective-C. Чтобы упростить задачу, вам лучше создать несколько вспомогательных классов и функций.
Откройте KoinIOS.kt в каталоге iosMain .
Создайте функцию внутри объекта для инициализации Koin на iOS. Swift не объединяет функции Kotlin с параметрами по умолчанию. Эта функция предназначена для компенсации этого ограничения.
object KoinIOS { fun initialize(): KoinApplication = initKoin() }
Затем создайте функцию расширения на Koin для получения экземпляров определенного класса Objective-C. К сожалению, нет простого способа написать их как универсальные функции, и при вызове сайта потребуется некоторое приведение типов.
fun Koin.get(objCClass: ObjCClass): Any { val kClazz = getOriginalKotlinClass(objCClass)!! return get(kClazz, null, null) }
Здесь вы проходите null
за qualifier
и parameter
. Если вам нужно передать параметры при запросе зависимости, вы также можете добавить эту функцию расширения:
fun Koin.get(objCClass: ObjCClass, qualifier: Qualifier?, parameter: Any): Any { val kClazz = getOriginalKotlinClass(objCClass)!! return get(kClazz, qualifier) { parametersOf(parameter) } }
Примечание . На устройствах Mac с Apple Silicon вы можете обнаружить, что Android Studio не может импортировать необходимые пакеты для совместимости с Objective-C. Обязательно добавьте директивы импорта следующим образом:
import kotlinx.cinterop.ObjCClass import kotlinx.cinterop.getOriginalKotlinClass
Далее открываем Xcode и переходим в Koin.swift и пишем класс следующим образом:
«import shared
final class Koin {
//1
private var core: Koin_coreKoin?
//2
static let instance = Koin()
//3
static func start() {
if instance.core == nil {
let app = KoinIOS.shared.initialize()
instance.core = app.koin
}
if instance.core == nil {
fatalError(«Can’t initialize Koin.»)
}
}
//4
private init() {
}
//5
func get() -> T {
guard let core = core else {
fatalError(«You should call start()
before using (#function)»)
}
guard let result = core.get(objCClass: T.self) as? T else {
fatalError(«Koin can’t provide an instance of type: (T.self)»)
}
return result
}
}«
- Сохраните ссылку на основной тип Koin. Это позволит просить объекты.
- Создайте статическое свойство для вновь созданного класса, чтобы использовать его как синглтон.
- Вызовите эту функцию при запуске приложения. Здесь вы вызываете Kotlin для инициализации Koin.
KoinIOS.shared
это способ, которым Kotlin раскрываетobject
созданный вами ранее. Если по какой-либо причине эта процедура завершится неудачно, приложение выйдет из строя. - Пометьте инициализатор для этого класса как закрытый. Это предотвратит случайную инициализацию
Koin
класса Swift людьми не так, как вы предполагали. - Этот метод использует
get
методы расширения, которые вы написали для Koin в Kotlin. Сначала он проверяет,core
не является ли этоnil
. Затем он пытается привестиAny
к общему типуT
. Это сделает эту функцию безопасной для типов на месте вызова.
Осталось два шага для использования Koin на iOS.
Сначала откройте iOSApp.swift и запустите Koin во время инициализации.
@main struct iOSApp: App { init() { Koin.start() } // ... }
Наконец, откройте RemindersViewModelWrapper.swift и выполните инициализацию viewModel
следующим образом:
let viewModel: RemindersViewModel = Koin.instance.get()
Стройте и запускайте. Приложение будет работать как прежде.

Рис. 9.3 — Приложение для iOS после интеграции Koin.
Рабочий стол
Это самая простая из всех платформ. Сначала откройте Main.kt и добавьте ссылку на объект Koin . Инициализируйте его в main
функции.
`lateinit var koin: Koin
fun main() {
koin = initKoin().koin
return application { // … }
//`
Затем откройте RemindersView.kt в модуле desktopAppRemindersView
и измените определение составной функции следующим образом:
@Composable fun RemindersView( viewModel: RemindersViewModel = koin.get(), onAboutButtonClick: () -> Unit, ) { // ... }
Используйте koin
созданный вами экземпляр и воспользуйтесь get()
функцией.
Стройте и запускайте — наслаждайтесь!

Рис. 9.4 — Приложение для рабочего стола после интеграции Koin.
Обновление AboutViewModel
Теперь вы знакомы с процессом, пришло время обновиться AboutViewModel
, чтобы использовать DI. Отложите книгу и посмотрите, сможете ли вы сделать все это самостоятельно.
Вот что вы должны сделать:
Откройте AboutViewModel.kt из каталога commonMain/presentation и переместите platform
определение в конструктор следующим образом:
class AboutViewModel( platform: Platform ) : BaseViewModel() { // ... }
Затем откройте KoinCommon.kt в каталоге commonMain и добавьте factory
в модуль еще один блок viewModels
.
val viewModels = module { factory { RemindersViewModel(get()) } factory { AboutViewModel(get()) } }
Объявите Koin, как разрешить зависимость AboutViewModel
класса, который имеет тип Platform
. Создайте константу, вызываемую core
внутри Modules
объекта, для хранения этой и некоторых других зависимостей, которые появятся в следующей главе.
object Modules { val core = module { factory { Platform() } } // ... }
И последнее, но не менее важное: в этом файле обновите определение, initKoin
чтобы принять новые определенные вами модули:
fun initKoin( appModule: Module = module { }, coreModule: Module = Modules.core, repositoriesModule: Module = Modules.repositories, viewModelsModule: Module = Modules.viewModels, ): KoinApplication = startKoin { modules( appModule, coreModule, repositoriesModule, viewModelsModule, ) }
Следуя обычному подходу, вы обновите коды для платформ по порядку.
Андроид
Откройте AndroidView.kt в модуле androidAppAboutView
и измените составное определение на это:
fun AboutView( viewModel: AboutViewModel = getViewModel(), onUpButtonClick: () -> Unit ) { // ... }
Вы снова используете getViewModel()
метод из библиотеки Koin Android.
Наконец, откройте OrganizeApp.kt для приложения Android и также удалите модуль для AboutViewModel
:
initKoin( viewModelsModule = module { viewModel { RemindersViewModel(get()) } viewModel { AboutViewModel(get()) } } )
Соберите и запустите приложение, чтобы убедиться, что все работает должным образом.
iOS
Это довольно просто. Откройте AboutView.swift и измените определение viewModel
следующим образом:
@StateObject private var viewModel: AboutViewModel = Koin.instance.get()
Вот и все! Создайте и запустите приложение для iOS. Откройте страницу «О программе», чтобы убедиться, что DI работает правильно.
Рабочий стол
Это тоже кусок пирога. Откройте AboutView.kt в модуле desktopAppAboutView
и измените определение составной функции:
fun AboutView( viewModel: AboutViewModel = koin.get() ) { ContentView(items = viewModel.items) }
На этом интеграция Koin в модели представления всех трех приложений завершена.
Соберите, запустите и убедитесь, что приложение ведет себя как раньше.
Тестирование
Из-за изменений, внесенных вами в этой главе, тесты, которые вы написали в предыдущей главе, больше не будут компилироваться. К счастью, для прохождения этих тестов потребуется всего пара простых шагов. Вы также узнаете еще несколько приемов для тестирования кода.
Проверка интеграции с Koin
Как вы уже знаете, Koin разрешает граф зависимостей во время выполнения. Стоит проверить, может ли он разрешить все зависимости, предоставляющие объявленные вами модули.
В каталоге commonTest создайте файл с именем DITest.kt в качестве родственного файла PlatformTest.kt .
Создайте класс с именем DITest
и добавьте в него эту тестовую функцию:
class DITest { @Test fun testAllModules() { koinApplication { modules( Modules.viewModels, ) }.checkModules() } }
Здесь вы создаете экземпляр KoinApplication
и предоставляете список модулей. А пока добавьте viewModels
модуль. Это checkModules()
функция, которая выполняет проверку интеграции, о которой вы читали ранее. Он проверяет зависимости всех определений, запускает все модули, а затем проверяет, могут ли выполняться определения.
Запустите тест и проверьте результат.

Рис. 9.5 — Тест Koin checkModules не прошел
Тест провалился, и причина довольно очевидна. Предоставляя только viewModels
модуль, Koin не может создать экземпляр RemindersViewModel
или AboutViewModel
. Чтобы это исправить, просто добавьте Modules.repositories
и Modules.core
в список модулей в тестовой функции выше. Таким образом, модули, которые вы передаете, будут такими:
modules( Modules.core, Modules.repositories, Modules.viewModels, )
Запустите тест еще раз, и он пройдет успешно.
Примечание . Тест проходит только на ПК и iOS. На Android не получается. Причина этого кроется в процессе создания
ScreenInfo
. Если вы посмотрите на реальную реализацию этого класса в Android, вы увидите, что он вызываетResources.getSystem()
. При выполнении тестов этот вызов завершится ошибкой, так как система Android недоступна во время модульного тестирования. Для решения этой проблемы необходимо создать макетResources
класса, что выходит за рамки этой главы.
Хороший гражданин не мусорит, как и хороший разработчик при тестировании Koin. Всякий раз, когда вы создаете экземпляр KoinApplication
или вызываете startKoin
, обязательно остановите его, когда он вам больше не понадобится. Как вы знаете, хорошее место для этого — создать функцию с @AfterTest
аннотацией.
Добавьте эту функцию в DITest
класс, который использует предоставленный Koin stopKoin
метод для очистки.
@AfterTest fun tearDown() { stopKoin() }
Обновление RemindersViewModelTest
Откройте RemindersViewModelTest.kt . Есть lateinit
свойство, которое содержит ссылку на экземпляр RemindersViewModel
. В setup
методе вы инициализируете это свойство следующим образом:
viewModel = RemindersViewModel(RemindersRepository())
Фу! Это больше никому не нравится. Лучше вызвать Коина для выполнения задания!
Есть несколько шагов, которые необходимо предпринять, чтобы интегрировать Koin в тесты.
Во- первых, сделайте RemindersViewModelTest
соответствие KoinTest
. Это соответствие сделает тестовый класс KoinComponent
. Интерфейс KoinComponent
здесь, чтобы помочь вам получить экземпляры непосредственно из Koin, используя некоторые специальные ключевые слова.
Затем сделайте RemindersViewModelTest
расширение из KoinTest
и измените viewModel
определение свойства на это:
class RemindersViewModelTest : KoinTest { private val viewModel: RemindersViewModel by inject() //... }
Ключевое by
слово в Kotlin делегирует реализацию средств доступа к свойству другому объекту. Используя by inject()
, Koin будет лениво извлекать ваши экземпляры из графа зависимостей. Функция inject()
является расширением KoinTest
.
Последняя часть — это инициализация Koin перед тестом и его остановка после теста — очень похоже на то, когда вы проверяли целостность Koin.
Реализуйте эти две функции в тестовом классе:
@BeforeTest fun setup() { initKoin() } @AfterTest fun tearDown() { stopKoin() }
В setup
методе вам больше не нужно инициализировать viewModel
свойство самостоятельно. Вы просто вызываете initKoin
метод, который вызывали при запуске приложения. Он предоставляет значения по умолчанию для модулей. Функция tearDown
точно такая же, как и в DITest
классе.
Запустите тесты для RemindersViewModelTest
класса, и они пройдут, как и раньше.
Ключевые моменты
- Классы должны соблюдать принцип единой ответственности и не должны создавать свои собственные зависимости.
- Внедрение зависимостей — это необходимый шаг для создания поддерживаемой, масштабируемой и тестируемой кодовой базы.
- Вы можете внедрить зависимости в классы вручную или использовать библиотеку, которая сделает за вас весь шаблонный код.
- Koin — популярная декларативная библиотека для DI, поддерживающая Kotlin Multiplatform.
- Вы объявляете зависимости как модули, и Koin разрешает их во время выполнения, когда это необходимо.
- При тестировании кода вы можете использовать Koin для инъекций.
- Вы можете проверить, может ли Koin разрешать зависимости во время выполнения, используя
checkModules
метод. - Соответствуя
KoinTest
, ваши тестовые классы могут получать экземпляры объектов из Koin, используя некоторые операторы, такие какby inject()
.
Куда пойти отсюда?
В этой главе вы получили общее представление о Koin и внедрении зависимостей. В следующей главе вы снова вернетесь к DI, чтобы изучить новый способ внедрения в классы зависящих от платформы зависимостей.
Чтобы узнать больше о Koin, в частности, лучшим источником может быть официальная документация , которая довольно кратка и не требует пояснений, но при этом охватывает множество сценариев.
Как упоминалось в главе, существуют и другие библиотеки DI. Однако либо они не работают в мультиплатформенных сценариях, либо не так известны, как Koin.
Если вы не нацелены на многоплатформенность, вы можете обратиться к Hilt and Dagger на Android. В мире iOS нет доступной библиотеки. Тем не менее, Resolver в последнее время набирает обороты.
В частности, для тестирования Koin также предоставляет вам возможность имитировать или заглушать различные объекты.