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-Acitivty SDK
-
Приложение и SDK имеют общий контекст, т.е. для пользователя это выглядит как абсолютно единое приложение.
-
Основное приложение имеет свой стек фрагментов, а SDK — свой через
childFragmentManager. -
Можно организовать любой порядок экранов и наложений элементов, т.к. навигация доступна и для внешнего приложения.
Минусы No-Acitivty SDK
-
Внешнее приложение должно изначально работать с фрагментами, желательно вообще быть Single-Activity.
-
У SDK нет своего контекста, если хотите использовать dagger — придется исхитриться (но это все же возможно).
-
SDK может влиять на внешнее
Acitivty, т.к.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 приложением, намного сложнее, но порой интереснее. Также требования к качеству конечного продукта выше — если что-то упадет, то упадет не у вас, а у вашего клиента, что прямо очень плохо.
ссылка на оригинал статьи https://habr.com/ru/post/523918/
Добавить комментарий