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

Kotlin android

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

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 используй для того, чтобы подключать общие модули между собой.

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

Kotlin. Лямбда vs Ссылка на функцию

10.03.2021 12:22:46 | Автор: admin

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

Представим, что у нас есть класс Button, который в конструкторе получает как параметр функцию onClick

class Button(    private val onClick: () -> Unit) {    fun performClick() = onClick()}

И есть класс ButtonClickListener, который реализует логику нажатий на кнопку

class ButtonClickListener {    fun onClick() {        print("Кнопка нажата")    }}

В классе ScreenView у нас хранится переменная lateinit var listener: ButtonClickListener и создается кнопка, которой передается лямбда, внутри которой вызывается метод ButtonClickListener.onClick

class ScreenView {    lateinit var listener: ButtonClickListener    val button = Button { listener.onClick() }}

В методе main создаем объект ScreenView, инициализируем переменную listener и имитируем нажатие по кнопке

fun main() {    val screenView = ScreenView()    screenView.listener = ButtonClickListener()    screenView.button.performClick()}

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

А теперь давайте вернемся в класс ScreenView и посмотрим на строку, где создается кнопка - val button = Button { listener.onClick() }. Вы могли заметить, что метод ButtonClickListener.onClick по сигнатуре схож с функцией onClick: () -> Unit, которую принимает конструктор нашей кнопки, а это значит, что мы можем заменить лямбда выражение ссылкой на функцию. В итоге получим

class ScreenView {    lateinit var listener: ButtonClickListener    val button = Button(listener::onClick)}

Но при запуске программа вылетает со следующей ошибкой - поле listener не инициализированно

Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property listener has not been initializedat lambdas.ScreenView.<init>(ScreenView.kt:6)at lambdas.ScreenViewKt.main(ScreenView.kt:10)at lambdas.ScreenViewKt.main(ScreenView.kt)

Чтобы понять в чем проблема, посмотрим чем отличается полученный Java код в обоих случаях. Опущу детали и покажу основную разницу.

При использовании лямбды создается анонимный класс Function0 и в методе invoke вызывается код, который мы передали в нашу лямбду. В нашем случае - listener.onClick()

private final Button button = new Button((Function0)(new Function0() {    public final void invoke() {       ScreenView.this.getListener().onClick();    }}));

То есть если мы передаем лямбду, наша переменная listener будет использована после имитации нажатия и она уже будет инициализирована.

А вот что происходит при использовании ссылки на функцию. Тут также создается анонимный класс Function0, но если посмотреть на метод invoke(), то мы заметим, что метод onClick вызывается на переменной this.receiver. Поле receiver принадлежит классу Function0 и должно проинициализироваться переменной listener, но так как переменная listener является lateinit переменной, то перед инициализацией receiver-а происходит проверка переменной listener на null и выброс ошибки, так как она пока не инициализирована. Поэтому наша программа завершается с ошибкой.

Button var10001 = new Button;Function0 var10003 = new Function0() {   public final void invoke() {      ((ButtonClickListener)this.receiver).onClick();   }};ButtonClickListener var10005 = this.listener;if (var10005 == null) {   Intrinsics.throwUninitializedPropertyAccessException("listener");}var10003.<init>(var10005);var10001.<init>((Function0)var10003);this.button = var10001;

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

Отсюда вытекает следующая интересная задача: Что напечатается после запуска программы?

class Button(    private val onClick: () -> Unit) {    fun performClick() = onClick()}class ButtonClickListener(    private val name: String) {    fun onClick() {        print(name)    }}class ScreenView {    var listener = ButtonClickListener("First")    val buttonLambda = Button { listener.onClick() }    val buttonReference = Button(listener::onClick)}fun main() {    val screenView = ScreenView()    screenView.listener = ButtonClickListener("Second")    screenView.buttonLambda.performClick()    screenView.buttonReference.performClick()}
  1. FirstFirst

  2. FirstSecond

  3. SecondFirst

  4. SecondSecond

Ответ

3

Спасибо за прочтение, надеюсь кому-то было интересно и полезно!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    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