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

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 13

      0

      А почему бы просто не сделать Custom View? Программист просто добавляет ваш SDKView к себе в верстку и пользуется. Это создаст меньше ограничений для использования

        0

        SDK, которое делаем — это вообще по сути отдельное помещение внутри другого приложения, со сложной логикой, своим стеком экранов и своей архитектурой. Custom view тут не отделаешься.

          0

          Я делал SDK, для ввода данных карт. Там тоже было несколько экранов: ввод данных карты, 3DS и некоторые другие. И это все замечательно лежало внутри одной View. Да, мне пришлось написать свой велосипед для стека экранов, но это, на мой взгляд, стоило того.

            0

            В целом я примерно представляю, но кажется вы заново изобрели фрагменты, но зачем)?

              0

              В те времена моя любовь к велосипедостроению не сдерживалась здравым смыслом. И я сделал свои фрагменты ещё до этого SDK, ну и использовать CustomView все таки удобнее, чем фрагмент. Да и вообще, так инкапсуляция куда более ярко выражена получается.

                0

                Все таки наверное я слабо представляю реализацию если вы говорите что это лучше фрагментов и понятнее.

                  0

                  Ну снаружи это просто View, добавляется в верстку, а потом уже в коде добавляется колбэк на добавление карты

                    0
                    В случае с одной view карты, или же пары через viewSwitcher — делал бы также. Прямо полноценное приложение на singleView — видел есть такие наработки, но я что-то таком подходу слабо доверяю, очень новый велосипед.
                      0

                      Я делал приложение с несколькими десятками экранов на самодельном стеке с версткой в коде. Да, это нестандартно, но если бы меня спросили, какой мне подход ближе, я бы сказал, что этот. Но проблема в том, что после меня это сложно будет поддерживать, так что приходится сдерживать свои велосипедные порывы и пользоваться стандартным инструментарием.

        0

        Будет ли подход с dimensions работать, если SDK распространяется как .arr или придётся делать свой .aar для каждого из dimension?

          0

          В рамках сборки sdk будут собираться все необходимые модули, т.е будут разные артефакты, что кажется логично. Первые тесты показали что вроде работает, будем тестировать дальше.

          0
          Activity будет получать onActivityResult, и, вероятно, придется его прокидывать во фрагменты.

          Но фрагменты же по-умолчанию получают вызов своего собственного onActivityResult, который можно только сломать, переопределив его в активити и не вызвав super.onActivityResult, разве нет? Так что из минусов можно вычеркнуть
            0
            И да и нет. Они получат их ЕСЛИ только будет вызвано startActivityForResult на фрагменте.
            Я встречал либы (которые нужно внедрить в конкретно моем случае), которые поддерживают запуск только на Acitivity.

            Т.е. в некоторых случаях есть шанс, что при вызове из Fragment придется использовать requireActivity.start… и получать ответ в Activity. Если нужен ответ во фрагменте, придется докинуть.

          Only users with full accounts can post comments. Log in, please.