Проект для демонстрации работы с межмодульной передачей зависимостей через DI-фреймворк Toothpick.
Приложение состоит из 5 модулей:
- app
- photo-picker (feature-модуль) -- фича выбора фотографии пользователя
- profile (feature-модуль) -- фича профиля пользователя
- di (core-модуль) -- общие инструменты для построения DI
- android-utils (core-модуль) -- немного вспомогательных вещей для работы с Android-фреймворком
Скоупы делятся на два типа: структурные (выделены белым на картинке) и присоединяемые (выделены красным).
-
структурные скоупы -- описывают статичные связи между классами в приложении. Не имеют жизненного цикла, могут быть открыты в любой момент и не требуют закрытия. Основное назначение - описывать связи для межмодульного взаимодействия. Структурными скоупами являются root scope (aka app scope) и feature scope фичемодулей.
-
присоединяемые скоупы -- локальные скоупы, жизненный цикл которых эквивалентен жизненному циклу фрагментов. Присоединяемые скоупы на уровне фичи имеют иерархию идентичную вложенности фрагментов, но при этом их иерархии не пересекаются за пределами фичемодулей. Рутовый присоединяемый скоуп фичемодуля открывается от структурного feature scope.
Для реализации DI в приложении используется Toothpick, но идея 1-к-1 реализуема и для других DI-фреймворков, поддерживающих ведение дерева и скоупинг DI-контейнеров.
Для того, чтобы фичемодуль мог получить снаружи нужные ему Deps
, необходимо реализовать для него DepsImpl
в app-модуле
и предоставить байндинг в app scope. Пример:
// в feature-модуле
interface ProfileDeps {
fun photoPickerFragment(profileId: String): Fragment
fun photoSelections(profileId: String): Observable<String>
}
// в app-модуле
@InjectConstructor
internal class ProfileDepsImpl(
// для реализации зависимостей feature-модуля могут понадобиться зависимости из другой фичи
// для этого инжектим feature-фасад вместо прямого инжекта Api, чтобы избежать циклических зависимостей
private val photoPicker: PhotoPickerFacade
) : ProfileDeps {
override fun photoPickerFragment(profileId: String): Fragment =
photoPicker.api.photoPickerFragment(PhotoPickerArgs((profileId)))
override fun photoSelections(profileId: String): Observable<String> =
photoPicker.api.photoSelections()
.filter { it.selectionId == profileId }
.map { it.photo.url }
}
// в app-модуле, при описании DI-байндингов:
bind<ProfileDeps>().toClass<ProfileDepsImpl>()
bind<PhotoPickerFacade>().toClass<PhotoPickerFacade>().singleton()
Здесь используется вспомогательный stateless-класс FeatureFacade
, который является точкой входа в межмодульное взаимодействие для фичи.
Этот класс обслуживает структурный Toothpick-скоуп фичи (feature scope), где хранится DI-байндинг для API фичи.
Также FeatureFacade предоставляет доступ к feature scope как извне (из app-модуля), так и внутри модуля.
Пример объявления FeatureFacade внутри фичемодуля:
class ProfileFacade : FeatureFacade<ProfileDeps, ProfileApi>(
depsClass = ProfileDeps::class.java,
apiClass = ProfileApi::class.java,
featureScopeName = "ProfileFeature",
featureScopeModule = {
Module().apply {
bind<ProfileApi>().singleton().releasable()
}
}
)
Пример использования FeatureFacade:
// Получение feature scope внутри фичи (например, для открытия скоупа фрагмента от него):
ProfileFacade().featureScope
// Получение API фичи в app-модуле (например, для реализации DepsImpl других фич):
ProfileFacade().api
Реализуется при помощи вспомогательного класса DiFragmentPlugin
, который обеспечивает автоматическую связку
DI-скоупа Toothpick и жизненного цикла фрагмента. DI-скоуп фрагмента переживает смену конфигурации.
Пример использования:
internal class ProfileFragment : Fragment(R.layout.fragment_profile) {
private val di = DiFragmentPlugin(
fragment = this,
parentScope = { ProfileFacade().featureScope },
scopeNameSuffix = { userProfile.id },
scopeModules = { arrayOf(ProfileScreenModule(userProfile)) }
)
private val viewModel by lazy { di.get<ProfileViewModel>() }
// ...
}
В скоупе фрагмента также будет заинсталлен примитив для отмены асинхронных операций (в данном приложении это Disposable
из Rx), который тоже связан с жизненным циклом фрагмента.
В данном приложении не используется ViewModel из AAC,
так как DiFragmentPlugin
уже предоставляет скоуп для переживания смены конфигурации и связанный с ним примитив для отмены асинхронных операций.
Поэтому ViewModel в данном примере реализованы без наследования от базового класса.
Для отмены подписок используется инжект CompositeDisposable
, предоставляемый скоупом фрагмента.
В данном примере используется RxJava, но аналогичным образом реактивное взаимодействие и управление жизненным циклом подписок может бысть построено на основе Flow.
В приложении нет акцента на реализации навигации, используется простая передача фрагментов между фичемодулями.
Но вместо методов fun getExternalFragment(): Fragment
фичемодули могут объявлять в своих Deps метод в стиле
fun openExternalScreen()
. Тогда app-модуль перенаправит обработку навигации в API фичемодуля,
предоставляющего контейнер для навигации, где можно использовать любую навигационную библиотеку.
KarenkovID за реализацию способа устранения цииклических зависимостей между Deps и Api.