Tasks и Back Stack в Android
Итак. Каждое Android приложение, как минимум, состоит из фундаментальных объектов системы — Activity. Activity — это отдельный экран который имеет свою отдельную логику и UI. Количество Activity в приложении бывает разное, от одного до много. При переходах между различными Activity пользователь всегда может вернуться на предыдущую, закрытую Activity при нажатии кнопки back на устройстве. Подобная логика реализована с помощью стека (Activity Stack). Его организация «last in, first out» — т.е. последний вошел, первый вышел. При открытии новой Activity она становится вершиной, а предыдущая уходит в режим stop. Стек не может перемешиваться, он имеет возможность добавления на вершину новой Activity и удаление верхней текущей. Одна и та же Activity может находиться в стеке, сколько угодно раз.
Task — это набор Activity. Каждый таск содержит свой стек. В стандартной ситуации, каждое приложение имеет свой таск и свой стек. При сворачивании приложения, таск уходит в background, но не умирает. Он хранит весь свой стек и при очередном открытии приложения через менеджер или через launcher, существующий таск восстановится и продолжит свою работу.
Ниже покажу картинку, как работает стек.
Если продолжать нажимать кнопку back, то стек будет удалять Activity до того, пока не останется главная корневая. Если же на ней пользователь нажмет back, приложение закроется и таск умрет. Кстати, я говорил о том, что когда мы сворачиваем наше приложение и запускам например новое, то наш таск просто уходит в background и будет ждать момента, пока мы его вызовем. На самом деле есть одно «но». Если мы будем иметь много задач в background или же просто сильно нагружать свое устройство, не мала вероятность того, что таск умрет из за нехватки системных ресурсов. Это не война конечно, но то что мы потеряем все наши текущие данные и наш стек очистится — это точно. Кстати для избежания потери данных в таком случаи, вам стоит почитать про SavingActivityState.
Маленький итог
Управление тасками
Существует два пути для изменения стандартной организации тасков. Мы можем устанавливать специальные атрибуты в манифесте для каждой Activity. Также мы можем устанавливать специальные флаги для Intent, который запускает новую Activity с помощью startActivity(). Заметьте, что иногда атрибуты в манифесте и флаги в Intent могут противоречить друг другу. В этом случаи флаги Intent будут более приоритетны.
Атрибут launchMode
Флаги
Affinity
Стандартно все Activity нашего приложения работают в одном таске. По желанию мы можем изменять такое поведение и указывать, чтобы в одном приложении Activity работали в разных тасках, или Activity разных приложений работали в одном. Для этого мы можем в манифесте для каждой Activity указывать название таска параметром taskAffinity. Это строковое значение, которое не должно совпадать с названием package, т.к. стандартный таск приложения называется именно как наш пакет. В общем случаи данный параметр указывает, что Activity будет гарантированно открываться в своём отдельном таске. Данный параметр актуален, если мы указываем флаг FLAG_ACTIVITY_NEW_TASK или устанавливаем для Activity атрибут allowTaskReparenting=«true». Этот атрибут указывает, что Activity может перемещаться между тасками, который её запустил и таском, который указан в taskAffinity, если один из них становится активным.
Чистка стека
Это всё для данного топика. Статья не импровизированная, а по сути является вольным переводом официальной документации. Рекомендую собрать легкий пример и поэксперементировать с флагами и атрибутами. Некоторые моменты, лично для меня были, неожиданно интересными. любые ошибки и недоработки учту в лс. Спасибо.
Правила работы с Tasks API. Часть 1
В этом посте я попытаюсь показать проблему, решение и истоки.
Проблема
Пусть у нас есть код, который выглядит так:
Вопрос знатокам: есть ли в данном примере проблема? Если да, то какая? Код компилируется, возвращаемый тип Task на месте, модификатор async при использовании await — тоже.
Думаете, речь идет о пропущенном ConfigureAwait? Хаха!
NB: вопрос о ConfigureAwait я опущу, ибо о другом статья.
Истоки
До идиомы async/await основным способом использования Tasks API был метод Task.Factory.StartNew() с кучей перегрузок. Так, Task.Run() немного облегчает данный подход, опуская указание планировщика (TaskScheduler) и т.п.
Ничего особенно в примере выше нет, но именно здесь начинаются отличия, и возникает главная проблема — многие начинают думать, что Task.Run() — это облегченный Task.Factory.StartNew().
Чтобы стало нагляднее, рассмотрим пример:
Что? Два await’a? Именно так.
Все дело в перегрузках:
Несмотря на то, что возвращаемый тип у обоих методов — Task<TResult>, входным параметром у Run является Func<Task<TResult>>.
В случае с async () => await inner Task.Run получит уже готовую state-машину (а мы знаем, что await — есть не что иное, как трансформация кода в state-машину), где все оборачивается в Task.
StartNew получит то же самое, но TResult уже будет Task<Task<T>>.
В одной статье, я уже описывал работу dynamic: каждый statement в C# превращается в узел вызова (call-site), который относится ко времени исполнения, а не компиляции. При этом сам компилятор старается побольше метаданных передать рантайму.
Метод Compute() использует и возвращает Task<dynamic>, что заставляет компилятор создавать эти самые узлы вызовов.
Причем, это корректный код — результатом в рантайме будет Task<Task<dynamic>>.
Решение
Оно весьма простое: необходимо использовать метод Unwrap().
В коде без dynamic вместо двух await’ов можно обойтись одним:
Теперь, как и ожидалось, результатом будет Task<dynamic>, где dynamic — именно возвращаемое значение inner’a, но не еще один таск.
Словарик айтишника или Что? Где? Куда? Часть 1
«Привет! Добро пожаловать! Спасибо, что приняла наш оффер. Пойдем знакомиться с твоей командой. У них как раз сейчас дейли. Ты вышла под конец спринта, поэтому пока работы для тебя не запланировали. Как стендап закончится, можешь почитать спеки, командные окиары и просмотреть бэклог на следующий спринт. По всем вопросам обращайся к своему пио.»
Язык айтишников
Каждый, кто работает в IT, непременно сталкивался с профессиональным жаргоном и компьютерным сленгом. Его можно любить или ненавидеть, принимать или терпеть, но непреложным остается факт — IT-жаргон существует и от него никуда не деться.
Когда приходишь в новую компанию, на тебя наваливается куча незнакомых слов. Кажется, их так много, что потребуется немало времени, чтобы понять и выучить их все. Многие слова ты уже знаешь, о смысле других догадываешься, часть из них является англицизмами, поэтому догадаться об их значении несложно Первая реакция — неприятие: «Зачем использовать английские слова в русский речи, когда есть достаточно русских альтернатив?» Потом ты пытаешься сохранить чистоту языка. В итоге, начинаешь говорить так же, как и все. Это неизбежно.
Профессиональный жаргон существует не для того, чтобы испортить русский язык. Он позволяет ускорить устное общение IT-специалистов и наладить их взаимопонимание. Обычно слова получаются короткими и емкими. Иногда одно слово заключает в себе целую фразу. Поэтому польза в них, на мой взгляд, есть.
Я послушала, как говорят разработчики в Wrike, и составила словарик из самых распространенных слов. Слова собраны по тематическим группам.
Scrum-терминология
Scrum — это методология по управлению проектами. Набор принципов, ценностей, политик, ритуалов для организации работы. В скраме полно терминов, но в ежедневный обиход попала и закрепилась только часть из них.
Бэклог
От англ. backlog (дословно — очередь работ) — еще не запланированный объем работы, который требуется выполнить команде. Каждая созданная задача вначале попадает в бэклог, а потом уже в спринт.
Как и в случае со спринтом, термин используется и в отрыве от скрама. Часто бэклогом называют отложенные задачи. Которые сделать нужно, но не сейчас.
Гол, голевой
От англ. goal (дословно — цель) — цель спринта (бывает одна или несколько), которую команда берется сделать. Цель состоит из ряда задач, которые нужно выполнить, чтобы его достигнуть.
Слово употребляется и как существительное, и как прилагательное. Может быть множественного числа.
Дейли
От англ. daily (дословно — ежедневно) — ежедневные короткие (от 5 до 30 минут) встречи команды с целью поделиться прогрессом по выполненным задачам за предыдущий день и озвучить план работ на текущий день. Также дейли могут называть стендапом (от daily standup), потому что обычно такие встречи происходят стоя — для большей эффективности.
Коммититься
Глагол от англ. существительного commitment (дословно — ответственность). Коммититься — значит обещать выполнить определенный объем работы в оговоренные сроки. Это не просто обещание, это сознательное обязательство перед собой и командой. Человек, который закоммитился, обязан сделать всё возможное, чтобы выполнить то, что сам и пообещал реализовать.
Спринт
От англ. sprint (дословно — бег на короткую дистанцию) — заданный отрезок времени, за который нужно выполнить запланированный объем работы, чтобы в конце этого отрезка был ожидаемый результат.
Термин используют не только те, кто работает по скраму, но и те, кто просто хочет организовать свою работу и сформировать ясные рамки, во время которых должны быть выполнены задачи.
Инструменты для работы
Технические, информационные и вспомогательные средства и приложения для работы.
Ветка
От англ. branch (дословно — ветка) — тот редкий случай, когда в ходу русский перевод термина. Веткой (термин git) называют полную копию проекта, в которой ведется разработка. В проекте может быть создано много веток, что позволяет работать одновременно с разными частями кода. Потом все ветки загружаются в мастер. Процесс «ответвления» иногда называют «бранчеванием», уже как раз от branch.
От англ. mock-up (дословно — эскиз) — макет с UX-дизайном для разработки. Несмотря на то, что слово дословно переводится как «эскиз» или «прототип», в Wrike моками называют готовые проработанные макеты с дизайном.
От англ. production (дословно — промышленная среда) — ветка с рабочей версией продукта, которую видят пользователи. Это окончательная точка куда попадает результат разработки. Иногда так же называют мастер.
От англ. reference (дословно — пример) — схожий функционал или внешний вид, который используется для ориентира. Он служит для сравнения.
Спека
От англ. specification (дословно — спецификация) — документ с подробным описанием требований, условий и технических характеристик, как должен работать разрабатываемый функционал.
Таска
От англ. task (дословно — задача) — задача, заведенная или планируемая на любого работника.
Разработка
Термины, употребляющиеся разработчиками при работе над задачами.
От англ. boost (дословно — ускорение) — процесс повышения производительности, ускорение загрузки.
Катить
Отправлять готовую работу в деплой, предпринимать шаги для подготовки ветки к мерджу в продуктовую ветку.
Комплитить
От англ. complete (дословно — заканчивать) — завершать задачу, закрывать задачу, когда она полностью готова.
Консистентность
От англ. consistency (дословно — системность) — общее единообразие во всех частях продукта.
Матчится
От англ. match (дословно — совпадать) — полное соответствие чего-либо с чем-либо. Процесс приведения к единообразию.
Пинать
Термин, подобный глаголу «пинать», который также имеет значение «делать» и «работать». Конкретное значение определяется по приставке. Подопнуть — сделать немного, допинать — доделать.
Ручка
От англ. handler (дословно — обработчик) — бэкэнд-термин, означающий ответ от сервера, в котором приходят данные.
Скоуп
От англ. scope (дословно — объем) — набор фич и частей продукта, закрепленных за отдельной командой.
От англ. feature (дословно — характеристика) — определенная часть или деталь от общего продукта, которая разрабатывается изолированно.
От англ. flow (дословно — течение) — порядок действий при работе над задачей. Например, вначале задача берётся в разработку, потом проходит ревью, далее тестируется и т.д.
Должности
Некоторые должности, названия которых вошли в обиход в виде сокращений с английского.
Девопс
От англ. DevOps, сокращенно от Developer Operations (дословно — интеграция разработки и эксплуатации) — специалист, занимающийся внедрением DevOps-методологии. Полное название должности — DevOps-инженер, но в речи вторую часть всегда отбрасывают.
От англ. PO, сокращенно от Product Owner (дословно — владелец продукта) — роль по скрам-методологии, человек, ответственный за проработку продукта и распределение бэклога. Он знает о требованиях пользователя и возможностях команды.
От англ. PM, сокращенно от Product Manager (дословно — менеджер продукта) — менеджер, который отвечает за продукт, его обязанности совпадают с обязанностями пио, отличие только в том, что это название должности, а не роли в скраме. Так же, как пио, пиэмов могут называть продакт.
Организационное
Термины, относящиеся к организации работы, а также термины, употребляющиеся в неформальной речи при обсуждении чего-либо.
Дейоф
От англ. day-off (дословно — выходной) — просто выходной.
Драйвер
От англ. driver (дословно — водитель) — человек, который берет на себя инициативу управления проектом/процессом/задачей. В его обязанности входит следить за тем, как протекает созданный им процесс, и руководить им. Он мотивирует других людей выполнять работу для достижения поставленных целей.
Консёрн
От англ. concern (дословно — тревога, участие) — в английском языке слово «консёрн» имеет много различных значений, при этом очень часто употребляется в русской речи. Какое именно значение вкладывает в него автор, известно только ему самому. Иногда — это смесь многих значений, таких как: особый интерес, беспокойство, цель, настороженность, опасение и т.д.
Окиары
От англ. OKR, сокращенно от Objectives and Key Results (дословно — цели и ключевые результаты) — система по постановке и достижению целей. Она нужна для синхронизации работы всех участников компании/отдела/команды, чтобы все двигались в одном направлении, с понятными приоритетами и постоянным ритмом. В отличие от KPI, это амбициозное целеполагание, достижение окиаров (окров) на 70-80% — отличный результат.
Оффер
От англ. offer (дословно — предложение) — предложение о работе / приглашение на работу.
Поинт
От англ. point (дословно — точка) — чаще всего употребляется в значении «точка зрения», сокращенно от point of view. Также в значениях: «суть», «смысл», «довод».
Tasks и Back Stack в Android
Итак. Каждое Android приложение, как минимум, состоит из фундаментальных объектов системы — Activity. Activity — это отдельный экран который имеет свою отдельную логику и UI. Количество Activity в приложении бывает разное, от одного до много. При переходах между различными Activity пользователь всегда может вернуться на предыдущую, закрытую Activity при нажатии кнопки back на устройстве. Подобная логика реализована с помощью стека (Activity Stack). Его организация «last in, first out» — т.е. последний вошел, первый вышел. При открытии новой Activity она становится вершиной, а предыдущая уходит в режим stop. Стек не может перемешиваться, он имеет возможность добавления на вершину новой Activity и удаление верхней текущей. Одна и та же Activity может находиться в стеке, сколько угодно раз.
Task — это набор Activity. Каждый таск содержит свой стек. В стандартной ситуации, каждое приложение имеет свой таск и свой стек. При сворачивании приложения, таск уходит в background, но не умирает. Он хранит весь свой стек и при очередном открытии приложения через менеджер или через launcher, существующий таск восстановится и продолжит свою работу.
Ниже покажу картинку, как работает стек.
Если продолжать нажимать кнопку back, то стек будет удалять Activity до того, пока не останется главная корневая. Если же на ней пользователь нажмет back, приложение закроется и таск умрет. Кстати, я говорил о том, что когда мы сворачиваем наше приложение и запускам например новое, то наш таск просто уходит в background и будет ждать момента, пока мы его вызовем. На самом деле есть одно «но». Если мы будем иметь много задач в background или же просто сильно нагружать свое устройство, не мала вероятность того, что таск умрет из за нехватки системных ресурсов. Это не война конечно, но то что мы потеряем все наши текущие данные и наш стек очистится — это точно. Кстати для избежания потери данных в таком случаи, вам стоит почитать про SavingActivityState.
Маленький итог
Управление тасками
Существует два пути для изменения стандартной организации тасков. Мы можем устанавливать специальные атрибуты в манифесте для каждой Activity. Также мы можем устанавливать специальные флаги для Intent, который запускает новую Activity с помощью startActivity(). Заметьте, что иногда атрибуты в манифесте и флаги в Intent могут противоречить друг другу. В этом случаи флаги Intent будут более приоритетны.
Атрибут launchMode
Флаги
Affinity
Стандартно все Activity нашего приложения работают в одном таске. По желанию мы можем изменять такое поведение и указывать, чтобы в одном приложении Activity работали в разных тасках, или Activity разных приложений работали в одном. Для этого мы можем в манифесте для каждой Activity указывать название таска параметром taskAffinity. Это строковое значение, которое не должно совпадать с названием package, т.к. стандартный таск приложения называется именно как наш пакет. В общем случаи данный параметр указывает, что Activity будет гарантированно открываться в своём отдельном таске. Данный параметр актуален, если мы указываем флаг FLAG_ACTIVITY_NEW_TASK или устанавливаем для Activity атрибут allowTaskReparenting=«true». Этот атрибут указывает, что Activity может перемещаться между тасками, который её запустил и таском, который указан в taskAffinity, если один из них становится активным.
Чистка стека
Это всё для данного топика. Статья не импровизированная, а по сути является вольным переводом официальной документации. Рекомендую собрать легкий пример и поэксперементировать с флагами и атрибутами. Некоторые моменты, лично для меня были, неожиданно интересными. любые ошибки и недоработки учту в лс. Спасибо.
Дикая природа Gradle Task: руководство по выживанию
Приветствую, Gradle-адепт. В статье тебя ждёт авторский тур по Gradle Task. В маршрут включено хождение по граблям, изучение секретных практик buildscript-тасок, проведение раскопок по deprecated API, а ближе к концу зарядимся силой Custom Gradle Task, попрактикуемся в строительстве билд-кеша и узнаем, кто такой Worker API.
В предыдущих двух топиках разобрали основные принципы работы Gradle и работу Gradle Plugin. Кому интересно, добро пожаловать:
Дикая природа Gradle Task: руководство по выживанию
В этой статье я постарался раскрыть основные особенности Gradle Task, изучив которые, можно подходить к решению боевых задач вооружившись. Для удобства навигации по статье, ниже представляю оглавление. Можно смело переходить к интересующей вас теме уже сейчас:
Что такое Gradle Task?
Gradle Task представляет собой атомарную единицу работы при сборке проекта. Например, это может быть компиляция классов, создание Javadoc, публикация в репозиторий и т. д. Gradle-таска является рабочим, который выполняет строго возложенную на него обязанность. Рабочих можно упорядочить так, чтобы результатом их работы стал собранный проект.
Перед выполнением Gradle помечает таску одним из доступных состояний. Состояние таски основано на том, есть ли у неё задачи, которые нужно выполнить и принесёт ли выполнение этих задач какие-либо изменения.
Состояния Gradle Task
Всего для Gradle Task представлено 5 состояний, в первых четырёх из которых таска пропускает своё выполнение.
Входные и выходные параметры таски не изменились с момента её последнего выполнения;
Таска самостоятельно сообщила о состоянии UP-TO-DATE;
Все зависимости таски находятся в состояниях UP-TO-DATE / SKIPPED / FROM-CACHE / NO-SOURCE;
У таски нет зависимостей и задач к выполнению.
Hello, Gradle!
Для начала создадим примитивную таску, которая будет выводить сообщение в консоль при сборке проекта. Самый простой способ создать таску для Gradle – реализовать её в buildscript-е, давайте так и поступим. Для примеров, как и раньше, я буду использовать Kotlin DSL. Не особо задумываясь, пробуем написать в build.gradle.kts следующее:
Жмём Gradle Sync и видим следующее:
Выполняем созданную таску и смотрим, что получилось:
Очередь, прекрасно! На самом деле её наличие несёт в себе более чем философский смысл и добавляет возможности для удобного расширения таски.
Кажется, можно идти дальше и разобрать пример посложнее, но в коде до сих пор кроется одна загвоздка, на которую тоже следует обратить внимание, а именно – на способ создания.

Эффективное создание Buildscript Gradle Task
Создание таски является довольно трудоёмкой операцией для Gradle, поскольку для этого необходимо провести её конфигурацию, up-to-date проверки и добавить её в таск-контейнер. При использовании функции create это будет происходить каждый раз при конфигурации проекта, вне зависимости от того, будет ли таска использоваться. При этом то же самое будет происходить с тасками, от которых зависит создаваемая таска, и так далее по цепочке.
Конфигурация проекта и без того выполняется в однопоточном режиме (на момент Gradle 7.2), а здесь мы добавляем на неё неоправданную нагрузку.
В итоге правильным вариантом написания “Hello, Gradle“-таски будет следующий:
P.S. Также не будет лишним проверить ваши скрипты сборки на наличие старого API и заменить его на новый по официальному гайду. Здесь, как и везде в Gradle, не без подводных камней, но результат того стоит. На своём рабочем проекте мне удалось снизить среднее время конфигурации с 1 минуты 10 секунд до 40 секунд без особых сложностей.
Теперь предлагаю двинуться дальше и разобраться в том, каким образом в buildscript-таску передавать аргументы.
Передача аргументов в Buildscript Gradle Task
Усложним «Hello, Gradle»-пример и добавим таске возможность выводить строку из файла, который передадим в качестве параметра. Здесь-то мы и сталкиваемся с первым весомым ограничением buildscript-таски. Поскольку таска не представляет собой отдельного класса, то и каких-либо осознанных property или функций для передачи аргументов сделать не получится. Для начала попробуем захардкодить файл, из которого будем считывать:
Сам файл создадим самостоятельно. Запускаем:
Всё работает. Но возникает вполне резонный вопрос – неужели нельзя по-другому, ведь не факт, что считывать всегда будем из одного и того же файла. Закапываемся в документацию и находим решение – TaskInputs. TaskInputs представляет собой контейнер, который призван хранить в себе аргументы для таски. По сути, это интерфейс, который предоставляет необходимые функции для передачи аргументов различного типа, а также доступа к ним. Попробуем воспользоваться:
Запускаем, и… Всё работает! Но подход всё ещё остаётся не самым очевидным, поскольку при обращении к inputs мы не знаем, что там на самом деле лежит. Тем не менее, такой вариант уже намного лучше.
Другим часто встречающимся кейсом является передача аргументов через командную строку. Рассмотрим его чуть позже, когда затронем тему кастомных тасок.
Также сложно поспорить с тем, что в подавляющем большинстве случаев файл создаём не мы, а какая-нибудь другая Gradle-таска. Дальше давайте попробуем устроить такое взаимодействие.
Зависимости между Gradle-тасками
Для начала попробуем написать таску, создающую файл. Начнём с простого и захардкодим путь к выходному файлу:
Работает. Однако при использовании dependsOn необходима уверенность, что таска createTextFile обязательно положит в аргументы таски printFileContent файл. Если этого не произойдёт, всё сломается. Поломка возможна, если таска createTextFile будет выполняться инкрементально, то есть пропускать своё выполнение.
Здесь на помощь приходит TaskOutputs – контейнер для хранения результатов выполнения таски. Gradle обещает работоспособность, если связать TaskOutputs одной таски с TaskInputs другой даже при инкрементальном выполнении. Давайте в этом убедимся.
Немного переделаем таску createTextFile :
Здесь с помощью outputs.file(«input.txt») регистрируем выходной файл, а с помощью outputs.files.singleFile обращаемся к нему при выполнении таски. Теперь давайте свяжем таски друг с другом через их inputs и outputs :
Всё готово, пробуем:
Порядок. Попробуем ещё раз:
Более того, если outputs одной таски связан с inputs другой, то dependsOn также можно убрать. Gradle самостоятельно поймёт, что таски связаны друг с другом. По этим причинам способ объявления зависимостей через inputs и outputs является намного более удобным, и я бы рекомендовал использовать его.
Что получилось в итоге
Расширяем поведение Gradle Task
Например, несложно дополнить таску для вывода в файл какой-нибудь дополнительной строкой. Сделать это можно следующим образом:
С помощью doFirst и doLast можно добавить сколько угодно действий существующей таске.
Не работает для таски, которая уже была создана и добавлена в task-контейнер;
В Kotlin DSL поддерживает замену только на кастомную таску (имплементированную с помощью класса);
Исходя из этого, использование replace выглядит странным, и проще выполнить подобную конструкцию:
Результат получим тот же самый.
Также у Gradle есть некоторое количество стандартных тасок, которые призваны упростить жизнь разработчикам. Например, для копирования директорий удобно пользоваться таской Copy :
, а для удаления – таской Delete :
О том, какие таски уже реализованы в стандартном API, можно посмотреть в документации в разделе «Task Types».
Теперь самое время подвести промежуточные итоги и выпить чаю.
Резюме по использованию Buildscript Gradle Task
Резюме составим по основным преимуществам и недостаткам buildscript-тасок:
Быстрая и простая реализация;
Возможность повторного использования таски в других Gradle Project;
Конфигурируемость: скрипт-потребитель должен знать только про входные параметры таски;
Логика таски не может быть распределена по классам и пакетам, как мы к этому привыкли;
Нет возможности создавать осознанные property для передачи аргументов;
Чем больше тасок в скрипте, тем менее поддерживаемым становится скрипт;
Невозможность интеграционного/юнит тестирования;
Невозможность использования параллелизма внутри таски.
Таким образом, buildscript-таски хорошо подходят для разовых задач, не требующих дальнейшего развития и поддержки. Несмотря на то, что нам удалось создать действительно конфигурируемый и полностью рабочий код, возможности для его поддержки и тестирования оставляют желать лучшего.
Поэтому если ваша таска представляет собой полноценную самостоятельную логику, лучшим вариантом для её имплементации будет создание кастомной Gradle-таски. Дальше рассмотрим примеры имплементации и во всём убедимся.
Custom Gradle Task
Custom Gradle Task представляет собой класс, унаследованный от DefaultTask или любого другого его наследника:
Поскольку все Gradle-таски обязаны быть open или abstract для Kotlin и public для Java, унаследоваться можно от любой существующей таски. Такое же правило распространяется и на только что реализованный нами класс.
Аннотацией @TaskAction помечается функция, которая будет выполняться при выполнении таски. Это как раз тот самый Action, который Gradle добавит в начало выполнения. Если для таски определено несколько @TaskAction функций, они будут выполняться в обратном порядке.
Для очевидности происходящего лучше оставить функцию @TaskAction в единственном экземпляре. Она как раз и будет служить точкой входа для выполнения таски.
Класс можно поместить в одно из трех мест. Начнём от простого к сложному:
Реализация таски в отдельном Gradle-проекте. Такой вариант актуален, если вы пишете полноценный Gradle-плагин, или пишете библиотеку, которая будет состоять из нескольких тасок. Как правило, если возникла необходимость в реализации кастомных тасок, то почти всегда есть смысл упаковать их в плагин и поставлять в проект как полноценную логическую единицу. Поэтому такая реализация будет предпочтительна в большинстве случаев.
Давайте убедимся в том, насколько удобным и очевидным будет подход к реализации тасок из предыдущих примеров. Таска для создания файла будет выглядеть следующим образом:
Все возможные варианты контейнеров можно посмотреть в документации и выбрать подходящий для вас.
Но тогда и о дефолтных значениях говорить не приходится.
Теперь создадим таску для печати содержимого файла в консоль:
Теперь связываем таски друг с другом:
build.gradle.kts
P.S. В большинстве случаев связывание лучше проводить именно внутри плагина, в котором таски будут жить:
Такой плагин может быть реализован как в buildSrc, так и в отдельном Gradle-проекте. Подробнее о том, как реализовать и подключить плагин в проект, можно узнать из предыдущей статьи.
Передача аргументов через командную строку
Популярным вариантом использования Gradle-тасок является CI (Continuous integration), где зачастую аргументы необходимо передавать через командную строку. Кастомная Gradle-таска позволяет нам это сделать. Для наглядности попробуем написать таску-справочник, которая выводит в консоль информацию по одной из доступных тем:
Далее регистрируем таску, например, в build.gradle.kts:
По заветам документации, для указания параметра в командной строке необходимо воспользоваться двойным тире. Пробуем:
Порядок! Также необходимо учесть, что аргументы для командной строки имеют ограниченное количество типов данных. О доступных к использованию типах можно узнать в документации.
Gradle Task и build-cache
Предположим, на нашем проекте реализован билд-кеш, который используется командой разработчиков на разных хостах, и мы хотим, чтобы параметры таски также кешировались и распространялись по билд-машинам.
ABSOLUTE – кеш этого параметра завязан на абсолютный путь к нему. То есть при попытке запустить таску на другом хосте, значение данного параметра всегда будет считаться недействительным. Является стратегией по умолчанию.
NONE – кеш завязан только на содержимое. Если это файл, то для определения валидности кеша производится расчёт контрольной суммы. Если это директория, то производится расчёт контрольной суммы всего её содержимого. Например, данную стратегию можно применить к файлам конфигурации.
Теперь Gradle может бережно складывать выходной файл в билд-кеш и отправлять в путешествие по разным билд-машинам.
Инкрементальное выполнение
Например, немного переделаем таску для вывода в консоль и будем выводить содержимое коллекции файлов. Выводить в консоль будем только те файлы, которые изменились с момента последнего билда:
Как видно из примера, c помощью InputChanges также удобно итерироваться по коллекции файлов.
И напоследок предлагаю коснуться темы параллельных вычислений в Gradle и того, как их можно осуществлять при помощи Worker API.
Worker API
Чаще всего таски не выглядят так примитивно и требуют весомого времени для выполнения. Поскольку время сборки проекта достаточно дорогое, а проблема медленных сборок актуальна для большинства пользователей Gradle, необходим механизм для распараллеливания вычислений. Этим механизмом в Gradle выступает Worker API, который построен на основе очереди задач и воркеров. Воркеры расхватывают задачи из очереди и выполняют их. Идея не нова, а также давно используется в Gradle для распараллеливания тасок.
Представим, что таска создаёт несколько файлов и проводит тяжёлые вычисления для расчёта содержимого. В реальном мире это может быть компиляция (например, KotlinCompile или JavaCompile).
Пример таски с использованием Worker API
В нашем случае нет необходимости выполнять работу в разных процессах или classLoader, поэтому достаточно noIsolation очереди.
С помощью submit отправляем задачу в очередь, а в лямбде задаём задаче параметры. Теперь выполнение функции execute () внутри WorkerAction будет подхватываться несколькими потоками.
Итоги
Gradle Task представляет собой одну из фундаментальных единиц сборки проекта. В статье я постарался рассмотреть базовые принципы работы с ними, разобравшись в которых, можно значительно облегчить понимание процесса сборки.
К сожалению, Gradle API по-прежнему способствует хождению по граблям, однако с приходом Kotlin DSL процесс изучения значительно упростился и, по крайней мере для меня, успехи в использовании Gradle уже перестали походить на везение.
Буду благодарен за любые замечания, предложения и другие обсуждения по материалу. Спасибо за внимание!