Все мы знаем, что Dagger - бич современного общества стандарт индустрии, если это касается Dependency Injection. Все мы знаем, что Dagger хоть и является мощным фреймворком, но сборка проекта с ним занимает довольно много времени, Dagger - страшный сон для многих. А что если отказаться от него? Но в пользу чего? Koin и другие сервис локаторы - так себе идея, ведь весь injection происходит в рантайме и рано или поздно приложение из-за этого упадет. Может быть писать все руками? Именно так я подумал и решил реализовать ручной DI в своем небольшом проекте. Моя реализация не претендует на роль лучшей или даже хорошей. Мои исходники не являются идеальными и далеко не всегда следуют советам дядюшки Боба, не надо задавать вопросы по типу "А почему у тебя есть WallpaperProvider, WallpaperManager и WallpaperRepository?" Однако, я всегда открыт к улучшениям и буду рад, если вы поделитесь своими идеями по её улучшению. Концепция такова: у нас есть единый граф для всего приложения, в котором есть все зависимости приложения. Он хранится в application-классе, что дает нам единственность графа для всего приложения. (Почти) все зависимости создаются только по надобности: достигается это путем делегата lazy. Зависимости, которые нуждаются в activity(PermissionHandler), изначально имеют Noopреализацию. Они доставляются только при создании Activity. Естественно, это несет за собой и минусы: что если кто-то возьмет Noopреализацию до замены на нормальную? Это хороший (и пока не решенный) вопрос. Зависимости без начальной реализации создаются внутри activity. Так же мы прокидываем граф через CompositionLocal в compose для дальнейшего использования: Как создаются вьюмодели? Очень просто! Создается extension-функция над графом для создания вьюмодели. Это дает нам разгрузить граф от ненужных функций. Так же провайдить зависимости становится легче, когда эта функция лежит в одном файле с самой вьюмоделью: Очень элегантно, правда? Все зависимости видны в конструкторе, а их внедрение не доставляет трудностей. Как же доставить вьюмодель в ui? Здесь тоже все довольно просто. Создаем Composable функцию для предоставления зависимостей: Этот подход создает жесткую связь между ui и графом, поэтому мы разделяем экран на две функции: с вьюмоделью и без нее. В моем случае это MVI архитектура: Кроме того, при использовании такого подхода можно легко передавать параметры. Представим, что у нас есть экран с подробной информацией об обоях, и мы хотим передать хешкод выбранной обоины из списка, который хранится в репозитории. Делается это очень просто: Однако, такой подход имеет свои недостатки. При росте проекта становится все труднее поддерживать один Gradle-модуль, поэтому следует разделять фичи на отдельные модули. Исходники итерации 1: gitlab. Вот здесь все становится интереснее. Так как все фичи разделены на разные модули, мы как-то должны связать их в один граф. Для этого мы разделяем наш начальный граф на интерфейс и реализацию(в :di:api и :di:impl, например). Соответственно все фичи делятся на :api, :impl и :ui(опционально) примерно как на диаграмме: Как было сказано ранее, все фичи делятся на несколько модулей: :api - интерфейсы и extension-фунции/проперти к этим интерфейсам(чтоб жизнь слаще была) :impl - реализации интерфейсов из :api :ui - опционально, графический интерфейс фичи. Может зависеть от :di:api для внедрения зависимостей во вьюмодели. Этот модуль не зависит от :impl! В остальном все остается тем же самым. И снова минусы: при росте проекта граф может сильно разрастись, поэтому его нужно разделить на подграфы. Исходники итерации 2: gitlab. Чтобы основной граф не выглядел так страшно, можно разделить его на feature-модули: Тогда реализация модуля будет выглядеть так: Теперь мы можем менять зависимости вьюмодели с графа на соответствующий модуль: Как сделать так, чтобы можно было запустить фичу без всего графа? Убрать граф из зависимостей фичи. Здесь есть 2 стула варианта. Для каждого модуля будем добавлять CompositionLocal, который будет проброшен где-то сверху compose-дерева. Рядом с WallpapersModule добавляем соответствующий CompositionLocal: Чтобы CompositionLocals всех модулей подтянулись, нужно создать специальный провайдер для них в :di:api и вставить его в наше Activity: Чтобы вызвать эту вьюмодель, нам нужно сменить прошлый вызов новой функцией в :my_feature:ui: И создать viewModelWithReceiver в :core:api: С помощью этих изменений мы можем убрать зависимость :di:api из :my_feature:ui. Это дает нам возможность запускать фичу без создания всего графа. Опять минусы: можно забыть запровайдить какой-нибудь модуль в ProvideGraphModules, из-за чего приложение может крашнуться. Не очень благоприятный поворот событий. Исходники варианта 1: gitlab. Этот вариант гарантирует нам предоставление зависимостей.  Context receivers многим похожи extension-функции, но имеют смысловые и функциональные отличия. На примерах: Функция allLogsByTag может выполнять операцию над Logger, в то время как storeAppState может выполняться только в том скоупе, где есть Logger. Подробнее можно почитать на сайте Jetbrains На момент написания статью context receivers на стадии prototype. Пока эта функция ограничена Kotlin/JVM, а Jetbrains не рекомендуют использовать ее в продакшене: The feature is a prototype available only for Kotlin/JVM. With -Xcontext-receivers enabled, the compiler will produce pre-release binaries that cannot be used in production code. Use context receivers only in your toy projects. We appreciate your feedback in YouTrack. Если вы готовы к context receivers, то подключаем флаг в build.gradle.kts: Убираем из composable-функции вьюмодели какие-либо зависимости от графа: Добавляем контекст в функцию с вызовом вьюмодели в ui: В месте вызова(в навигации, например) добавляем блок with для добавления контекста: Так как под капотом context receivers - еще один (или несколько) параметр в функции, а модули не являются стабильными, то посмотрим, что нам выдал compose об этой функции: Видим, что composable-функция, использующая context receivers, non-skippable. Критично ли это? Нет, ведь composable-функция, принимающая state - skippable. То есть при обходе дерева compose всегда будет вызывать функцию с вьюмоделью, а функцию со стейтом - только при необходимости. Подробности про оптимизацию compose-кода: статья от Ozon Tech. Хороший вопрос. На практике я не заметил разницы, да и layout inspector не показывал ненужных рекомпозиций. Из минусов: context receivers пока нестабильны(ожидается стабилизация после прихода K2 компилятора) и работают только в JVM (то есть нет поддержки IOS  и браузера). Если Jetbrains решит изменить способ вызова, то придется адаптировать весь проект под изменения.  Как костыль альтернативное решение - самим добавлять параметры в функции. Например: Заменяем на: Исходники варианта 2: gitlab. Более перспективным вариантом, по-моему, здесь является context receivers, они дают гарантию предоставления зависимостей. Что мы имеем от этой реализации: Разрешение зависимости на этапе компиляции Уменьшенное время компиляции по сравнению с Dagger Подсветка в IDE при отсутствии каких-либо зависимостей (уменьшение feedback loop) Независимость от изменений в Dagger Про минусы не забываем: Возможное падение приложения при использовании CompositionLocal Невозможность использовать на context receivers на IOS и в браузере без костылей Легко ли было реализовать ручной DI? Довольно просто. А было ли нужно? Я оставлю этот вопрос для вас. Советую ознакомиться с альтернативными реализациями ручного DI: Youtube Статьи: раз, два Что делать в вашем проекте - решать прежде всего вам.