Русский
Русский
English
Статистика
Реклама

Gradle

Конфигурация многомодульных проектов

26.08.2020 14:06:49 | Автор: admin

Предыстория


Иногда, когда я прокрастинирую, я занимаюсь уборкой: чищу стол, раскладываю вещи, прибираюсь в комнате. По сути, привожу окружающую среду в порядок это заряжает энергией и настраивает на рабочий лад. С программированием у меня та же ситуация, только я чищу проект: провожу рефакторинги, делаю различные инструменты и всячески стараюсь упростить жизнь себе и коллегам.

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

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



Первая итерация вынос версий библиотек


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

Подход заключается в том, что надо выносить версии библиотек в отдельные глобальные свойства проекта, тогда они становятся доступны по всему проекту, что помогает использовать их многократно. Обычно это делается в файле build.gradle на уровне проекта, но иногда эти переменные выносят в отдельный .gradle файл и подключают в основном build.gradle.

Скорее всего, вы уже видели такой код в проекте. В нем нет никакой магии, это просто одно из расширений Gradle под названием ExtraPropertiesExtension. Если кратко, то это просто Map<String, Object>, доступный по имени ext в объектe project, а все остальное работа как будто с объектом, блоки конфигурации и прочее магия Gradle. Примеры:
.gradle .gradle.kts
// creationext {  dagger = '2.25.3'  fabric = '1.25.4'  mindk = 17}// usageprintln(dagger)println(fabric)println(mindk)

// creationval dagger by extra { "2.25.3" }val fabric by extra { "1.25.4" }val minSdk by extra { 17 }// usageval dagger: String by extra.propertiesval fabric: String by extra.propertiesval minSdk: Int by extra.properties


Что мне нравится в этом подходе: он крайне простой и помогает версиям не разъезжаться. Но у него есть минусы: надо следить, чтобы разработчики использовали версии из этого набора, и это не сильно упрощает создание новых модулей, потому что всё равно приходится копировать еще много чего.

Кстати, подобного эффекта можно добиться, используя gradle.properties вместо ExtraPropertiesExtension, только будьте осторожны: ваши версии можно будет переопределить при сборке с помощью -P флагов, а если вы обращаетесь к переменной просто по имени в groovy-cкриптах, то gradle.properties заменят и их. Пример с gradle.properties и переопределением:

// grdle.propertiesoverriden=2// build.gradleext.dagger = 1ext.overriden = 1// module/build.gradleprintln(rootProject.ext.dagger)   // 1println(dagger)                   // 1println(rootProject.ext.overriden)// 1println(overriden)                // 2

Вторая итерация project.subprojects


Моя любознательность, помноноженная на нежелание копировать код и разбираться с настройкой каждого модуля, привела меня к следующему шагу: я вспомнил, что в корневом build.gradle есть блок, который генерируется по умолчанию allprojects.

allprojects {    repositories {        google()        jcenter()    }}

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

Пример конфигурации модулей через project.subprojects
subprojects { project ->    afterEvaluate {        final boolean isAndroidProject =            (project.pluginManager.hasPlugin('com.android.application') ||                project.pluginManager.hasPlugin('com.android.library'))        if (isAndroidProject) {            apply plugin: 'kotlin-android'            apply plugin: 'kotlin-android-extensions'            apply plugin: 'kotlin-kapt'                        android {                compileSdkVersion rootProject.ext.compileSdkVersion                                defaultConfig {                    minSdkVersion rootProject.ext.minSdkVersion                    targetSdkVersion rootProject.ext.targetSdkVersion                                        vectorDrawables.useSupportLibrary = true                }                compileOptions {                    encoding 'UTF-8'                    sourceCompatibility JavaVersion.VERSION_1_8                    targetCompatibility JavaVersion.VERSION_1_8                }                androidExtensions {                    experimental = true                }            }        }        dependencies {            if (isAndroidProject) {                // android dependencies here            }                        // all subprojects dependencies here        }        project.tasks            .withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile)            .all {                kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()            }    }}


Теперь для любого модуля с подключенным плагином com.android.application или com.android.library мы можем настраивать что угодно: подключаемые плагины, конфигурации плагинов, зависимости.

Все было бы отлично, если бы не пара проблем: если мы захотим в модуле переопределить какие-то параметры, заданные в subprojects, то у нас это не получится, потому что конфигурация модуля происходит до применения subprojects (спасибо afterEvaluate). А еще если мы захотим не применять это автоматическое конфигурирование в отдельных модулях, то в блоке subprojects начнет появляться много дополнительных проверок. Поэтому я стал думать дальше.

Третья итерация buildSrc и plugin


До этого момента я уже несколько раз слышал про buildSrc и видел примеры, в которых buildSrc использовали как альтернативу первому шагу из этой статьи. А еще я слышал про gradle pluginы, поэтому стал копать в этом направлении. Все оказалось очень просто: у Gradle есть документация по разработке кастомных плагинов, в которой все написано.

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

Код плагина
import org.gradle.api.JavaVersionimport org.gradle.api.Pluginimport org.gradle.api.Projectclass ModulePlugin implements Plugin<Project> {    @Override    void apply(Project target) {        target.pluginManager.apply("com.android.library")        target.pluginManager.apply("kotlin-android")        target.pluginManager.apply("kotlin-android-extensions")        target.pluginManager.apply("kotlin-kapt")        target.android {            compileSdkVersion Versions.sdk.compile            defaultConfig {                minSdkVersion Versions.sdk.min                targetSdkVersion Versions.sdk.target                javaCompileOptions {                    annotationProcessorOptions {                        arguments << ["dagger.gradle.incremental": "true"]                    }                }            }            // resources prefix: modulename_            resourcePrefix "${target.name.replace("-", "_")}_"            lintOptions {                baseline "lint-baseline.xml"            }            compileOptions {                encoding 'UTF-8'                sourceCompatibility JavaVersion.VERSION_1_8                targetCompatibility JavaVersion.VERSION_1_8            }            testOptions {                unitTests {                    returnDefaultValues true                    includeAndroidResources true                }            }        }        target.repositories {            google()            mavenCentral()            jcenter()                        // add other repositories here        }        target.dependencies {            implementation Dependencies.dagger.dagger            implementation Dependencies.dagger.android            kapt Dependencies.dagger.compiler            kapt Dependencies.dagger.androidProcessor            testImplementation Dependencies.test.junit                        // add other dependencies here        }    }}


Теперь конфигурация нового проекта выглядит как applyplugin:ru.yandex.money.module и все. Можно вносить свои дополнения в блок android или dependencies, можно добавлять плагины или настраивать их, но главное, что новый модуль конфигурируется одной строкой, а его конфигурация всегда актуальна и продуктовому разработчику больше не надо думать про настройку.

Из минусов я бы отметил то, что для этого решения нужны дополнительное время и изучение материала, но, с моей точки зрения, оно того стоит. Если вы захотите в будущем выносить плагин, как отдельный проект, то я бы не рекомендовал настраивать зависимости между модулями в плагине.

Важный момент: если вы используете android gradle plugin ниже 4.0, то некоторые вещи очень сложно сделать в kotlin-скриптах по крайней мере, блок android проще конфигурировать в groovy-скриптах. Там есть проблема с тем, что некоторые типы недоступны при компиляции, а groovy динамически типизированный, и ему это не важно =)

Дальше standalone plugin или монорепо


Конечно же, третий шаг это еще не всё. Нет предела совершенству, поэтому есть варианты, куда двигаться дальше.

Первый вариант standalone plugin для gradle. После третьего шага это уже не так сложно: надо создать отдельный проект, перенести туда код и настроить публикацию.

Плюсы: плагин можно шарить между несколькими проектами, что упростит жизнь не в одном проекте, а в экосистеме.

Минусы: версионирование при обновлении плагина придется обновлять и проверять его работоспособность в нескольких проектах сразу, а это может занять время. Кстати, на эту тему у моих коллег из бэкенд-разработки есть отличное решение, ключевое слово modernizer инструмент, который сам ходит по репозиториям и обновляет зависимости. Не буду на этом долго задерживаться, пусть лучше они сами расскажут.

Монорепо это звучит громко, но у меня нет опыта работы с ним, а есть только соображения, что один проект, вроде buildSrc, можно использовать сразу в нескольких других проектах, и это могло бы помочь решить вопрос с версионированием. Если вдруг у тебя есть опыт работы с монорепо, то поделись в комментариях, чтобы я и другие читатели могли что-то узнать про это.

Итого


В новом проекте делай сразу третий шаг buildSrc и plugin проще будет всем, тем более, что код я приложил. А второй шаг project.subprojects используй для того, чтобы подключать общие модули между собой.

Если у тебя есть что добавить или возразить, пиши в комментарии или ищи меня в соцсетях.
Подробнее..

Знакомство с App Gallery. Создаем аккаунт разработчика

22.09.2020 18:18:39 | Автор: admin


Что происходит, кто виноват и что делать


Недавно Google прекратил сотрудничество с Huawei. Это привело к тому, что Huawei на своих новых девайсах уже не может использовать сервисы Google (магазин приложений, геолокация, карты, пуши, аналитика etc), что для пользователя превращает девайс в кирпич. Если бы это не была китайская компания, то, скорее всего, на этом её бизнес, связанный с Android, просто бы прекратился. Но компания китайская, большая и они пошли по пути импортозамещения, в кратчайшие сроки реализовав функционал, аналогичный Google сервисам.


В этой серии статей мы хотим поделиться своим опытом использования Huawei Mobile Services в уже готовом приложении, использующем Google Mobile Services для аналитики (Firebase Analytics), карт и геолокации. Текста получилось довольно много и о сильно разных сервисах, засим статей будет несколько. Начнём мы с основ регистрации аккаунта разработчика и базовых вещей в коде.


  1. Создаём аккаунт разработчика, подключаем зависимости, подготавливаем код к внедрению. вы тут
  2. Встраиваем Huawei Analytics.
  3. Используем геолокацию от Huawei.
  4. Huawei maps. Используем вместо Google maps для AppGallery.

Если вам интересно узнать технических подробностей, посмотреть код и узнать про подводные камни добро пожаловать под кат!


Что нужно для успешного внедрения


Всё было бы просто, если бы приложение писалось с нуля и не нужно было бы поддерживать как Google так и Huawei. Но мы живём в реальном мире и без сложностей не обойтись. Однако дело сильно упростится, если соблюдён ряд условий.


Но перед перечислением условий надо составить ТЗ. Оно у нас получилось такое:


  1. Нам нужно получить 2 версии APK одну для Google Play, с библиотеками от Google, другую для AppGallery, с библиотеками от Huawei.
  2. В приложении уже используется Firebase Analytics. Надо его заменить на аналог от Huawei.
  3. Есть определение местоположения пользователя. Аналогично заменяем на аналог.
  4. Есть карты. Нужно также заменить на аналог, по максимуму сохранив функционал, т.к. в реализации от Huawei некоторые вещи ещё не сделаны.

А вот что сильно сократит прикладываемые усилия:


  1. Код должен быть написан хорошо. И быть без багов (хотя это само собой разумеется зачем код с багами писать?). Под хорошо будем подразумевать более-менее стандартную архитектуру, мимикрирующую под Clean.
  2. Если код из Google библиотек размазан ровным слоем по всему проекту, то у меня для вас плохие новости. Например у вас может не быть абстракции над аналитикой и/или над полученными от Google координатами. В этом случае придётся её завести, чтобы почистить код от импортов гугловых классов, которые будут недоступны, когда мы уберём их из сборки.
  3. Использование DI. Очень упрощает абстрагирование над аналитикой и геолокацией. Используем интерфейсы, через DI передавая нужную реализацию.
  4. Карты не слишком сильно кастомизированы. В частности, основная сложность будет с абстрагированием над кластеризацией маркеров.

Подготовка к внедрению


Как и в случае с Google, надо зарегистрироваться, создать проект приложения, получить файл конфигурации.


  1. Регистрируемся на https://developer.huawei.com. Тут понадобится паспорт/права + пластиковая карта. День-два вас будут проверять, потом аккаунт заработает. Если вдруг что-то пойдёт не так (забудете что-то указать или укажете неправильно) вам напишут и подробно объяснят. После общения с Google Play всё выглядит очень круто русскоязычная техподдержка отвечает быстро и по делу.
  2. Принимаем всякие соглашения об обработке персональных данных. Внимательно читая, конечно же)
  3. Создаём проект приложения, указывая пакет (он же ApplicationId).
  4. Если вам нужно ещё и встроенные покупки реализовать то надо: а) Заполнить данные банковского счёта б) Распечатать и заполнить заявление о трансграничной передаче персональных данных в КНР в) Отправить скан оного вместе с данными из пункта а г) Отправить заявление из пункта б по почте в Москву. Когда заявление дойдёт вам придёт e-mail и останется только активировать сервис в настройках проекта. На почте бывают накладки возможно, придётся подождать. Я пару недель ждал, потом позвонил ответственному за это в Huawei уверили, что проблему решат. И решили. На русском тоже всё общение очень круто)
  5. Включаем сервис аналитики. В отличие от геолокации и карт, включённых по умолчанию, это нужно сделать вручную.
  6. Добавляем SHA-256 для всех ключей, которыми будет подписано приложение. Т.е. дебажные ключи и релизный ключ.
  7. Скачиваем аналог google-services.json, в случае Huawei называемый agconnect-services.json
  8. Создаём разные flavors для Google и Huawei. Наконец-то можно перейти к коду:

В build.gradle (module app) создаём flavors и указываем, что в папках src/google/kotlin, src/google/res, src/huawei/kotlin, src/huawei/res также находиться будет наш код.


android {  ...  sourceSets {      google.java.srcDirs += 'src/google/kotlin'      google.res.srcDirs += 'src/google/res'      huawei.java.srcDirs += 'src/huawei/kotlin'      huawei.res.srcDirs += 'src/huawei/res'  }  flavorDimensions "store"  productFlavors {      google {          dimension "store"      }      huawei {          dimension "store"      }  }}

Также создаём папки src/huaweiDebug и src/huaweiRelease. В них помещаем наш файл конфигурации agconnect-services.json


И добавляем apply plugin: 'com.huawei.agconnect' в конец build.gradle (module app).


И наконец, добавляем в build.gradle проекта:


buildscript {    ...    repositories {        ...        maven {url 'https://developer.huawei.com/repo/'}    }    dependencies {        ...        classpath 'com.huawei.agconnect:agcp:1.2.1.301'    }}allprojects {    repositories {        ...        maven {url 'https://developer.huawei.com/repo/'}    }}

В следующей части встраиваем аналитику


Теперь мы полностью готовы. У нас есть 2 разных варианта сборки для Huawei и Google. У нас подключены необходимые зависимости. Созданы папки, где будет наш код. Создан аккаунт разработчика и выполнены необходимые действия по созданию проекта приложения. У нас даже какое-то ТЗ есть. И мы уже выполнили первый пункт из ТЗ! Отличный повод на этом статью закончить. И уже в следующей встроить аналитику не от Google, а от Huawei.


Весь код, который есть в этом цикле статей вы можете посмотреть в репозитории на GitHub. Вот ссылка.

Подробнее..

Встраиваем аналитику от Huawei в Android приложение

06.10.2020 18:23:53 | Автор: admin

image


В прошлой статье мы создавали аккаунт разработчика для использования Huawei Mobile Services и подготавливали проект к их использованию. Теперь пора приступить к встраиванию конкретных сервисов.


Вот полный список статей из цикла:


  1. Создаём аккаунт разработчика, подключаем зависимости, подготавливаем код к внедрению. тык
  2. Встраиваем Huawei Analytics. вы тут
  3. Используем геолокацию от Huawei.
  4. Huawei maps. Используем вместо Google maps для AppGallery.

Как должен выглядеть код в уже готовом проекте


Начнём с аналитики. И исходить будем из того, что она у вас спрятана за примерно такой абстракцией:


interface Analytics {    fun send(event: AnalyticsEvent)}interface AnalyticsEvent {    val key: String    val data: Map<String, Any>}fun Map<String, Any>.toBundle() =    Bundle().apply {        forEach { (key, value) ->            when (value) {                is String -> putString(key, value)                is Int -> putInt(key, value)                is Boolean -> putBoolean(key, value)                is Double -> putDouble(key, value)                is Float -> putFloat(key, value)                else -> throw IllegalArgumentException("Unknown data type: ${value::class.simpleName}")            }        }    }open class SimpleEvent(override val key: String) : AnalyticsEvent {    override val data: Map<String, Any> = hashMapOf()    override fun toString(): String = "AnalyticsEvent { key = $key, data = $data }"}open class ParamsEvent(key: String, vararg params: Pair<String, Any>): SimpleEvent(key) {    override val data = params.toMap()}class EventOpenSomeScreen : SimpleEvent("screen_some_screen")

Соответственно, где требуется какое-то событие отправить, вы делаете что-то такое:


@Injectlateinit var analytics: Analytics...analytics.send(EventOpenSomeScreen())

Используем разные реализации аналитики


Если всё вышеописанное верно, то подставлять разные реализации аналитики в разных сборках проще простого.


  1. Указываем, что для huawei flavor-а мы используем одну библиотеку, а для google другую:

dependencies {  huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.3.1.300'  huaweiImplementation 'com.huawei.hms:hianalytics:5.0.0.301'  googleImplementation 'com.google.firebase:firebase-analytics:17.2.3'}

  1. В DI биндим для типа Analytics экземпляр класса AnalyticsImpl. Сам же AnalyticsImpl у нас будет в двух вариантах. Один в папке src/huawei/kotlin/com/example и выглядеть так:

class AnalyticsImpl(context: Context) : Analytics {    private val analytics = HiAnalytics.getInstance(context)    override fun send(event: AnalyticsEvent) {        analytics.onEvent(event.key, event.data.toBundle())    }}

Другой в папке src/google/kotlin/com/example:


class AnalyticsImpl(context: Context) : Analytics {  private val firebaseAnalytics = FirebaseAnalytics.getInstance(context)  override fun send(event: AnalyticsEvent) {      firebaseAnalytics.logEvent(event.key, event.data.toBundle())  }}

Вот собственно и всё с аналитикой. API библиотек очень похожи и никаких проблем не возникает.


Проверяем, что всё работает


Также, очень удобно можно проверить, что Huawei аналитика работает. Для этого надо:


  1. Подсоединить девайс к компьютеру.
  2. Выполнить в консоли adb shell setprop debug.huawei.hms.analytics.app ТУТ_APPLICATION_ID_ВАШЕГО_ПРИЛОЖЕНИЯ
  3. Открыть консоль разработчика в браузере, перейти в AppGallery Connect -> Мои приложения -> Выбрать приложение -> Раздел "Разработка" -> Управление -> Отладка приложения.
  4. Теперь отправленные из приложения события вы будете видеть в реальном времени прямо на сайте.
  5. Чтобы отключить режим отладки выполните adb shell setprop debug.huawei.hms.analytics.app .none.

Вот так режим отладки выглядит в браузере:


image


Дальше геолокация


С аналитикой мы разобрались, в следующей статье покажем как встроить определение геолокации от Huawei в приложение, которое уже использует аналог от Google.


Весь код, который есть в этом цикле статей вы можете посмотреть в репозитории на GitHub. Вот ссылка.

Подробнее..

Встраиваем геолокацию от Huawei в Android приложение

14.10.2020 16:16:12 | Автор: admin

image


В предыдущих статьях мы создавали аккаунт разработчика для использования Huawei Mobile Services и подготавливали проект к их использованию. И использовали аналитику от Huawei вместо аналога от Google. В этой статье мы будем встраивать определение геолокации от Huawei.


Вот полный список статей из цикла:


  1. Создаём аккаунт разработчика, подключаем зависимости, подготавливаем код к внедрению. тык
  2. Встраиваем Huawei Analytics. тык
  3. Используем геолокацию от Huawei. вы тут
  4. Huawei maps. Используем вместо Google maps для AppGallery.

С геолокацией немного сложнее, т.к. надо нюансы учитывать. О них, конечно, тоже расскажем.


Как должен выглядеть код в уже готовом проекте


Исходить будем, опять таки, из того, что у вас геолокация для гугла сделана примерно так:


1) Для проверки разрешения пользователя на доступ к его местоположению использована библиотека RxPermissions примерно так:


class PermissionsHelper {    private var rxPermissions: RxPermissions? = null    /**     * Вызываем в Activity#onCreate     */    fun attach(activity: FragmentActivity) {        rxPermissions = RxPermissions(activity)    }    /**     * Вызываем в Activity#onDestroy     */    fun detach() {        rxPermissions = null    }    fun requestPermission(vararg permissionName: String): Single<Boolean> {        return rxPermissions?.request(*permissionName)            ?.firstOrError()            ?: Single.error(                IllegalStateException("PermissionHelper is not attached to Activity")            )    }}

2) Создан свой класс для местоположения:


data class Location(    val latitude: Double,    val longitude: Double) {    companion object {        val DEFAULT_LOCATION = Location(59.927752, 30.346944)    }}

3) Создана абстракция над поставщиком местоположения:


interface FusedLocationClient {    fun checkPermissions(): Single<Boolean>    fun getLastLocation(): Single<Location>    fun requestLastLocation(): Single<Location>}

4) И используется она примерно так:


class LocationGateway(    private val fusedLocationClient: FusedLocationClient) {    fun requestLastLocation(): Single<Location> {        return fusedLocationClient.checkPermissions()            .flatMap { granted ->                if (granted) {                    fusedLocationClient.getLastLocation()                        .onErrorResumeNext(fusedLocationClient.requestLastLocation())                } else {                    Single.just(Location.DEFAULT_LOCATION) // или ошибку кидаем какую-то                }            }    }}

Используем разные реализации определения геолокации


Если вышеописанное верно для вашего случая, то как и в случае с аналитикой нам понадобятся 2 разные реализации FusedLocationClient FusedLocationClientImpl:


1) В папке src/huawei/kotlin/com/example:


class FusedLocationClientImpl(    private val permissionsHelper: PermissionsHelper,    context: Context) : FusedLocationClient {    private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)    override fun checkPermissions(): Single<Boolean> {        val permissions = mutableListOf(Manifest.permission.ACCESS_FINE_LOCATION)        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {            // for huawei we need this permission too after API=28            permissions += Manifest.permission.ACCESS_BACKGROUND_LOCATION        }        return permissionsHelper.requestPermission(*permissions.toTypedArray())    }    override fun getLastLocation(): Single<Location> {        return Single.create { singleEmitter ->            fusedLocationClient.lastLocation                .addOnFailureListener {                    if (singleEmitter.isDisposed) return@addOnFailureListener                    singleEmitter.onError(it)                }                .addOnSuccessListener { newLocation ->                    if (singleEmitter.isDisposed) return@addOnSuccessListener                    if (newLocation == null) {                        singleEmitter.onError(UnknownLocationException())                    } else {                        singleEmitter.onSuccess(                            Location(                                newLocation.latitude,                                newLocation.longitude                            )                        )                    }                }        }    }    override fun requestLastLocation(): Single<Location> {        return Single.create { singleEmitter ->            val locationRequest = LocationRequest.create()                .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)                .setInterval(5000)                .setSmallestDisplacement(5.5F)                .setNumUpdates(1)            val callback = object : LocationCallback() {                override fun onLocationResult(result: LocationResult) {                    if (singleEmitter.isDisposed) return                    singleEmitter.onSuccess(                        Location(                            result.lastLocation.latitude,                            result.lastLocation.longitude                        )                    )                }            }            fusedLocationClient.requestLocationUpdates(locationRequest, callback, null)            singleEmitter.setCancellable {                fusedLocationClient.removeLocationUpdates(callback)            }        }    }}

2) В папке src/google/kotlin/com/example:


class FusedLocationClientImpl(    private val permissionsHelper: PermissionsHelper,    context: Context) : FusedLocationClient {    private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)    override fun checkPermissions(): Single<Boolean> {        return permissionsHelper.requestPermission(Manifest.permission.ACCESS_FINE_LOCATION)    }    @SuppressLint("MissingPermission")    override fun getLastLocation(): Single<Location> {        return Single.create { singleEmitter ->            fusedLocationClient.lastLocation                .addOnFailureListener {                    if (singleEmitter.isDisposed) return@addOnFailureListener                    singleEmitter.onError(it)                }                .addOnSuccessListener { newLocation ->                    if (singleEmitter.isDisposed) return@addOnSuccessListener                    if (newLocation == null) {                        singleEmitter.onError(UnknownLocationException())                    } else {                        singleEmitter.onSuccess(                            Location(                                newLocation.latitude,                                newLocation.longitude                            )                        )                    }                }        }    }    @SuppressLint("MissingPermission")    override fun requestLastLocation(): Single<Location> {        return Single.create { singleEmitter ->            val locationRequest = LocationRequest.create()                .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)                .setInterval(5000)                .setSmallestDisplacement(5.5F)                .setNumUpdates(1)            val callback = object : LocationCallback() {                override fun onLocationResult(result: LocationResult) {                    if (singleEmitter.isDisposed) return                    singleEmitter.onSuccess(                        Location(                            result.lastLocation.latitude,                            result.lastLocation.longitude                        )                    )                }            }            fusedLocationClient.requestLocationUpdates(locationRequest, callback, null)            singleEmitter.setCancellable {                fusedLocationClient.removeLocationUpdates(callback)            }        }    }}

В итоге реализации отличаются 2 вещами: импортами и тем, что в случае Huawei надо запрашивать разрешение на запрос геолокации в фоне на API>28.


Аналогично с аналитикой, в DI биндим для типа FusedLocationClient экземпляр FusedLocationClientImpl. Для разных сборок будет взята та или иная реализация.
Ну и не забываем, конечно, зависимости в скрипте сборки прописать:


dependencies {  huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.3.1.300'  huaweiImplementation 'com.huawei.hms:location:5.0.0.301'  googleImplementation 'com.google.android.gms:play-services-location:17.0.0'}

И не забудьте добавить разрешение на доступ к местоположению в фоне для Huawei сборки! Если такое разрешение уже есть в основном файле AndroidManifest.xml то можете этот пункт пропустить. Если нет то создайте ещё один файл манифеста в папке src/huawei/ с таким содержимым:


<manifest xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    package="com.example">    <!-- huawei location throws error without this permission -->    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /></manifest>

Подводные камни


Надо иметь в виду, что геолокация от Huawei будет работать при следующих условиях:


  1. У вас установлены Huawei Mobile Services на девайсе.
  2. Им выданы нужные разрешения.
  3. Юзер согласился на определение местоположения в фоне, а не только во время использования.

Дальше встраиваем карты


С геолокацией мы разобрались, в следующей статье покажем как встроить карты от Huawei в приложение, которое уже использует аналог от Google.


Весь код, который есть в этом цикле статей вы можете посмотреть в репозитории на GitHub. Вот ссылка: https://github.com/MobileUpLLC/huawei_and_google_services.

Подробнее..

Встраиваем карты от Huawei в Android приложение

16.10.2020 12:13:24 | Автор: admin

image


В предыдущих статьях мы создавали аккаунт разработчика для использования Huawei Mobile Services и подготавливали проект к их использованию. Потом использовали аналитику от Huawei вместо аналога от Google. Также поступили и с определением геолокации. В этой же статье мы будем использовать карты от Huawei вместо карт от Google.


Вот полный список статей из цикла:


  1. Создаём аккаунт разработчика, подключаем зависимости, подготавливаем код к внедрению. тык
  2. Встраиваем Huawei Analytics. тык
  3. Используем геолокацию от Huawei. тык
  4. Huawei maps. Используем вместо Google maps для AppGallery. вы тут

В чём сложность


К сожалению, с картами не получится так просто, как было с аналитикой и геолокацией. Что и неудивительно, т.к. это гораздо более сложная система сама по себе. И очень часто в приложениях карты и взаимодействие с ними кастомизируется. Например, отображают маркеры, кластеризуют их. Поэтому кода будет много, т.к. надо всё это заабстрагировать, имея в виду некоторые отличия в API карт разных реализаций.


Создаём абстракцию над картой


Надо в разметке использовать разные классы для отображения карты. com.google.android.libraries.maps.MapView для гугло-карт и com.huawei.hms.maps.MapView для Huawei. Сделаем так: создадим собственную абстрактную вьюху, унаследовавшись от FrameLayout и в неё будет загружать конкретную реализацию MapView в разных flavors. Также создадим в нашей абстрактной вьюхе все нужные методы, которые мы должны вызывать на конкретных реализациях. И ещё метод для получения объекта самой карты. И методы для непосредственного внедрения реализации MapView от гугла и Huawei и прокидывания атрибутов для карт из разметки. Вот такой класс получится:


abstract class MapView : FrameLayout {    enum class MapType(val value: Int) {        NONE(0), NORMAL(1), SATELLITE(2), TERRAIN(3), HYBRID(4)    }    protected var mapType = MapType.NORMAL    protected var liteModeEnabled = false    constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {        initView(context, attrs)    }    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(        context,        attrs,        defStyleAttr    ) {        initView(context, attrs)    }    private fun initView(context: Context, attrs: AttributeSet) {        initAttributes(context, attrs)        inflateMapViewImpl()    }    private fun initAttributes(context: Context, attrs: AttributeSet) {        val attributeInfo = context.obtainStyledAttributes(            attrs,            R.styleable.MapView        )        mapType = MapType.values()[attributeInfo.getInt(            R.styleable.MapView_someMapType,            MapType.NORMAL.value        )]        liteModeEnabled = attributeInfo.getBoolean(R.styleable.MapView_liteModeEnabled, false)        attributeInfo.recycle()    }    abstract fun inflateMapViewImpl()    abstract fun onCreate(mapViewBundle: Bundle?)    abstract fun onStart()    abstract fun onResume()    abstract fun onPause()    abstract fun onStop()    abstract fun onLowMemory()    abstract fun onDestroy()    abstract fun onSaveInstanceState(mapViewBundle: Bundle?)    abstract fun getMapAsync(function: (SomeMap) -> Unit)}

Чтобы работали атрибуты в разметке нам, конечно, надо их определить. Добавляем в res/values/attrs.xml вот это:


<declare-styleable name="MapView">    <attr name="someMapType">        <enum name="none" value="0"/>        <enum name="normal" value="1"/>        <enum name="satellite" value="2"/>        <enum name="terrain" value="3"/>        <enum name="hybrid" value="4"/>    </attr>    <attr format="boolean" name="liteModeEnabled"/></declare-styleable>

Это нам позволит прямо в разметке, используя нашу абстрактную карту передавать тип карты и нужен ли нам облегчённый режим для неё. Выглядеть в разметке это будет как-то так (реализация MapViewImpl будет показана далее):


<com.example.ui.base.widget.map.MapViewImpl    android:layout_width="match_parent"    android:layout_height="150dp"    app:liteModeEnabled="true"    app:someMapType="normal"/>

Как можно заметить в коде нашего абстрактного класса MapView, там используется некий SomeMap в методе getMapAsync. Так что давайте сразу покажем какие ещё общие классы и интерфейсы нам понадобятся, прежде чем перейдём к использованию различных реализаций карт.


SomeMap основной класс для работы с картами. В его переопределениях мы будет прокидывать вызовы методов для показа маркеров, назначения слушателей событий и опций отображения и для перемещения камеры по карте:


abstract class SomeMap {    abstract fun setUiSettings(        isMapToolbarEnabled: Boolean? = null,        isCompassEnabled: Boolean? = null,        isRotateGesturesEnabled: Boolean? = null,        isMyLocationButtonEnabled: Boolean? = null,        isZoomControlsEnabled: Boolean? = null    )    abstract fun setPadding(left: Int, top: Int, right: Int, bottom: Int)    abstract fun animateCamera(someCameraUpdate: SomeCameraUpdate)    abstract fun moveCamera(someCameraUpdate: SomeCameraUpdate)    abstract fun setOnCameraIdleListener(function: () -> Unit)    abstract fun setOnCameraMoveStartedListener(onCameraMoveStartedListener: (Int) -> Unit)    abstract fun setOnCameraMoveListener(function: () -> Unit)    abstract fun setOnMarkerClickListener(function: (SomeMarker) -> Boolean)    abstract fun setOnMapClickListener(function: () -> Unit)    abstract fun addMarker(markerOptions: SomeMarkerOptions): SomeMarker    abstract fun <Item : SomeClusterItem> addMarkers(        context: Context,        markers: List<Item>,        clusterItemClickListener: (Item) -> Boolean,        clusterClickListener: (SomeCluster<Item>) -> Boolean,        generateClusterItemIconFun: ((Item, Boolean) -> Bitmap)? = null    ): (Item?) -> Unit    companion object {        const val REASON_GESTURE = 1        const val REASON_API_ANIMATION = 2        const val REASON_DEVELOPER_ANIMATION = 3    }}

А вот и остальные классы/интерфейсы:


SomeCameraUpdate нужен для перемещения камеры на карте к какой-то точке или области.


class SomeCameraUpdate private constructor(    val location: Location? = null,    val zoom: Float? = null,    val bounds: SomeLatLngBounds? = null,    val width: Int? = null,    val height: Int? = null,    val padding: Int? = null) {    constructor(        location: Location? = null,        zoom: Float? = null    ) : this(location, zoom, null, null, null, null)    constructor(        bounds: SomeLatLngBounds? = null,        width: Int? = null,        height: Int? = null,        padding: Int? = null    ) : this(null, null, bounds, width, height, padding)}

SomeLatLngBounds класс для описания области на карте, куда можно переместить камеру.


abstract class SomeLatLngBounds(val southwest: Location? = null, val northeast: Location? = null) {      abstract fun forLocations(locations: List<Location>): SomeLatLngBounds}

И классы для маркеров.


SomeMarker собственно маркер:


abstract class SomeMarker {    abstract fun remove()}

SomeMarkerOptions для указания иконки и местоположения маркера.


data class SomeMarkerOptions(    val icon: Bitmap,    val position: Location)

SomeClusterItem для маркера при кластеризации.


interface SomeClusterItem {    fun getLocation(): Location    fun getTitle(): String?    fun getSnippet(): String?    fun getDrawableResourceId(): Int}

SomeCluster для кластера маркеров.


data class SomeCluster<T : SomeClusterItem>(    val location: Location,    val items: List<T>)

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


interface SelectableMarkerRenderer<Item : SomeClusterItem> {    val pinBitmapDescriptorsCache: Map<Int, Bitmap>    var selectedItem: Item?    fun selectItem(item: Item?)    fun getVectorResourceAsBitmap(@DrawableRes vectorResourceId: Int): Bitmap}

Также мы хотим иметь возможность сложной настройки внешнего вида маркера. Например генерируя иконку для него из разметки. Для этого скопируем класс из гугловой библиотеки IconGenerator:


/** * Not full copy of com.google.maps.android.ui.IconGenerator */class IconGenerator(private val context: Context) {    private val mContainer = LayoutInflater.from(context)        .inflate(R.layout.map_marker_view, null as ViewGroup?) as ViewGroup    private var mTextView: TextView?    private var mContentView: View?    init {        mTextView = mContainer.findViewById(R.id.amu_text) as TextView        mContentView = mTextView    }    fun makeIcon(text: CharSequence?): Bitmap {        if (mTextView != null) {            mTextView!!.text = text        }        return this.makeIcon()    }    fun makeIcon(): Bitmap {        val measureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)        mContainer.measure(measureSpec, measureSpec)        val measuredWidth = mContainer.measuredWidth        val measuredHeight = mContainer.measuredHeight        mContainer.layout(0, 0, measuredWidth, measuredHeight)        val r = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888)        r.eraseColor(0)        val canvas = Canvas(r)        mContainer.draw(canvas)        return r    }    fun setContentView(contentView: View?) {        mContainer.removeAllViews()        mContainer.addView(contentView)        mContentView = contentView        val view = mContainer.findViewById<View>(R.id.amu_text)        mTextView = if (view is TextView) view else null    }    fun setBackground(background: Drawable?) {        mContainer.setBackgroundDrawable(background)        if (background != null) {            val rect = Rect()            background.getPadding(rect)            mContainer.setPadding(rect.left, rect.top, rect.right, rect.bottom)        } else {            mContainer.setPadding(0, 0, 0, 0)        }    }    fun setContentPadding(left: Int, top: Int, right: Int, bottom: Int) {        mContentView!!.setPadding(left, top, right, bottom)    }}

Создаём реализации нашей абстрактной карты


Наконец приступаем к переопределению созданных нами абстрактных классов.


Подключим библиотеки:


//google mapsgoogleImplementation 'com.google.android.gms:play-services-location:17.0.0'googleImplementation 'com.google.maps.android:android-maps-utils-sdk-v3-compat:0.1' //clasterization//huawei mapshuaweiImplementation 'com.huawei.hms:maps:4.0.1.302'

Также добавляем необходимое для карт разрешение в манифест. Для этого создайте ещё один файл манифеста (AndroidManifest.xml) в папке src/huawei/ с таким содержимым:


<manifest xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    package="com.example">    <!-- used for MapKit -->    <uses-permission android:name="com.huawei.appmarket.service.commondata.permission.GET_COMMON_DATA"/></manifest>

Вот так будет выглядеть реализация карт для гугл. Добавляем в папку src/google/kotlin/com/example класс MapViewImpl:


class MapViewImpl : MapView {    private lateinit var mapView: com.google.android.libraries.maps.MapView    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(        context,        attrs,        defStyleAttr    )    override fun inflateMapViewImpl() {        mapView = com.google.android.libraries.maps.MapView(            context,            GoogleMapOptions().liteMode(liteModeEnabled).mapType(mapType.value)        )        addView(mapView)    }    override fun getMapAsync(function: (SomeMap) -> Unit) {        mapView.getMapAsync { function(SomeMapImpl(it)) }    }    override fun onCreate(mapViewBundle: Bundle?) {        mapView.onCreate(mapViewBundle)    }    override fun onStart() {        mapView.onStart()    }    override fun onResume() {        mapView.onResume()    }    override fun onPause() {        mapView.onPause()    }    override fun onStop() {        mapView.onStop()    }    override fun onLowMemory() {        mapView.onLowMemory()    }    override fun onDestroy() {        mapView.onDestroy()    }    override fun onSaveInstanceState(mapViewBundle: Bundle?) {        mapView.onSaveInstanceState(mapViewBundle)    }    /**     * We need to manually pass touch events to MapView     */    override fun onTouchEvent(event: MotionEvent?): Boolean {        mapView.onTouchEvent(event)        return true    }    /**     * We need to manually pass touch events to MapView     */    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {        mapView.dispatchTouchEvent(event)        return true    }}

А в папку src/huawei/kotlin/com/example аналогичный класс MapViewImpl но уже с использование карт от Huawei:


class MapViewImpl : MapView {    private lateinit var mapView: com.huawei.hms.maps.MapView    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(        context,        attrs,        defStyleAttr    )    override fun inflateMapViewImpl() {        mapView = com.huawei.hms.maps.MapView(            context,            HuaweiMapOptions().liteMode(liteModeEnabled).mapType(mapType.value)        )        addView(mapView)    }    override fun getMapAsync(function: (SomeMap) -> Unit) {        mapView.getMapAsync { function(SomeMapImpl(it)) }    }    override fun onCreate(mapViewBundle: Bundle?) {        mapView.onCreate(mapViewBundle)    }    override fun onStart() {        mapView.onStart()    }    override fun onResume() {        mapView.onResume()    }    override fun onPause() {        try {            mapView.onPause()        } catch (e: Exception) {            // there can be ClassCastException: com.exmaple.App cannot be cast to android.app.Activity            // at com.huawei.hms.maps.MapView$MapViewLifecycleDelegate.onPause(MapView.java:348)            Log.wtf("MapView", "Error while pausing MapView", e)        }    }    override fun onStop() {        mapView.onStop()    }    override fun onLowMemory() {        mapView.onLowMemory()    }    override fun onDestroy() {        mapView.onDestroy()    }    override fun onSaveInstanceState(mapViewBundle: Bundle?) {        mapView.onSaveInstanceState(mapViewBundle)    }    /**     * We need to manually pass touch events to MapView     */    override fun onTouchEvent(event: MotionEvent?): Boolean {        mapView.onTouchEvent(event)        return true    }    /**     * We need to manually pass touch events to MapView     */    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {        mapView.dispatchTouchEvent(event)        return true    }}

Тут надо обратить внимание на 3 момента:


  1. Вьюху карты мы создаём программно, а не загружаем из разметки, т.к. только так можно передать в неё опции (лёгкий режим, тип карты etc).
  2. Переопределены onTouchEvent и dispatchTouchEvent, с прокидывание вызовов в mapView без этого карты не будут реагировать на касания.
  3. В реализации для Huawei был обнаружен крэш при приостановке карты в методе onPause, пришлось в try-catch обернуть. Надеюсь это поправят в обновлениях библиотеки)

Реализуем дополнительные абстракции


А теперь самое сложное. У нас в приложении было достаточно много кода для отображения, кастомизации и обработки нажатия на маркеры и кластеры маркеров. Когда начали это всё пытаться заабстрагировать возникли сложности. Почти сразу выяснилось, что хотя в картах от Huawei есть кластеризация, она не полностью аналогична по функционалу кластеризации от гугла. Например нельзя влиять на внешний вид кластера и обрабатывать нажатия на него. Также в Huawei картах внешний вид отдельных маркеров (и обработка их событий) работает также как и маркеры, которые должны кластеризироваться. А вот в гугло-картах для кластеризующихся маркеров всё иначе отдельная обработка событий, отдельный способ настройки внешнего вида и вообще всё это сделано в рамках отдельной библиотеки. В итоге пришлось думать как переписать код так, чтобы и сохранить функционал для гугло-карт и чтобы карты от Huawei работали.


В общем, пришли в итоге к такому варианту: создаём метод для показа множества маркеров, которые должны кластеризоваться, в него передаём нужные нам слушатели событий и возвращаем лямбду, для функционала выбора маркера. Вот реализация SomeMap для гугло-карт:


class SomeMapImpl(val map: GoogleMap) : SomeMap() {    override fun setUiSettings(        isMapToolbarEnabled: Boolean?,        isCompassEnabled: Boolean?,        isRotateGesturesEnabled: Boolean?,        isMyLocationButtonEnabled: Boolean?,        isZoomControlsEnabled: Boolean?    ) {        map.uiSettings.apply {            isMapToolbarEnabled?.let {                this.isMapToolbarEnabled = isMapToolbarEnabled            }            isCompassEnabled?.let {                this.isCompassEnabled = isCompassEnabled            }            isRotateGesturesEnabled?.let {                this.isRotateGesturesEnabled = isRotateGesturesEnabled            }            isMyLocationButtonEnabled?.let {                this.isMyLocationButtonEnabled = isMyLocationButtonEnabled            }            isZoomControlsEnabled?.let {                this.isZoomControlsEnabled = isZoomControlsEnabled            }            setAllGesturesEnabled(true)        }    }    override fun animateCamera(someCameraUpdate: SomeCameraUpdate) {        someCameraUpdate.toCameraUpdate()?.let { map.animateCamera(it) }    }    override fun moveCamera(someCameraUpdate: SomeCameraUpdate) {        someCameraUpdate.toCameraUpdate()?.let { map.moveCamera(it) }    }    override fun setOnCameraIdleListener(function: () -> Unit) {        map.setOnCameraIdleListener { function() }    }    override fun setOnMarkerClickListener(function: (SomeMarker) -> Boolean) {        map.setOnMarkerClickListener { function(MarkerImpl(it)) }    }    override fun setOnMapClickListener(function: () -> Unit) {        map.setOnMapClickListener { function() }    }    override fun setOnCameraMoveStartedListener(onCameraMoveStartedListener: (Int) -> Unit) {        map.setOnCameraMoveStartedListener { onCameraMoveStartedListener(it) }    }    override fun addMarker(markerOptions: SomeMarkerOptions): SomeMarker {        return MarkerImpl(            map.addMarker(                MarkerOptions()                    .position(markerOptions.position.toLatLng())                    .icon(BitmapDescriptorFactory.fromBitmap(markerOptions.icon))            )        )    }    override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {        map.setPadding(left, top, right, bottom)    }    override fun setOnCameraMoveListener(function: () -> Unit) {        map.setOnCameraMoveListener { function() }    }    override fun <Item : SomeClusterItem> addMarkers(        context: Context,        markers: List<Item>,        clusterItemClickListener: (Item) -> Boolean,        clusterClickListener: (SomeCluster<Item>) -> Boolean,        generateClusterItemIconFun: ((Item, Boolean) -> Bitmap)?    ): (Item?) -> Unit {        val clusterManager = ClusterManager<SomeClusterItemImpl<Item>>(context, map)            .apply {                setOnClusterItemClickListener {                    clusterItemClickListener(it.someClusterItem)                }                setOnClusterClickListener { cluster ->                    val position = Location(cluster.position.latitude, cluster.position.longitude)                    val items: List<Item> = cluster.items.map { it.someClusterItem }                    val someCluster: SomeCluster<Item> = SomeCluster(position, items)                    clusterClickListener(someCluster)                }            }        map.setOnCameraIdleListener(clusterManager)        map.setOnMarkerClickListener(clusterManager)        val renderer =            object :                DefaultClusterRenderer<SomeClusterItemImpl<Item>>(context, map, clusterManager),                SelectableMarkerRenderer<SomeClusterItemImpl<Item>> {                override val pinBitmapDescriptorsCache = mutableMapOf<Int, Bitmap>()                override var selectedItem: SomeClusterItemImpl<Item>? = null                override fun onBeforeClusterItemRendered(                    item: SomeClusterItemImpl<Item>,                    markerOptions: MarkerOptions                ) {                    val icon = generateClusterItemIconFun                        ?.invoke(item.someClusterItem, item == selectedItem)                        ?: getVectorResourceAsBitmap(                            item.someClusterItem.getDrawableResourceId(item == selectedItem)                        )                    markerOptions                        .icon(BitmapDescriptorFactory.fromBitmap(icon))                        .zIndex(1.0f) // to hide cluster pin under the office pin                }                override fun getColor(clusterSize: Int): Int {                    return context.resources.color(R.color.primary)                }                override fun selectItem(item: SomeClusterItemImpl<Item>?) {                    selectedItem?.let {                        val icon = generateClusterItemIconFun                            ?.invoke(it.someClusterItem, false)                            ?: getVectorResourceAsBitmap(                                it.someClusterItem.getDrawableResourceId(false)                            )                        getMarker(it)?.setIcon(BitmapDescriptorFactory.fromBitmap(icon))                    }                    selectedItem = item                    item?.let {                        val icon = generateClusterItemIconFun                            ?.invoke(it.someClusterItem, true)                            ?: getVectorResourceAsBitmap(                                it.someClusterItem.getDrawableResourceId(true)                            )                        getMarker(it)?.setIcon(BitmapDescriptorFactory.fromBitmap(icon))                    }                }                override fun getVectorResourceAsBitmap(@DrawableRes vectorResourceId: Int): Bitmap {                    return pinBitmapDescriptorsCache[vectorResourceId]                        ?: context.resources.generateBitmapFromVectorResource(vectorResourceId)                            .also { pinBitmapDescriptorsCache[vectorResourceId] = it }                }            }        clusterManager.renderer = renderer        clusterManager.clearItems()        clusterManager.addItems(markers.map { SomeClusterItemImpl(it) })        clusterManager.cluster()        @Suppress("UnnecessaryVariable")        val pinItemSelectedCallback = fun(item: Item?) {            renderer.selectItem(item?.let { SomeClusterItemImpl(it) })        }        return pinItemSelectedCallback    }}fun Location.toLatLng() = LatLng(latitude, longitude)fun SomeLatLngBounds.toLatLngBounds() = LatLngBounds(southwest?.toLatLng(), northeast?.toLatLng())fun SomeCameraUpdate.toCameraUpdate(): CameraUpdate? {    return if (zoom != null) {        CameraUpdateFactory.newCameraPosition(            CameraPosition.fromLatLngZoom(                location?.toLatLng()                    ?: Location.DEFAULT_LOCATION.toLatLng(),                zoom            )        )    } else if (bounds != null && width != null && height != null && padding != null) {        CameraUpdateFactory.newLatLngBounds(            bounds.toLatLngBounds(),            width,            height,            padding        )    } else {        null    }}

Самое сложное, как уже и говорилось в addMarkers методе. В нём используются ClusterManager и ClusterRenderer, аналогов которых нет в Huawei картах. К тому же, эти классы требуют, чтобы объекты, из которых будут создаваться маркеты для кластеризации реализовывали интерфейс ClusterItem, аналога которому также нет у Huawei. В итоге пришлось изворачиваться и комбинировать наследование с инкапсуляцией. Data классы в проекте будут реализовывать наш интерфейс SomeClusterItem, а гугловый интерфейс ClusterItem будет реализовывать обёртка над классом с данными маркера. Вот такая:


data class SomeClusterItemImpl<T : SomeClusterItem>(    val someClusterItem: T) : ClusterItem, SomeClusterItem {    override fun getSnippet(): String {        return someClusterItem.getSnippet() ?: ""    }    override fun getTitle(): String {        return someClusterItem.getTitle() ?: ""    }    override fun getPosition(): LatLng {        return someClusterItem.getLocation().toLatLng()    }    override fun getLocation(): Location {        return someClusterItem.getLocation()    }}

В итоге, снаружи мы будем использовать библиотеко-независимый интерфейс, а внутри карт для гугла будем оборачивать его экземпляры в класс, реализующий ClusterItem из гугловой библиотеки. Подробнее смотрите реализацию addMarkers выше.


Чтобы всё это работало, осталось только вот эти классы добавить:


class SomeLatLngBoundsImpl(bounds: LatLngBounds? = null) :    SomeLatLngBounds(bounds?.southwest?.toLocation(), bounds?.northeast?.toLocation()) {    override fun forLocations(locations: List<Location>): SomeLatLngBounds {        val bounds = LatLngBounds.builder()            .apply { locations.map { it.toLatLng() }.forEach { include(it) } }            .build()        return SomeLatLngBoundsImpl(bounds)    }}fun LatLng.toLocation(): Location {    return Location(latitude, longitude)}

class MarkerImpl(private val marker: Marker?) : SomeMarker() {    override fun remove() {        marker?.remove()    }}

С реализацией для Huawei будет проще не надо возиться с оборачиванием SomeClusterItem. Вот все классы, которые надо положить в src/huawei/kotlin/com/example:


Реализация SomeMap:


class SomeMapImpl(val map: HuaweiMap) : SomeMap() {    override fun setUiSettings(        isMapToolbarEnabled: Boolean?,        isCompassEnabled: Boolean?,        isRotateGesturesEnabled: Boolean?,        isMyLocationButtonEnabled: Boolean?,        isZoomControlsEnabled: Boolean?    ) {        map.uiSettings.apply {            isMapToolbarEnabled?.let {                this.isMapToolbarEnabled = isMapToolbarEnabled            }            isCompassEnabled?.let {                this.isCompassEnabled = isCompassEnabled            }            isRotateGesturesEnabled?.let {                this.isRotateGesturesEnabled = isRotateGesturesEnabled            }            isMyLocationButtonEnabled?.let {                this.isMyLocationButtonEnabled = isMyLocationButtonEnabled            }            isZoomControlsEnabled?.let {                this.isZoomControlsEnabled = isZoomControlsEnabled            }            setAllGesturesEnabled(true)        }    }    override fun animateCamera(someCameraUpdate: SomeCameraUpdate) {        someCameraUpdate.toCameraUpdate()?.let { map.animateCamera(it) }    }    override fun moveCamera(someCameraUpdate: SomeCameraUpdate) {        someCameraUpdate.toCameraUpdate()?.let { map.moveCamera(it) }    }    override fun setOnCameraIdleListener(function: () -> Unit) {        map.setOnCameraIdleListener { function() }    }    override fun setOnMarkerClickListener(function: (SomeMarker) -> Boolean) {        map.setOnMarkerClickListener { function(MarkerImpl(it)) }    }    override fun setOnMapClickListener(function: () -> Unit) {        map.setOnMapClickListener { function() }    }    override fun setOnCameraMoveStartedListener(onCameraMoveStartedListener: (Int) -> Unit) {        map.setOnCameraMoveStartedListener { onCameraMoveStartedListener(it) }    }    override fun addMarker(markerOptions: SomeMarkerOptions): SomeMarker {        return MarkerImpl(            map.addMarker(                MarkerOptions()                    .position(markerOptions.position.toLatLng())                    .icon(BitmapDescriptorFactory.fromBitmap(markerOptions.icon))            )        )    }    override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {        map.setPadding(left, top, right, bottom)    }    override fun setOnCameraMoveListener(function: () -> Unit) {        map.setOnCameraMoveListener { function() }    }    override fun <Item : SomeClusterItem> addMarkers(        context: Context,        markers: List<Item>,        clusterItemClickListener: (Item) -> Boolean,        clusterClickListener: (SomeCluster<Item>) -> Boolean,        generateClusterItemIconFun: ((Item, Boolean) -> Bitmap)?    ): (Item?) -> Unit {        val addedMarkers = mutableListOf<Pair<Item, Marker>>()        val selectableMarkerRenderer = object : SelectableMarkerRenderer<Item> {            override val pinBitmapDescriptorsCache = mutableMapOf<Int, Bitmap>()            override var selectedItem: Item? = null            override fun selectItem(item: Item?) {                selectedItem?.let {                    val icon = generateClusterItemIconFun                        ?.invoke(it, false)                        ?: getVectorResourceAsBitmap(it.getDrawableResourceId(false))                    getMarker(it)?.setIcon(BitmapDescriptorFactory.fromBitmap(icon))                }                selectedItem = item                item?.let {                    val icon = generateClusterItemIconFun                        ?.invoke(it, true)                        ?: getVectorResourceAsBitmap(                            it.getDrawableResourceId(true)                        )                    getMarker(it)?.setIcon(BitmapDescriptorFactory.fromBitmap(icon))                }            }            private fun getMarker(item: Item): Marker? {                return addedMarkers.firstOrNull { it.first == item }?.second            }            override fun getVectorResourceAsBitmap(@DrawableRes vectorResourceId: Int): Bitmap {                return pinBitmapDescriptorsCache[vectorResourceId]                    ?: context.resources.generateBitmapFromVectorResource(vectorResourceId)                        .also { pinBitmapDescriptorsCache[vectorResourceId] = it }            }        }        addedMarkers += markers.map {            val selected = selectableMarkerRenderer.selectedItem == it            val icon = generateClusterItemIconFun                ?.invoke(it, selected)                ?: selectableMarkerRenderer.getVectorResourceAsBitmap(it.getDrawableResourceId(selected))            val markerOptions = MarkerOptions()                .position(it.getLocation().toLatLng())                .icon(BitmapDescriptorFactory.fromBitmap(icon))                .clusterable(true)            val marker = map.addMarker(markerOptions)            it to marker        }        map.setMarkersClustering(true)        map.setOnMarkerClickListener { clickedMarker ->            val clickedItem = addedMarkers.firstOrNull { it.second == clickedMarker }?.first            clickedItem?.let { clusterItemClickListener(it) } ?: false        }        return selectableMarkerRenderer::selectItem    }}fun Location.toLatLng() = LatLng(latitude, longitude)fun SomeLatLngBounds.toLatLngBounds() = LatLngBounds(southwest?.toLatLng(), northeast?.toLatLng())fun SomeCameraUpdate.toCameraUpdate(): CameraUpdate? {    return if (zoom != null) {        CameraUpdateFactory.newCameraPosition(            CameraPosition.fromLatLngZoom(                location?.toLatLng()                    ?: Location.DEFAULT_LOCATION.toLatLng(),                zoom            )        )    } else if (bounds != null && width != null && height != null && padding != null) {        CameraUpdateFactory.newLatLngBounds(            bounds.toLatLngBounds(),            width,            height,            padding        )    } else {        null    }}

class SomeLatLngBoundsImpl(bounds: LatLngBounds? = null) :    SomeLatLngBounds(bounds?.southwest?.toLocation(), bounds?.northeast?.toLocation()) {    override fun forLocations(locations: List<Location>): SomeLatLngBounds {        val bounds = LatLngBounds.builder()            .apply { locations.map { it.toLatLng() }.forEach { include(it) } }            .build()        return SomeLatLngBoundsImpl(bounds)    }}fun LatLng.toLocation(): Location {    return Location(latitude, longitude)}

class MarkerImpl(private val marker: Marker?) : SomeMarker() {    override fun remove() {        marker?.remove()    }}

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


Используем нашу абстрактную карту


Итак, в разметку мы добавляем MapViewImpl, как было показано выше и переходим к коду. Для начала нам надо из нашей MapView получить объект карты:


mapView.getMapAsync { onMapReady(it) }

Когда она будет получена будем рисовать на ней маркеры с помощью нашей абстракции. А также, при нажатии, выделять их и отображать сообщение. И ещё обрабатывать нажатие на кластер. При этом мы, как и планировалось, не зависим от реализации карт:


private fun onMapReady(map: SomeMap) {    map.setUiSettings(isMapToolbarEnabled = false, isCompassEnabled = false)    var pinItemSelected: ((MarkerItem?) -> Unit)? = null    fun onMarkerSelected(selectedMarkerItem: MarkerItem?) {        pinItemSelected?.invoke(selectedMarkerItem)        selectedMarkerItem?.let {            map.animateCamera(SomeCameraUpdate(it.getLocation(), DEFAULT_ZOOM))            Snackbar.make(root, "Marker selected: ${it.markerTitle}", Snackbar.LENGTH_SHORT).show()        }    }    with(map) {        setOnMapClickListener {            onMarkerSelected(null)        }        setOnCameraMoveStartedListener { reason ->            if (reason == SomeMap.REASON_GESTURE) {                onMarkerSelected(null)            }        }    }    locationGateway.requestLastLocation()        .flatMap { mapMarkersGateway.getMapMarkers(it) }        .subscribeBy { itemList ->            pinItemSelected = map.addMarkers(                requireContext(),                itemList.map { it },                {                    onMarkerSelected(it)                    true                },                { someCluster ->                    mapView?.let { mapViewRef ->                        val bounds = SomeLatLngBoundsImpl()                            .forLocations(someCluster.items.map { it.getLocation() })                        val someCameraUpdate = SomeCameraUpdate(                            bounds = bounds,                            width = mapViewRef.width,                            height = mapViewRef.height,                            padding = 32.dp()                        )                        map.animateCamera(someCameraUpdate)                    }                    onMarkerSelected(null)                    true                }            )        }}

Часть кода, понятно, опущена для краткости. Полный пример вы можете найти на GitHub: https://github.com/MobileUpLLC/huawei_and_google_services.


А вот как выглядят карты разных реализаций (сначала Huawei, потом Google):


Huawei maps


Google maps


По итогу работы с картами можно сказать следующее с картами гораздо сложнее, чем с местоположением и аналитикой. Особенно, если есть маркеры и кластеризация. Хотя могло быть и хуже, конечно, если бы API для работы с картами отличалось сильнее. Так что можно сказать спасибо команде Huawei за облегчение поддержки карт их реализации.


Заключение


Рынок мобильных приложений меняется. Ещё вчера казавшиеся незыблемыми монополии Google и Apple наконец-то замечены не только пострадавшими от них разработчиками (из самых известных последних Telegram, Microsoft, Epic Games) но и государственными структурами уровня EC и США. Надеюсь, на этом фоне наконец-то появится здоровая конкурентная среда хотя бы на Android. Работа Huawei в этой области радует видно, что люди стараются максимально упростить жизнь разработчикам. После опыта общения с тех. поддержкой GooglePlay, где тебе отвечают роботы в основном (и они же и банят) в Huawei тебе отвечают люди. Мало того когда у меня возник вопрос по их магазину приложений у меня была возможность просто взять и позвонить живому человеку из Москвы и быстро решить проблему.


В общем, если вы хотите расширить свою аудиторию и получить качественную тех.поддержку в процессе идите в Huawei. Чем больше там будет разработчиков, тем выше шанс, что и в GooglePlay что-то поменяется в лучшую сторону. И выиграют все.


Весь код, который есть в этом цикле статей вы можете посмотреть в репозитории на GitHub. Вот ссылка: https://github.com/MobileUpLLC/huawei_and_google_services.

Подробнее..

Шаблон Kotlin микросервисов

27.02.2021 20:23:19 | Автор: admin

Для разработчиков не секрет, что создание нового сервиса влечет за собой немало рутиной настройки: билд скрипты, зависимости, тесты, docker, k8s дескрипторы. Раз мы выполняем эту работу, значит текущих шаблонов IDE недосточно. Под катом мои попытки автоматизировать все до одной кроссплатформенной кнопки "сделать хорошо" сопровождаемые кодом, примерами и финальным результатом.
Если перспективы создания сервисов в один клик с последующим автоматическим деплоем в Digital Ocean звучат заманчиво, значит эта статья для вас.

Начнем создавать наш шаблон и прежде всего рассмотрим организацию сборки. Несмотря на любовь многих к maven за его простоту и декларативность, использовать будем gradle, ибо он современее и позволяет писать скрипт сборки на одном языке с проектом. Помимо самого Kotlin плагина, нам потребуется еще два:

plugins {  kotlin("jvm") version "1.4.30"  // Чтобы собрать fat jar  id("com.github.johnrengelman.shadow") version "6.1.0"  // Чтобы собрать self-executable приложение с jvm  id("org.beryx.runtime") version "1.12.1"}

Из зависимостей, в качестве серверного фреймворка был выбран "родной" для Kotlin Ktor. Для тестирования используется связка testNG + Hamkrest с его выразительным DSL, позволяющим писать тесты таким образом:

assertThat("xyzzy", startsWith("x") and endsWith("y") and !containsSubstring("a"))

Собираем все вместе, ориентируясь на Java 15+

dependencies {  // для парсинга аргументов командой строки  implementation("com.github.ajalt.clikt:clikt:3.1.0")  implementation("io.ktor:ktor-server-netty:1.5.1")  testImplementation("org.testng:testng:7.3.0")  testImplementation("com.natpryce:hamkrest:1.8.0.1")  testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")}application {  @Suppress("DEPRECATION") // for compatibility with shadowJar  mainClassName = "AppKt"}tasks {  test {    useTestNG()  }  compileKotlin {    kotlinOptions {      jvmTarget = "15"    }  }}

В исходный код генерируемый шаблоном по умолчанию добавлен entry-point обработки аргументов командой строки, заготовка ддя роутинга, и простой тест (заодно служащий примером использоования testNG с Hamkrest).
Из того что следует отметить, позволил себе небольшую вольность с официальным Kotlin codestyle чуть-чуть поправив его в .editorsconfig:

[*.{kt, kts, java, xml, html, js}]max_line_length = 120indent_size = 2continuation_indent_size = 2

В первую очередь, это вопрос вкуса, но ещё так в одну строчку помещается больше кода, что помогает избежать переноса в выражениях внутри класса и функции, лямбды, например.
Чтобы собрать self-executable jar используйте команду

gradle clean test shadowJar

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

Осталось написать Dockerfile, его привожу целиком. Сборка и запуск разделены и производятся в два этапа:

# syntax = docker/dockerfile:experimentalFROM gradle:jdk15 as builderWORKDIR /appCOPY src ./srcCOPY build.gradle.kts ./build.gradle.ktsRUN --mount=type=cache,target=./.gradle gradle clean test shadowJarFROM openjdk:15-alpine as backendWORKDIR /rootCOPY --from=builder /app/*.jar ./app

Работать сервис будет в контейнере с jdk (а не jvm), ради простоты ручной диагностики с помощью jstack/jmap и других поставляемых с jdk инструментов.
Сконфигурируем запуск приложения при помощи Docker Compose:

version: "3.9"services:  backend:    build: .    command: java -jar app $BACKEND_OPTIONS    ports:      - "80:80"

Теперь мы можем запускать наш сервис на целевой машине, без дополнительных зависимостей в виде Jdk/Gradle, при помощи простой команды

docker-compose up

Как деплоить сервис в облако? Выбрал Digital Ocean по причине дешевой стоимости и простоты управления. Благодаря тому что мы только что сконфигурировали сборку и запуск в контейнере, можно выбрать наш репозиторий с проектом в разделе Apps Platform и... все! Файлы конфигурации Docker будут подцеплены автоматически, мы увидим логи сборки, а после этого получим доступ к веб адресу, логам приложения, консоли управления, простым метрикам потребления памяти и процессорного времени. Выглядит это удовольствие примерно так и стоит 5$ в месяц:

При последующих изменениях в master ветке репозитория, передеплой запустится автоматически. Очень удобно. И наконец, все описанное в статье, подробно задокументировано в README.md файле шаблона проекта, чтобы после создания последующая сборка и деплой не вызывали сложностей.

Исползьовать описаный шаблон чтобы получить готовый репозиторий, можно просто нажав кнопочку "Use this template"на GitHub:
github.com/demidko/Projekt

Или, если вам нужен варинт с портабельным jvm:
github.com/demidko/Projekt-portable

Интересно услышать предложения, комментарии и критику.

Подробнее..

Готовьсь, цельсь, пли! Как не обжечься при сборке Gradle-приложения, и настолько ли всё серьезно?

18.03.2021 16:05:16 | Автор: admin

Доброго дня, читатель! Меня зовут Стручков Михаил и я Android-разработчик в команде мобильного оператора Yota.

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

Предлагаю упростить тернистый путь к пониманию сборки Gradle-приложений, разобрать основные этапы и их особенности, и попробовать совместно сократить трафик stackoverflow на тему Gradle. В качестве бонуса немного коснемся Gradle-плагинов и разберемся, как к ним подходить.

Очень сложно рассмотреть работу всю работу с Gradle в одной статье, поэтому планирую последовательно пополнять список:

  1. Готовьсь, цельсь, пли! Как не обжечься при сборке Gradle-приложения, и настолько ли всё серьезно?

  2. Пишем свой Gradle-плагин

  3. Пишем эффективные Gradle-задачи

Для удобства навигации по статье, ниже представлено оглавление:

Структура проекта

Инициализация

Конфигурация плагинов

Подключение Gradle-проектов и композитная сборка

Конфигурация

Управление зависимостями

Типы зависимостей

Производительность конфигурации

Как сделать задачу для этапа конфигурации?

Немного о buildSrc

Куда вынести общую логику билдскриптов?

Сборка

Коротко про Gradle Task

Gradle Daemon

Несколько советов по оптимизации скорости сборки

Заключение

Список докладов

Gradle приобрел широкую популярность в качестве системы сборки не только JVM-приложений. Широта предоставляемого инструментария, возможности его адаптации под кастомные задачи, возможности для сборки крупных проектов, простота интеграции с различными CI-системами делают свое дело.

В этой статье поговорим про основные этапы сборки Gradle-приложения - инициализацию, конфигурацию и сборку, и про то, как нам с ними обращаться при работе. Для своих примеров я буду использовать Kotlin и конфигурацию на Kotlin DSL. Стильно, модно. Kotlin в сравнении c Groovy привносит много удобств. Ведь теперь мы можем легко выяснить - что и где, и какого оно типа, а Gradle API наконец начинает нам в этом помогать. С Groovy этого определенно не хватало. Кто еще не в курсе, в документации хорошо рассказано о том, как попробовать Kotlin DSL.

Структура проекта

Итак, главная сущность в мире Gradle это Project. Project-ом выступает само наше приложение, которое мы только что создали. У project-а могут быть subproject-ы, у subproject-а могут быть свои subproject-ы, и таким образом получается древовидная структура. Хорошей практикой является подход, при котором степень вложенности не превышает двух. Такая структура подходит для большинства проектов. При этом subproject-ы можно просто называть Gradle-модулями.

Если вы пользуетесь IDE от JetBrains или Android Studio, по умолчанию они не дадут вам создать иерархию больше двух (для стандартных проектов), что уже наталкивает на мысль, что так делать не стоит.

На картинке снизу subproject-ы (Gradle-модули) - это app и lib. Корнем Gradle-структуры является сам проект (вот эта маленькая точка сверху):

Project узнаёт обо всех своих subproject из файла конфигурации settings.gradle (.kts). Написанный здесь скрипт будет выполняться на этапе инициализации проекта.

Инициализация

Итак, в settings.gradle (.kts) мы определяем содержимое нашего приложения, и учим Gradle к нему подступаться. В самом начале задаем имя нашего проекта:

rootProject.name = "myproject"

Теперь мы можем легко его получить из любого subproject-а на последующих этапах.

Конфигурация плагинов

Далее мы можем определить блок pluginManagement, где есть возможность сконфигурировать плагины, которые используются в нашем приложении.

Внутри блока pluginsесть возможность указать дефолтную версию для плагина, если не используется никакая другая. При этом эту версию можно брать извне, например, из gradle.properties:

val helloPluginVersion: String by settingspluginManagement {plugins {id("com.example.hello") version "${helloPluginVersion}"}}

gradle.properties:

helloPluginVersion=1.0.0

Подключать дефолтные версии плагинов из внешних источников, на мой взгляд, уникальный случай, но, если это ваш случай - вы теперь знаете как это сделать. Если вы уже используете такую возможность в вашем проекте - напишите в комментариях, кейс интересный.

По умолчанию все наши плагины подключаются из gradlePluginPortal. Если же плагин необходимо достать из другого репозитория, в блоке repositories можно его определить. Например, мы хотим подключить Android Gradle Plugin. Он лежит в гугловском репозитории, что и объявляем явно:

repositories {gradlePluginPortal()google()}

Готово! Теперь можно добавить зависимость Android Gradle Plugin в classpath (о котором чуть позже) и затем успешно подключать Android-плагины в билдскриптах.

В блоке resolutionStrategy мы можем определить правила для подключения плагинов, используемых проекте. На коде ниже приведен пример того, как можно хитрым образом динамически подключить Android Gradle Plugin на тот случай, если в проекте он начнёт использоваться:

resolutionStrategy {  eachPlugin {    if (requested.id.namespace == "com.android") {      useModule("com.android.tools.build:gradle:${requested.version}")    }  }}

После такого финта ушами, AGP автоматически подключится в classpath по необходимости.Здесь в поле requested находится реквест плагина, который мы явно сформировали:

build.gradle.kts (root):

plugins {  id("com.android.application") version "4.1.0" apply false}

Чтобы узнать, что будет запрошено в конечном итоге, следует использовать поле target.

Важный момент: версию плагинов в явном виде стоит определять именно в рутовом build.gradle (.kts). Если сделать это как-то по-другому, чаще всего будет появляться следующее:

Caused by: org.gradle.plugin.management.internal.InvalidPluginRequestException: Plugin request for plugin already on the classpath must not include a version

Реже следующее:

Caused by: java.lang.IllegalArgumentException: org.gradle.api.internal.initialization.DefaultClassLoaderScope@68d65269 must be locked before it can be used to compute a classpath!

Полный зоопарк. Я не буду загружать подробностями (ибо у автора тоже с них взрывается мозг), но суть такова, что рутовый build.gradle (.kts) является для этого централизованным местом.

Подключение Gradle-проектов и композитная сборка

Далее с помощью includeобъявляем существующие в нашем проекте подпроекты (они же по-простому gradle-модули). При этом, если модуль лежит где-то в другом месте и вообще он гриб, можно явно указать, как и где его найти с помощью project:

include("some-subproject")project("some-subproject").projectDir = file("../somesubproject")project(":some-subproject ").buildFileName = "some-subproject.gradle"

И последнее, но не менее интересное includeBuild. С помощью него можно определить проект, который мы бы хотели собрать, и определить интерфейс до сборки основного проекта. Таким образом, имеем возможность организовать композитную сборку:

includeBuild("some-other-project") {  dependencySubstitution {    substitute(module("org.sample:mysample")).with(project(":"))   }}

C помощью лямбды в dependencySubstitution можно подменить зависимости, используемые в includeBuild-проекте, в том числе на те, что предоставляет наш проект, как на примере выше.

Где можно использовать композитную сборку?

Из своего опыта могу сказать, что композитная сборка может быть очень полезна, когда мы хотим отдебажить какой-нибудь Gradle-проект из другого репозитория. Например, это может быть наш собственный Gradle-плагин. Для этого просто выбираем Debug у Gradle-задачи из нашего плагина, который мы подключаем через композитную сборку:

После этого можно пользоваться всеми радостями отладки от IDE: ставить брейкпоинты, просматривать значения переменных и т.д. при выполнении кода Gradle-плагина.

P.S. Отладить таким образом можно не только Gradle-плагин, но и все содержимое build.gradle (kts), и в том числе на Groovy!Где ещё может пригодиться композитная сборка, поговорим немного дальше по ходу статьи.

Также settings.gradle (.kts) может с успехом отсутствовать. В этом случае Gradle поймёт, что других модулей проекте нет, и на этом успокоится.

Многие уже привыкли видеть settings.gradle (.kts) как скрипт, где только перечислены все известные в приложении Gradle-проекты. По опыту починки различных поломок с Gradle могу сказать, что сюда посмотрят в последнюю очередь. Если есть возможность не дописывать сложную логику сюда, лучше этого не делать. В большинстве случаев эту логику можно реализовать в build.gradle (.kts) рутового Gradle-проекта, что будет более очевидно для других разработчиков.

После выполнения инициализации Gradle создает объекты типа Project, с которыми мы продолжаем работу уже на этапе конфигурации.

Конфигурация

Также известна под именами Sync Project With Gradle Files, Load Gradle Changes, Reload All Gradle Projects. На данном этапе главными игроками выступают билдскрипты build.gradle (.kts).

Механизм достаточно простой - для построенного дерева Gradle-проектов на этапе инициализации последовательно вызывается соответствующий билдскрипт. Здесь выполняется следующее:

  1. Резолвятся и загружаются зависимости для каждого из известного в settings.gradle (.kts) Gradle-проекта;

  2. Строится граф выполнения Gradle-задач для этапа сборки;

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

О подкапотном пространстве 2 и 3 этапов предлагаю поговорить в статье про Gradle-задачи и в комментариях, а сейчас давайте немного сосредоточимся на том, как быть с зависимостями.

Управление зависимостями

Для подключения зависимостей в Gradle-проект существует два ключевых блока: repositoriesи dependencies.

repositories {  mavenCentral()  maven(url = "https://www.myrepo.io")  flatDir {    dirs("lib1", "lib2")}} dependencies {  implementation(kotlin("stdlib"))}

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

При поиске зависимостей Gradle осуществляет поиск по репозиториям в том порядке, в котором мы эти репозитории у себя объявили. Из этого вытекает, что репозитории с максимальным числом зависимостей стоит выносить в самый верх, и в целом стараться держать как можно меньше репозиториев.

Сами зависимости мы определяем в блоке dependencies. Здесь правило похожее - чем меньше, тем лучше.

В блоке buildscript определяются зависимости, которые необходимо загрузить и положить в classpath для их доступности на этапе конфигурации:

buildscript {  repositories {    //...  }  dependencies {    classpath("com.android.tools.build:gradle:$agpVersion")}}

На примере выше я добавляю зависимость от Android Gradle Plugin чтобы использовать com.android.* плагины для конфигурации Android-приложения.

Для подключения зависимостей на этапе сборки, блоки dependencies и repositoriesдобавляются в корень билдскрипта build.gradle (.kts). В качестве зависимостей могут быть как другие gradle-проекты, так и внешние/локальные библиотеки:

dependencies {  runtimeOnly(group = "org.springframework", name = "spring-core", version = "2.5")  implementation(fileTree(mapOf("include" to listOf(".jar"), "dir" to "libs")))  implementation(kotlin("stdlib"))  implementation(project(:project-a))}

Для подключения Gradle-проектов используется символ :. С его помощью мы определяем путь, по которому лежит подключаемый Gradle-проект относительно рутового Gradle-проекта.

Важно следить за тем, чтобы не организовать цикл между зависимостями. Если такое произойдет, в лучшем случае Gradle-сборка начнет падать с

Could not resolve project :project-a.Required by:    project :project-b

, а в худшем - c NullPointerException, и проблему тогда станет искать намного сложнее.

В управлении зависимостями иногда бывают загвоздки. Например, при разрешении версий могут возникнуть конфликты, или же мы захотим явно определить нужную нам версию транзитивной зависимости ввиду проблем совместимости. Как решать подобные проблемы, хорошо описано в этой статье. А я не стану изобретать велосипед и доверю вас опытному автору.

Типы зависимостей

У Gradle бывают два типа зависимостей - runtime и compile-time. Хорошо объясняет положение вещей следующий рисунок:

При подключении зависимостей в compileClasspath, мы получим runtime exception при попытке достучаться до кода зависимости во время выполнения, поскольку зависимость не попала в приложение. Но в то же время код зависимости будет доступен на этапе сборки проекта. Подключая зависимости в runtimeClasspath, мы гарантируем их попадание в приложение, а значит, и безопасность выполнения кода в runtime. Здесь-то и приходит понимание, что implementation и api добавляют зависимости в оба classpath-а. При этом api также позволяет получить доступ к коду gradle-проекта в случае его транзитивного подключения.

В качестве apiElements и runtimeElements на рисунке обозначен код, который мы хотим отдать на использование в другие gradle-модули.

Производительность конфигурации

Gradle старается, насколько это возможно, оптимизировать процесс конфигурации. Наша задача этому не мешать, а именно, стараться не взаимодействовать с другими gradle-проектами на этапе конфигурации напрямую. Это приводит к связности проектов и замедляет скорость конфигурации.

Самый простой способ сделать проекты связными определить блоки allprojectsили subprojects. Чаще всего эти блоки используются чтобы добавить общее поведение, например, определить общую зависимость/репозиторий, определить Gradle-задачу, которую мы бы хотели выполнять для каждого проекта и.т.д.:

subprojects {  apply(plugin = "java")    repositories {    mavenCentral()  }    tasks.register<MyTask>("myTask")}

Такие блоки можно определить в root-проекте, поскольку он и так неявно связан со всеми подпроектами. Но стоит помнить, что выполнение подобного трюка в дочернем проекте оставит негативный след на скорости конфигурации. Если потребность всё же есть, лучше сделать зависимость между Gradle-задачами на этапе сборки.

Как сделать задачу для этапа конфигурации?

У Gradle есть на этот случай колбэки. Самый простой способ добавить колбэк для этапа конфигурации определить блоки afterEvaluate или beforeEvaluate. Блок afterEvaluate можно использовать, например, в случае, когда мы хотим добавить задачу в граф выполнения и хотим, чтобы она выполнялась по определенному правилу. Например, после задачи myTask, как на примере ниже:

аfterEvaluate {  tasks.all {    if (this is org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {      dependsOn(tasks.named("myTask"))    }  }}
Почему не следует использовать dependsOn

Связывая задачи таким образом, мы должны быть уверены в том, что, как и когда выполняет Gradle-задача, от которой мы хотим зависеть. Нет никаких гарантий, что при её изменении наш код не сломается.

Чтобы избежать подобных проблем, задачи следует связывать с помощью их inputs и outputs. В этом случае в качестве бонуса мы получим еще и инкрементальность (aka up-to-date проверки), что значительно снизит время повторной сборки. Чтобы во всем разобраться, можно посмотреть доклад Степана Гончарова (таймкод) и пример из документации.

Немного о buildSrc

Еще одна вещь, о которой я не могу не упомянуть, это buildSrc. Модуль buildSrc собирается каждый раз перед конфигурацией нашего рутового проекта и поставляется на этап конфигурации в виде jar. Его довольно удобно использовать для объявления зависимостей, а также содержать общую логику для билдскриптов. Подробнее о том, как использовать buildSrc в Gradle-проекте хорошо описано в этой статье.

Но, к сожалению, не всё так гладко, как хотелось бы. У buildSrc есть одна достаточно весомая проблема - при любом его изменении мы теряем наш билд-кеш, и как следствие, заставляем проект пересобираться "на холодную". Если проект большой, это может быть особенно критично. О том, как решать проблему buildSrc, можно почитать в статье от ребят из Badoo. (Спойлер - решается миграцией на композитный билд).

P.S. Сейчас в нашем проекте несколько десятков модулей и сложно сказать, что проблема buildSrc ощутимо по нам ударила. Но я не исключаю, что в относительно близкое время композитный билд действительно станет нашим будущим.

Куда еще можно вынести общую логику билдскриптов?

Если вы используете Groovy, можно вынести логику в отдельный билдскрипт и подключать его в нужные места вот так:

build.gradle (Gradle-модуля):

 apply from: common.gradle

При этом скрипт можно положить напрямую в папку с проектом. Поскольку Groovy интерпретируемый язык, то ничего заранее компилировать не придется.

Если вы пишете подключаемые билдскрипты на Kotlin DSL, то они обязательно должны лежать в buildSrc. Связано это с тем, что Kotlin-скрипт должен быть скомпилирован перед использованием. При этом для подключения нужно выполнить небольшой финт ушами и в build.gradle (.kts) модуля buildSrc добавить следующее:

build.gradle.kts (buildSrc):

plugins {`kotlin-dsl``kotlin-dsl-precompiled-script-plugins`}

Теперь можем добавить Kotlin-скрипт в buildSrc/src/main/kotlin (java):

common.gradle.kts:

dependencies {  add("implementation", "org.sample:some-shared-dependency:1.0.0")}

И подключить его вот так:

build.gradle.kts (Gradle-модуля):

apply(id = "common")//...

Скрипт, подключаемый таким образом, компилируется всего один раз перед использованием, что не повлияет на скорость конфигурации, и даже ускорит её, если вынести достаточно жирный кусочек. Однако в buildSrc при этом будет сгенерировано приличное количество кода.

Для того, чтобы связать Kotlin и Groovy, в Kotlin-скрипты можно подключать стандартные *.gradle с помощью того же apply:

build.gradle.kts (Gradle-модуля):

apply(from = "common.gradle")//...

Сборка

Вот мы и плавно подобрались к тому, что чаще всего поджигает наш ноутбук рождает приложение на свет. На этом этапе Gradle начинает выполнять задачи, которые были добавлены в граф зависимости на этапе конфигурации в нужном порядке.

Коротко про Gradle Task

Gradle Task представляет собой единицу работы, которую выполняет сборка. К примеру, это может быть компиляция классов, создание JAR, создание документации Javadoc или публикация в репозиторий.

Простейшим вариантом написать задачу для этапа сборки является реализация лямбды в doFirst/doLast, как на примере ниже:

tasks.register("printAllProjectNames") {  group = project names  description = Prints all projects names  doFirst {    println(Start execution)  }  doLast {    println("Root project name is: ${project.name}") project.subprojects.forEach { project ->      println("Child project name is: ${project.name}")    }  }

Задаче очень желательно добавлять description и group,чтобы без лишних телодвижений было понятно, чем она занимается. После добавления group задачу будет удобно искать и запускать:

Задачу можно выполнить также и через консоль с помощью команды:

gradle ${your_task_name}

Gradle Daemon

Непосредственным выполнением сборки занимается Gradle Deamon. Он включен по умолчанию для Gradle версии 3.0 и выше. Gradle Daemon является долгоживущим системным процессом, периодически осуществляющим сборку, когда мы этого хотим. Внутри него происходит много in-memory кеша, оптимизации работы с файловой системой и оптимизации кода выполнения сборки. Если коротко - всё идет на пользу. Пожалуй, исключение только одно - он довольно прожорлив, и Gradle любит держать несколько демонов на разные случаи жизни. Если система начинает ощутимо лагать, всегда можно всех за раз прибить командой

gradle --stop

Несколько советов по оптимизации скорости сборки

Нетрудно понять, что сборка - это самый трудозатратный этап, занимающий львиную долю всего времени работы Gradle. Есть несколько базовых рекомендаций для того, как нам её ускорить, первая из которых - запастись терпением:

  1. Параллельное выполнение тасок.

    Всё просто - org.gradle.parallel=true в вашем gradle.properties, и Gradle будет стараться максимально распараллелить выполнения тасок на этапе сборки. Этот параметр по умолчанию включен при создании проекта.

  2. Обновление Gradle.

    Gradle важно периодически обновлять. Связано это с тем, что с каждой версией в Gradle привносят улучшения по скорости сборки, исправления багов и новые интересные плюшки. Для удобства обновления, был придуман Gradle Wrapper (или просто Wrapper).Это ни что иное, как обычная Gradle-задача. Теперь для обновления версии можно написать в рутовом build.gradle (.kts) следующее:

    tasks.withType<Wrapper> {  val projectGradleVersion = "6.8.3"  gradleVersion = projectGradleVersion}
    

    , а затем выполнить команду gradle wrapper. Или же выполнить

    gradle wrapper --gradle-version ${gradle_version}
    

    , где вместо gradle_version указываем желаемую версию Gradle. Тем самым Gradle сам загрузит нужную версию и положит её в папку с проектом. Она же будет использоваться в дальнейшем по умолчанию, а счастливые мы сможем запускать задачи с помощью скрипта gradlew, который появится в папке с нашим проектом.

  3. Правильное использование api/implementation.

    При изменении реализации зависимости Gradle пересобирает "на холодную" все зависящие от неё модули. Я уже упомянул про api и implementation в разговоре о способах подключения зависимостей, и теперь понятно, что при подключении зависимости через api, она также транзитивно попадает в classpath-ы модулей, в которые мы подключили наш модуль. Тем самым увеличивается количество модулей, которые gradle будет пересобирать при изменении зависимости. Если нет необходимости в транзитивном использовании кода из зависимости, для её подключения следует использовать implementation.

  4. Инкрементальность и build-кеш.

    Оптимизация Gradle позволяет нам пропускать выполнение задачи при определенных условиях. У Gradle существует 5 состояний задач - EXECUTED, UP-TO-DATE, FROM-CACHE, SKIPPED и NO-SOURCE. В разговоре про кеш нам интересны два из них - UP-TO-DATE и FROM-CACHE. Они сигнализируют о том, что результаты выполнения наших тасок были успешно подтянуты гредлом из результатов предыдущей сборки. Об остальных состояниях можно почитать в документации.

    UP-TO-DATE. Возникает в случае, если разработчик задачи позаботился о ней и самостоятельно реализовал инкрементальность её выполнения (проверки на up-to-date), или же все зависимости этой задачи, если они есть, были успешно взяты из кеша, или признаны гредлом up-to-date.

    FROM-CACHE. Это состояние возникает в случае, если Gradle смог восстановить outputs задачи из билд-кеша. При этом по умолчанию build-cache выключен. Build-cache довольно удобно использовать на CI, таким образом ускорив выполнение пайплайнов Gradle-сборки. Как организовать build-cache всегда можно узнать тут.

    При использовании билд-кеша в локальных сборках, знаменитый Clean/Rebuild теряет свою магическую силу, поскольку параметры для задач всегда будут тянуться из Gradle-кеша. Однако для больших проектов отсутствие кеша может быть проблемой, поскольку время сборки "на холодную" способно занимать несколько десятков минут. Если это ваш случай, и вы сталкивались с проблемами инвалидации кеша, напишите в комментариях.

Бонус для дочитавших до конца: Gradle-плагины

Gradle-плагины являются своеобразными контейнерами, в которых может содержаться логика инициализации, конфигурации и сборки. Как правило, плагины используются для внедрения какой-то законченной бизнес-логики в Gradle. На самом деле, плагин есть ни что иное как обычный интерфейс с одним методом apply, где в качестве generic-типа чаще всего выступает Project:

public interface Plugin<T> {  void apply(T target);}

Для написания плагина можно пойти тремя путями. Каждый из них рассмотрим в следующей статье, а сейчас лишь скажу что самый простой из них мы уже видели в разговоре про то, куда еще можно вынести общую логику билдскриптов. Да, это есть ни что иное, как script-плагин для Gradle.

Пожалуй, самыми яркими примерами будут являться представители из Android Gradle Plugin: "com.android.application" и "com.android.library". Подключая их в билдскрипты, мы получаем возможность собирать и конфигурировать сборку Android-приложений.

Подключать плагины в наши билдскрипты мы можем с помощью apply:

apply(plugin = "com.android.application")

, или в блоке plugins:

plugins {  `application`}

На примере выше видно, как красиво Kotlin позволяет подключать плагины с помощью extension-функций. По аналогии можно написать свои экстеншены и использовать их для подключения плагинов. Красота!

Кому уже сейчас хочется попробовать себя в разработке плагинов, добро пожаловать в документацию. Также можно посмотреть доклад с AppsConf 2019. В нем раскрываются способы реализации плагинов, а также идет разговор об областях их применения. Возможно, после просмотра вы поймете, что с их помощью вам будет проще решить производственную задачу.

Заключение

Gradle действительно мощный инструмент для сборки проектов, хоть и работа с ним не всегда проста. После появления возможности использовать Kotlin для реализации своих Gradle-задумок, магия постепенно начинает уходить и на stackoverflow хочется заходить реже. К тому же Gradle ведут работу над созданием хорошей документации, что, несомненно, очень радует (правда, её космический объём не остановит только настоящих энтузиастов).

И конечно же, Gradle это open-source. Можете с удовольствием скрасить один из вечеров в репозитории Gradle на Github.

К сожалению, показать весь Gradle в одной, и даже в двух статьях очень сложно. Думаю, теперь самое время ответить для себя на риторический вопрос - на самом ли деле всё настолько серьёзно?

В следующей статье хотел бы подойти к практике и рассмотреть реализацию плагинов, а также рассказать про то, для чего мы их используем в своем проекте. Что еще вы бы хотели видеть в следующей статье? Пишите в комментариях любые замечания и предложения как по текущей статье, так и по будущим, буду рад!

Список докладов

Степан Гончаров - Gradle [A-Z]. Доклад про Gradle под капотом, объясняет способы реализации Gradle-тасок и (о боже) про недостатки Kotlin DSL.

Stefan Oehme - Composite Builds with Gradle. Доклад про суть композитной сборки в Gradle и способах ее применения (на английском).

Филипп Уваров - Gradle Plugin Development. Доклад с AppsConf про разработку Gradle-плагинов. Раскрывает область применения плагинов, способы их реализации и по каждому их преимущества/недостатки.

Подробнее..

Дайджест интересных материалов для мобильного разработчика 388 (28 марта 4 апреля)

04.04.2021 12:12:21 | Автор: admin
В новой недельной подборке архитектурные паттерны и новая WWDC21, распознавание карт и 13 подвохов мобильного приложения, траты пользователей, тестирование иконок и многое другое!



Этот дайджест доступен в виде еженедельной рассылки. А ежедневно новости мы рассылаем в Telegram-канале.

iOS

Как меня Apple навечно забанил
Архитектурные паттерны в iOS: страх и ненависть в диаграммах. MV(X)
Compositional Layout: стоит ли игра свеч?
Почему мы не обновляли приложение ВКонтакте для iPad пять лет, а теперь обновили
Подключаем нагрудный датчик пульса по Bluetooth на Swift
Настало время офигительных историй[1/2]
Разрабатываем своего первого голосового ассистента на iOS
App Store отклоняет приложения, использующие сторонние SDK, которые собирают пользовательские данные
WWDC21 пройдет онлайн с 7 по 11 июня
Как создавать виджеты с WidgetKit
7 эффективных ключевых слов для оптимизации вашего Swift-кода
Представляем Epoxy для iOS
Синглтон против внедрения зависимостей в Swift
Удаляем фон в изображениях на Swift с помощью Core ML
2 iOS-инструмента для обнаружения мертвого и клонированного кода
Как перенести Луну в вашу комнату с помощью ARKit
Три типа дыр в безопасности, которые я вижу во многих iOS-приложениях
SwiftUI Animations: анимации на SwiftUI
ProgressHUD: анимированные иконки

Android

Доказательное программирование
CameraX+ML Kit для распознавания номера карты в действии
Google ограничивает, какие приложения могут видеть другие установленные приложения
Jetpack Activity Result API. Часть 2. Как работает под капотом
Google выпустил сканер документов Stack
Android Broadcast: как попасть на стажировку в Redmadrobot
Отладка скриптов сборки и плагинов Gradle [IntelliJ/Android Studio]
Самое простое руководство по пониманию Gradle!
Непустые списки в Kotlin
Более безопасный способ сбора потоков из пользовательских интерфейсов Android
Системный сбой в Android WebView: как разработчики могут избежать такой ошибки
Знакомимся с поведением ваших зависимостей
Запускаем ARM-приложения в эмуляторе Android
Реализация Snackbar для отмены действий в Jetpack Compose
Motion Layout: создание простой анимации Recycler View
Десять #AndroidLifeHacks, которые вы можете использовать прямо сейчас
LabeledSeekSlider: настраиваемый слайдер
Flux: погода на Jetpack Compose
KanbanBoard: канбан-доска на Kotlin

Разработка

13 подвохов мобильного приложения, о которых лучше знать до старта разработки
Осмысленные интерфейсы
TestOps: писать автотесты недостаточно
Какие вопросы ожидать на позицию автоматизатора и причем тут сортировка?
Дайджест релизов мобильной разработки Mail.ru Group за время пандемии
Storybook + Flutter = storybook_flutter
Паттерны и Методологии Автоматизации UI: Примеры из жизни
make sense: О карьерном росте до руководителя, необходимых навыках, лидерстве и доверии
Podlodka #208: операционные системы
GitHub обновил уведомления в приложении
Дизайн приложений: примеры для вдохновения #38
Google улучшает установку PWA
20 обязательных навыков для разработчиков 2021
CoScreen создает общую среду для разработки
Опыт 10,000+ экранов: 10 советов от ведущего продуктового дизайнера
Как мы разработали приложение за 300 тысяч и чуть не потеряли 4 млн рублей
Проектирование микро-взаимодействий в Figma с помощью интерактивных компонентов
Это начало конца PWA?
Бесшовная разработка мультиплатформенных приложений с Flutter
4 простых совета, чтобы стать более ценным разработчиком
6 основных различий между Junior и Senior разработчиком
Как мы ускорили нашу систему Continuous Integration на 50%
Как спланировать успех при запуске нового технического проекта
7 уроков моего пути от Junior-разработчика до Senior за 2 года
10 самых популярных вопросов на собеседовании по системному дизайну
ГОНКА к маркетинговому успеху
Инструменты для создания мобильных приложений с дополненной реальностью (AR)
Основы GitHub Actions
4 ошибки, которые я сделал как программист, но мне пришлось стать техническим директором, чтобы увидеть их
Разработка программного обеспечения игра проигравших
Как реализовать покупку подписок в приложении на Flutter
Доставка лучшего программного обеспечения быстрее: как мы сэкономили полмиллиона долларов
Чем мы можем делиться в Kotlin MultiPlatform: модули? данные? экраны?
Создайте свое приложение на Flutter за 5 дней

Аналитика, маркетинг и монетизация

Маркетологи в мобайле: Игорь Посталенко (Тинькофф)
Средний пользователь iPhone в США потратил в 2020 году на приложения $138
Траты пользователей на приложения и игры поставили новый рекорд в 1 квартале 2021
Прекращается работа Facebook Analytics
TechIntern: биржа IT студентов
A/B-тестирование иконок: опыт DEVGAME
Российский игровой рынок в 2020 году вырос на 35%
Lookout for Metrics от Amazon оценивает бизнес с помощью машинного обучения
Доверяете ли вы статистике от Google?
Яндекс попросил Samsung и других производителей не устанавливать неудаляемые приложения компании

AI, Устройства, IoT

Что такое IoT и что о нем следует знать
Microsoft поставит 120,000 HoloLens в армию
Snapchat готовит новые AR-очки Spectacles
IoT-устройства переведут на российский софт

Предыдущий дайджест. Если у вас есть другие интересные материалы или вы нашли ошибку пришлите, пожалуйста, в почту.
Подробнее..

Использование Spring Cloud Stream Binding с брокером сообщений Kafka

15.04.2021 16:11:54 | Автор: admin

Всем привет! Меня зовут Виталий, я разработчик в компании Web3Tech. В этом посте я представлю основные концепции и конструкции платформы Spring Cloud Stream для поддержки и работы с брокерами сообщений Kafka, с полным циклом их контекстного unit-тестирования. Мы используем такую схему в своем проекте всероссийского электронного голосования на блокчейн-платформе Waves Enterprise.

Являясь частью группы проектов Spring Cloud, Spring Cloud Stream основан на Spring Boot и использует Spring Integration для обеспечения связи с брокерами сообщений. При этом он легко интегрируется с различными брокерами сообщений и требует минимальной конфигурации для создания event-driven или message-driven микросервисов.

Конфигурация и зависимости

Для начала нам нужно добавить зависимость spring-cloud-starter-stream-kafka в build.gradle:

dependencies {   implementation(kotlin("stdlib"))   implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion")   implementation("com.fasterxml.jackson.module:jackson-module-kotlin")   implementation("org.springframework.boot:spring-boot-starter-web")   implementation("org.springframework.cloud:spring-cloud-starter-stream-kafka")   testImplementation("org.springframework.boot:spring-boot-starter-test")   testImplementation("org.springframework.cloud:spring-cloud-stream-test-support")   testImplementation("org.springframework.kafka:spring-kafka-test:springKafkaTestVersion")}

В конфигурацию проекта Spring Cloud Stream необходимо включить URL Kafka-брокера, имя очереди (топик) и другие параметры биндинга. Вот пример YAML-конфигурации для сервиса application.yaml:

spring: application:   name: cloud-stream-binding-kafka-app cloud:   stream:     kafka:       binder:         brokers: 0.0.0.0:8080         configuration:           auto-offset-reset: latest     bindings:       customChannel:                   #Channel name         destination: 0.0.0.0:8080      #Destination to which the message is sent (topic)         group: input-group-N         contentType: application/json         consumer:           max-attempts: 1           autoCommitOffset: true           autoCommitOnError: false

Концепция и классы

По сути, мы имеем дело с сервисом, построенным на Spring Cloud Stream, который прослушивает входящую очередь, используя биндинги (SpringCloudStreamBindingKafkaApp.kt):

@EnableBinding(ProducerBinding::class)@SpringBootApplication  class SpringCloudStreamBindingKafkaApp fun main(args: Array<String>) { SpringApplication.run(SpringCloudStreamBindingKafkaApp::class.java, *args) }

Аннотация @EnableBinding указывает сервису на биндинг как входящего, так и исходящего канала.

Здесь необходимо уточнить ряд концепций.

Binding интерфейс, в котором описаны входящие и исходящие каналы.
Binder имплементация middleware для сообщений.
Channel представляет канал для передачи сообщений между middleware и приложением.
StreamListeners методы обработки сообщений в виде бинов (beans), которые будут автоматически вызваны после того, как MessageConverter осуществит сериализацию или десериализацию между событиями в middleware и типами объектов в домене DTO.
Message Schema схемы, используемые для сериализации и десериализации сообщений. Могут быть прочитаны из источника или динамически загружены.

Тестирование

Чтобы протестировать сообщение и операции send/receive, нам нужно создать как минимум одного producer и одного consumer. Вот простейший пример того, как это можно сделать в Spring Cloud Stream.

Инстанс бина Producer будет отправлять сообщение в топик Kafka, используя биндер (ProducerBinding.kt):

interface ProducerBinding {   @Output(BINDING_TARGET_NAME)   fun messageChannel(): MessageChannel}

Инстанс бина Сonsumer будет слушать топик Kafka и получать сообщения.

ConsumerBinding.kt:

interface ConsumerBinding {   companion object {       const val BINDING_TARGET_NAME = "customChannel"   }   @Input(BINDING_TARGET_NAME)   fun messageChannel(): MessageChannel}

Consumer.kt:

@EnableBinding(ConsumerBinding::class)class Consumer(val messageService: MessageService) {   @StreamListener(target = ConsumerBinding.BINDING_TARGET_NAME)   fun process(       @Payload message: Map<String, Any?>,       @Header(value = KafkaHeaders.OFFSET, required = false) offset: Int?   ) {       messageService.consume(message)   }}

Мы создали брокер Kafka с топиком. Для тестирования будем использовать встроенную Kafka, доступную нам с зависимостью spring-kafka-test.

Функциональное тестирование с MessageCollector

Мы имеем дело с имплементацией биндера, позволяющей взаимодействовать с каналами и получать сообщения. Отправим сообщение в канал ProducerBinding и затем получим его в виде payloadProducerTest.kt:

@SpringBootTestclass ProducerTest {   @Autowired   lateinit var producerBinding: ProducerBinding   @Autowired   lateinit var messageCollector: MessageCollector   @Test   fun `should produce somePayload to channel`() {       // ARRANGE       val request = mapOf(1 to "foo", 2 to "bar", "three" to 10101)       // ACTproducerBinding.messageChannel().send(MessageBuilder.withPayload(request).build())       val payload = messageCollector.forChannel(producerBinding.messageChannel())           .poll()           .payload       // ASSERT       val payloadAsMap = jacksonObjectMapper().readValue(payload.toString(), Map::class.java)       assertTrue(request.entries.stream().allMatch { re ->           re.value == payloadAsMap[re.key.toString()]       })       messageCollector.forChannel(producerBinding.messageChannel()).clear()   }}

Тестирование с брокером Embedded Kafka

Используем аннотацию @ClassRule для создания брокера. Так мы сможем поднять сервера Kafka и Zookeeper на случайном порте перед началом теста и выключить их, когда тест завершится. Это избавляет нас от необходимости в рабочем инстансе Kafka и Zookeper на всё время проведения теста (ConsumerTest.kt):

@SpringBootTest@ActiveProfiles("test")@EnableAutoConfiguration(exclude = [TestSupportBinderAutoConfiguration::class])@EnableBinding(ProducerBinding::class)class ConsumerTest {   @Autowired   lateinit var producerBinding: ProducerBinding   @Autowired   lateinit var objectMapper: ObjectMapper   @MockBean   lateinit var messageService: MessageService   companion object {       @ClassRule @JvmField       var embeddedKafka = EmbeddedKafkaRule(1, true, "any-name-of-topic")   }   @Test   fun `should consume via txConsumer process`() {       // ACT       val request = mapOf(1 to "foo", 2 to "bar")       producerBinding.messageChannel().send(MessageBuilder.withPayload(request)           .setHeader("someHeaderName", "someHeaderValue")           .build())       // ASSERT       val requestAsMap = objectMapper.readValue<Map<String, Any?>>(objectMapper.writeValueAsString(request))       runBlocking {           delay(20)           verify(messageService).consume(requestAsMap)       }   }}

Заключение

В этом посте я продемонстрировал возможности Spring Cloud Stream и использования его с Kafka. Spring Cloud Stream предлагает удобный интерфейс с упрощенными нюансами настройки брокера, быстро внедряется, стабильно работает и поддерживает современные популярные брокеры, такие как Kafka. По итогам я привел ряд примеров с unit-тестированием на основе EmbeddedKafkaRule с использованием MessageCollector.

Все исходники можно найти на Github. Спасибо за прочтение!

Подробнее..

Аналог R.string в android приложении

20.06.2021 12:09:43 | Автор: admin

Всем привет! Меня зовут Владимир, я Android-разработчик в компании Альфа-Капитал. Наверняка любое мобильное приложение в процессе развития нуждается в гибкой настройке текстовой информации за счет серверной части. В этой статье я поделюсь мыслями и решениями нашей команды. Также я покажу пример генерации кода с помощью gradle скрипта, сильно упростивший жизнь android команде.

С чего всё начиналось

В нашем приложении довольно много текстовой информации для пользователей, начиная от подсказок и описаний и заканчивая дисклеймерами и большими информационными экранами. Мы работаем в сфере финансовых технологий, и порой чтобы соответствовать требованиям Центробанка необходимо быстрое обновление информации. Это иногда нужно делать и по запросам наших юристов. Кроме того, порой требуется экран, где на первой неделе будет один текст, а на второй неделе другой текст.

Сразу оговорюсь: в текущем виде (и в ближайшем будущем) бизнес ориентирован на Россию, поэтому в проекте нет необходимости поддерживать несколько языков в приложении.

Столкнувшись с проблемой обновления информации, мы стали задумываться о хранении текстов (которые в перспективе придется корректировать) не в приложении, а на сервере. Часть (например, большой список дисклеймеров для юристов) удалось вытащить. Но оставалось еще множество строк, которые сложно как-то объединить и для которых не хотелось каждый раз делать сетевой запрос и экран загрузки.

Сначала мы попробовали держать тексты на Firebase. По функциональности такое решение вполне подходило, к тому же оно добавляло версионирование и возможность создания a/b тестов. Вскоре стало ясно, что это все-таки не то, что нам нужно. Тогда мы сформулировали свои требования:

  1. Удобный и единый источник текстов для всех мобильных платформ (android/ios);

  2. Обновление текстов в рантайме при старте приложения (для обновления важных мест без выпуска фиксов/релизов);

  3. В приложении мы не должны страдать от необходимости выполнения сетевого запроса или показа лоадинга ради загрузки лексем;

  4. Обновление текстов должно быть доступно без вмешательства разработчиков (т.е. чтобы условный аналитик / тестировщик смог спокойно обновить тексты при необходимости);

  5. Максимально простое создание дефолтных значений, которые будут лежать в приложении.

Firebase Remote Config не подошел слишком хороший функционал для простых текстов. У нас быстро получился большой список необходимых лексем, а их добавление / редактирование становилось слишком сложным. Нелегкой задачей была и установка дефолтных значений в приложении. Нам хотелось чего-то попроще.

Мы решили, что самым оптимальным будет объединение необходимых текстов в JSON файл. Почему именно JSON, а не XML, который кажется более нативным для Android? Так показалось удобней для обеих команд (Android и iOS). JSON понятный формат данных, его легко разберет любая платформа. Этот файл можно легко скачать, положить в проект и получить дефолтные данные.Схема работает и в обратную сторону. Пришла задача с новым текстом? Нужно добавить новые строки в проект, закинуть этот же JSON c ключами на сервер.

Пример json файла:

{ "screen1_text1": "Text 1", "screen1_text2": "Text 2 \nnext line", "screen1_text3": "Text 3", "screen1_text4": "Text 4"}

Первая реализация

В итоге мы получили JSON файл с текстами на сервере, этот же файл храним в проекте в папке assets. Сначала мы создали объект Lexemator, у которого можно по ключу запросить какой-то текст. При старте приложение подкачивает тексты с сервера в Lexemator, а если что-то пошло не так, берет дефолтные текста из папки assets.

object Lexemator {fun getString(key: String): String}

Использование в коде выглядит следующим образом:

class MainActivity : Activity() {   override fun onCreate(savedInstanceState: Bundle?) { ...       val textView = findViewById<TextView>(R.id.text1)       textView.text = Lexemator.getString("screen1_text1")   }}

По сравнению с Firebase стало лучше, но был один существенный недостаток: достаточно одной ошибки в ключе, и мы получаем не тот текст. Нам хотелось получить статическую поддержку, похожую на R.string, где Android Studio подсказывала бы константы и проект не компилировался бы при ошибке.

Это была предыстория, теперь переходим к коду.

Gradle - наше всё

Раз в проекте имеется JSON файл с текстами, значит, уже на этапе сборки мы понимаем, какие есть ключи для разных текстов. Если каких-то ключей нет, то они либо не нужны, либо их всё равно надо добавить для дефолтных значений. Выходит, на этапе сборки можно сгенерировать код, который будет содержать ключи для текстов. Мы решили сделать это с помощью gradle task.

Ниже представлен получившийся скрипт

import groovy.json.JsonSlurper/*** Таска ищет файл с текстами с названием strings.json и создает объект LL.* Для каждого текста из strings.json создает переменную LL.key внутри объекта** Если файла strings.json не существует - скрипт кинет Exception.** Чтобы сгенерить текста заново, достаточно перебилдить проект, или изменить файл strings.json*/def classFileName = "LL"def stringsFileName = "strings.json"def filePath = project.rootProject.getProjectDir().path + "/app/src/main/assets/json"def outputPath = project.rootProject.getProjectDir().path + "/app/build/generated/strings"def inputFile = new File(filePath + "/${stringsFileName}")def outputFile = new File(outputPath + "/${classFileName}.kt")task createStrings {   /**    * Если что-то изменится в inputFile, то при следующей сборке будет заново сгенерирован    * outputFile.    * Если ничего не изменилось, и outputFile уже есть, таска будет помечена "UP-TO-DATE" и    * не будет выполняться лишний раз.    */   inputs.file(inputFile)   outputs.file(outputFile)   doLast {       if (!inputFile.exists()) {           throw RuntimeException("файл ${inputFile} не найден")       }       println("Начало создания файла ${outputFile.path}")       outputFile.delete()       outputFile.createNewFile()       /**        * Тройные кавычки нужны для того, чтобы перевод строки (\n) в strings.json        * не ломал строки в созданном LL.kt файле.        */       def s1 = """package com.obolonnyy.lexemator//<!--Этот файл создан автоматически gradle скриптом из create_strings.gradle -->object ${classFileName} {"""       def s2 =               """      fun addLexems(map: Map<String, String>) {       map.forEach { k, v -> addLexem(k, v) }   }   fun addLexem(key: String, value: String) {       when(key) {"""       def json = new JsonSlurper().parse(inputFile)       assert json instanceof Map       json.each { entry ->           s1 += "    var ${entry.key} = \"\"\"${entry.value}\"\"\"\n        private set\n"           s2 += "            \"${entry.key}\" -> ${entry.key} = value\n"       }       def result = s1 + "\n\n" + s2 + """        }   }}"""       outputFile.write(result)       println("файл ${outputFile.path} успешно создан.")   }}/*** Показываем, что созданный файл теперь тоже является частью проекта.* Без этого мы не сможем использовать созданный LL.kt класс в своих классах.*/android {   sourceSets {       main {           java {               srcDirs += outputPath           }       }   }}

Скрипт создает object LL, у которого есть список ключей (поля типа String, с приватным сеттером) с дефолтными значениями, и две функции для обновления значения ключей. При старте приложения мы запрашиваем с сервера текста и обновляем значения через функцию addLexems().

Комментарий про название объекта LL: сначала мы думали назвать L (от слова Lexemator), чтобы было привычно как с R, но мешала константа android.icu.lang.UCharacter.GraphemeClusterBreak.L.Поэтому, мы не придумали ничего лучше, чем назвать класс LL.

Сгенерированный объект LL выглядит следующим образом:

//<!--Этот файл создан автоматически gradle скриптом из create_strings.gradle -->object LL {   var screen1_text1 = """Text 1"""       private set   var screen1_text2 = """Text 2next line"""       private set   var screen1_text3 = """Text 3"""       private set   var screen1_text4 = """Text 4"""       private set     fun addLexems(map: Map<String, String>) {       map.forEach { k, v -> addLexem(k, v) }   }   fun addLexem(key: String, value: String) {       when(key) {           "screen1_text1" -> screen1_text1 = value           "screen1_text2" -> screen1_text2 = value           "screen1_text3" -> screen1_text3 = value           "screen1_text4" -> screen1_text4 = value       }   }}

Пример использования объекта LL в коде выглядит следующим образом:

class MainActivity : Activity() {   override fun onCreate(savedInstanceState: Bundle?) {...       val textView = findViewById<TextView>(R.id.text1)       textView.text = LL.screen1_text1   }}

Получилось довольно просто и привычно.

Итоги

Мы сделали механизм управления текстами в приложении без необходимости перевыпуска релиза. Тексты хранятся на сервере, обновляются через git репозиторий. Для бизнеса планируется создать админку для управления текстами. Для Android команды мы сделали удобный механизм работы с этими текстами и статическую поддержку текстов в коде. Сейчас наш JSON файл насчитывает 180 различных строк, и найденное решение всех устраивает.

Рабочий пример можно найти по ссылке.

Подробнее..

Если у вас не работает Spring BootJar

28.12.2020 00:15:11 | Автор: admin

Проблема с загрузкой Spring Boot Jar


image


Сталкивались ли вы с проблемой запуска нового загрузочного архива Spring Boot?


Вообще, новация в этом направлении уже не первая, стандартов особых нет. Поэтому многие огребают проблемы и решают их на форумах и стек-оверфлоу.
Если вы также столкнулись с проблемой, я помогу её решить. В таком случае читаем дальше.


Итак, проблем с BootJar на самом деле хватает. Особенно учитывая, что уже минимум три версии формата поменялось.


Здесь я расскажу о часто встречающемся случае с потерей ресурсов. Конкретнее в моём случае с потерей JSP шаблонов. Почему JSP? Мне так привычнее, проект я по-быстрому начал с них, и не думал, что будут такие проблемы.


Итак структура проекта (стандартный веб):


/src/main/    java/    resources/        static/            some.html        public/    webapp/        WEB-INF/jsp            index.jsp

По заявлению создателей BootJar / BootWar, jsp не поддерживается толком в новом BootJar формате. Но совместимость должна быть в BootWar. На это я и надеялся, когда ваял код. Пока ваял, никаких проблем всё запускается, всё работает, как обычно, в общем. BootRun отрабатывает, только опции успевай подставлять.


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


Итак, дубль раз. Чуть ли не в первый раз запускаю задачу BootJar. Ярхив готов, деплоим Готово! Сигнала нет, сыпет ошибки 302 + 404 (это авторизация не находит вью). Но это пока не понятно.
Отключаем Секурити всё равно ничего не находит, кроме голимой статики, и то не всей, а только из webjars. ???


Дубль два. Догадываясь о несовместимости jsp и BootJar, пакуем BootWar. Деплоим Не работает. Не помню точно, но примерно то же самое в результате.


Хм, странно. Запускаем BootJar локально всё работает. Чудеса.


Выяснилось: Spring Boot слишком умный, он при запуске из каталога разработки даже релизного ярника всё равно все тащит из каталога разработки. Из другого запускаем перестаёт работать. Фух! Ну хоть отлаживать можно.


Что и делам. Выясняется ресурсы BootJar запакованные не из библиотек (webjars), а из проекта, не включаются в перечисление, и это, оказывается, по дизайну! Подробности здесь.


Вернее не так есть спец-ресурсы в каталогах типа static/, public/. И они вроде находятся, если объявить. Но jsp не видит в упор хоть тресни. И дело не в том, что не там лежат. Оказалось, что Томкат (в нашем плохом случае), грузит jsp особым механизмом после редиректа. И сами jsp можно загрузить без рендеринга, если правильно задать их положение в настройках
spring.resources.static-locations


Но это нас не устраивает.
Оказалось, что при использования встроенного томката, ресурсы вью он грузит отдельно и в первую очередь своей старой встроенной логикой, которую Спрингисты настраивать не научились. А этой логике нужен либо архив Вар, либо он же распакованный (почему кстати при разработке нормально отрабатывает расположение webapp/), либо ресурсы из библиотек, которые прекрасно видны, если правильно упакованы в изначальных либах нужно чтоб лежали в META-INF/resources, как в стандарте сервлетов. Последнее работает даже внутри BootJar. Удивительно.


Почему не сработал распакованный архив? Ё-маё, на амазоне он распакуется черти-куда, и приложение про это место ничего не знает, если не сказать. Но хардкодить пути плохая привычка. Сам ярник запускается затем безо всякой распаковки, хотя вроде права позволяют распаковать прям на месте. Ну да ладно, способ пролетел.


Почему не работает способ с Вар-архивом? Ё-маё, Амазон решил, что лучше меня знает, что я буду грузить именно в яр-формате, хотя интерфейс заявлет о готовности съесть варник. Он этот варник переименовывает в ярник, умник такой. А Томкат не умничает, он смотрит расширение: не Вар ну тогда, извините, это не мой случай.


В итоге корень деплоя не находится. И ресурсы оттуда тоже. Ресурсы из статики не грузятся, потому что ищутся в корне и ресурсных либах, а не в classpath.


Ладно, проблема понятна. Решение?


Было три варианта.


  1. Сделать свой spring-загрузчик ресурсов. Вариант отпал, поскольку, как я уже сказал, Томкат отрабатывает jsp до их запуска.
  2. Прокинуть загрузку в Томкат. Стал прикидывать: надо расширить контекст spring-а, прокинуть пути в контекст Томката, там ещё раза два переложить непонятно, насколько сложно, и можно ли без изменения самого томката. Спрингисты не осилили, и я не хочу.
  3. Вариант попроще пакуем ресурсы в ресурсную либу в BootJar.

Вот о нём подробнее. Пихать всё в отдельный модуль, как предлагали здесь, не хотелось.
Делаем отдельной задачей в Gradle.
Создаём конфигурацию.


sourceSets {    jsp {        resources.source(sourceSets.main.resources);        resources.srcDirs += ['src/main/webapp'];    }    jmh {        .. ..    }}

Сама задача


task jsp(type: Jar, description: 'JSP Packaging') {    archiveBaseName = 'jsp'    group = "build"    def art = sourceSets.jsp.output    from(art) {        exclude('META-INF')        into('META-INF/resources/')    }    from(art) {        include('META-INF/*')        into('/')    }    dependsOn(processJspResources)}

Задача processJspResources уже создана, её не надо делать. Ставим всё в зависимость и пакуем:


bootJar {    dependsOn(jsp)    bootInf.with {        from(jsp.archiveFile) {            include('**/*.jar')        }        into('lib/')    }}

Как добавить другим способом (прямым), не нашёл подключить в зависимости конфиг jspImplementation самого же проекта нельзя, а хотелось бы. Но если все же из другого модуля забирать, то вот так ещё делаем:


artifacts {    jspImplementation jsp}

Всё, теперь имеем ресурсную либу, которую по всем стандартам томкат должен грузить, и он грузит. Запускаем, как BootJar.

Подробнее..

Хамелеон, которого мы создали и приручили

01.12.2020 14:07:50 | Автор: admin

Речь, конечно, пойдет не о харизматичных ящерках, а о нашем собственном инструменте автоматизированного тестирования. Представляем вашему вниманию кейс создания фреймворка Хамелеон и рассказ о его функциональности.


Его появлению предшествовало 15 лет практики тестирования в компании IBS AppLine* (лидера российского рынка аутсорсинга услуг тестирования по версии TAdviser за 2018 год на минуточку!). На базе этих знаний и экспертизы мы задались целью ускорить старт проектов, повысить качество тестирования, упростить введение в работу новичков. Решение должно позволить автоматизировать функциональное тестирование веб, мобильных, десктоп-приложений и различных видов API.




В общем, исследовательский центр IBS AppLine Innovation** суммировал весь опыт компании и создал Хамелеон инструмент для автоматизации функционального тестирования. Делался с использованием языка программирования Java и инструментов Cucucmber, Selenium, Appium, Winium, Spring. Этот фреймворк:


  • позволяет сэкономить до 30% времени разработки и сопровождения тестов;
  • снижает риск ошибок за счет автоматического заполнения параметров этапов теста;
  • помогает обучать и привлекать к разработке тестов стажеров, владеющих только базовыми навыками программирования;
  • при переходе с проекта на проект дает возможность использовать одну и ту же библиотеку шагов, а также единый подход к написанию тестов;
  • может работать с экзотическими компонентами тестируемых систем благодаря удобству подключения дополнительных расширений.

Теперь подробнее о функционале



Как устроен Хамелеон


Вот несколько особенностей нашего фреймворка:


  • Это многомодульный maven-проект, который включает модули тестирования web, мобильных приложений, SAP-приложений, баз данных, Rest API и web-сервисов. Необходимые модули подключаются к проекту.
  • Мы взяли проверенные временем оpen source-инструменты, в том числе Selenium, Appium, Winium, и удачно их объединили в одном решении.
  • Для ускорения разработки автоматизированных тестов мы создали плагин для среды разработки IntelliJ IDEA. Получился полезный инструмент разработчика автоматизированных тестов. Плагин дополняет возможности IDEA, делая ее полноценным рабочим местом.
  • Для удобства разработки автоматизированных тестов для web-приложений мы создали расширение для браузера Google Chrome, которое позволяет добавлять элементы тестируемого приложения в проект прямо из браузера и имеет возможность записи автоматизированного теста в формате Cucumber методом Record&Playback.

Open source-библиотеки


В основе инструмента лежат оpen source-библиотеки Selenium, Appium, Winium, UIAutomation. Для разработки самих тестов используется фреймворк Cucumber, который позволяет писать на русском языке. Такой формат понятен и ручным тестировщикам, и не имеющим отношения к написанию конкретного теста специалистам автоматического тестирования, что снижает порог вхождения сотрудников в проект. Всему, что происходит в Cucumber, соответствуют свои Java-команды, так что при необходимости тесты можно разрабатывать на чистой Java.



Простота установки


Для разработки автоматизированных тестов с использованием Java на рабочую станцию устанавливаются Java JDK, Apache Maven/Gradle, IntelliJ IDEA, плагины для Intellij IDEA, Git Client. У начинающих специалистов это занимает много времени. Мы упростили процесс, разработав общий инсталлятор, который представляет собой .exe-файл с возможностью выбора необходимого ПО для установки на рабочее место:




Начало разработки


Для разработки автоматизированных тестов можно использовать готовые стартеры проектов. Стартеры это архетипы maven, которые содержат готовую структуру проекта. Они хранятся во внутреннем репозитории компании. При создании проекта в IntelliJ IDEA нужно лишь выбрать необходимые. Например, для разработки тестов, которые взаимодействуют с web-приложением и REST, необходимо подключить модули chameleon-selenium-cucumber и chameleon-rest-cucumber.




Немного о фреймворке


В основном автоматизированные тесты разрабатываются с помощью инструмента Cucumber, который позволяет писать тесты на русском языке. Автоматизированный тест состоит из следующих блоков:


  • переменные для тестовых сценариев;
  • наименование тестируемой функциональности;
  • тестовые сценарии, которые состоят из последовательных шагов действий с тестируемым приложением.

Пример автоматизированного теста:



# language: ru# Тестовые данные:  # $ФИО Иванов Иван Иванович  # $ссылка https://www.appline.ru/Функция: Заявка на обучение  Сценарий: Заявка на обучение    * страница "Главная" загружена    * выбран элемент коллекции "Меню" с параметрами:      | field        | operator | value             |      | Наименование | равно    | Start IBS AppLine |    * нажатием на кнопку "Наименование" загружена страница "Start IBS AppLine"    * поле "Имя" заполняется значением "#{ФИО}"    * поле "Ссылка на резюме" заполняется значением "#{ссылка}"    * выполнено нажатие на "Отправить"    * поле "Required field" видимо

Существуют шаблоны для работы с переменными: операции с датами, математические операции, выполнение кода и т.д. Например, для работы с текущей датой используется шаблон #now{дата; исходный_формат; смещение}. Предположим, в автоматизированном тесте необходимо проверить дату операции, которая была только что осуществлена. Такая проверка будет выглядеть так:


* значение поля "Дата операции" равно "#now{dd.MM.yyyy HH:mm}"

А, например, создать отложенную операцию, которая исполнится завтра:


* поле "Дата операции" заполняется значением "#now{dd.MM.yyyy;+1d}}"

Выполнить программный код можно с использованием шаблона #script{RESULT=выражение_java}. Например, удаление лишних символов в переменной будет выглядеть следующим образом:


* в переменной "Номер_счета" сохранено значение поля "Номер счета"* значение поля "Номер счета" равно "#script{RESULT = Номер_счета.replaceAll("", "")}"

В нашем фреймворке разработаны основные Cucumber-шаги, которые напоминают шаги из ручных тестовых сценариев. Они описывают действия, которые человек может совершить с тестируемым приложением: нажать на элемент, ввести значение в поле, прочитать значение, сравнить значения и т.д. Таких шагов примерно 30. Для часто повторяемых операций такие шаги объединяются в более крупный шаг.


Например, авторизация в приложении может описываться в 3 шага:


Когда заполняются поля:  | field  | value        |  | Логин  | test@test.ru |  | Пароль | 123123       |И выполнено нажатие на "Войти"Тогда страница "Главная" загружена

Или на основе этих шагов создается 1 шаг (пароль в этом случае хранится в отдельном файле с пользователями):


Дано авторизован пользователь "test@test.ru"

Все размеченные элементы тестируемого приложения имеют свой тип, например, Button, TextInput, Combobox, Checkbox и т.д., это позволяет использовать одни и те же Cucumber-шаги для работы с разными типами элементов. Например, есть шаг:


* поле "field" заполяется значением "value"

Он сможет работать как c текстовыми полями, так и чекбоксами, выпадающими списками и т.д. Это позволяет при разработке автоматизированного теста не задумываться о заполнении полей этим занимается сам фреймворк, мы просто указываем ему заполнить поле.



АРМ тестировщика или разработка теста


Стандартное представление проекта для разработки тестов содержит много классов и вспомогательных файлов, которые очень редко нужны при разработке самого автоматизированного теста. Чтобы упростить этот процесс мы постарались оградить тестировщика от этой информации и разработали плагин для Intellij IDEA. Его интерфейс стал рабочим местом тестировщика, и содержит следующие разделы:


  • список тестов,
  • объектный репозиторий,
  • список доступных действий,
  • настройки запуска,
  • документацию,
  • различные автокомплиты, ускоряющие процесс создания теста.


Список тестов


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




Репозиторий объектов


Традиционный и очень популярный инструмент разработчика автоматизированных тестов шаблон Page Object Pattern. При его использовании элементы тестируемого приложения описываются в отдельных java-классах, которые соответствуют страницам тестируемого приложения. На крупных проектах таких Page Object-классов становится очень много. Кроме того, иногда они еще и называются беспорядочно. В результате для больших проектов такой подход не очень удобен: неизбежно дублирование работы на проектах работает много специалистов, приходится расшифровывать работу коллег и анализировать разметку элементов, а это не всегда проходит гладко.


Поэтому мы создали репозиторий объектов тестируемого приложения. Он хранит все его элементы не в классах, а в одном или нескольких xml-файлах, а плагин для IntelliJ Idea показывает эти xml-файлы в виде древовидной структуры, в которой узлами являются страницы тестируемого приложения. Вместо разномастных английских названий мы используем русский язык, что упрощает работу тестировщиков, так им легче находить размеченные страницы и элементы, а также добавлять новые.


Приведем конкретный пример. Для одного крупнейшего отечественного банка (название по понятным причинам не называем) нами было разработано 20 автотестов, использовавших 78 страниц тестируемых приложений (достаточно длинный бизнес-процесс). В обычных условиях тестировщикам пришлось бы создать 78 java-классов и разметить на них более 2000 элементов. С Хамелеоном мы всю эту громаду открываем в древовидной структуре, в которой их легко и просто просматривать, перетаскивать, компоновать.




Существует два способа добавления элементов в репозиторий.


Добавление в среде разработки IntelliJ IDEA:



Интерфейс мы постарались сделать максимально удобным: из него можно добавлять, редактировать и удалять элементы, есть несколько видов сортировок и поиска элементов в дереве. При добавлении элемента необходимо указать его тип, наименование и локатор.


Также есть возможность добавлять элементы прямо из тестируемого приложения с помощью созданного нами расширения для браузера Google Chrome. Расширение самостоятельно определяет тип элемента, его наименование и подставляет локатор. Для этого достаточно навести мышкой на элемент. Расширение синхронизируется с плагином в IntelliJ IDEA, поэтому все, что происходит в браузере, передается в среду разработки. Так можно наполнять репозиторий объектов, проходя ручной тест в браузере. С помощью расширения можно проверить корректность локатора уже существующего в репозитории элемента.




Чтобы не было дублирования элементов, существует тип элемента Block. Например, у нас в тестируемом приложении есть блок Меню, и он доступен на всех страницах приложения: в этом случае в репозитории элементов мы размечаем его как страницу Меню, содержащую элементы меню, а на других страницах добавляем его как элемент с типом Block. Таким образом, получается переиспользование элементов, которые встречаются более чем на одной странице.



Разработка автоматизированного теста


Автотест состоит из последовательности шагов, имитирующих действия пользователя (нажатия на кнопку, заполнение поля). Все эти шаги уже есть во фреймворке, так что тестировщик может просто собрать тест из готовых кубиков.


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


При этом доступны подсказки не только для списка возможных шагов, но и для элементов, которые могут участвовать в выбранном действии. Плагин определяет страницу (контекст) и предлагает элементы из репозитория объектов или переменные для сucumber-шага, которые относятся к выбранной странице. Это исключает возможность ошибки при использовании еще не размеченного элемента страницы и ускоряет процесс разработки теста.




Второй способ разработки автоматизированного теста использование репозитория шагов. В плагине в древовидной структуре показываются шаги, которые можно перетащить методом Drag&Drop, как показано ниже. При использовании такого варианта создания автоматизированного теста тоже присутствуют подсказки элементов и переменных. Таким вариантом обычно пользуются начинающие сотрудники, которым проще искать необходимые шаги в репозитории, а не java-классах.




Третий способ разработки автотеста (самый удобный и популярный) с помощью нашего расширения для браузера Google Chrome. Плагин записывает действия пользователя на странице тестируемого приложения и конвертирует их в автоматизированный тест.


Например, пользователь логинится на сайте, нажимая для этого на кнопку Логин. Рекордер записывает шаг * выполнено нажатие на поле Логин. Пользователь заполняет поле Логин значением User, рекордер записывает шаг * поле Логин заполняется значением User и так далее. После этого получившийся автотест можно скопировать и вставить в среду разработки для редактирования. Плагин автоматически, на основе объектного репозитория, определяет, на какой странице находится пользователь, и выполняет поиск размеченных элементов. Если элемента в репозитории нет, то будет предложено его добавить.


Пока рекордер существует только для браузера. В процессе разработки находится рекордер для SAP GUI.



Запуск и отладка


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


  • подсветку элементов будут выделяться элементы, с которыми взаимодействует тест;
  • запись видео в случае ошибки к отчету будет прикреплено видео прохождения автоматизированного теста;
  • конфигурацию запуска браузер, в котором будет выполнен тест;
  • режим запуска с графическим интерфейсом или headless-режиме (без графического интерфейса);
  • закрытие браузера после теста удобно при разработке длинных тестов или локализации ошибок;
  • подключение к открытому браузеру чтобы начать автоматизированный тест не с начала, а с выбранного места, подключаясь к открытому браузеру.



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


Чтобы отслеживать ход выполнения теста, мы разработали консоль, куда перенаправляется весь лог с IntelliJ IDEA. Это позволяет контролировать процесс и понимать, что сейчас выполняет автоматизированный тест. Консоль отображается поверх содержимого экрана и не мешает выполнению теста. Есть возможность настраивать формат вывода в консоль (размер, шрифт, цвет и т.д.).




Документация


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




Тестирование API


Для тестирования API (REST и SOAP) также используется объектный репозиторий. В этом случае страницами будут endpoint, а элементами заголовки и тело запроса.




Во фреймворке реализованы базовые действия с API, при наборе действий происходит автоподстановка параметров вызова.




Отчет о выполнении автоматизированных тестов


В качестве инструмента отчетности был выбран Allure. При запуске тестов из среды разработки IntelliJ IDEA аllure-отчет можно открыть прямо из плагина.


Если использовать классическое подключение Allure к проекту, то отчет о прохождении автоматизированных тестов строится после выполнения всех тестов. Такой подход не очень удобен при запуске большого количества автоматизированных тестов, поэтому мы создали отдельный сервис, который строит и перестраивает отчет в процессе всего запуска, а также показывает статистику очереди тестов на выполнение. Таким образом, мы получаем информацию о дефектах тестирования в режиме реального времени и можем вовремя среагировать.



Результаты


Хамелеон помог нам не на словах, а на деле.


Во-первых, с ним старт проектов стал гораздо быстрее. Ранее на него могло уйти до трех недель, теперь же обычный срок два дня.


Во-вторых, унификация подхода позволила минимизировать время переключения тестировщика при переходе на другой проект.


В-третьих, теперь мы привлекаем ручных теcтировщиков для написания автотестов. Временные затраты на их обучение минимальны, а этапы разработки системы автоматизации функционального тестирования это удешевляет.


Наконец, мы получили еще одно преимущество на отечественном рынке: кодирование на русском языке легче освоить. Для этого у нас есть корпоративный университет, где мы обучаем будущих тестировщиков писать автотесты с нуля буквально за месяц. И в этом очень важную роль играет Хамелеон: с его помощью мы закладываем общую базу, на нем обязательно проводим несколько занятий по разработке тестов. Ежемесячно через этот курс проходит 5-10 человек, имеющих базовые знания языка программирования Java.



Планы на будущее


На данный момент команда разработки трудится над инструментом управления тестированием Кайман, который объединит в себе ручное, автоматизированное и нагрузочное тестирование. Отдельная команда выделена на изучение технологии машинного зрения, что в дальнейшем должно позволить отказаться от локаторов элементов и разрабатывать автоматизированные тесты для приложений, где стандартными средствами распознавание элементов невозможно. Об этих инструментах мы расскажем в следующих наших постах.





* IBS AppLine лидер российского рынка услуг тестирования и обеспечения качества программного обеспечения, разработчик решений для автоматизации тестирования. Компания была создана в 2001 году, в штате более 700 специалистов, общее количество проектов превысило 1500.


** IBS AppLine Innovation (ранее Аплана АйТи Инновации) была создана в декабре 2017 года как центр исследований и разработки компании IBS AppLine (ранее Аплана). Основу команды составили ее собственные опытные инженеры и разработчики.

Подробнее..

Материалы митапа для андроид-инженеров поиск проблем сборки, защита от них и работа с Gradle

16.03.2021 18:09:12 | Автор: admin

Недавно прошёл наш Android meetup, где ребята из платформенной команды Авито делились своим опытом работы с Gradle, показывали способы защиты от частых проблем при сборке проектов и рассказывали о нашем подходе к решению проблем.

Собрали в посте видеозаписи выступлений с таймкодами и ссылки на презентации спикеров.

Gradle в 2021: сonvention plugins workshop Дмитрий Воронин

Воркшоп о способе организации проектов,который позиционируется командой Gradle как идиоматический.

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

00:00 Представление спикера и темы

05:27 Проект, который будет примером в воркшопе

06:44 Лайвкодинг: пошаговая оптимизация проекта

28:31 Ответы на вопросы

Полезные ссылки:

Lint для сборки: как защищаться от проблем при сборке проекта Евгений Кривобоков

Женя рассказал, как мы защищаемся от довольно частых проблем при сборках проектов. В докладе нет частей про диагностику проблем и ускорение сборки, он сконцентрирован на том, что делать для предотвращения найденных проблем в будущем. Подход, о котором идёт речь, не универсальный, но довольно много где применим.

00:00 Представление спикера и темы

01:38 Какие бывают проблемы

09:05 Как контролировать окружение

14:16 Пример специфической проблемы для конкретного проекта и её решения

20:04 Зачем вообще писать свои проверки

22:35 Ответы на вопросы

Посмотреть презентацию Евгения

Gradle build scan на коленке Сергей Боиштян

На боевом примере Сергей разбирает, как мы упростили поиск ошибок в своих CI-сборках. Вы узнаете, как мы применяем продуктовый подход в решении проблем и немного о том, как работаем с Gradle.

Доклад будет полезен тимлидам в больших командах, разработчикам, которые настраивают CI/CD и разработчикам, которые решают любого рода проблемы.

00:00 Представление спикера, темы и её пользы

04:12 Поиск проблемы: разбираем на примере падения сборки

06:51 Определяем приоритет задач по RICE

14:36 Как мы искали решение проблемы

18:30 Пишем прототип с помощью TestProjectGenerator

26:11 Версия инструмента 1.0

30:30 Отдаём инструмент пользователям и смотрим на результат

34:02 Сравнение: как было и как стало

36:52 Ответы на вопросы

Посмотреть презентацию Сергея

На этом всё, до встречи на новых митапах!

Подробнее..

Avito Android meetup работа с Gradle и проблемы при сборке проектов

01.03.2021 12:17:20 | Автор: admin

Привет, Хабр! 11 марта в 18:00 по Москве мы проведём онлайн-митап для андроид-разработчиков.

В этот раз без внешних спикеров все доклады будут от инженеров нашей платформенной команды Speed, которые отвечают за быструю доставку изменений во всех андроид-приложениях Авито до пользователей. Ребята каждый день решают задачи, связанные с CI/CD и локальной работой с проектами, так что им есть, чем поделиться.

Доклады

Как правильно писать на Gradle в 2021 Дмитрий Воронин

В Gradle есть куча способов решить одну и ту же задачу, однако часть из них оптимальнее других. Я разберу несколько распространённых ошибок и покажу, как сделать лучше.

Как защищаться от частых проблем при сборке проекта Евгений Кривобоков

На сборку проекта влияет много неочевидных настроек. Они могут привести к медленной сборке или даже ошибкам в рантайме. Как защититься от неправильной настройки проекта? Что делать, если соглашения не работают или не получается написать Android lint проверку?

Посмотрим на примерах, какие неочевидные проблемы с окружением и сборкой бывают, и разберёмся, как с ними бороться.

Gradle build scan на коленке Сергей Боиштян

Мы сделали свой Gradle-плагин с генерацией HTML-странички с полезной информацией, чтобы наши разработчики не тратили время на поиск ошибок в CI-сборках. Я расскажу об этом инструменте от зарождения идеи до реализации и том, как подключить его в свой проект.

Доклад будет интересен тем, кто страдает от поиска ошибок в своём CI и developer experience инженерам, которые могут переиспользовать наше решение или идеи.

Когда закончим с докладами, можно будет неформально пообщаться в Зуме со спикерами. Ссылка появится в чате трансляции.

Пароли и явки

Трансляция на нашем ютуб-канале стартует в четверг 11 марта в 18:00 по московскому времени. На трансляции можно сразу нажать кнопку напомнить, чтобы ничего не пропустить.

Ещё вы можете добавить событие себе в календарь вот ссылка на событие для ICS-календаря, который обычно работает на айфонах и маках, а вот для Google-календаря. Если хочется получить напоминание со ссылкой на эфир на электронную почту, можно зарегистрироваться на таймпаде. Запись тоже будет опубликуем ссылку на неё в отдельном посте.

До встречи в онлайне!

Подробнее..

Советы по работе с Gradle для Android-разработчиков

25.03.2021 12:22:32 | Автор: admin

Всем привет! Я пишу приложения под Android, в мире которого система сборки Gradle является стандартом де-факто. Я решил поделиться некоторыми советами по работе с Gradle с теми, у кого нет чёткого понимания, как правильно структурировать свои проекты и писать build-скрипты.



Часто разработчики используют Gradle по наитию и не изучают целенаправленно, потому что не всегда хватает ресурсов на инфраструктурные задачи. А если возникают какие-либо проблемы, то просто копируют готовые куски build-скриптов из ответов на Stack Overflow. Во многом проблема кроется в сложности и чрезмерной гибкости Gradle, а также в отсутствии описания лучших практик в официальной документации.


Поработав больше пяти лет на аутсорсе, я видел много проектов разной сложности. И на всех этих проектах build-скрипты писались по-разному, где-то встречались не очень удачные решения. Я провел небольшую ретроспективу и резюмировал свой опыт в виде разных советов по использованию Gradle и рассказал их на одном из наших внутренних митапов. В статье я перевел эти советы в текст.



Небольшой оффтоп для тех, кому совсем ничего не понятно в Gradle-скриптах


Я заметил, что в Android-сообществе встречаются люди, которые могут годами разрабатывать приложения, но при этом не понимать, как работает Gradle. И достаточно продолжительное время и я был одним из них. Но однажды всё же решил, что гораздо проще потратить время на системное изучение Gradle, чем постоянно натыкаться на непонятные проблемы.


А так как лучший способ изучить что-то это попытаться рассказать об этом другим людям, то я подготовил рассказ об основах использования Gradle в контексте Android-разработки специально для тех, кто совсем не разбирается в теме. Так что, возможно, этот митап вам поможет.


Теперь к советам.


#1 Не редактируйте Gradle-скрипты через IDE


Project structure


Android studio умеет генерировать стартовый проект с базовой структурой и готовыми build-скриптами, а также удалять и добавлять модули в существующем проекте. А при редактировании Gradle-скриптов IDE нам заботливо подсказывает, что можно вносить изменения в скрипты через графический интерфейс в меню "File -> Project structure...". И в начале своей карьеры я вполне успешно пользовался этим инструментом. Но у него есть существенный недостаток: IDE не запускает честную фазу конфигурации Gradle и не смотрит на то, что формируется в памяти при сборке, а всего лишь пытается как-то парсить build-скрипты. Зачастую этот инструмент не распознает то, что было написано вручную, что, на мой взгляд, перечеркивает его пользу.


Мой совет: лучше не редактировать скрипты через IDE, а использовать редактор кода.


#2 Обращайте внимание на соглашение по именованию модулей


В Gradle имя проекта (модуля) берется из соответствующего поля name в объекте Project или названия директории, в которой он лежит. В своей практике я видел разные стили именования модулей, например, в camel- или snake- кейсе: MyAwesomeModule, my_awesome_module. Но есть ли какие-то устоявшиеся соглашения об именах модулей? Кажется, официальная документация Gradle ничего нам об этом не говорит. Но нужно принять во внимание тот факт, что проекты Gradle при публикации в Maven будут соответствовать один к одному модулям Maven. И у Maven есть соглашение, что слова в названиях модулей должны разделяться через символ -. То есть правильнее будет такое название модуля: my-awesome-module.


#3 Что выбрать: Kotlin vs Groovy


Изначально в Gradle для DSL использовался язык Groovy, но впоследствии была добавлена возможность писать build-скрипты на Kotlin. Возникает вопрос: что же сейчас использовать? И однозначного ответа на него пока что нет.


Лично я за использование Kotlin, так как не очень хочу только лишь ради build-системы изучать ещё один язык Groovy. Наверно, для всего Android сообщества DSL на Kotlin существенно понижает порог вхождения в Gradle. Кроме того, у build-скриптов на Kotlin лучше поддержка в IDE с автокомплитом, но, тем не менее, она все еще далека от идеала.


В качестве минуса Kotlin я бы выделил то, что могут встретиться какие-то старые плагины, которые изначально были заточены только под Groovy, и для их подключения придется потратить больше времени.


Если у вас старый большой проект с build-скриптами на Groovy, то могу посоветовать частично попробовать какие-то скрипты перевести на Kotlin, если вам понравится, то можно будет постепенно делать рефакторинг и переписывать все скрипты на Kotlin, не обязательно делать это единовременно.


#4 Как прописывать зависимости в многомодульных проектах


Возьмем небольшой пример проекта со следующей структурой:
Project structure


Основной модуль, из которого собирается apk, зависит от feature-модулей, а также в нём прописаны какие-то внешние зависимости. feature-модули, в свою очередь, содержат транзитивные зависимости, которые могут пересекаться с зависимостями в других модулях.


В чем проблема такого проекта? Здесь будет тяжело глобально обновлять зависимости в каждом из файлов. Очень легко забыть поднять версию в одном из скриптов, и тогда возникнут конфликты. По умолчанию Gradle умеет разрешать такие конфликты, выбирая максимальную версию, так что, скорее всего, сборка будет успешной (поведение можно менять через resolution strategy).


Но, конечно же, сознательно допускать конфликты версий не стоит, и для решения этой проблемы есть официальный способ, описанный в документации Gradle, использование которого я никогда не встречал на практике. Вместо него сообщество придумало достаточно простой трюк: прописывать строки с зависимостями где-то глобально и обращаться к ним из build-скриптов. Тут я хочу рассмотреть эти способы чуть подробнее.


Java platform plugin


Разработчики Gradle предлагают для описания зависимостей создать отдельный специальный модуль, где будут описаны только зависимости с конкретными версиями. К этому модулю надо применить java platform plugin. Далее подключаем этот модуль в остальные модули и при указании каких-то внешних зависимостей не пишем конкретную версию:
Java platform plugin


Такие platform-проекты можно даже публиковать на внешние maven репозитории и переиспользовать. В качестве минуса подхода можно назвать то, что при мажорных обновлениях библиотек часто меняются не только версии, но и названия модулей, и тогда все равно придется вносить изменения сразу в нескольких скриптах.


Перейду к общепринятым в сообществе способам описания зависимостей.


Описание зависимостей в extra properties


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


В корневом проекте описываем зависимости. Вот кусок build-скрипта из библиотеки Google, где зависимость возвращается функцией compatibility:


ext {    compileSdkVersion = 29    minSdkVersion = 14    targetSdkVersion = 29    androidXVersions = [            annotation: '1.0.1',            appCompat: '1.1.0'      // ...    ]}/** * Return the module dependency for the given compatibility library name. */def compatibility(name) {  switch (name) {    case "annotation":      return "androidx.annotation:annotation:${androidXVersions.annotation}"    case "appcompat":      return "androidx.appcompat:appcompat:${androidXVersions.appCompat}"

И обращаемся к ним из дочерних модулей:


dependencies {    api compatibility("annotation")    api compatibility("appcompat")    api compatibility("cardview")    api compatibility("coordinatorlayout")    api compatibility("constraintlayout")    api compatibility("core")    api compatibility("dynamicanimation")    // ...}

Описание зависимостей в скриптовом плагине


Описанный способ с extra properties можно немного модифицировать и вынести описание зависимостей в скриптовый плагин, чтобы не засорять корневой проект. А уже скриптовый плагин можно применить или к корневому, или ко всем дочерним проектам сразу (через allprojects {}), или к отдельным. Такой способ я тоже встречал.


Описание зависимостей в buildSrc


В buildSrc можно писать любой код, который будет компилироваться и добавляться в classpath build-скриптов. В последнее время стало популярно использовать buildSrc для описания там зависимостей. Например, в библиотеке Insetter Chris Banes так и делает.


Все, что нужно, это добавить синглтон со строками в buildSrc, и он станет виден всем модулям в проекте:


object Versions {    const val minSdk = 23    const val compileSdk = 29    const val kotlin = "1.4.20"    const val kotlinCoroutines = "1.4.0"    const val okHttp = "4.9.0"    const val retrofit = "2.9.0"    const val moshi = "1.11.0"}object Dependencies {    object Kotlin {        const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}"        const val reflect = org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}"    }    object Google {        const val materialComponents = "com.google.android.material:material:1.2.1"        const val googleAuth = "com.google.android.gms:play-services-auth:18.1.0"        const val location = "com.google.android.gms:play-services-location:17.1.0"        const val tasks = "com.google.android.gms:play-services-tasks:17.2.0"    }}

Использовать buildSrc для зависимостей очень удобно, так как будут статические проверки и автокомплит в IDE:


Но у этого решения есть один недостаток: любое изменение в buildSrc будет инвалидировать весь кэш сборки, что может быть несущественно для средних и маленьких проектов, но выливаться в серьезные проблемы для больших команд.


Композитные сборки


Можно достичь похожего результата со статическими проверками и автокомплитом, используя композитные сборки, при этом избавившись от проблемы инвалидации всего кэша. Я расскажу про него лишь кратко, а подробный гайд по миграции с buildSrc можно прочитать в статье из блога Badoo или статье от Josef Raska.


В композитных сборках мы создаем так называемые включенные сборки (included build), в которых можно писать плагины и подключать их в своих модулях. Включенные сборки описываются в файле settings.gradle.



Если мы хотим всего лишь подсунуть в classpath build-скриптов строки с зависимостями, то достаточно создать пустой плагин, а рядом с ним положить тот же файл с зависимостями, который мы использовали раньше в buildSrc:


// Build скрипт включенной сборки// ./dependencies/build.gradle.ktsplugins {    `kotlin-dsl`}repositories {    jcenter()    google()}gradlePlugin {    plugins {        register("dependencies") {            id = "dependencies"            implementationClass = com.rmr.dependencies.DependenciesPlugin        }    }}

// Плагин из включенной сборки// ./dependencies/src/main/kotlin/com/rmr/depenendencies/DependenciesPlugin.ktpackage com.rmr.dependenciesimport org.gradle.api.Pluginimport org.gradle.api.Projectclass DependenciesPlugin : Plugin<Project> {    override fun apply(target: Project) {        // Do nothing, just load dependencies to classpath    }}

Все, что осталось сделать, применить плагин к корневому проекту:


plugins{ id("dependencies") }

И мы получим практически тот же результат, как и с использованием buildSrc.


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


#5 Как обновлять зависимости


В любом активном проекте надо постоянно следить за обновлением библиотек. IDE умеет подсвечивать для каждого модуля, описанного в блоке dependencies {}, наличие новых стабильных версий в репозиториях:


New dependencies


Но этот инструмент работает только для зависимостей, описанных строковыми литералами в build-скриптах, а если мы попытаемся использовать способ с композитными сборками, buildSrc или extra properties, то IDE перестанет нам помогать. Кроме того, визуально просматривать build-скрипты в модулях, для того чтобы сделать обновление библиотек, на мой взгляд, не очень удобно.


Но есть решение использовать gradle-versions-plugin. Для этого просто применяем плагин к корневому проекту и регистрируем task для проверки новых версий зависимостей. Этот task надо настроить, передав ему лямбду для определения нестабильных версий:


fun isNonStable(version: String): Boolean {    val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.toUpperCase().contains(it) }    val regex = "^[0-9,.v-]+(-r)?$".toRegex()    val isStable = stableKeyword || regex.matches(version)    return isStable.not()}tasks.withType<DependencyUpdatesTask> {    rejectVersionIf {        isNonStable(candidate.version) && !isNonStable(currentVersion)    }}

Теперь запуск команды ./gradlew dependencyUpdates выведет список зависимостей, для которых есть новые версии:


The following dependencies have later milestone versions: - androidx.constraintlayout:constraintlayout [2.0.1 -> 2.0.4]     http://tools.android.com - androidx.core:core-ktx [1.5.0-alpha05 -> 1.5.0-beta03]     https://developer.android.com/jetpack/androidx/releases/core#1.5.0-beta03 - androidx.fragment:fragment-ktx [1.3.0-beta02 -> 1.3.1]     https://developer.android.com/jetpack/androidx/releases/fragment#1.3.1 - androidx.lifecycle:lifecycle-runtime-ktx [2.2.0 -> 2.3.0]     https://developer.android.com/jetpack/androidx/releases/lifecycle#2.3.0 - com.github.ben-manes:gradle-versions-plugin [0.36.0 -> 0.38.0] - com.github.ben-manes.versions:com.github.ben-manes.versions.gradle.plugin [0.36.0 -> 0.38.0] - com.github.bumptech.glide:glide [4.11.0 -> 4.12.0]     https://github.com/bumptech/glide

#6 Старайтесь не использовать feature-флаги в build config


Во многих проектах release, debug и другие сборки отличаются по функциональности. Например, в отладочных сборках могут быть включены какие-то логи, мониторинг сетевого трафика через прокси, debug menu для смены окружений и т.д. И часто для реализации такого используют флаги, прописанные в build config, например:


android {    buildTypes {        getByName("debug") {            buildConfigField("Boolean", "ENABLE_DEBUG_SCREEN", "true")        }        getByName("release") {            buildConfigField("Boolean", "ENABLE_DEBUG_SCREEN", "false")        }    }}

А дальше такие флаги используются в коде приложения:


if (BuildConfig.ENABLE_DEBUG_SCREEN) {} else {}

И у такого решения есть недостатки. Довольно легко перепутать значения флагов и ветки if/else: if(enabled) {} else {} или if(disabled) {} else {}. Именно так однажды, во время рефакторинга, я случайно отправил в релиз то, что должно было включаться только в сборках для QA-отдела (думаю, у многих были похожие истории). Тогда проблему обнаружили оперативно, мы перевыпустили сборку в маркет. Кроме того, недостижимый код может быть скомпилирован и попасть в релиз (здесь я не буду рассуждать о том, что мертвый код может вырезаться из итогового приложения). Ну и многим известно, что любые операторы ветвления лучше заменять полиморфизмом. И в Gradle есть статический полиморфизм.


Вместо флагов можно использовать разные source set для различных build variant ("src/release/java ...", "src/debug/java ..."). А если такой код хочется вынести в отдельные модули, то можно использовать специальные конфигурации: debugImplementation, releaseImplementation и т.д. Тогда мы сможем написать код с одним и тем же интерфейсом, но разной реализацией для различных типов сборок.


Например, мы можем выделить debug menu в отдельный модуль и подключать его только для debug и QA-сборок:


dependencies {    val qaImplementation by configurations    debugImplementation(project(":feature-debug-screen"))    qaImplementation(project(":feature-debug-screen"))}

А stub реализацию для релизной сборки можно реализовать через source set.


#7 Несколько слов про базовую структуру проекта


Совет немного не по теме, но я решил добавить его как часть своего опыта, так как модуляризация напрямую связана с Gradle.


Если для крупных проектов модуляризация кажется вполне очевидным решением, то не совсем понятно, как стоит поступать при старте небольших проектов или когда невозможно предсказать дальнейшее развитие кодовой базы продукта. Нужно ли выделять какие-то модули или достаточно начать с монолита? Я бы, помимо app модуля с основным приложением, всегда выделял как минимум два отдельных модуля:


  • UI kit: стили, кастомные элементы управления, виджеты и т.д. Обычно элементы управления на всех экранах приложения делаются консистентно в одном стиле (а возможно, у вас целая дизайн-система), и если в какой-то момент захочется выделить feature-модуль, то он также будет опираться на единый UI kit. Заранее подготовленный модуль с элементами управления и стилями упростит задачу и не потребует значительного рефакторинга приложения.
  • Common / utils: все утилитные классы и любые решения, которые не только потребуются в нескольких модулях, но и могут даже копироваться из проекта в проект. Особенно в контексте аутсорса такой модуль будет удобным при старте новых проектов. При вынесении классов в отдельный модуль, а не пакет, можно быть уверенным, что в его коде не окажется каких-либо бизнес сущностей конкретного приложения. Потенциально такой модуль может стать полноценной библиотекой, опубликованной в репозиторий.

#8 Не забывайте про matchingFallbacks


Часто, помимо debug и release, мы создаем и другие типы сборок, например, qa для тестов. И при создании модулей в приложении необязательно их прописывать в каждом build-скрипте. Достаточно при создании своего build type указать в модуле основного приложения те build type, которые следует выбирать при отсутствии каких-то конкретных:


android {    buildTypes {        create("qa") {            setMatchingFallbacks("debug", "release") // если не найдется qa, то искать сначала debug, затем release        }    }}

#9 Убирайте ненужные build variant


Build variant формируются из всех возможных сочетаний product flavor и build type. Возьмем небольшой синтетический пример: создадим три build type debug (отладочная сборка), release (сборка в маркет) и qa (сборка для тестирования), а во flavor вынесем разные сервера, на которые может смотреть сборка, production и staging (тестовое окружение). Возможные build variant будут выглядеть так:
Build variants


Очевидно, что сборка в маркет, которая будет смотреть на тестовое окружение, совершенно бессмысленна и не нужна (stagingRelease). И чтобы исключить ее, можно добавить variant filter:


android {    variantFilter {        if (name == "stagingRelease") {            setIgnore(true)        }    }}

#10 В некоторых модулях, завязанных на Android Framework, можно не использовать Android Gradle Plugin


Если какой-то из ваших модулей завязан на классы из Android Framework, то вовсе не обязательно сразу применять к нему Android Gradle Plugin и писать там файл манифеста. Модули с AGP более тяжеловесные, чем чистые java/kotlin модули, так как при сборке будут объединяться манифесты, ресурсы и т.д. Когда вам всего лишь требуется для компиляции модуля что-то вроде классов Activity, Context и т.д., то можно просто добавить Android Framework как зависимость:


dependencies {    compileOnly("com.google.android:android:4.1.1.4")}

#11 Как написать Gradle-плагин для CI на примере gitlab


Настройка CI отдельная большая тема, которая потянет на целую увесистую статью. Но я решил немного коснуться её и рассказать, как при помощи написания Gradle-плагина настроить версионирование сборок. Возможно, этот совет поможет тем, кто только поднимает CI, но не знает, как лучше это сделать.


Задача сделать так, чтобы в сборках на CI versionCode ставился автоматически и представлял из себя последовательные номера 1, 2, 3 и т.д. Я встречал в своей практике, когда в качестве versionCode брался CI job id или каким-то образом использовался timestamp. В таких случаях versionCode с каждой новой версией повышался и был уникальным, но семантически такие версии выглядели достаточно странно.


Основная идея проста нужно хранить номер будущего релиза где-то во внешнем источнике, куда имеет доступ только сборка, выполняемая на CI. А после каждой успешной публикации нужно инкрементировать этот номер и перезаписывать (нам важно, чтобы сборка не просто успешно выполнилась, но и полученные артефакты распространились для тестировщиков). Стоит оговориться, что такое решение не позволит корректно делать одновременно несколько сборок. Всю эту логику достаточно просто оформить в Gradle-плагин. Как мы уже выяснили, плагины лучше писать, используя композитные сборки.


В случае использования Gitlab CI подставляемый versionCode можно хранить в переменной окружения Gitlab. В его API есть метод для обновления переменных окружения: PUT /projects/:id/variables/:key. Для авторизации используем или project access token, или personal access token для старых версий gitlab.


Расписал добавление такого плагина по шагам, чтобы показать, насколько это просто.


Шаг 1: в настройках проекта на gitlab создать переменные окружения


Нам понадобятся переменные VERSION_CODE_NEXT для хранения номера версии и токен для доступа к API gitlab:


Шаг 2: создать композитную сборку


Добавим в корне проекта директорию ./includedBuilds/ci, а в ней файл build.gradle.kts:


plugins {    `kotlin-dsl`}kotlinDslPluginOptions {    experimentalWarning.set(false)}repositories {    jcenter()    google()}dependencies {    implementation("com.squareup.okhttp3:okhttp:4.9.0")}gradlePlugin {    plugins {        register("gitlab-ci") {            id = "gitlab-ci"            implementationClass = "com.redmadrobot.app.ci.CIPlugin" // Этот плагин создадим в следующих шагах        }    }}

Рядом создадим пустой файл ./includedBuilds/ci/settings.gradle.kts, если этого не сделать, то у вас сломается clean проекта.


В корневом проекте в файл settings.gradle.kts добавим строку includeBuild("includedBuilds/ci").


Шаг 3: написать плагин


Так будет выглядеть метод для получения versionCode, его можно будет использовать в build-скрипте (можно добавить в любой файл: при применении плагина код будет скомпилирован и добавлен в classpath build-скрипта):


const val GITLAB_VARIABLE_NEXT_VERSION = "VERSION_CODE_NEXT"private const val DEFAULT_VERSION_CODE = 1fun getVersionCode(): Int = if (isRunningOnCi()) {    getNextVersionCode()} else {    DEFAULT_VERSION_CODE}private fun isRunningOnCi(): Boolean = System.getenv()["CI"] != nullprivate fun getNextVersionCode(): Int {    return System.getenv()[GITLAB_VARIABLE_NEXT_VERSION]?.toInt()        ?: error("$GITLAB_VARIABLE_NEXT_VERSION must be specified")}

Примерно так можно написать метод для обновления переменной на gitlab:


fun updateGitlabVariable(variable: String, value: String) {    val projectId = System.getenv()["CI_PROJECT_ID"]    val accessToken = System.getenv()["TOKEN_FOR_GITLAB_API"]    val client = OkHttpClient()    val body = MultipartBody.Builder()        .setType(MultipartBody.FORM)        .addFormDataPart("value", value)        .build()    val request = Request.Builder()        .header("PRIVATE-TOKEN", accessToken)        .url("https://gitlab.com/api/v4/projects/$projectId/variables/$variable")        .method("PUT", body)        .build()    client.newCall(request).execute().use { response ->        if (!response.isSuccessful) throw IOException("Couldn't update variable)    }}

Далее пишем task, который при выполнении будет инкрементировать версию:


open class IncrementVersionCode : DefaultTask() {    @TaskAction    fun action() {        val nextVersionCode = getDistributionVersionCode() + 1        updateGitlabVariable(            GITLAB_VARIABLE_NEXT_VERSION,            nextVersionCode.toString()        )    }}

И напишем плагин, который добавит task в проект:


package com.redmadrobot.app.ciimport org.gradle.api.Pluginimport org.gradle.api.Projectclass CIPlugin : Plugin<Project> {    override fun apply(target: Project) {        target.tasks.register("incrementVersionCode", IncrementVersionCode::class.java)    }}

Шаг 4: подключить плагин


В build-скрипте проекта, из которого собирается apk, добавим следующие строки:


plugins {    id("gitlab-ci")}android {    defaultConfig {        versionCode = com.redmadrobot.app.ci.getVersionCode()    }}project.afterEvaluate {    tasks["incrementVersionCode"].mustRunAfter( // Обновление должно происходить только после публикации сборки в Firebase App Distribution        tasks["appDistributionUploadRelease"],        tasks["appDistributionUploadQa"]    )}

Теперь команда ./gradlew assembleRelease appDistributionUploadRelease incrementVersionCode будет делать новую сборку, публиковать ее и инкрементировать версию. Остается добавить эту команду на нужный триггер в ваш скрипт в .gitlab-ci.yml.


В заключение


Думаю, что у многих есть свои best practices по работе с Gradle, которыми вы бы могли поделиться с сообществом. Или что-то описанное в этой статье можно сделать лучше. Так что буду рад увидеть ваши советы в комментариях.


Что ещё посмотреть


Мне очень помогли доклады про работу с Gradle, которые делал Степан Гончаров в разные годы. Ссылки на них, если кому-то интересна эта тема:



И в формате дискуссии: Разбор доклада Степана Гончарова Gradle от А до Я

Подробнее..

Проекты в Gradle 7 как не зависеть от зависимостей

03.06.2021 16:16:28 | Автор: admin

Привет! Меня зовут Ксения Кайшева, я пишу приложения под Android в компании 65apps. Сегодня расскажу о новой возможности, которая позволяет централизованно описывать зависимости на проектах с системой сборки Gradle.

На текущий момент существует множество вариантов описания зависимостей в проектах, использующих Gradle. Рекомендуемого стандарта нет, поэтому используются самые разные подходы: кто-то просто перечисляет зависимости в блоке dependencies, кто-то выносит зависимости в отдельный файл, блок ext и т.д. И для новых разработчиков не всегда очевидно, что, где и как используется в большом и многомодульном проекте.

В 7й версии Gradle представлена новая функция, позволяющая описывать все зависимости централизованно. Эта функция находится на стадии превью, и чтобы воспользоваться ей в файле settings.gradle(.kts) необходимо добавить строку:

enableFeaturePreview("VERSION_CATALOGS")

Так выглядит использование (описанных в централизованном каталоге) зависимостей в любом build.gradle скрипте:

dependencies {
implementation libs.lifecycle.runtime
implementation libs.lifecycle.viewmodel.ktx
implementation libs.lifecycle.extentions
implementation libs.lifecycle.livedata.ktx
}

Здесь:

libs это сам каталог
lifecycle.runtime это зависимость в этом каталоге.

Каталог описывается в settings.gradle(.kts) файле:

dependencyResolutionManagement {
versionCatalogs {
libs {
alias('lifecycle-runtime')
.to(androidx.lifecycle:lifecycle-runtime:2.2.0')
alias('lifecycle-viewmodel-ktx').to(androidx.lifecycle', 'lifecycle-viewmodel-ktx').version {
strictly '[2.2.0, 2.3.0['
prefer '2.3.1'
}
}
}
}

Псевдоним (alias) должен состоять из имен, которые могут быть разделены тире, нижним подчеркиванием или точкой.

Разделение через тире является рекомендованным.

Сами имена должны состоять из буквенных символов, предпочтительно в нижнем регистре. Числа можно использовать в конце имени.

Например,

lifecycle-runtime
lifecycle_runtime
lifecycle.runtime
junit5-test-core
spek-runner-junit5

Недопустимо иметь псевдоним для зависимости, которая также принадлежит вложенной группе. Например, lifecycle-viewmodel и lifecycle-viewmodel-ktx.

Gradle рекомендует в таком случае использовать регистр для различения.
Например, lifecycleViewmodel и lifecycleViewmodelKtx.

Версии можно объявлять отдельно и затем ссылаться на них в описаниях самих зависимостей:

dependencyResolutionManagement {
versionCatalogs {
libs {
version('lifecycle', '2.3.1')
alias('lifecycle-viewmodel-ktx').to('androidx.lifecycle', 'lifecycle-viewmodel-ktx').versionRef('lifecycle')
}
}
}

Объявленные таким образом версии также доступны из любогоbuild.gradle файла:

version = libs.versions.lifecycle.get()

Часто бывает, что некоторые зависимости используются вместе. В каталоге такие зависимости можно объединять в пакеты и подключать их сразу через псевдоним пакета. То есть пакет это список нескольких зависимостей:

dependencyResolutionManagement {
versionCatalogs {
libs {
version('lifecycle', '2.3.1')
alias('lifecycle-runtime').to('androidx.lifecycle, 'lifecycle-runtime').versionRef('lifecycle')
alias('lifecycle-viewmodel-ktx').to('androidx.lifecycle, 'lifecycle-viewmodel-ktx').versionRef('lifecycle')
bundle('lifecycle',
['lifecycle-runtime', 'lifecycle-viewmodel-ktx'])
}
}
}

Подключение пакета зависимостей будет выглядеть так:

dependencies {
implementation libs.bundles.lifecycle
}

Добавление одного пакета эквивалентно добавлению всех зависимостей из пакета по отдельности.

Помимо описания каталога в settings.gradle(.kts) файле, есть более простая возможность собрать все зависимости вместе использовать toml-файл каталоге gradle: libs.versions.toml.

То есть, плюс еще один стандарт к представленному стандарту описания зависимостей.

По умолчанию libs.versions.toml файл будет входом в libs каталог. Можно также изменить имя каталога по умолчанию, например:

dependencyResolutionManagement {
defaultLibrariesExtensionName.set('deps')
}

Toml-файл состоит из 3 основных разделов:

[versions] - раздел для объявления версий
[libraries] - раздел для объявления зависимостей
[bundles] - раздел для объявления пакетов зависимостей

Например,

[versions]
lifecycle = "2.3.1"

[libraries]
lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime", version.ref = "lifecycle" }
lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" }

[bundles]
dagger = ["lifecycle-runtime", "lifecycle-viewmodel-ktx"]

Версию можно объявить просто как строку с номером, и в этом случае она считается обязательной версией, или в расширенном варианте, комбинируя информацию о версиях разного уровня:

[versions]
any-lib1 = 1.0
any-lib2 = { strictly = "[1.0, 2.0[", prefer = "1.2" }

Более подробно о расширенном варианте версии по ссылке

Семантика объявления номера версии по ссылке

Саму зависимость можно прописать, указав полное имя или отделив версию от группы и имени:

[libraries]
any-lib = "com.company:anylib:1.4"
any-other-lib = { module = "com.company:other", version="1.4" }
any-other-lib2 = { group = "com.company", name="alternate", version="1.4" }
anylib-full-format = { group = "com.company", name="alternate", version={ require = "1.4" } }

Если необходимо сослаться на версию, объявленную в [versions] разделе, то следует использовать свойство version.ref:

[versions]
some = "1.4"

[libraries]
any-lib = { group = "com.company", name="anylib", version.ref="some" }

Можно использовать несколько toml-файлов.Для этого нужно указать, как импортировать соответствующий файл:

dependencyResolutionManagement {
versionCatalogs {
testLibs {
from(files('gradle/test-libs.versions.toml'))
}
}
}

Также есть возможность создать каталог зависимостей и использовать его между разными проектами в рамках одной компании, например.

Подробнее по ссылке

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

При использовании Groovy не работает автоподстановка при указании зависимости в build.gradle файле и, соответственно, нет возможности провалиться в описание зависимости при нажатии на нее. Исправлять это не планируют. Решение для автоподстановки использовать Kotlin DSL.

Подробнее..

Перевод Как устроен билд APK файла внутри

15.11.2020 18:09:06 | Автор: admin

Процесс создания APK и компиляции кода


Рассматриваемые темы


  • Архитектура процессоров и необходимость для виртуальной машины
  • Понимание Java виртуальной машины
  • Компиляция исходного кода
  • Виртуальная машина Андроид
  • Процесс компиляции в .dex файл
  • ART против Dalvik
  • Описание каждой части билд процесса
  • Исходный код
  • Файлы ресурсов
  • AIDL файлы
  • Модули библиотек
  • AAR библиотеки
  • JAR библиотеки
  • Android Asset Packaging Tool
  • resources.arsc
  • D8 и R8
  • Dex и Multidex
  • Подписывание APK файла
  • Ссылки


Понимание флоу процесса билда APK файла, среда исполнения и компиляция кода
Этот пост нацелен быть отправной точкой для разработчиков, чтобы они ближе познакомились с билд процессом и созданием APK файла.



Архитектура процессоров и зачем нужна виртуальная машина


Андроид после того как вышел в 2007 году претерпел множество изменений связанный с билд процессом, средой исполнения и улучшениями производительности.

У андроида много удивительных характеристик и одна из них разные архитектуры процессоров такие как ARM64 и x86

Невозможно скомпилировать код, который поддерживает каждую архитектуру. Вот именно поэтому используется Java виртуальная машина.

image

Понимание Java виртуальной машины


JVM это виртуальная машина, позволяющая устройству запускать код, который скомпилирован в Java байткод

Используя JVM, вы избавляетесь от проблемы с разной архитектурой процессоров.

JVM предоставляет переносимость и она позволяет запускать Java код в виртуальной среде, вместо того, чтобы запускать его сразу на железе

Но JVM была создана для систем с большими мощностями по ресурсам, а наш андроид имеет сравнительно мало памяти и заряда батареи.

По этой причине Google создал адаптированную под андроид виртуальную машину, которая называется Dalvik.

image

Компилируем исходный код

image

Наш исходный Java код для андроида компилируется в класс файл .class с байткодом с помощью javac компилятора и запускается на JVM

Для котлина есть kotlinc компилятор, который делает совместимый с Java байткод.

Байткод это набор инструкций, который выполняется на целевом устройстве.

Java байткод это набор инструкций для Java виртуальной машины.

Андроид виртуальная машина


Каждое андроид приложение работает на своей виртуальной машине. С версий 1.0 до 4.4, это был Dalvik. В андроид 4.4, вместе с Dalvik, Google представил в качестве эксперимента новый андроид runtime, который назывался ART

Сгенерированный класс файл .class содержит JVM Java байткод.

Но у андроида есть свой собственный оптимизированный формат байткода, который называется Dalvik bytecode это просто инструкции машинного кода для процессора также как и JVM байткод.

image

Комплияция в .dex файл


Во время компиляции происходит конвертация .class класс файл и .jar библиотеки в один classes.dex файл, который содержит Dalvik байткод.

Команда dx превращает все .class и .jar файлы в один classes.dex файл, который написан с форматом Dalvik байткода.

Dex это аббревиатура с английского Dalvik Executable.

image

ART против Dalvik


C версии 4.4 андроид мигрировал на ART. ART также работает с .dex файлом.

Преимущество ART над Dalvik проявляется в том, что приложения запускаются быстрее, потому что весь DEX байткод транслируется в машинный код во время установки, не нужно дополнительного времени на компиляцию в рантайме.

ART и Dalvik совместимы, так что приложения разработанные для Dalvik должны работать и на ART.

Компиляция Dalvik (JIT- just in time) имела такие минусы как быстрая трата батареи, лаги в приложениях и плохой перформанс. В Dalvik трансляция происходит только когда это нужно. Мы открываем новый экран и только в этот момент происходит трансляция, за счет этого установка происходит быстрее, но при этом проседает перформанс.

Это причина по которой Google сделал Android Runtime (ART).

ART основан на AOT (ahead of time) компиляции, она происходит до того как приложение запустится.

В ART, компиляция происходит во время установки приложения. Это ведет к более долгому времени установки, но уменьшает трату батареи и избавляет от лагов, которые были на Dalvik.

Несмотря на то, что Dalvik был заменен на ART, .dex формат файлов еще используется

В андроид 7.0 JIT вернулся. Гибридная среда сочетает фичи как от JIT компиляции так и
от ART


Среда запуска байткода это очень важная часть андроида и она вовлечена в процесс запуска и установки приложения

image

Каждый этап описанного процесса


image

Source Code (Исходный код)


Это Java и Kotlin файлы в src пакете.

Resource Files


Файлы находящиеся в директории с ресурсами

AIDL Files


AIDL аббревиатура Android Interface Definition Language, позволяет вам описать интерфейс межпроцессорного взаимодействия.

AIDL может использоваться между любыми процессами в андроиде.

Library Modules


Модули библиотек содержат Java или Kotlin классы, компоненты андроида и ресурсы.

Код и ресурсы бибилотеки компилируются и пакуются вместе с приложением.

Поэтому модуль библиотеки может считаться компайл тайм артефактом.

AAR Libraries


Андроид библиотеки компилируются в AAR android archive файл, который вы можете использовать как зависимость для вашего android app модуля.

AAR файлы могут содержать андроид ресурсы и файл манифеста, что позволяет вам упаковать туда общие ресурсы такие как layouts и drawables в дополнение к Java или Kotlin классам и методам.

JAR Libraries


JAR это Java библиотека и в отличие от AAR она не может содержать андроид ресурсы и манифесты.

Android Asset Packaging Tool


AAPT2 аббревиатура (Android Asset Packaging Tool) компилирует манифест и файлы ресурсов в один APK.

Этот процесс разделен на два шага компиляцию и линковку Это улучшает производительность так как если вы поменяете один файл, вам нужно компилировать только его и прилинковать к остальным файлам командой 'link'

AAPT2 может компилировать все типы андроид ресурсов, таких как drawables и XML файлы.

При вызове AAPT2 для компиляции, туда передается по одному ресурсному файлу на каждый вызов

Затем APPT2 парсит файл и генерирует промежуточный бинарный файл с расширением .flat

Фаза линковки склеивает все промежуточные файлы сгенерированные в фазе компиляции и дает нам на выход один .apk файл. Вы также можете сгенерировать R.java файл и правила для proguard в это же время.

resources.arsc


Полученный на выходе .apk файл не включает в себя DEX файл, APK не подписан и не может быть запущен на устройстве.

APK содержит AndroidManifest, бинарные XML файлы и resources.arsc

resource.arsc содержит всю мета информацию о ресурсах, такую как индексы всех ресурсов в пакете

Это бинарный файл и APK который может быть запущен. APK который вы обычно создаете и запускаете не сжат и может быть использован просто посредством размещения в памяти.

R.java файл это выходной файл вместе с APK ему назначен уникальный id, который позволяет Java коду использовать ресурсы во время компиляции.

arsc это индекс ресурса который используется во время запуска приложения

image

D8 и R8


Начиная с андроид студии 3.1 и далее, D8 был сделан дефолтным компилятором.

D8 производит более маленькие dex файлы с лучшей производительностью, если сравнивать со старым dx.

R8 используется для компиляции кода. R8 это оптимизированная версия D8

D8 играет роль конвертера класс файлов в Dex файлы, а также производит дешугаринг функций из Java 8 в байткод, который может быть запущен на андроиде

R8 оптимизирует dex байткод. Он предоставляет такие фичи как оптимизация, обфускация, удаление ненужных классов.

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

Обфускация имеет и другие преимущества для предотвращения реверс инжиниринга, но основная цель уменьшить размер.

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

С помощью дешугаринга мы можем использовать удобные фичи языка Java 8 на андроиде.

image

Dex and Multidex


R8 дает на выходе один DEX файл, который называется classes.dex

Если количество методов приложения переваливает за 65,536, включая подключенные библиотеки, то произойдет ошибка при билде</b

The method ID range is 0 to 0xFFFF.

Другими словами, вы можете ссылаться на 65,536, или от 0 до. 65,535, если говорить цифрами

Чтобы избежать этого, нужно внимательно следить за зависимостями своего проекта и использовать R8, чтобы удалять неиспользуемый код, или включать мультидекс (multidex)


image

Подписывание APK файла


Все Apk файлы требуют цифровую подпись до того как они могут быть установлены на ваш девайс

Для дебаг билдов, андроид студия автоматически подписывает приложение используя дебажный сертификат сгенерированный с помощью android sdk tools.

Дебажный кейстор и дебажный сертификат создаются автоматически

Для релиз билдов вам нужен кейстор, которым вы подпишете свой apk файл. Вы можете создать APK файл в андроид студии через Generated Signed Apk опцию.

image

Ссылки


Подробнее..

Перевод Запускаем Gatling из Gradle Полное руководство для начинающих

02.04.2021 14:12:19 | Автор: admin

Привет, Хабр. Для будущих учащихся на курсе Нагрузочное тестирование подготовили перевод статьи.

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


Хотите узнать, как использовать Gatling через Gradle? Тогда вы по адресу. В последнее время я достаточно часто использую инструмент стресс-тестирования Gatling. Он стал одним из моих излюбленных инструментов для тестирования производительности. На сайте Gatling есть неплохая документация по началу работы. Но она подразумевает загрузку zip-файла, а затем запуск BAT или SH скрипта для запуска Gatling. А затем вам нужно выбрать из списка тест, который вы хотите запустить.

Так что да, было бы намного приятнее делать все вышеперечисленное через Gradle. И естественно, намного удобнее. В частности, если вы хотите запускать Gatling-тесты как часть вашего Continuous Integration. Одним из наибольших преимуществ этого подхода является то, что Gatling может зафейлить вашу CI-сборку, если будет нарушен определенный порог производительности (например, слишком много ошибок или слишком большое среднее время отклика и т. д.).

Если вы хотите запускать Gatling через Gradle, вам понадобится плагин Gatling Gradle.

Это руководство проведет вас через настройку плагина Gradle для нового Gatling-проекта.

Чтобы сделать это руководство еще более удобным для новичков, мы будем производить настройку в IDE (Intellij). После завершения работы по настройке вы можете переключиться на запуск исключительно через командную строку, если вам угодно.

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

Предварительные требования

Перед тем, как мы начнем, вы должны удостовериться в наличии всех пунктов из списка предварительных требований, приведенных ниже:

1. Java 8 JDK

Вероятно, он у вас уже есть, но если нет, то здесь можно найти подробное руководство по установке JDK для всех типов ОС.

Я настоятельно рекомендую вам использовать Java 8 с Gatling, так как он наиболее с ним совместим.

2. Intellij

В этом руководстве я буду использовать Intellij, вы можете скачать последнюю версию Intellij здесь.

И это все, что вам нужно! Gatling Gradle плагин берет на себя установку Scala, и если же вы запускаетесь через Gradle Wrapper, то нет никакой необходимости даже загружать или устанавливать Gradle на вашей системе.

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


Руководство по запуску Gatling из Gradle

Создать Gradle-проект для Scala в Intellij, как я выяснил с годами, удручающе сложно.

Лучший способ начать создать образец проекта (sample project).

  1. Выполните следующую команду в терминале или командной строке, чтобы создать образец проекта с плагином Gatling Gradle:

curl -sL https://raw.githubusercontent.com/lkishalmi/gradle-gatling-plugin/master/bootstrap.sh | \    bash -s ~/sample-gradle-gatling && \    cd ~/sample-gradle-gatling && ./gradlew gatlingRun

2. Откройте начальную страницу IntelliJ и выберите Import Project.

Import Project to IntelliJImport Project to IntelliJ

3. Выберите файл build.gradle из репозитория, который вы загрузили на шаге 1, и нажмите Open.

Select build.gradle fileSelect build.gradle file

4. Откройте файл SampleSimulation.

Open Sample SimluationOpen Sample Simluation

5. Вы можете увидеть всплывающее окно, подобное ниже. Выберите Setup Scala SDK.

Setup Scala SDKSetup Scala SDK

6. Выберите SDK для Scala. Если его нет в списке, вам вместо этого может потребоваться кликнуть Configure и сначала загрузить бинарники Scala.

Choose Scala SDKChoose Scala SDK

7. На этом этапе уже все должно быть настроено. Чтобы запустить Gatling-тест из Gradle, введите:

./gradlew gatlingRun

Или, чтобы запустить конкретный тест:

./gradlew gatlingRun-SampleSimulation

Для получения дополнительных сведений об использовании и настройке плагина с официальной документацией Gatling Gradle Plugin.


Узнать подробнее о курсе Нагрузочное тестирование.

Смотреть открытый вебинар на тему Начало работы с Jmeter: запись скрипта, параметризация, отладка.

Подробнее..

Перевод Нагрузочное тестирование на Gatling Полное руководство. Часть 1

16.04.2021 20:09:46 | Автор: admin

Эта статья представляет собой полное, подробное и исчерпывающее руководство по эффективному использованию Gatling для нагрузочного тестирования. Вы также можете ознакомиться с моими видеоуроками по Gatling на YouTube, где я более подробно раскрываю эту тему.

Краткий обзор руководства

Если вам нужно провести нагрузочное тестирование вашего сервера HTTP-приложений, то Gatling - идеальный инструмент для этой задачи! Эта серия статей будет представлять собой подробное практическое руководство по нагрузочному тестировании на Gatling. К концу этого руководства у вас появится хорошее представление об использовании Gatling, которое вы сможете применить в ваших последующих проектах по нагрузочному тестированию.

Первый раз слышите о Gatling? Тогда для начала уделите внимание моей вводной статье о Gatling. Но если в двух словах:

  • Gatling - это инструмент для нагрузочного тестирования с открытым исходным кодом, полностью написанный на Scala.

  • Простой и выразительный DSL, который предлагает Gatling, упрощает написание скриптов нагрузочного тестирования.

  • Он не содержит графического интерфейса (например, как JMeter), хотя поставляется с графическим интерфейсом для облегчения записи скриптов.

  • Он может обрабатывать огромный объем трафика на одном компьютере, устраняя необходимость в сложной распределенной инфраструктуре тестирования.

  • Проверку кода с помощью Gatling можно встроить в систему контроля версий и легко использовать в связке с инструментами непрерывной интеграции для запуска нагрузочных тестов и тестов производительности как части вашей CI-сборки.

Что такое тестирование производительности?

Прежде чем мы начнем, давайте вкратце разберемся, что такое тестирование производительности (performance testing).

Тестирование производительности - это тестирование, которое вы выполняете, когда хотите увидеть, как ваша система справляется с различными уровнями трафика и пропускной способности.

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

Существует несколько различных типов тестирования производительности, такие как:

  • Нагрузочное тестирование (Load Testing) - тестирование системы на заранее определенном объеме пользователей и трафике (пропускной способности);

  • Стресс-тестирование (Stress Testing) - тестирование системы под постоянно увеличивающейся нагрузкой, чтобы найти точку останова (breakpoint).

  • Тестирование стабильности (Soak Testing) - тестирование со стабильным уровнем трафика в системе на более длительном периоде времени для выявления узких мест.

Gatling подходит для всех этих подвидов тестирования производительности.

Вот некоторые из метрик, которые вы можете собрать во время тестирования производительности:

  • Время отклика транзакции (Transaction Response Times) - сколько времени требуется серверу, чтобы ответить на запрос.

  • Пропускная способность (Throughput) - количество транзакций, которые могут быть обработаны за определенный период времени.

  • Ошибки (Errors) - сообщения об ошибках, которые начинают появляться в определенные моменты нагрузочного теста (например, тайм-ауты).

Это исчерпывающее учебное руководство по нагрузочному тестированию на Gatling, разделенное на несколько разделов. Если вы новичок в нагрузочном тестировании на Gatling, я бы рекомендовал вам ознакомиться с каждый из разделов по порядку.

Исходный код

Вы можете найти весь исходный код из этого руководства в моем репозитории на Github.


1. Установка Gatling

Прежде чем начать что-либо делать, убедитесь, что у вас установлен JDK8 (или более новый). Если вам нужна помощь с этим, ознакомьтесь с этим руководством по установке JDK.

Самый простой способ установить Gatling - загрузить версию Gatling с открытым исходным кодом с сайта Gatling.io. Кликните Download Now, и начнется загрузка ZIP-архива:

Download GatlingDownload Gatling

Разархивируйте архив в любое место на вашем компьютере. Откройте полученную папку и перейдите в каталог bin. Оттуда запустите:

  • gatling.bat - если вы используете Windows

  • gatling.sh - если вы работаете на Mac или Unix

После окончания загрузки Gatling вам будет предложено выбрать симуляцию для запуска:

Choose Gatling Simulation to runChoose Gatling Simulation to run

Введите 0, чтобы выбрать computerdatabase.BasicSimulation. Вам будет предложено ввести описание запуска (run description), но это необязательно, и его можно оставить пустым.

Gatling запустит выбранный вами скрипт, который выполнит базовый нагрузочный тест на тестовом сайте Gatling с базой данных компьютеров.


2. Gatling Recorder

Можно, конечно, запускать скрипты, поставляемые с Gatling, но, несомненно, вы захотите разработать скрипты под ваше собственное приложение.

Как только вы овладеете Gatling (к концу этого руководства!), вы сможете писать скрипты с нуля в своей IDE или даже просто в текстовом редакторе.

Но прежде чем заняться этим, для начала вам будет проще использовать встроенный Gatling Recorder для записи вашего пользовательского пути (user journey).

2.1 Создание HAR-файла

Самый удобный способ использования Gatling Recorder, как мне кажется, предполагает генерацию HAR-файла (Http-архива) вашего пользовательского пути в Google Chrome.

Создание этих файлов и их импорт в Gatling Recorder позволяет обойти проблемы с записью на HTTPS.

Чтобы создать HAR-файл, выполните следующие действия:

  1. Откройте тестовый сайт Gatling с базой данных компьютеров - это сайт, с которого мы будем записывать пользовательский путь.

  2. Откройте Chrome Developer Tools и перейдите на вкладку Network.

  3. Кликните Clear, чтобы удалить все предыдущие сетевые вызовы, а затем убедитесь, что красная кнопка записи включена.

  4. Полистайте сайт, чтобы сгенерировать свой пользовательский путь - например, поищите какой-нибудь компьютер, создайте другой компьютер и т. д. Вы должны увидеть, что внутри вкладки Network начинают появляться новые записи.

  5. Кликните правой кнопкой мыши в любом месте вкладки Network и выберите Save all as HAR with content. Сохраните этот файл где-нибудь на вашем компьютере.

  1. Теперь перейдите в свою папку bin Gatling (в которой вы впервые запустили Gatling в предыдущем разделе) и запустите файл recorder.sh в Mac/Unix или recorder.bat в Windows. Загрузится Gatling Recorder.

  2. Измените Recorder Mode в правом верхнем углу на HAR Converter.

  3. В разделе HAR File перейдите к местоположению HAR-файла, созданного на шаге 5.

  4. Назовите свой скрипт, изменив Class Name на, например, MyComputerTest.

  5. Все остальное оставьте как было по умолчанию и кликните Start!

  6. Если все сработает так, как должно, вы увидите сообщение, что все прошло успешно.

Gatling Recorder screenshotGatling Recorder screenshot
  1. Чтобы запустить скрипт, вернитесь в папку bin Gatling и снова запустите gatling.sh или gatling.bat. После того, как Gatling загрузится, вы сможете выбрать только что созданный скрипт.

Если вы хотите посмотреть на только что созданный скрипт, вы можете найти его в папке user-files/simulations в вашем каталоге Gatling. Откройте скрипт MyComputerTest, который вы только что записали, в текстовом редакторе. Он должен выглядеть как-то так:

import scala.concurrent.duration._import io.gatling.core.Predef._import io.gatling.http.Predef._import io.gatling.jdbc.Predef._class MyComputerTest extends Simulation {val httpProtocol = http.baseUrl("http://computer-database.gatling.io").inferHtmlResources().userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36")val headers_0 = Map("Accept" -> "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9","Accept-Encoding" -> "gzip, deflate","Accept-Language" -> "en-GB,en-US;q=0.9,en;q=0.8","Upgrade-Insecure-Requests" -> "1")val scn = scenario("MyComputerTest").exec(http("request_0").get("/computers").headers(headers_0)).pause(9).exec(http("request_1").get("/computers?f=amstrad").headers(headers_0)).pause(4).exec(http("request_2").get("/assets/stylesheets/bootstrap.min.css").resources(http("request_3").get("/assets/stylesheets/main.css")))setUp(scn.inject(atOnceUsers(1))).protocols(httpProtocol)}

Это ваш скрипт Gatling. Если весь этот код кажется вам чересчур непривычным - не волнуйтесь, мы расскажем обо всем, что вам нужно знать, в последующих разделах руководства.


3. Настройка проекта Gatling

Итак, теперь, когда вы получили представление о том, как выглядит скрипт Gatling (и как с ним запустить нагрузочный тест), давайте приступим к настройке среды для создания наших скриптов. Первое, что нам нужно выбрать, это IDE:

3.1 Выберите IDE для создания скриптов нагрузочного тестирования на Gatling

Хотя вы вполне можете создавать скрипты Gatling в любом текстовом редакторе, гораздо проще (и эффективнее) делать это в IDE. В конце концов, мы будем писать код Scala. Scala работает поверх JVM, поэтому любая IDE, поддерживающая JVM, должна нам подойти.

У вас есть несколько вариантов:

  • Другой вариант, который (недавно) стал доступен, - это Visual Studio Code или сокращенно VS Code. Эта IDE последние несколько лет развивалась с головокружительной скоростью и стала популярной среди разработчиков множества различных технологических стеков. Ознакомьтесь с другой моей статьей Создаем Gatling скрипты с помощью VS Code для получения указаний по настройке.

  • Мой личный выбор, который я и буду показывать в этом руководстве, - это IntelliJ IDEA. Бесплатная версия достаточно хороша и поставляется со встроенной поддержкой Scala. Она идеально подходит для разработки скриптов Gatling.

Теперь, когда мы выбрали нашу IDE, нам понадобится система сборки, которая будет сочетаться с ней:

3.2 Выберите систему сборки для Gatling

Конечно, вы можете использовать Gatling без системы сборки и просто запускать его из первичных zip-файлов (как мы делали в первом разделе). Но есть вероятность, что вскоре вы все-таки захотите использовать систему сборки в своем проекте нагрузочного тестирования Gatling. Это упростит обслуживание в системе контроля версий. Опять же, у вас есть несколько вариантов на выбор:

Я расскажу о настройке нового проекта в IntelliJ Idea с Maven в оставшейся части этого раздела.

3.3 Создание проекта Gatling из архетипа Maven

Откройте терминал или командную строку и введите:

mvn archetype:generate

В конце, вы увидите этот запрос:

Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains):

Введите gatling.

Затем вы должны увидеть:

1: remote -> io.gatling.highcharts:gatling-highcharts-maven-archetype (gatling-highcharts-maven-archetype)

Просто введите 1 для выбора архетипа Gatling. На следующем экране выберите последнюю версию:

Choose Gatling VersionChoose Gatling Version

Я ввел 35, чтобы выбрать версию 3.3.1 для этого туториала.

Для groupId введите com.gatlingTest.

Для artifactId введите myGatlingTest.

Для version просто нажмите ENTER, чтобы принять 1.0-SNAPSHOT.

Для package нажмите ENTER еще раз, чтобы принять в качестве имени пакета com.gatlingTest.

В конце введите Y, чтобы подтвердить все настройки, и Maven создаст для вас новый проект.

Следующее, что нужно сделать, - это импортировать этот проект в вашу IDE. Я импортирую проект в IntelliJ.

На стартовой странице IntelliJ выберите Import Project.

Import IntelliJ Gatling projectImport IntelliJ Gatling project

Перейдите в папку проекта, которую вы только что создали, и выберите файл pom.xml. Кликните open, и Intellij начнет импортировать проект в IDE за вас.

После того, как импорт проекта будет завершен, откройте панель Project Directory слева и раскройте папку src>test>scala. Дважды кликните по классу Engine.scala. Вы можете увидеть сообщение No Scala SDK in module вверху экрана. Если это так, нажмите Setup Scala SDK:

Import Scala SDK in IntelliJImport Scala SDK in IntelliJ

Проверьте, какие версии Scala у вас есть:

Scala versions in IntelliJScala versions in IntelliJ

Если у вас нет версии, указанной здесь, кликните Create, выберите версию 2.12 и кликните кнопку download:

ПРИМЕЧАНИЕ: Я НАСТОЯТЕЛЬНО рекомендую использовать версию Scala 2.12 с IntelliJ - 2.13, похоже, не очень хорошо работает с Gatling

Download Scala in IntelliDownload Scala in Intelli

В качестве альтернативы, если у вас возникли проблемы с загрузкой бинарников Scala через IntelliJ, вы можете вместо этого загрузить бинарники Scala непосредственно со Scala-lang. Кликните Download the Scala binaries, как показано на этом скриншоте:

Download Scala binaries from Scala-langDownload Scala binaries from Scala-lang

Сохраните бинарники где-нибудь на жестком диске и распакуйте ZIP-архив. Вернувшись в IntelliJ, снова кликните Setup Scala SDK, и на этот раз кликните Configure. Нажмите кнопку Add в левом нижнем углу:

Add the Scala binaries to IntelliJAdd the Scala binaries to IntelliJ

Перейдите в папку, которую вы только что загрузили и распаковали, и выберите папку lib:

Select the Lib folder to import Scala BinariesSelect the Lib folder to import Scala Binaries

Теперь в диалоге Add Scala Support вы сможете выбрать библиотеку Scala, которую вы загрузили:

Add Scala SupportAdd Scala Support

На этом моменте, вам также может потребоваться пометить папку scala как source root в IntelliJ. Для этого кликните правой кнопкой мыши по папке scala и выберите Mark Directory As -> Test Sources Root:

Mark Test Sources as Root in IntelliJMark Test Sources as Root in IntelliJ

На всякий случай также отметьте всю папку src как source root:

Mark Sources RootMark Sources Root

Наконец, кликните правой кнопкой мыши объект Engine и выберите Run:

Run the Engine Object in IntelliJRun the Engine Object in IntelliJ

Вы должны увидеть сообщение типа There is no simulation script. Please check that your scripts are in user-files/simulations. Так и должно быть, далее мы приступим к настройке наших скриптов нагрузочных тестов Gatling.

3.4 Добавление базового скрипта Gatling

Чтобы протестировать нашу новую среду разработки, давайте добавим базовый скрипт Gatling. Этот скрипт запустит тест на базе данных компьютеров Gatling.

Примечание: В оставшейся части этого руководства мы будем писать Gatling-тесты для пользовательского приложения, которое я создал для этого туториала... Сейчас же я просто хотел продемонстрировать простой скрипт для базы данных компьютеров Gatling, чтобы мы могли проверить, правильно ли настроена наша среда.

Кликните правой кнопкой мыши папку scala и выберите New > Scala Package - назовите пакет computerdatabase. Кликните эту папку правой кнопкой мыши и выберите New > Scala Class - назовите этот класс BasicSimulation. Скопируйте весь приведенный ниже код в новый класс:

package computerdatabaseimport io.gatling.core.Predef._import io.gatling.http.Predef._import scala.concurrent.duration._class BasicSimulation extends Simulation {  val httpProtocol = http    .baseUrl("http://computer-database.gatling.io") // Здесь находится корень для всех относительных URL    .acceptHeader(      "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"    ) // Вот общие заголовки    .acceptEncodingHeader("gzip, deflate")    .acceptLanguageHeader("en-US,en;q=0.5")    .userAgentHeader(      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0"    )  val scn =    scenario("Scenario Name") // Сценарий представляет собой цепь запросов и пауз       .exec(        http("request_1")          .get("/")      )      .pause(7) // Обратите внимание, что Gatling записал паузы в реальном времени  setUp(scn.inject(atOnceUsers(1)).protocols(httpProtocol))}

Теперь давайте запустим скрипт. Кликните правой кнопкой мыши объект Engine и выберите Run Engine. Gatling загрузится, и вы должны увидеть сообщение computerdatabase.BasicSimulation is the only simulation, executing it. Нажмите Enter, и скрипт выполнится.

Мы также можем запустить наш Gatling тест напрямую через Maven из командной строки. Для этого откройте терминал в каталоге вашего проекта и введите mvn gatling:test. Эта команда выполнит Gatling тест с помощью плагина Maven для Gatling.

Наша среда разработки Gatling готова к работе! Теперь нам нужно приложение, которое мы будем тестировать. Мы могли бы использовать базу данных компьютеров Gatling, но вместо этого я разработал API-приложение специально для этого туториала - базу данных игр!


В преддверии старта курса "Нагрузочное тестирование" приглашаем всех желающих записаться на бесплатный демо-урок в рамках которого рассмотрим интерфейс LoadRunner Virtual User Generator, запишем скрипт тестирования web-сайта, проведём его отладку и параметризацию. В результате вы научитесь создавать скрипты нагрузочного тестирования web-сайтов.

ЗАПИСАТЬСЯ НА ДЕМО-УРОК

Подробнее..

Организация разработки в изолированной сети как управлять зависимостями?

30.07.2020 10:07:11 | Автор: admin

Всем привет,


Наша компания занимается разработкой CUBA Open Source Java фреймворка для разработки корпоративных приложений. Платформа CUBA это целая экосистема, которая включает в себя сам фреймворк и разнообразные аддоны, предоставляющие прикладной функционал, готовый к использованию в несколько кликов. За последние несколько лет фреймворк сильно набрал популярность и сейчас используется более 20 000 разработчиками по всему земному шару. С ростом популярности мы сталкивались с множеством интересных кейсов и в этой статье хотим затронуть один из них. Возможно, этот материал поможет в вашей практике, особенно если вы работаете в организации, в которой вопросы безопасности больно бьют по рукам разработчиков.


image


Конкретно мы поговорим про тот случай, когда в компании лимитирован доступ к всемирной паутине настолько, что вы не можете пользоваться публичными репозиториями артефактов, например таким, как maven central. Представьте себе, что мы (или вы) поставляем наш фреймворк в такой закрытый контур. Как же нам предоставить все необходимые артефакты, от которых мы зависим, включая транзитивные зависимости? Как потом их обновлять вместе с версиями фреймворка?


В данной статье я хочу рассказать о нашем новом инструменте CUBA SDK консольной утилите, которая позволяет определять все транзитивные зависимости для Maven-библиотек и управлять ими в удаленных репозиториях. Также в статье мы рассмотрим пример, который позволит вам использовать наши наработки для любого Java приложения с применением Maven-зависимостей.


Проблема загрузки транзитивных зависимостей для внутренних репозиториев


Практически любой современный JVM проект, задействующий Apache Maven или Gradle в качестве инструмента сборки, имеет большое количество транзитивных зависимостей для используемых библиотек или фреймворков. При наличии доступного репозитория в сети эти зависимости автоматически разрешаются с помощью менеджера зависимостей.


Но что делать в случае, если публичные репозитории недоступны из внутренней сети?


Возможные варианты решения проблемы


Казалось бы, все просто берем Nexus и проксируем обращение к репозиториям через него. Действительно, это решение в большинстве случаев может подойти, но не в тех случаях, где к безопасности предъявляются беспрецедентные требования. К таким организациям относятся предприятия военной промышленности, министерства, центробанк и прочие государственные институты. Выход в интернет в таких местах строго регламентирован и зачастую не позволяет использовать традиционное проксирование.


Что же нам остается?


  • Вариант 0. Упрашивание безопасников.
  • Вариант 1. Шлюз.
  • Вариант 2. Ручное управление зависимостями.

Вариант 0 рассматривать не будем, рассмотрим вариант 1 и 2.


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


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


Как видим из доступных примеров, добавление новых или обновление существующих зависимостей это довольно сложное действие.


Если не рассматривать систему безопасности в организациях, то проблема доступности репозиториев актуальна и для разработчиков, например, разработчик планирует поработать в самоизоляции в глухой деревне у бабушки, но интернет там не ловит. Чтобы подготовиться для таких случаев, всегда можно попробовать использовать оффлайн плагины для Gradle или Maven. Но если проектов несколько, то для каждого проекта нужно будет


  • настроить оффлайн плагин
  • на всякий случай добавить в проект все зависимости, которые потенциально могут пригодиться, чтобы оффлайн плагины смогли их корректно добавить в свой кэш
  • выгрузить все зависимости в локальную папку

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


Как мы предлагаем решать эти проблемы?


CUBA SDK


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


В чем отличие CUBA SDK от оффлайн плагинов для Gradle или Maven?
Главное отличие CUBA SDK не кэширует зависимости конкретного проекта, а позволяет синхронизировать артефакты между внешними и внутренними репозиториями, чтобы разработчикам было комфортно создавать и разрабатывать приложения в закрытой сети.
CUBA SDK не требует проекта и позволяет создать необходимый оффлайн стек фреймворков, аддонов и библиотек со всеми зависимостями.


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


Для организаций это может быть полезно для централизованной синхронизации внутренних репозиториев.


image


CUBA SDK позволяет с помощью нескольких консольных команд определять все зависимости для артефактов и заливать их в нужные репозитории. Для полностью закрытых сетей можно использовать команды импорта и экспорта или установить CUBA SDK на шлюз.


Преимущества использования CUBA SDK:


  • автоматически собирает все зависимости с исходным кодом для загружаемых библиотек
  • определяет зависимости для платформы и аддонов CUBA Platform
  • проверяет и устанавливает новые версии библиотек
  • может работать одновременно с несколькими репозиториями для поиска артефактов, включая локальные maven репозитории
  • имеет встроенный Nexus OSS репозиторий артефактов
  • даёт возможность загрузки артефактов одновременно в несколько репозиториев, включая локальные maven
  • производит импорт и экспорт артефактов со всеми зависимостями
  • предоставляет интерактивный режим с подсказками для установки платформы и аддонов CUBA Platform
  • использует механизмы Gradle для определения зависимостей
  • не зависит от IDE
  • может быть установлен на CI сервере

Команды SDK


Полный список доступных команд можно посмотреть на странице GitHub.


CUBA SDK изначально поддерживает три типа компонентов: CUBA Framework, CUBA addon и библиотека, которая может быть загружена через maven координаты. Этот список может быть расширен для других типов компонентов через плагины для CUBA SDK.


Установка компонента в удаленный репозиторий может быть выполнена через команду install. При создании SDK мы предусмотрели вариант, когда SDK может быть установлен на шлюзовом компьютере или на переносном носителе, в этом случае установку компонентов можно сделать через команды resolve и push.


resolve просто определяет и скачивает все зависимости в локальный кэш SDK
push заливает уже скачанные артефакты с зависимостями в настроенные target репозитории


Для работы с репозиториями в SDK есть встроенный менеджер репозиториев.
Менеджер репозиториев поддерживает локальные и удаленные репозитории, которые в SDK разделены на две группы:


  • source это репозитории, которые будут использоваться для поиска артефактов
  • target репозитории, в которые нужно будет залить эти артефакты

SDK сам может использоваться как репозиторий, для этого с помощью команды setup-nexus SDK скачивает, устанавливает и настраивает репозиторий Nexus OSS. Для запуска и остановки репозитория используются команды start и stop.


Для проверки и установки обновлений достаточно выполнить команду check-updates.


Определение зависимостей


Самая главная проблема, которую должен был решать SDK это корректное определение и сбор зависимостей для компонентов. При разработке мы попробовали несколько подходов для определения транзитивных зависимостей компонентов. Изначально возникла идея, что можно просто распарсить .pom файлы и составить дерево зависимостей. Но идея парсить файлы вручную оказалась не очень хорошей, тем более что Apache Maven все это уже умеет делать из коробки.


Maven как менеджер зависимостей


Поэтому мы решили использовать Apache Maven для определения транзитивных зависимостей компонентов.


Для этого в CUBA SDK скачивается дистрибутив maven в домашнюю папку SDK и через Java Runtime запускаются команды.


Например, с помощью команды


dependency:resolve -Dtransitive=true -DincludeParents=true -DoverWriteSnapshots=true -Dclassifier=<classifier> -f pom.xml

мы определяли все зависимости для компонентов, которые описаны в pom.xml, и эти компоненты автоматически скачивались в локальный кэш maven, после чего с помощью команды


org.apache.maven.plugins:maven-deploy-plugin:3.0.0-M1:deploy-file -Durl=<repository URL>

артефакты заливались в нужный репозиторий.


Следующая команда позволяет загрузить библиотеку в локальный репозиторий.


org.apache.maven.plugins:maven-dependency-plugin:3.1.1:get -Dartifact=<maven coordinates>

Для выполнения команд Maven в приложении CUBA SDK сгенерировался settings.xml файл. Он содержал список всех репозиториев, которые должны были использоваться для загрузки и выгрузки артефактов.


Gradle как менеджер зависимостей


В первой версии приложения зависимости определялись корректно, но довольно медленно, и при тестировании функционала стали возникать конфликты при определении зависимостей для некоторых аддонов CUBA Platform, хотя при сборке проекта через Gradle таких проблем не возникало.


Поэтому мы решили попробовать переписать всю логику определения зависимостей на Gradle. Для этого мы создали build.gradle скрипт, который содержал необходимые задачи по загрузке и определению зависимостей для компонентов.


Для вызова задач Gradle используется Gradle Tooling API.


Для определения пути к зависимостями через Gradle мы используем artifact resolution query API. Следующий код позволяет получить путь к исходникам библиотеки:


 def component = project.dependencies.createArtifactResolutionQuery()            .forComponents(artifact.id.componentIdentifier)            .withArtifacts(JvmLibrary, SourcesArtifact)            .execute()            .resolvedComponents[0] def sourceFile = component?.getArtifacts(SourcesArtifact)[0]?.file

Таким образом, мы получили пути ко всем файлам в локальном кэше Gradle и сохраняли их в хранилище SDK.


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


project.ext.properties["toResolve"].tokenize(';').each {            dependencies.add 'extraLibs', it        }        def resolved = [:]        configurations.all.collect {            if (it.canBeResolved) {                it.resolvedConfiguration.lenientConfiguration.artifacts.each { art ->                    try {                        ...                    } catch (e) {                        logger.error("Error: " + e.getMessage(), e)                        logger.error("could not find pom for {}", art.file)                    }                }            }        }

Мы используем lenientConfiguration, чтобы Gradle скрипт пропускал не найденные зависимости и не падал в случае если компонент не найден в репозиториях.


Для загрузки артефактов в репозитории SDK использует PublishToMavenRepository задачу Gradle.


task publishArtifact(type: PublishToMavenRepository) {    doLast {        if (project.ext.hasProperty("toUpload")) {            def toUpload = new JsonSlurper().parseText(project.ext.properties["toUpload"])            def descriptors = new JsonSlurper().parseText(project.ext.properties["descriptors"])            artifactId toUpload.artifactId            groupId toUpload.groupId            version toUpload.version            descriptors.each { descriptor ->                artifact(descriptor.filePath) {                    classifier descriptor.classifier.type                    extension descriptor.classifier.extenstion                }            }        }    }}

В результате использования Gradle мы избавились от конфликтов при определении транзитивных зависимостей и заметно ускорили работу приложения.


Сборка проекта


Для сборки CUBA SDK мы использовали тот же подход, что и для CUBA CLI. Мы с помощью инструмента jlink собирали все необходимые модули в кастомную JRE, которая поставляется вместе с приложением. Это позволило сделать SDK независимым от установленной на компьютерах пользователей Java. Пример такой сборки можно посмотреть в CLI Core Sample проекте.


Поддержка сторонних плагинов


Так как CUBA SDK построен на основе библиотеки CLI Core, CUBA SDK поддерживает сторонние плагины. С помощью системы плагинов сейчас в SDK реализованы maven и gradle менеджеры зависимостей компонентов и провайдеры для CUBA компонентов.


Рассмотрим пример, как мы можем расширить функционал SDK с помощью плагина. В данном примере мы напишем провайдер для стартеров Spring Boot из хорошо известного Spring Initializr.


Для начала создадим новый проект, для примера возьмем плагин для CUBA CLI, как описано здесь, и добавим зависимости:


implementation "com.haulmont.cli.core:cli-core:1.0.0"implementation "com.haulmont.cli.sdk:cuba-sdk:1.0.1"

Создадим новый провайдер для стартеров spring boot SpringBootProvider, который наследуем от BintraySearchComponentProvider. BintraySearchComponentProvider позволяет автоматически находить доступные версии компонентов, используя Bintray API.


class SpringBootProvider : BintraySearchComponentProvider() {   var springComponentsInfo: SpringComponentsInfo? = null   override fun getType() = "boot-starter"   override fun getName() = "Spring boot starter" ...   override fun load() {       springComponentsInfo = Gson().fromJson(readSpringFile(), SpringComponentsInfo::class.java)   }   private fun readSpringFile(): String {       return SpringComponentsPlugin::class.java.getResourceAsStream("spring-components.json")           .bufferedReader()           .use { it.readText() }   }

Этот провайдер будет искать доступные компоненты из файла spring-components.json, который является json версией yml файла приложения Spring Initializr.


Для маппинга из json в объекты создадим простые data классы:


data class SpringComponent(   val name: String,   val id: String,   val groupId: String?,   val artifactId: String?,   val description: String?,   val starter: Boolean? = true)data class SpringComponentCategory(   val name: String,   val content: List<SpringComponent>)data class SpringInitializr(   val dependencies: List<SpringComponentCategory>)data class SpringComponentsInfo(   val initializr: SpringInitializr)

Для того, чтобы добавить наш провайдер к остальным провайдерам SDK, нужно в init событии плагина зарегистрировать провайдер:


class SpringBootComponentsPlugin : CliPlugin {   private val componentRegistry: ComponentRegistry by sdkKodein.instance<ComponentRegistry>()   @Subscribe   fun onInit(event: InitPluginEvent) {       val bootProvider = SpringBootProvider()       componentRegistry.addProviders(bootProvider)       bootProvider.load()   }}

Все готово. Теперь, чтобы установить наш плагин в терминале или через IDE, нужно выполнить команду gradle installPlugin.


Запустим SDK
image


Видим, что наш плагин успешно загрузился. Теперь проверим, что вся наша логика работает с помощью команды resolve boot-starter:


image


image


Как видим, подсказки для компонентов и их версий успешно заработали.


Если в вашем хранилище артефактов артефакты и зависимости хранятся отлично от maven репозиториев, то через плагины можно использовать свою реализацию интерфейса ArtifactManager, в которой будет логика работы с вашим хранилищем.


Исходный код тестового плагина можно найти на странице GitHub.


Заключение


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


Если вы самоизолировались в глухой деревне или летите 8 часов в самолете и не готовы платить по 300 евро за 10 мегабайт трафика, то CUBA SDK отличное решение, которое позволит собрать актуальный стек используемых библиотек и фреймворков локально у вас на компьютере.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru