Урок 3: DSL, Императивный и Декларативный подход

Урок 3: DSL, Императивный и Декларативный подход

Введение

Почему в Jetpack Compose можно верстать функциями? В свое время Android разработка активно опиралась на Java, но постепенно отошла от него в пользу Kotlin. Сегодня Kotlin практически полностью вытеснил Java с этой арены.

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

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

fun main() {

    fun sayHello(name: String, message: () -> String) {
        println("Hello, $name! ${message()}")
    }

    fun sayBye(name: String, message: () -> String) {
        println("Bye, $name! ${message()}")
    }

    // Вызов без вынесения лямбды
    sayHello("Alice", {"How are you today?"})

    // Вызов с вынесением лямбды
    sayBye("John") {
		    "See you later!"    
    }
    
}

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

Это дало возможность разработчикам выстраивать более хитрые вложенные конструкции типа таких:

sayBye("John") {
    sayHello("Alice") {
        "How are you today?"
    }
    "See you later!"
}

Пример выше не имеет большого смысла, однако же такая небольшая особенность языка позволяет строить Domain-Specific Language на основе котлин.

DSL — это когда мы даем разработчику возможность описывать какую-то логику или конфигурацию максимально компактным и наглядным образом.

Примером могут служить файлы конфигурации сборки Gradle (те, которые с раcширением .kts, пришедшие на замену Groovy). Сейчас они пишутся на компактном Kotlin DSL, где все что мы здесь видим является вызовом функций. Мы даже можем в них провалиться и немножко подглядеть на сигнатуру.

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

Свой Kotlin DSL

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

class GreetingDSL(val mood: String) {
    fun sayHello(name: String, message: String) {
        println("[$mood] Hello, $name! $message")
    }

    fun sayBye(name: String, message: String) {
        println("[$mood] Bye, $name! $message")
    }
}

fun main() {

    // Функция для создания DSL
    fun greeting(
        mood: String,
        block: GreetingDSL.() -> Unit
    ) {
        val dsl = GreetingDSL(mood) // Передаём настроение в DSL
        dsl.block()                 // Выполняем переданный блок в контексте объекта
    }

    // !!! Объект класса GreetingDSL НЕ создается явно
    greeting("Happy") {
        sayHello("Alice", "How are you today?")
    }

    greeting("Serious") {
        sayBye("Mark", "Let’s talk tomorrow.")
    }
}

Здесь мы уже обернули два наших метода в класс GreetingDSL, а в классе добавили свойство mood. Больше ничего.

Но интересно дальше – в функции greeting(), которая принимает строку с настроением и лямбду. Лямбда содержит GreetingDSL с точкой перед круглыми скобками. Если видите такую конструкцию, значит, что всё, что в этой функции, будет выполняться так, будто мы уже находимся внутри объекта GreetingDSL.

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

DSL в Jetpack Compose

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

  • Хочешь вывести на экран текст? Вызови системную функцию, которая так и называется Text c параметром text. Да, как вы уже должно быть заметили, это все функции. И вопреки привычному синтаксису Kotlin – Composable функции пишутся с большой буквы и не в инфинитиве.
  • Хочешь заверстать кнопку? Легко, напиши слово “Кнопка” (а именно вызови соответствующую функцию), в параметры отправь какой-то коллбэк (какой метод должен дергаться при нажатии). А в лямбду можно поместить другую функцию Text, которая разместится внутри Button и будет представлять текст на кнопке. Причем это параметр, который можно опустить и записать именно лямбдой помните почему?
Button(onClick = { }) {
    Text(text = "Button")
}

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

Урок 3: DSL, Императивный и Декларативный подход

Напоминает что-то, не так ли? Да, близко к этому код мы видели в примере буквально пару минут назад.

Разработчики Jetpack Compose представили для нас мощный DSL, который скрыл от нас все несущественное для верстки. Туда действительно не нужно глубоко лезть без надобности, все использование можно сравнить с конструкцией строительного набора, где у тебя есть готовые детальки. Ты собираешь свой лего конструктор и радуешься. Все что нужно знать – это язык Kotlin. Ну а если понадобилось, создать свой кубик лего и использовать наряду со стандартными.

И сейчас мы немного подробнее остановимся на последнем параметре, а именно на интерфейсе RowScope.().

Scope функции

Важной особенностью языка является то, что в Kotlin есть так называемые scope-функции, которые мы уже проходили в рамках KotlinSprint (иногда их ещё называют функции области видимости). Они позволяют временно “погружаться” в контекст объекта или лямбды. Напомню: к стандартным scope-функциям относят: let, run, with, apply и also.

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

  • Во-первых то, за что мы любим Kotlin. Это элегантная инициализация. Представьте, что вам нужно инициализировать объект и сразу же настроить его свойства. С помощью, например, apply или also можно не создавать промежуточных переменных. Мы просто берём объект и “входим” в его контекст, где можно напрямую обращаться к полям (через this) или самому объекту (через it).
val person = Person().apply {
name = "Alice"
age = 20
}

  • Во-вторых, некоторые из них позволяют избежать проверки на null. Например, через let мы можем вызвать цепочку операций только если объект не равен null.
val text: String? = "Hello World"
text?.let {
println(it.length) // Только если text не null
}

  • И, пожалуй, самое интересное – локальный контекст. with(object) { ... } позволяет не засорять пространство имён и группировать вызовы. Мы будто бы “заходим” в объект и там уже вызываем его методы и обращаемся к свойствам — всё в одной компактной блок-конструкции.

Однако, в дополнение к стандартным scope-функциям Kotlin, ничто не мешает вам создавать собственные. Это может дать дополнительные возможности для контроля контекста и области видимости.

Почему это важно для Jetpack Compose?

Jetpack Compose использует похожий подход “погружения” в контекст, как у scope-функций. Только вместо объектов вроде apply или with мы работаем с DSL-областями (scope), например, с RowScope или ColumnScope.

Что это значит? Представьте, что у вас есть комната с набором инструментов. Если вы зашли в эту комнату, то всё, что вы делаете, относится только к этим инструментам. Например, если в комнате есть только молоток и гвозди, то вы можете забивать гвозди, но не сможете, скажем, покрасить стены. Так вот, RowScope — это такая «комната», где доступны только функции и свойства, связанные с размещением элементов в строку.

В коде с кнопкой мы пишем Text и Button как будто они просто отдельные функции. Но на самом деле они работают внутри специальной области RowScope. То есть мы неявно уже находимся в контексте RowScope, и всё, что мы добавлям в Row, автоматически относится к нему. Не нужно вручную указывать, что Text и Button принадлежат Row. Это работает потому, что Row “открывает” свою область видимости, и внутри фигурных скобок вы как будто находитесь “внутри Row”.

Если представить это без Compose, то можно сказать так:

  1. У вас есть объект RowScope, который умеет, например, добавлять элементы.
  2. В лямбде вы пишете команды: “Добавь текст”, “Добавь кнопку”.
  3. Каждая команда понимается как команда для RowScope.
val rowScope = RowScope()

rowScope.apply {
    addText("Hello")
    addButton {
        addText("Click me!")
    }
}

В Compose мы пишем то же самое, но более лаконично:

Row {
    Text("Hello")
    Button {
        Text("Click me!")
    }
}

Теперь давайте повторим первичное определение. Jetpack Compose – это декларативный UI фреймворк, где мы пишем интерфейс с помощью кода на языке программирования Kotlin.

Императивный и Декларативный подход в Android

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

Еще раз. Раньше, при так называемом классическом подходе через XML, мы описывали в коде “как” мы должны получить тот или иной результат – такой подход назывался императивным. Слово происходит от латинского imperativus – повелительный. Грубо говоря термин означает описание определенного алгоритма или свода правил.

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

Открою пустой проект c Empty Views для примера, где я добавил немного кода с одним TextView. То есть для того, чтобы вывести на экране текст нужно было:

  • Получить экземпляр TextView или создать его программно.
  • Установить текст, который должен отображаться.
  • Также можно было бы задать параметры отображения, например размер шрифта, положение текста на экране, выравнивание и так далее.
  • И добавить этот элемент в контейнер (например, LinearLayout, ConstraintLayout или другой тип макета).

И если обратимся к коду на Compose, то на первый взгляд может показаться, что он тоже императивный. Мол мы же пишем какие-то инструкции. Однако, это не так. Это именно декларативный подход, потому что мы, по сути, декларируем, что:

  • вот здесь будет вот такая функция приветствия Greeting – это произвольное пользовательское название.
  • В ней уже системная Composable функция Text, которая отвечает за отрисовку текста. Аналог TextView, но к классу View не имеет никакого отношения.
  • Что у функции есть вот такие параметры самого текста и какой-то модификатор в виде переданного паддинга.
  • А выше мы это все дело просто размещаем на экране в другой функции setContent. Условный аналог setContentView.

Вот и все, не нужно создавать экземпляр View, обращаться к элементам и их свойствам. Сразу объявляем кастомные Composable функции с нужными параметрами, вызываем эти функции в setContent и система отрисовывает то, что нужно отрисовать. Вот что есть настоящая декларация.

Далее пойдем в хорошем темпе по ключевым темам технологии и наконец начнем писать код.

Бесплатные Telegram-боты для обучения

Практика с проверкой кода и помощью ИИ-ментора

AndroidSprint AI Mentor

Проверяет Pull Request'ы в GitHub, проводит тестовые собеседования с голосом и таймером, помогает разбираться с кодом 24/7

Попробовать ИИ-ментора →

KotlinSprint Bot

22 урока Kotlin, 220 тестов, 120 практических задач с код-ревью

Начать обучение Kotlin →

AndroidSprint Bot

Тесты по Android SDK, Jetpack Compose, архитектуре приложений

Тесты по Android →

Тебе также может быть интересно

Узнать подробнее
Курс AndroidSprint

Глубокое обучение Android разработке с 0 до получения оффера. Только персональная практика с гарантией получения продуктового опыта.

Курс AndroidSprint - Глубокое <strong>обучение Android разработке с 0 до получения оффера</strong>. Только персональная практика с гарантией получения продуктового опыта.
Узнать подробнее
Узнать подробнее
Практикум по Kotlin

Изучение Котлин с 0 для профессиональной разработки. Личный ментор и разбор кода задач через git-flow.

Практикум по Kotlin - Изучение Котлин <strong>с 0 для профессиональной разработки</strong>. Личный ментор и разбор кода задач через git-flow.
Узнать подробнее
Узнать подробнее
Бесплатные уроки по Kotlin разработке

Самостоятельное освоение базы по языку для дальнейшего развития в Android/back-end разработке или в автотестах.

Бесплатные уроки по Kotlin разработке - <span>Самостоятельное освоение базы по языку для дальнейшего развития в Android/back-end разработке или в автотестах.</span>
Узнать подробнее
Узнать подробнее
Onboarding в разработку

Полное обучение Android разработке с нуля до получения оффера. Делаем упор на практику и обратную связь

Onboarding в разработку - <span>Полное обучение Android разработке с нуля до получения оффера. Делаем упор на практику и обратную связь</span>
Узнать подробнее
Узнать подробнее
Обучающий Kotlin телеграм бот (с тестами)

Ваш основной инструмент для изучения основ языка. Бесплатные тесты и практика внутри.

Обучающий Kotlin телеграм бот (с тестами) - Ваш основной <span>инструмент для изучения основ языка.</span> Бесплатные тесты и практика внутри.
Узнать подробнее
Узнать подробнее
Бесплатные уроки по Android разработке

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

Бесплатные уроки  по Android разработке - Самостоятельное <span>обучение разработке Андроид приложений.</span> Понятные видеоуроки с разжеванными примерами.
Узнать подробнее
Узнать подробнее
Курс по UI/Unit тестированию

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

Курс по UI/Unit тестированию - Для ручных тестировщиков, которые <span>готовы осваивать автотесты</span> с использованием актуального стека технологий. [в разработке]
Узнать подробнее
Узнать подробнее
Обучающий Android телеграм бот (с тестами)

Бесплатные теоретические тесты для самопроверки. А также информер на практических спринтах по Android.

Обучающий Android телеграм бот (с тестами) - Бесплатные <span>теоретические тесты для самопроверки.</span> А также информер на практических спринтах по Android.
Узнать подробнее