Создание SDK под Android в стиле Single-Activity

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Стандартные 3rd party SDK в Android

Как обычно работают внешние SDK в Android? Открывается Activity библиотеки, выполняется некая работа, при необходимости возвращается результат в onActivityResult.

Стандартная схема работы SDK.
Стандартная схема работы SDK.

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

Желаемый стек экранов приложения и SDK
Желаемый стек экранов приложения и SDK

Получается, что экраны нашего SDK должны быть частью внешнего приложения. Точно также, как вы можете использовать, например, MapFragment от Google. Итого, при стандартном подходе, мы сталкиваемся с рядом трудностей и ограничений.

Проблемы при стандартном подходе к SDK

  • Если вам нужно несколько взаимодействий между SDK и приложением, то придется открывать-закрывать Activity от SDK и аккуратно обрабатывать передачу данных туда-обратно.

  • Сложно поддержать такой логический порядок экранов, когда элементы приложения чередуются с SDK. (Спойлер: это можно понадобится, но редко).

  • При относительно долгом возможном нахождении в SDK внешнее приложение может уйти в Lock Screen. Такое может случиться, если Lock реализован на колбеках жизненного цикла Activity.

No-Activity подход при разработке SDK

Итак, мы решили, что основная проблема в том, что контекст (Activity) внешнего приложения и SDK разные. Отсюда следует резонное решение - отказаться от контекста SDK и во внешнее приложение поставлять только фрагменты. В таком случае разработчик сможет сам управлять стеком экранов.

No-Activity SDK на Фрагментах
No-Activity 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/


Интересные статьи

Интересные статьи

Добрый день! Меня зовут Дарья, я 3d artist в студии RainStyle production. В настоящее время мы заняты разработкой игры в жанре Sci-fi horror под названием “On Air”. Игра рассказывает о заг...
С того самого момента, когда я приобрел свой первый смартфон, работающий под ОС Android, я искал приложения под эту ОС, которые бы помогли мне делать несложные рабочие расчеты «на ладошке». Об од...
[Первая часть: тайлы и поиск пути] Размещение точек создания врагов. Появление врагов и их движение по полю. Создание плавного движения с постоянной скоростью. Изменение размера, скорости...
Думаю, многие читатели хаба по android-разработке слышали, что Java позволяет в рантайме через ClassLoader модифицировать dex уже установленного приложения. С помощью этого можно в рантайме п...
Вышедшая в 2015 году Agar.io стала прародителем нового жанра игр .io, популярность которого с тех пор сильно возросла. Рост популярности игр .io я испытал на себе: за последние три года я созда...