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 приложением, намного сложнее, но порой интереснее. Также требования к качеству конечного продукта выше - если что-то упадет, то упадет не у вас, а у вашего клиента, что прямо очень плохо.