Single activity подходом при создании конечного приложения под Android никого не удивишь. Но мы пошли дальше и использовали No-Activity при разработке SDK. Сейчас разберемся для чего это понадобилось, возникшие сложности и как их решали.
Стандартные 3rd party SDK в Android
Как обычно работают внешние SDK в Android? Открывается Activity библиотеки, выполняется некая работа, при необходимости возвращается результат в onActivityResult.

Обычно такого подхода хватает, когда SDK выполняет целиком инкапсулированную функцию и не зависит от внешнего приложения. Но иногда необходимо обеспечить более тесное взаимодействие между приложением и SDK, например:

Получается, что экраны нашего SDK должны быть частью внешнего приложения. Точно также, как вы можете использовать, например, MapFragment от Google. Итого, при стандартном подходе, мы сталкиваемся с рядом трудностей и ограничений.
Проблемы при стандартном подходе к SDK
-
Если вам нужно несколько взаимодействий между SDK и приложением, то придется открывать-закрывать
Activityот SDK и аккуратно обрабатывать передачу данных туда-обратно. -
Сложно поддержать такой логический порядок экранов, когда элементы приложения чередуются с SDK. (Спойлер: это может понадобиться, но редко).
-
При относительно долгом возможном нахождении в SDK внешнее приложение может уйти в Lock Screen. Такое может случиться, если Lock реализован на колбеках жизненного цикла
Activity.
No-Activity подход при разработке SDK
Итак, мы решили, что основная проблема в том, что контекст (Activity) внешнего приложения и SDK разные. Отсюда следует резонное решение — отказаться от контекста SDK и во внешнее приложение поставлять только фрагменты. В таком случае разработчик сможет сам управлять стеком экранов.

Данный подход имеет как ряд плюсов, так и значительные минусы. Какие же?
Плюсы No-Activity SDK
-
Приложение и SDK имеют общий контекст, т.е. для пользователя это выглядит как абсолютно единое приложение.
-
Основное приложение имеет свой стек фрагментов, а SDK — свой через
childFragmentManager. -
Можно организовать любой порядок экранов и наложений элементов, т.к. навигация доступна и для внешнего приложения.
Минусы No-ActivitySDK
-
Внешнее приложение должно изначально работать с фрагментами, желательно вообще быть Single-Activity.
-
У SDK нет своего контекста, если хотите использовать dagger — придется исхитриться (но это все же возможно).
-
SDK может влиять на внешнее
Activityт.к.requireActivityвернет именно его. Надо полностью доверять SDK. -
Activityбудет получатьonActivityResult, и, вероятно, придется его прокидывать во фрагменты. -
Разработчику внешнего приложения сложнее интегрировать SDK, т.к. простой вызов
Activityуже не сработает.
Использование 3rd party библиотек внутри SDK
При любом подходе так или иначе придется использовать библиотеки внутри SDK. Это в свою очередь может привести к коллизии версий с внешним приложением. А части библиотек, например dagger2 нужен будет выделенный контекст.
Dagger2 внутри SDK
Для использования dagger зачастую в приложении используется класс Application. В случае с SDK так сделать не получится, потому что Application, вероятно, будет перетерт со стороны внешнего приложения.
Нужен отдельный класс, который заведомо не будет испорчен внешним приложением.
internal object ComponentHolder {
lateinit var appComponent: SdkAppComponent
private set
@Synchronized
fun init(ctx: Context) {
if (this::appComponent.isInitialized) return
appComponent = DaggerSdkAppComponent
.builder()
.sdkAppModule(SdkAppModule(ctx))
.build()
}
}
Остается только лишь понять, откуда вызвать init, да так, чтобы в процессе жизни SDK быть уверенным, что инициализация выполнилась до любой другой работы. Для этого можно использовать одну точку входа в SDK. Назовем ее EntryPointFragment. Данный фрагмент и будет виден внешнему приложению как единственная точка входа в SDK. Вся дальнейшая навигация внутри SDK будет происходить уже в нем через childFragmentManager.
Как раз при создании EntryPointFragment можно и инициализировать ComponentHolder для Dagger.
override fun onCreate(savedInstanceState: Bundle?) {
ComponentHolder.init(requireActivity())
ComponentHolder.appComponent.inject(this)
super.onCreate(savedInstanceState)
}
Итого, на выходе мы получили ComponentHolder, который можно использовать внутри SDK для инъекции нужных компонент.
Устранение коллизии в версиях
С данной проблемой столкнулись при обновлении версии okhttp3 до новой major версии 4.+. В ней добавили улучшенную поддержку Kotlin, в том числе, например, доступ к коду ошибки через code() теперь стало ошибкой. Клиенты SDK, используя либо 3, либо 4 версию должны получать ту же внутри SDK, иначе все сломается.
Это реально сделать, вынеся код с коллизиями в отдельный модуль. В нем будут 2 flavor:
flavorDimensions("okhttpVersion")
productFlavors {
v3 {
dimension = "okhttpVersion"
}
v4 {
dimension = "okhttpVersion"
}
}
dependencies {
v3Api okhttp3.core
v3Api okhttp3.logging
v4Api okhttp4.core
v4Api okhttp4.logging
}
В двух разных папках, отвечающих за каждый flavor будут одинаковые классы, один из которых будет использовать code() а другой code.
// Code in v3 folder
class ResponseWrapper(private val response: Response) {
val code : Int
get() = response.code()
}
// Code in v4 folder
class ResponseWrapper(private val response: Response) {
val code : Int
get() = response.code
}
Остается только на уровне приложения выбрать необходимый конфиг и дальше правильная версия приедет в финальный проект.
Подсказка: если вы сами используете свой же модуль в проекте и подключаете как исходники, не забудьте следующее:
defaultConfig {
...
missingDimensionStrategy 'okhttpVersion', 'v4'
}
В таком случае вы избавитесь от конфликта при сборке. Иначе просто версия не найдется.
Заключение
Разработка SDK, если сравнивать с просто Android приложением, намного сложнее, но порой интереснее. Также требования к качеству конечного продукта выше — если что-то упадет, то упадет не у вас, а у вашего клиента, что прямо очень плохо.
- Хабы: