Когда ваше приложение построено на многомодульной архитектуре, вам приходится уделять много времени тому, чтобы все связи между модулями были правильно прописаны в коде.
Половину этой работы можно доверить фреймворку Dagger 2. Руководитель группы «Яндекс.
Карты для Android» Владимир Тагаков Нокса рассказал о плюсах и минусах многомодульности и удобной организации DI внутри модулей с помощью Dagger 2. — Меня зовут Владимир, я занимаюсь разработкой Яндекс.
Карт и сегодня расскажу вам о модульности и втором Кинжале.
Самое длинное я понял, когда изучал сам, самое быстрое.
Вторую часть, над которой я работал несколько недель, я вам расскажу очень быстро и лаконично.
Почему мы запустили сложный процесс разделения на модули в Картах? Мы просто хотели увеличить скорость сборки, все об этом знают.
Вторая цель — уменьшить препятствия в коде.
Ссылку я взял из Википедии.
Это означает, что мы хотели уменьшить связь между модулями, чтобы модули были отдельными и могли использоваться вне приложения.
Исходная постановка задачи: другие проекты Яндекса должны иметь возможность использовать часть функционала Карт точно так же, как наш.
И чтобы мы развивали этот функционал в рамках развития проекта.
Хочу кинуть горящий тапок в сторону [k]apt, что замедляет скорость сборки.
Я не ненавижу его очень сильно, но очень люблю.
Это позволяет мне использовать Dagger.
Главным недостатком процесса модульизации является, как это ни парадоксально, замедление скорости сборки.
Особенно в самом начале, когда удаляешь первые два модуля, Common и некоторые свои фичи, общая скорость сборки проекта падает, как бы ты ни старался.
Со временем, поскольку в вашем основном модуле остается все меньше и меньше кода, скорость сборки увеличится.
И все же это не значит, что все очень плохо, есть способы обойти это и даже получить прибыль с первого модуля.
Второй недостаток — сложно разделить код на модули.
Любой, кто это пробовал, знает, что вы начинаете вытягивать какие-то зависимости, какую-то классику, и все заканчивается тем, что вы копируете весь свой основной модуль в другой модуль и начинаете все сначала.
Поэтому нужно четко понимать момент, когда нужно остановить и разорвать соединение, используя какую-то абстракцию.
Недостаток — больше абстракций.
Больше абстракций – более сложный проект – больше абстракций.
Сложно добавлять новые модули Gradle. Почему? Например, приходит разработчик, берет в разработку новую фичу, сразу делает ее хорошо, делает отдельный модуль.
В чем проблема? Он должен запомнить весь доступный код, который есть в основном модуле, чтобы в случае чего можно было его повторно использовать и выложить в Common. Потому что процесс перемещения модуля в Common является постоянным, пока ваш основной модуль приложения не превратится в тонкий слой.
Модули, модули, модули.
Модули Gradle, модули Dagger, интерфейсные модули — ужас.
Отчет будет состоять из трех частей: маленькой, большой и сложной.
Сначала о разнице между Implementation и API в AGP. Плагин Android Gradle 3.0 появился относительно недавно.
Как все было до него?
Вот типичный проект здорового разработчика, состоящий из трех модулей: модуля App, который является основным, собранным и установленным в приложении, и двух модулей Feature.
Давайте сразу поговорим о стрелках.
Это большая боль, каждый рисует в том направлении, в котором ему удобно рисовать.
Для меня они означают, что стрелка идет от ядра к функции.
Это означает, что Feature знает о Core и может использовать классы из Core. Как видите, между Core и App нет стрелки, что означает, что App не должен использовать Core. Ядро — это не Common модуль, оно существует, от него все зависит, оно отдельное, кода в нем мало.
Мы пока не будем это рассматривать.
У нас изменился модуль Core, надо его как-то переделать.
Изменяем в нем код. Желтый цвет – смена кода.
Затем мы восстанавливаем проект. Понятно, что после изменения модуля его придется собирать и перекомпилировать.
ХОРОШО.
Затем собирается зависимый от него модуль Feature. Также понятно, что его зависимость пересобрана, и ему необходимо обновиться.
Кто знает, что там изменилось.
И тут случается самое неприятное.
Модуль App собирается, хотя и непонятно зачем.
Я точно знаю, что вообще не использую Core, и почему Приложение пересобирается, непонятно.
И она очень большая, потому что она в самом начале пути, и это очень большая боль.
Кроме того, если от Core зависит несколько функций и множество модулей, то перестраивается весь мир, а это занимает очень много времени.
Перейдем на новую версию AGP и заменим, как сказано в инструкции, все компилируем с API, а не с Implementation, как вы думали.
Ничего не меняется.
Схемы идентичны.
Каков новый способ указания зависимостей реализации? Давайте представим ту же схему, используя только это ключевое слово, без API? Это будет выглядеть так.
Здесь в реализации хорошо видно, что между Core и App есть связь.
Здесь мы четко можем понять, что он нам не нужен, мы хотим от него избавиться, поэтому просто удаляем.
Кажется, все становится проще.
Сейчас все почти нормально, даже более того.
Если мы изменим какой-либо API в Core, добавим новый класс, новый общедоступный или приватный метод пакета, то ядро и функция будут перестроены.
Если вы измените реализацию внутри метода или добавите приватный метод, то теоретически при повторной сборке Feature вообще ничего не должно произойти, потому что ничего не изменилось.
Давайте двигаться дальше.
Так уж получилось, что от нашего Ядра зависят многие люди.
Вероятно Core — это какая-то Сеть или обработка пользовательских данных.
Потому что это Сеть, все довольно часто меняется, все собирается заново, и мы получили ту самую боль, от которой старательно убегали.
Давайте рассмотрим два способа справиться с этим.
Мы можем перенести из нашего модуля Core в отдельный модуль только API-интерфейсы, его API, то, что мы используем.
А реализацию этих интерфейсов мы можем вынести в отдельный модуль.
Вы можете посмотреть соединения на экране.
Core Impl не будет доступен для функций.
То есть не будет никакой связи между функциями и реализацией ядра.
А модуль, выделенный желтым, предоставит только фабрики, которые предоставят некоторые неизвестные реализации ваших интерфейсов.
После такого преобразования хотелось бы обратить ваше внимание на то, что Core API, благодаря включению ключевого слова API, будет доступен всем функциям транзитивно.
После этих преобразований мы что-то меняем в реализации, что вы делаете чаще всего, и пересобираться будет только модуль с фабриками, он очень легкий, маленький, даже не надо считать, сколько времени это занимает.
Другой вариант не всегда работает. Например, если это какая-то Сеть, то мне сложно представить, как такое могло произойти, но если это какой-то экран входа пользователя, то вполне может быть.
Мы можем сделать Sample, такой же полноценный корневой модуль, как App, и собрать в нем только одну фичу, это будет очень быстро, и его можно будет быстро итеративно разрабатывать.
В конце презентации я покажу, сколько времени занимает обычная сборка и выборочная сборка.
Мы закончили с первой частью.
Какие модули существуют?
Существует три типа модулей.
Common, конечно же, должен быть максимально облегченным и не должен содержать никаких возможностей, а только функционал, которым пользуются все.
Для нас в нашей команде это особенно важно.
Если мы предоставим наши Feature-модули другим приложениям, то мы в любом случае заставим их перетаскивать Common. Если он очень толстый, то нас никто не полюбит.
Если у вас проект поменьше, то с Common вы можете чувствовать себя свободнее, тут тоже не нужно слишком усердствовать.
Следующий тип модулей — Standalone. Самый обычный и интуитивно понятный модуль, содержащий определенную фичу: какой-то экран, какой-то пользовательский скрипт и так далее.
Оно должно быть максимально независимым, и чаще всего для него можно сделать образец приложения и развивать его в нем.
Примеры приложений очень важны в начале процесса разделения, потому что все еще строится медленно, и вы хотите получить прибыль как можно быстрее.
В конце, когда все будет разбито на модули, можно будет все восстановить, это будет быстро.
Потому что его больше не соберут.
Модули знаменитостей.
Это слово я придумал сам.
Дело в том, что он всем очень известен, и от него зависят многие люди.
Та же Сеть.
Я уже говорил вам, если вы часто его перестраиваете, как можно избежать перестройки всего.
Есть еще один метод, который можно использовать для небольших проектов, цель которых не состоит в том, чтобы выпустить все наружу как отдельную зависимость, отдельный артефакт.
Как это выглядит? Повторимся, вы вынимаете API из Celebrity, вынимаете его реализацию и теперь следите за своими руками, обратите внимание на стрелки от Feature до Celebrity. Это происходит. API вашего модуля оказался в Common, сама реализация осталась в нем, а в вашем основном модуле появилась фабрика, обеспечивающая реализацию этого API. Если кто и смотрел «Мебиус», то об этом говорил Денис Неклюдов.
Очень похожая схема.
Мы используем в проекте Dagger, он нам нравится, и мы хотели получить от него как можно больше пользы в контексте разных модулей.
Мы хотели, чтобы каждый модуль имел независимый граф зависимостей, имел определенный корневой компонент, из которого можно делать что угодно, мы хотели иметь свой собственный сгенерированный код для каждого модуля Gradle. Мы не хотели, чтобы сгенерированный код проник в основной код. Нам хотелось как можно больше проверок во время компиляции.
Мы страдаем от [k]apt, но должны получить хоть какую-то прибыль от того, что дает Dagger. И при всем при этом мы не хотели никого заставлять использовать Dagger. Ни тот, кто реализует новый отдельный функциональный модуль, ни тот, кто его потом потребляет, а наши коллеги, которые просят какие-то фичи для себя.
Как организовать отдельный граф зависимостей внутри нашего функционального модуля?
Вы можете попробовать использовать SubComponent, и он даже сработает. Но это имеет довольно много недостатков.
Вы можете видеть, что в SubComponent непонятно, какие зависимости он использует от Component. Чтобы это понять, придется долго и мучительно пересобирать проект, смотреть, на что жалуется Dagger, и добавлять это.
Кроме того, подкомпоненты устроены таким образом, что заставляют других использовать Dagger, и не получится легко запустить все это для своих клиентов и для себя, если вы вдруг решите отказаться в каком-то модуле.
Одна из самых отвратительных вещей — при использовании SubcomComponent все зависимости вытягиваются в основной модуль.
Dagger спроектирован таким образом, что подкомпоненты генерируются вложенным классом их каркасных компонентов, родительскими.
Может быть, кто-то смотрел на сгенерированный код и его размер, на их сгенерированные компоненты? У нас в нем 20 тысяч строк.
Поскольку подкомпоненты всегда являются вложенными классами для компонентов, то получается, что подкомпоненты подкомпонентов тоже вложены, и весь сгенерированный код попадает в основной модуль, этот двадцатитысячный файл строк, который нужно скомпилировать и провести рефакторинг.
Студия начинает тормозить - это боль.
Но есть решение.
Вы можете просто использовать Component.
В Dagger вы можете указать зависимости для компонента.
Это показано в коде и показано на картинке.
Зависимости — это место, где вы указываете методы обеспечения, фабричные методы, которые показывают, от каких объектов зависит ваш компонент. Он хочет получить их в момент творения.
Раньше я всегда думал, что в этих зависимостях можно указывать только другие компоненты, и вот почему — так написано в документации.
Теперь я понимаю, что значит использовать компонентный интерфейс, но раньше я думал, что это просто компонент. Фактически вам нужно использовать интерфейс, соответствующий правилам создания интерфейса компонента.
Короче говоря, это просто методы Provision, когда у вас есть просто геттеры для некоторых зависимостей.
В документации Dagger также есть пример кода.
Там также написано «otherComponent», и это сбивает с толку, потому что на самом деле туда можно помещать не только компоненты.
Как бы мы хотели использовать эту вещь в реальности?
На самом деле модуль Feature есть, у него есть видимый API-пакет, расположенный близко к корню всех пакетов, и он указывает на то, что есть точка входа — FeatureActivity. Нет необходимости использовать типалиасы, просто для ясности.
Это может быть фрагмент, может быть ViewController — не важно.
И есть его зависимости, FeatureDeps, где указано, что ему нужен контекст, какой-то Сетевой сервис, какая-то вещь из Common, которую вы хотите получить от Приложения, и любой клиент обязан это удовлетворить.
Когда он это сделает, все будет работать.
Как нам использовать все это в модуле Feature? Здесь я использую Activity, это необязательно.
Как обычно, мы создаем собственный корневой компонент Dagger и используем волшебный метод findComponentDependities, он очень похож на Dagger для Android, но мы не можем его использовать в первую очередь потому, что не хотим перетаскивать подкомпоненты.
В противном случае мы можем перенять у них всю логику.
Сначала я попытался рассказать вам, как это работает, но вы сможете увидеть это в примере проекта в пятницу.
Как клиенты вашей библиотеки должны использовать это в вашем основном модуле?
Во-первых, это просто типалиасы.
На самом деле у него другое имя, но это для краткости.
MapOfDepth с помощью класса интерфейса Dependency предоставляет вам его реализацию.
В App мы говорим, что можем реализовать зависимости так же, как в Dagger для Android, и очень важно, чтобы компонент наследовал этот интерфейс и автоматически получал методы Provision. Dagger с этого момента начинает заставлять нас предоставлять эту зависимость.
Пока вы его не предоставите, он не скомпилируется.
В этом главное удобство: вы решили создать фичу, расширили свой компонент этим интерфейсом — все, пока вы не сделаете все остальное, он не просто скомпилируется, а будет выдавать понятные сообщения об ошибках.
Модуль простой, суть в том, что он привязывает ваш компонент к реализации интерфейса.
Примерно так же, как в Dagger для Android. Перейдем к результатам.
Я протестировал на нашем мейнфрейме и на своем локальном ноутбуке, прежде чем отключить все возможное.
Если мы добавим общедоступный метод в Feature, время сборки существенно изменится.
Здесь я показываю различия при создании примера проекта.
Это 16 секунд. Или когда я собираю все карты, это значит сидеть и ждать по две минуты каждого, даже минимального изменения.
Поэтому мы разрабатываем и будем продолжать развивать многие функции в типовых проектах.
На мэйнфрейме время сопоставимо.
Еще один важный результат. До выделения модуля Feature это выглядело так: на мейнфрейме было 28 секунд, сейчас — 49 секунд. Мы выбрали первый модуль и уже заметили, что сборка замедлилась почти вдвое.
И еще один вариант — простая инкрементная сборка нашего модуля, а не фичи, как в предыдущем.
До выделения модуля прошло 28 секунд. Когда мы определили код, который не нужно каждый раз пересобирать, и [k]apt, который не нужно каждый раз запускать, мы выиграли три секунды.
Не черт знает что, но я надеюсь, что с каждым новым модулем время будет только уменьшаться.
Вот полезные ссылки на статьи: API против реализации , статья с измерениями времени сборки , образец модуля .
Презентация будет доступный .
Спасибо.
Теги: #Разработка Android #Разработка мобильных приложений #внедрение зависимостей #dagger 2 #мультимодальность
-
Реализовать Или Не Реализовать
19 Oct, 24 -
Набор Инструментов Mvvm Light
19 Oct, 24 -
Jor1K: Linux В Веб-Браузере
19 Oct, 24