/ Оригинал / Синтаксис языка Kotlin — довольно гибкая вещь и лаконичность кода, которой в Java можно добиться только с помощью кодогенерации, в Kotlin зачастую реализуется стандартными средствами языка (раз, два).  Под катом история про то, как мы испытывали Kotlin на гибкость (и пару раз сломали), реализуя MVP-библиотеку Summer без кодогенерации и с поддержкой Kotlin Multiplatform. Если вы программируете под Android, то ваши Fragment и Activity могут иногда пересоздаваться. Если не использовать никакой специальной библиотеки, то разработчику придётся думать о том, что делать, если view сейчас нет и как при повторном создании view доставить в него актуальные данные. Есть разные подходы, как упростить этот процесс. MVP-библиотеки создают иллюзию, что view всегда существует даже если это не так. Достигаться это может за счёт того, что создаётся специальный объект, с которым можно общаться как с view, но он всегда существует. В рамках статьи я буду называть такой объект прокси. К примеру, Moxy генерирует прокси с помощью APT (Annotation Processing Tool). Moxy является отличной MVP-библиотекой. Легко настраивается, имеет довольно приятный синтаксис и большую часть времени отлично работает.   Я, в полной уверенности, что Dagger опять сломался, иду чинить свой граф. Через 4 часа, подключив троих коллег, мы абсолютно случайно находим проблему. Спасибо, что проект только начинался и состоял всего из десятка экранов.  Ставим аннотацию на Fragment в проекте одного из коллег. Не повторяется. Делаем Clean Project в моём, удаляем все кэши, снова ставим и опять ошибка.  Я так и не раскопал эту проблему до конца. Единственное различие в стеке наших проектов — это AutoFactory, но даже если отключить её в моём проекте, всё равно возникает та же ошибка.  Такие ошибки возникают не очень часто и чаще всего с Dagger. Проблема в том, что возникают они обычно в самый неподходящий момент и абсолютно не очевидно, как их исправлять. Даже если у вас всё построено с помощью лучших практик проектирования и сущности изолированы друг от друга, то это вас не спасёт. Ошибки библиотек с APT нелокальны и выстреливают при любой архитектуре в самых неочевидных местах проекта. Для Kotlin/Multiplatform сейчас нет стабильного API компилятора. Поэтому Annotation Processing там очень сложен. Придётся завязаться на нестабильный API с сильным недостатком документации. Для Android-only проектов моим решением была минимизация использования APT. Koin/Kodein вместо Dagger, своя реализация для передачи аргументов фрагментам вместо FragmentArgs. Moxy всё ещё оставалась безальтернативной. Было время, когда я пробовал Google MVVM, но как-то слишком много кода получается для решения, казалось бы, простых задач. К тому же, никаких эвентов из коробки, приходится городить костыли. Решение для меня пришло из моих экспериментов с Kotlin/JS в 2017 году. Тогда я с помощью property-делегатов проксировал установку состояния компонента VueJS для того, чтобы убрать суффиксы. Мне в голову пришла мысль: "А почему бы по тому же принципу не проксировать установку состояния Fragment/Activity?" Так я написал первый прототип библиотеки для проксирования состояния. Выглядело это примерно вот так: Вместо обращения к настоящей view мы обращаемся к viewProxy, которая тоже реализует интерфейс view. К каждому свойству view привязан property delegate, созданный с помощью функции state. Этот делегат сохраняет ссылку на переданное ему свойство из view и удаляет эту ссылку когда view уничтожается. Сам делегат при этом продолжает существовать и, если его вызвать, когда view уже уничтожена, то приложение не падает. При каждой установке значения свойства делегат сохраняет его в Map. При каждом создании view функция createViewProxy вызывается заново и при каждом вызове state в переданное свойство устанавливается ранее сохранённое в Map значение. Проблемы тут две: Каждую из этих проблем я решил далеко не с первого раза. Порой я ожидал от языка невозможного и немного "перегибал". Как я уже написал выше, viewProxy будет доступен только при первом присоединении view. Решение проблемы, казалось бы, очевидно. Пусть viewProxy создаётся без view вообще, а потом делегаты, созданные с помощью функции state, получают свойства view c помощью переданной им лямбды. Первая реализация была такой: Kotlin поддерживает получение ссылки на свойство с помощью вот такого синтаксиса: obj::prop. Так же можно получить ссылку на свойство объекта изнутри него вот так: Я подумал: "Ага, наверное, можно получить свойство текущего this даже если он является receiver-ом функции!".  И да. Можно! Отлично!  Реализую полный вариант: И...  При этом, Android Studio ошибки не показывает. Навигация работает правильно. В чём же дело? Похоже, старый frontend компилятора Kotlin, который используется для сборки, берёт свойство у viewProxy, а новый, который используется в IDE, берёт его, как и надо, у this лямбды, переданной в state. Забавно, что если сделать counter не опциональным или получить свойство не с помощью ::counter, а с помощью this::counter, то всё работает. На следующий день я сделал view явным аргументом и решил для себя: поменьше неявных this внутри пользовательских классов. Теперь пользовательский код выглядит так: Меня это устроило. Читается неплохо. К тому же, теперь презентер не зависит от начального состояния view потому что сам приводит его в нужное начальное состояние при присоединении. Единственное, мне было лень самому писать viewProxy поэтому я написал плагин для Intellij IDEA/Android Studio. Оставался второй вопрос: как проксировать события? Первое решение было плохим: А потом выяснилось, что переходы между экранами нельзя делать через viewMethods потому что они могут происходить в фоне. Например, после сетевого запроса. Поэтому стало вот так: В целом, с шаблонами для презентера и фрагмента жить можно, но кода слишком много. Помогло то, что на этом этапе нам с командой нужно было разработать проект, код которого потом будет поддерживать другая компания. Тащить туда какие-то внутренние наработки было бы неправильно. Поэтому мы выбрали более традиционный стек: Dagger, Retrofit и Moxy.  В Moxy очень удобно реализованы команды. От выбранной стратегии зависит то, что произойдёт с командой при присоединении view.
При AddToEndSingleStrategy команда повторяется при каждом присоединении view.
При OneExecutionStrategy команда повторяется один раз только в том случае, если она была добавлена, когда view не существовало.
При SkipStrategy команда никогда не повторяется. Функцию AddToEndSingleStrategy в Summer выполняет state. Остальные были реализованы через механизм событий. К сожалению, в Kotlin нет синтаксиса для прокидывания аргументов функции в другую функцию или лямбду и код, вроде этого, не скомпилируется. Я попробовал несколько других синтаксисов. Например: В итоге, отказался от функций в интерфейсе View в пользу лямбд. Это решение далось мне не легко, но сейчас я считаю, что оно было правильным. В чём разница между функциями и лямбдами? Первый пункт – ключевой. Если функцию нельзя куда-то передать, то её нельзя и проксировать. В любом случае, нужно будет создать лямбду, вызывающую функцию, и передать её в прокси. К тому же, прокси не сможет реализовать на себе тип функции. В Kotlin у функции нет такого типа, который можно бы было на чём-то реализовать. Прокси реализует на себе функциональный тип. Такой же, как у лямбд, и ограничения у него будут такие же, как у лямбды. Соответственно, второй пункт тоже отпадает. Именно из-за третьего пункта у меня было больше всего сомнений. Насколько синтаксис лямбд будет мешать программированию?  Функция на Kotlin: Лямбда на Kotlin: Функция на Swift: Лямбда на Swift: Лямбда на идеальном языке программирования (つ✧ω✧)つ Что-то я замечтался. В целом, разница между записями функций и лямбд невелика. После реализации 5-6 лямбд, я уже почти не чувствовал разницы, а потом просто добавил генерацию реализаций в плагин.  Когда я определился с синтаксисом действий view, настало время реализации прокси для них. Первый прототип для действий без аргументов был таким: Всё работало отлично. К сожалению, в Kotlin нет возможности сделать у функции вариативное количество аргументов разных типов. Есть vararg, но все его аргументы должны иметь строго один тип. Поэтому я реализовал по функции для каждой возможной арности действия view.  Компилятор Kotlin не может определить, какую из перегрузок взять и всегда берёт первую. Судя по всему, так происходит потому что Kotlin может определить либо все типовые параметры сразу, либо не определить ни одного. Поэтому во всех doOnlyWhenAttached тип TView не определяется, а без него нельзя определить и T1. Видимо, особенность реализации компилятора. "Хорошо" – подумал я и разделил создание Event на 2 этапа: Теперь всё работает как надо. Оставалось только реализовать поддержку разных стратегий для событий и shorthands для основных из них. Синтаксис получился таким: Теперь синтаксис создания прокси и для состояния, и для событий готов! В этой статье я описал только удачные попытки проектирования синтаксиса. На самом деле, всего их было порядка 30. Многие из них мы тщательно тестировали, в том числе, в production. Разработка суммарно шла больше 2-х лет. И вот, наконец, релиз! Сейчас на Summer написано 5 production проектов. 2 из них – мультиплатформенные. Спасибо огромное MaksimNovikov, metnyov и deks1309 за то, что решились писать на новом фреймворке и за большое количество полезного фидбэка. Отдельное спасибо XanderZhu и umpteenthdev за вклад в разработку самого фреймворка и mowenik за консультирование по реализации iOS части.  За то время, что мы пишем на Summer, мы выработали несколько паттернов для решения типовых задач. Вот несколько из них: Внутри презентера экрана можно объявить несколько подпрезентеров и передать их в конструкторы android.widget.View, UIView или попапов. Даже когда сам попап ещё не был открыт, можно общаться с его презентером и заранее установить у него какие-то значения. При открытии, попап приведётся в нужное состояние. Также, в подпредентер можно прокинуть каллбэк. Получается что-то похожее на компоненты во VueJS или ReactJS. Вместо AddToEndSingleTagStrategy в Moxy, в Summer можно использовать state с sealed классом. Спасибо MaksimNovikov за этот трюк. В Kotlin довольно громоздкий синтаксис для переопределения set свойства. Поэтому я написал делегаты didSet и didSetNotNull, вдохновляясь didSet из Swift. Пример использования Обновление элемента в списке: На самом деле, здесь вы не берёте состояние view, как может показаться. Делегат, созданный функцией state, хранит состояние в специальном store и view не может модифицировать это состояние напрямую. Резюмируя наш опыт разработки с использованием Summer и Kotlin/Multiplatform в целом: Разработка с использованием Kotlin/Multiplatform почти ничем не отличается от обычной нативной разработки. Те iOS-программисты, которым я показывал, как это устроено, практически сразу начинали писать код в проект. Трудности возникали, в основном, с корутинами. Это новая концепция для iOS разработчика и с ними обычно нужно знакомить с 0. Впрочем, вы можете взять любую другую технологию для реализации асинхронности.  Мы постарались сделать Summer максимально интуитивной и простой в использовании для iOS-разработчиков. Можно писать в XCode. Вся платформенная часть может быть написана на Swift. Работает подсветка и дебаг общего кода на Kotlin в XCode при использовании плагина от TouchLab. Можно построить архитектуру, очень похожую на то, что популярно среди iOS-разработчиков. VIPER, Clean Swift и т.д. В своём проекте мы выбрали просто MVP. У нас получилось реализовать библиотеку, которая не модифицирует процесс компиляции и использует исключительно стандартные конструкции языка Kotlin. Так как в Summer всё ещё и строго типизировано, unsafe casts почти не используются, ошибки обычно выводятся компилятором Kotlin в понятном виде. Основная проблема, которую мы пока не придумали, как решить элегантно — это как под iOS на Swift разрешить присоединять презентер только к правильному view. Мы работаем над этим. Абсолютно нерелевантных ошибок вроде тех, что выдают библиотеки, использующие APT, при использовании Summer, у нас ни разу не было. Есть проблема с тем, что внутри viewProxy есть boilerplate. Код { it::counter } ничего не значит для клиентского кода. Теоретически, можно случайно передать ссылку на другое свойство такого же типа. У нас так не получалось потому что мы используем плагин, но у кого-нибудь может выстрелить. Я подумываю о том, чтобы написать lint на это. Также есть проблема с тем чтобы установить в state сложный объект при инициализации если приложение разрабатывается, в том числе, на Swift. На Kotlin делегаты didSet и didSetNotNull позволяют не передавать начальное значение при реализации view. На Swift внутри view для каждого свойства нужно задать начальное значение. Чаще всего, если объект сложный, его сначала нужно получить из сети. В этом случае свойство будет опциональным и в качестве начального значения можно задать nil. Если же свойство известно изначально — оно, скорее всего, примитивного типа.  Если же у вас, всё же, появился кейс когда нужно сделать state со сложным initial значением, то советую использовать не state, а event с RepeatLastStrategy и выполнять его в init блоке презентера. По моему опыту, перевод проекта с Moxy на Summer и с Summer на Moxy занимает очень мало времени. У меня без спешки получалось перевести 15-20 средних экранов за вечер. Перевод iOS приложения при наличии Android-приложения на Summer у меня шёл по 3-4 экрана за вечер. Проблема была в том, как в этом приложении был реализован VIPER. Презентер часто дёргал методы ViewController-а, использовал много iOS-специфичных возможностей. Мне приходилось сначала рефакторить экраны, а уже потом присоединять их к презентерам на Kotlin. Опыта перевода приложения под iOS, которое не надо сначала рефакторить, у меня пока нет. Также можно перевести 1-2 экрана чтобы попробовать. Kotlin имеет довольно хороший interop со Swift и Swift-у не так важно, на каком языке написан код. Классы, написанные на Kotlin, выглядят для Swift так, как будто они написаны на Objective-C с проставленными в правильных местах аннотациями совместимости. Спасибо всем, кто дочитал до конца! Надеюсь вы узнали что-то новое о Kotlin для себя, а возможно, даже попробуете Summer. Буду очень благодарен за конструктивный фидбэк. Android разработчик