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

Kotlin

Демократия в Telegram-группах

07.05.2021 14:19:42 | Автор: admin

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

Прежде всего, нужно определить что является ключевым аспектом, валютой системы. Не карма и не баллы, не лайки и не рейтинг. Им стало само время. Что из этого следует?

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

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

  • Каждый также может использовать накопленное время чтобы разбанить другого пользователя.
    Обратимость бана защищает от злоупотреблений свободного оборота банхаммеров. Одновременно эта функция является обратным к эмисии процессом. Время потраченное на выкуп из бана сгорает безвозвратно.

  • Накопленное время можно передать другим людям, полностью или по частям.
    Эта функция в перспективе обеспечивает подключенным группам свободный рынок обмена чем угодно.

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

  • Простой способ подключения чата к системе.
    - Реализована в виде бота

  • Максимально удобный для Telegram команд UX
    - Есть распознавание речи основанное на нормализованных семантических представлениях

Где примеры использования?

Бот понимает в запросах и русский и английский язык в свободной форме. Используются сокращения: d - дни, h - часы, m - минуты, s - секунды. Все уведомления публичны, но исчезают через 15 секунд, чтобы не засорять общий чат.

Вот как можно посмотреть сколько времени накопилось. Вот как можно посмотреть сколько времени накопилось. Забанить токсичного человека, спамера или мошенника можно указав в ответе в ему, в свободной форме, на сколько времени он получает бан (и за что по желанию). Забанить токсичного человека, спамера или мошенника можно указав в ответе в ему, в свободной форме, на сколько времени он получает бан (и за что по желанию). Если это было недопонимание и человек все же хороший, можно таким же образом, просто написать в ответе что его нужно разблокироватьЕсли это было недопонимание и человек все же хороший, можно таким же образом, просто написать в ответе что его нужно разблокироватьЧтобы перевести время другому пользователю указажите в ответе сколько времени нужно передать.Чтобы перевести время другому пользователю указажите в ответе сколько времени нужно передать.

Довольно слов, покажите мне код!

Для бекенда использовался язык Kotlin + JVM, в качестве базы данных используется Redis-кластер. Весь код продокументирован и доступен на GitHub: demidko/timecobot
Чтобы начать использовать бота в вашей телеграм-группе просто добавьте его с правами администратора: @timecbobot

Напишите пожалуйста свое мнение в комментариях, нужное ли это для сообщества дело и стоит ли развивать систему дальше? Что можно улучшить или поменять? Интересна ли подробная статья с кодом на тему как это реализовано внутри?

Всем удачного дня!

Подробнее..

Recovery mode Социальный эксперимент порядок из хаоса

14.05.2021 08:04:13 | Автор: admin

Бывало ли такое, что вы вас ограничивали за ваши мысли, так, что вы больше не могли высказаться в ответ? Весьма досадное чувство, не так ли? Как выстрел завершает жизнь человека, так и набор минусов способен лишить вас права существования на площадке.
Что будет если попытаться устроить общение по новым принципам, без администраторов, модераторов, без системы кармы, и даже без владельцев хоста? Сохранить свободу высказываний и при этом оградиться от хаоса? Я убежден что благодаря техническому прогрессу это стало возможно уже сегодня, и предлагаю на деле убедиться в этом, поучаствовав в эксперименте!

В чем суть?

Высшее дао рождает тьму вещей, но ею не владеет; творит многообразные изменения, но над ними не господствует. Те, что бегают и дышат, летают и пресмыкаются, наступает время и рождаются, но не из-за его благоволения; наступает время и умирают, но не из-за его вражды. Приобретая с пользой, оно не может быть восхваляемо; тратя и терпя убыток, оно не может быть порицаемо. Делает раздачи и одаряет, а не скудеет

(Лао-цзы)

Представьте себе затерянный телеграм чат, где нет владельца, и никто не был назначен модератором. У всех участников чата в фоновом режиме майнится некая валюта, просто по факту присутствия в чате, без регистрации. Благодаря встроеному в чат UX, валютой можно обмениваться и передавать друг другу, быстрее чем перевод в банке.
Ее также можно потратить чтобы закрепить свое сообщение на определенное время... или чтобы заблокировать спамера. Блокировка не удаляет человека, а лишь убирает ему поле ввода на время, в зависимости от затраченной на блокировку валюты. Система была бы несовершена без возможности отменить блокировку пользователю, точно также потратив на это часть своей накопленной валюты.

Звучит интересно, в чем отличие от кармы?

  1. Каждый может делать это! Нет инвайтов и все только в ваших и ваших друзей руках, нет централизованной точки отката, нет модератора который может затереть ваш пост или ограничить. Когда нет владельца, то нет и возможности для злоупотреблений "по праву владельца".

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

  3. Каждый "из коробки" обладает банхаммером, но его мощность ограничивается накопленной (и полученной от других пользователей) валютой. Поэтому систему не получится использовать для злоумышленного канселинга.

  4. Каждый может разбанить любого другого если у него достаточно валюты. Нет тотальной отмены с глубоким минусом, нет минуса которого нельзя отменить (как -100 на Хабре), нет лимитов прощения, было бы только желание подождать немного или попросить любого пользователя помочь вернуться сразу!

В чем цели эксперимента?

Проверить возможны ли новые, горизонтальные подходы во взаимодействии пользователей? Возможен ли отказ от иерархии? Можно ли общаться в интернете без концепции владения хостом? Можно ли автоматизировать модерацию? Успех эксперимента может стать толчком к расцвету таких систем. Эксперимент абсолютно некоммерческий, нет никакой рекламы, нет механизма извлечения выгоды. Я делаю это ради интереса посмотреть что получиться (и удовольствия пронаблюдать вышеописанную механику в действии).

Как поучаствовать?

Зайти в чат где все это реализовано: t.me/habr_experimental
В чате нет владельцев и админов, а вместо них подлючена и ждет наплыва пользователей вышеописанная система. Полагаю что тематика обсуждений может быть любая, на интересные целевой аудитории Хабра темы, и помните что никто ни в чем не может ограничить вас, кроме вас самих и таких же как вы людей.

Исходный код чата

Эта статья о социальном эксперименте, однако она была бы не для Хабра без технических деталей. Поэтому для тех кому это интересно, подробные принципы работы валюты и UX описаны в этой статье: habr.com/ru/post/556292/
Исходный код системы доступен на GitHub: github.com/demidko/timecobot
Для бекенда использован один из лучших виданных мною языков - Kotlin, за что хочу сказать здесь спасибо его разработчикам.

Возможно эта статья обрушит мою карму здесь, а возможно мы увидим как появится новый мир свободных интернет-систем. Напоследок хочу пожелать всем читавшим, эксперементируйте чаще и делайте это ради интереса!

Подробнее..

Перевод Языки любимые и языки страшные. Зелёные пастбища и коричневые поля

07.05.2021 14:19:42 | Автор: admin


Результаты опроса Stack Overflow являются отличным источником информации о том, что происходит в мире разработки. Я просматривал результаты 2020 года в поисках некоторых идей, какие языки добавить в нашу документацию по контейнерным сборкам, и заметил кое-что интересное о типах языков. Мне кажется, это не часто встречается в различных дискуссиях о предпочтениях разработчиков.

В опросах есть категории Самые страшные языки программирования (The Most Dreaded Programming Languages) и Самые любимые языки. Оба рейтинга составлены на основе одного вопроса:

На каких языках вы провели обширную работу по разработке за последний год, и на каких хотите работать в следующем году? (Если вы работаете с определённым языком и намерены продолжать это делать, пожалуйста, установите оба флажка).

Страшный язык это такой, с которым вы активно работаете в текущем году, но не хотите продолжать его использовать. Любимый язык тот, который вы широко используете и хотите продолжать использовать. Результаты интересны тем, что отражают мнения людей, которые активно используют каждый язык. Не учитываются мнения типа Я слышал, что Х это круто, когда люди высоко оценивают вещи, которые они НЕ используют, потому что они слышали, что это новый тренд. Обратное тоже правда: люди, которые выражают отвращение к какому-то языку, реально широко используют его. Они боятся языка не потому, что слышали о его сложности, а потому, что им приходится работать с ним и испытывать настоящую боль.

Топ-15 страшных языков программирования:
VBA, Objective-C, Perl, Assembly, C, PHP, Ruby, C++, Java, R, Haskell, Scala, HTML, Shell и SQL.

Топ-15 любимых языков программирования:
Rust, TypeScript, Python, Kotlin, Go, Julia, Dart, C#, Swift, JavaScript, SQL, Shell, HTML, Scala и Haskell.

В списке есть закономерность. Заметили?

Худший код тот, что написан до меня


Старый код хуже всего. Если кодовая база в активной разработке более трёх лет, то она уже непоследовательная. На простой первый слой накладываются особые случаи и оптимизация производительности, а также различные ветви, управляемые параметрами конфигурации. Реальный код эволюционирует, чтобы соответствовать своей нише, одновременно он становится всё сложнее и труднее для понимания. Причина проста, и я впервые услышал эту фразу от Джоэла Спольски.

Причина, по которой [разработчики] считают старый код бардаком, заключается в кардинальном, фундаментальном законе программирования: читать код труднее, чем писать его.

Джоэл Спольски Грабли, на которые не стоит наступать

Назовём это Законом Джоэла. Из этой посылки вытекает многое. Почему большинство разработчиков думают, что унаследованный ими код это бардак, и хотят выбросить его и начать всё сначала? Потому что написание чего-то нового проще для мозга, чем тяжёлая работа по пониманию существующей кодовой базы, по крайней мере, на начальном этапе. Почему попытки переписать код часто обречены на провал? Потому что многие мусорные артефакты это жизненно важные небольшие улучшения, которые накапливаются с течением времени. Без какого-то конкретного плана по рефакторингу вы в конечном итоге вернётесь к тому, с чего начали.


Scott Adams Understood

Легко понять код, который вы пишете. Вы его выполняете и совершенствуете по ходу дела. Но трудно понять код, просто прочитав его постфактум. Если вы вернётесь к своему же старому коду то можете обнаружить, что он непоследовательный. Возможно, вы выросли как разработчик и сегодня бы написали лучше. Но есть вероятность, что код сложен по своей сути и вы интерпретируете свою боль от понимания этой сложности как проблему качества кода. Может, именно поэтому постоянно растёт объём нерассмотренных PR? Ревью пул-реквестов работа только на чтение, и её трудно сделать хорошо, когда в голове ещё нет рабочей модели кода.

Вот почему вы их боитесь


Если реальный старый код незаслуженно считают бардаком, то может и языки программирования несправедливо оцениваются? Если вы пишете новый код на Go, но должны поддерживать обширную 20-летнюю кодовую базу C++, то способны ли справедливо их ранжировать? Думаю, именно это на самом деле измеряет опрос: страшные языки, вероятно, будут использоваться в существующих проектах на коричневом поле. Любимые языки чаще используются в новых проектах по созданию зелёных пастбищ. Давайте проверим это.1

Сравнение зелёных и коричневых языков


Индекс TIOBE измеряет количество квалифицированных инженеров, курсов и рабочих мест по всему миру для языков программирования. Вероятно, есть некоторые проблемы в методологии, но она достаточно точна для наших целей. Мы используем индекс TIOBE за июль 2016 года, самый старый из доступных в Wayback Machine, в качестве прокси для определения языков, накопивших много кода. Если язык был популярным в 2016 году, скорее всего, люди поддерживают написанный на нём код.

Топ-20 языков программирования в списке TIOBE по состоянию на июль 2016 года: Java, C, C++, Python, C#, PHP, JavaScript, VB.NET, Perl, ассемблер, Ruby, Pascal, Swift, Objective-C, MATLAB, R, SQL, COBOL и Groovy. Можем использовать это в качестве нашего списка языков, которые с большей вероятностью будут использоваться в проектах по поддержке кода. Назовём их коричневыми языками. Языки, не вошедшие в топ-20 в 2016 году, с большей вероятностью будут использоваться в новых проектах. Это зелёные языки.


Из 22 языков в объединённом списке страшных/любимых 63% коричневых

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

Java, C, C++, C#, Python, PHP, JavaScript, Swift, Perl, Ruby, Assembly, R, Objective-C, SQL


Зелёный язык: язык, который вы с большей вероятностью будете использовать в новом проекте.

Go, Rust, TypeScript, Kotlin, Julia, Dart, Scala и Haskell

У TIOBE и StackOverflow разные представления о том, что такое язык программирования. Чтобы преодолеть это, мы должны нормализовать два списка, удалив HTML/CSS, шелл-скрипты и VBA.2

Конечно, простое деление на зелёные и коричневые упускает много нюансов, в том числе по размеру полей. Я ожидаю, что больше зелёных пастбищ должно быть на Swift, чем на Objective-C, но и нынешняя методика, кажется, охватывает всё, что нам нужно. В этом списке гораздо больше коричневых языков, чем зелёных, но это вполне ожидаемо, ведь новых языков ежегодно появляется относительно немного.

Теперь можно ответить на вопрос: люди действительно боятся языков или же они просто боятся старого кода? Или скажем иначе: если бы Java и Ruby появились сегодня, без груды старых приложений Rails и старых корпоративных Java-приложений для поддержки, их всё ещё боялись бы? Или они с большей вероятностью появились бы в списке любимых?

Страшные коричневые языки



Страшные языки на 83% коричневые

Топ страшных языков почти полностью коричневый: на 83%. Это более высокий показатель, чем 68% коричневых языков в полном списке.

Любимые зелёные языки



Любимые языки на 54% зелёные

Среди любимых языков 54% зелёных. В то же время в полном списке всего лишь 36% языков являются зелёными. И каждый зелёный язык есть где-то в списке любимых.

Ещё один недостаток человеческого характера заключается в том, что все хотят строить и никто не хочет заниматься обслуживанием.

Курт Воннегут

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

Другими словами, Rust, Kotlin и другие зелёные языки пока находятся на этапе медового месяца. Любовь к ним может объясняться тем, что программистам не надо разбираться с 20-летними кодовыми базами.

Устранение предвзятости




Некоторые новые или исторически менее популярные языки программирования могут быть лучше, чем старые или более распространённые языки, но наша способность судить о них кажется довольно предвзятой. В частности, если язык новый или ранее не использовался, то у него некий ангельский образ. А чем дольше используется язык, тем более дьявольский лик он приобретает в глазах разработчиков. Думаю, причина в том, что никому не нравится поддерживать чужой код. А также из-за Закона Джоэла: читать в реальном мире очень сложно. Создавать что-то новое вот это весело, а для этого чаще используются новые языки.

Цикл хайпа языков программирования


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


Цикл хайпа языков программирования

У меня под рукой нет данных, но я отчётливо помню, что Ruby был самым популярным языком в 2007 году. И хотя сегодня у него больше конкурентов, но сегодня Ruby лучше, чем тогда. Однако теперь его боятся. Мне кажется, теперь у людей на руках появились 14-летние приложения Rails, которые нужно поддерживать. Это сильно уменьшает привлекательность Ruby по сравнению с временами, когда были одни только новые проекты. Так что берегитесь, Rust, Kotlin, Julia и Go: в конце концов, вы тоже лишитесь своих ангельских крылышек.3



1. Сначала я придумал критерии. Я не искал данных, подтверждающих первоначальную идею.

Была мысль определять статус зелёного или коричневого по дате создания языка, но некоторые старые языки нашли применение только относительно недавно.

Вот методика измерения TIOBE, а их исторические данные доступны только платным подписчикам, поэтому Wayback Machine. [вернуться]

2. HTML/CSS не являются тьюринг-полными языками, по этой причине TIOBE не считает их полноценными языками программирования. Шелл-скрипты измеряются отдельно, а VBA вообще не исследуется, насколько я понял. [вернуться]

3. Не все коричневые языки внушают страх: Python, C#, Swift, JavaScript и SQL остаются любимыми. Хотелось бы услышать какие-нибудь теории о причине этого феномена. Кроме того, Scala и Haskell два языка, к которым я питаю слабость единственные зелёные языки в страшном списке. Это просто шум или есть какое-то обоснование??? [вернуться]
Подробнее..

Как внедряли Kotlin Multiplatform в Профи

07.05.2021 16:08:17 | Автор: admin

Привет, Хабр! Я Миша Игнатов, тимлид в компании Профи. Моя команда отвечает за клиентские мобильные приложения на Android и iOS. Мы используем Kotlin Multiplatform в production с 2019 года. Расскажу, почему мы выбрали именно эту технологию, как внедряли её, какие ключевые этапы прошли и какие сделали выводы.

Коротко о Kotlin Multiplatform

Kotlin Multiplatform позволяет запускать один и тот же код, написанный на Kotlin, на множестве платформ. В августе 2020 года компания JetBrains представила Kotlin Multiplatform Mobile (КММ) SDK, который помогает упростить использование общего кода на Android и iOS. Цель технологии вынос бизнес-логики. UI-слой остаётся нативным, что хорошо сказывается на опыте пользователя и внешнем виде приложений.

Почему мы выбрали Kotlin Multiplatform

Мы изучали разные кросс-платформенные технологии. Например, React Native и Flutter позволяют писать сразу всё в одном проекте на обе платформы, но ограничивают разработчика языком и набором библиотек. Остановились на Kotlin Multiplatform по трём причинам.

  1. Легко интегрировать

    Общий код, написанный на Kotlin, можно внедрить с минимальными усилиями в готовое приложение. Он компилируется в привычные для платформ библиотеки. Для Android это jar или aar-библиотека, для iOS Universal Framework. Подключение и дальнейшая работа не сильно отличаются от взаимодействия с любой нативной библиотекой.

  2. Синтаксис языка Kotlin близок к Swift

    Схожесть языков снижает порог входа для iOS-разработчиков. Оба языка разделяют похожую идеологию скорость и удобство работы для разработчика. Понять, что происходит в общем коде, и дополнить его сможет любой в команде.

  3. Не нужно тратить ресурсы дважды на одну задачу

    Бизнес-логика наших приложений одинаковая. Более 70% кода не связано с платформой, на которой его запускают. Мы запрашиваем данные с сервера, преобразуем их, кешируем и готовим к отображению. Поэтому пишем код в двух проектах, дублируя логику, Android на языке Kotlin и iOS на Swift. Отличия есть только в дизайне из-за разного UX на мобильных платформах и взаимодействия с системой (запросы к различной периферии: камера, геолокация, галерея, уведомления и т.д.).

Как внедряли

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

Шаг 1. Первая строчка в общем коде

Первая задача сделать общие строки API-запросов, чтобы не было различий в структурах запрашиваемых данных на двух платформах.

Обмен данными с сервером у нас реализован на GraphQL. Запрос в коде это multiline строка. Бывает пять строк, а бывает под сотню. Если отправить такой объём, бэкенду придётся тратить время на парсинг структуры. С другой стороны, нужно контролировать запрашиваемые данные во время код-ревью и валидации запросов на проде. Поэтому перед релизом мы обучаем сервер новым запросам. Это позволяет использовать хеши вместо строк.

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

Решили вынести в общий код несколько запросов. Для этого в Android-проекте сделали мультиплатформенный модуль shared. Перенесли в него строки запросов и обернули в классы-синглтоны object, а в клиентских приложениях вызывали методы этих классов. Забавный факт использовать КММ предложил iOS-разработчик.

Первая строчка в общем коде
package ru.profi.shared.queries.client.city/*** Запрос поиска города по [Params.term]*/object GeoSelectorWarpQuery : WarpQuery<Params> {   override val hash: String? = "\$GQLID{c9d4adbb7b9ef49fc044064b9a3e662b}"   override val dirtyQuery = listOf("\$term").let { (term) ->       """       query geoSelector($term: String) {         suggestions: simpleGeoSelector(term: $term, first: 100) {           edges {             node {               name               geoCityId               regionName               hostname               countryId             }           }         }       }       """   }.trimIndent()}
Использование в Android проекте
override fun getQuery() = GeoSelectorWarpQuery.getQuery()
Использование в iOS проекте
import KotlinComponentsstruct GraphQLWarpRequests {    static let GeoSelectorWarpQuery = GeoSelectorWarpQuery()...}let model = GraphQLRequestModel(query: GraphQLWarpRequests.GeoSelectorWarpQuery.getQuery(), variables: variables)

Теперь структуры запросов лежат в одном месте. В следующем релизе общую библиотеку подключили на обеих платформах, и всё работало без проблем. Размер приложения на iOS увеличился всего на 0,8 Мб. Впоследствии вынос запросов в общий код сократил количество подходов к обучению в два раза.

Проблему ручного обучения решили утилитарной библиотекой из нескольких классов, написанных на Kotlin. Она находит в коде необученные запросы, генерирует и отправляет новые хеши через pull request в репозиторий backend. Теперь мы не тратим время на обучение, оно полностью автоматизировано.

На этом шаге мы построили инфраструктуру для общего кода на Kotlin Мultiplatform. Можно переходить к более серьёзным задачам.

Шаг 2. Создаём мультиплатформенный SDK

В один момент компания решила создать свою in-house аналитику на базе Clickhouse. Для этого на стороне backend создали API для приложений. Моей команде оставалось только отправлять события. Чтобы не мешать работе основного функционала и не терять события, если у пользователя нет сети, нужно было научиться кешировать, группировать пачки событий и отправлять их с меньшим приоритетом, чем запросы на основной функционал.

Модуль решили писать в общем коде. Для отправки событий взяли network client ktor. Для работы с сетью он нас полностью устраивал.

Когда сети нет, события надо сохранить до следующего сеанса связи. Для этого выбрали SQLDelight мультиплатформенную библиотеку для нативной базы данных.

Для асинхронных операций использовали kotlinx.coroutines. Для сериализации и десериализации выбрали kotlinx.serialization.

Чтобы повысить надёжность кода, функционал модуля покрыли unit-тестами. Удобно, что их можно запускать на разных платформах.

При интеграции приложения на Android проблем не возникло, но на iOS были падения на старте. В консоли XCode и логах Firebase Crashlytics трассировка стека не сильно приближала нас к причине. Но было ясно, что падает внутри общего кода.

Чтобы получить понятную трассировку стека, мы подключили библиотеку CrashKiOS от студии Touchlab. А при создании корутины добавили CoroutineExceptionHandler, который перехватывает исключения во время их выполнения.

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

Kotlin Multiplatform позволил объединить в один модуль ответственность за отправку и хранение аналитических событий. В итоге мы построили полноценный SDK в общем коде.

Шаг 3. Переносим бизнес-логику из приложения Android в мультиплатформу

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

В приложении на iOS был такой код в модуле бизнес-логики чатов. Это была наша боль. Добавлять новый функционал становилось всё дороже код написан на Objective-C с устаревшей и сложной архитектурой. Чувствовалось, что разработчики неохотно брали задачи по чатам.

В приложении на Android бизнес-логику чатов недавно уже переписали на Kotlin. Поэтому решили попробовать вынести существующий модуль в общий код и адаптировать его под iOS.

Нам помогли ребята из IceRock.dev. Они уже давно встали на путь мультиплатформы, активно продвигают KMM и развивают сообщество. Вместе мы составили план переезда.

  1. Настроить поддержку Kotlin Multiplatform в gradle-модуле.
    Создать модуль, подключить плагины, настроить sourceSets и зависимости.

  2. Перенести платформенно-независимые классы в commonMain.
    Перенести всё, что не зависит от JVM и Android, в commonMain. Это место для общего кода, в котором нет платформенных зависимостей.

  3. Заменить библиотеки JVM/Android на мультиплатформенные аналоги.
    Перейти с org.json на kotlinx.serialization и с JodaTime на klock. Некоторые части пришлось вынести в платформозависимый код в виде expect/actual.

  4. Перенести в commonMain JVM-зависимый код, который требует изменений.
    Например, заменить JVM IOException на kotlin.Exception, а ConcurrentHashMap на использование Stately.

  5. Перенести в commonMain Android-зависимый код, который требует изменений.
    Единственной зависимостью Android SDK был компонент Service, который работает с WebSocket. Стабильного мультиплатформенного аналога на Kotlin пока нет.

    Мы решили оставить нативные реализации в приложении и подключить их через интерфейс SocketService.

    Интерфейс SocketService
    interface SocketService {    /**     * Присоединиться по сокету к [chatUrl]. Все события из сокета необходимо отдавать в [callback]     */    fun connect(chatUrl: String, callback: (SocketEvent) -> Unit)    /**     * Отсоединиться от текущего подключения по сокету.     */    fun disconnect()    /**     * Отправить сообщение [msg] в текущем подключении по сокету     */    fun send(msg: String)}
    
  6. Сделать модуль API удобным для обеих платформ.
    Так как в iOS невозможно перехватить runtime-исключения из Kotlin, мы решили обрабатывать их внутри SDK и добавить в методы интерфейса callback onError. Поэтому пришлось немного переделать интерфейс взаимодействия с клиентскими приложениями.

При переносе кода в мультиплатформу мы сформировали алгоритм миграции модулей с бизнес-логикой в общий код. Теперь пользуемся им для выноса других модулей.

План от IceRock.dev помог нам двигаться увереннее и быстрее. Мы продолжаем созваниваться и делиться опытом разработки.

Что мы поняли

Kotlin Multiplatform помог сделать единый источник правды для бизнес-логики в клиентских приложениях Профи. UI и UX оставили для пользователя нативными. При грамотном проектировании интерфейса взаимодействия с общим кодом, изменение и расширение бизнес-логики происходят в одном месте, а клиентским приложениям нужно просто поддержать это.

Мы сэкономили ресурсы. При переносе модулей в Kotlin Multiplatform мы ощутили экономию времени на разработке модуль чатов на iOS не пришлось рефакторить. Вместо этого мы перенесли решение из Android-проекта в общий код и адаптировали его для iOS. Это обошлось дешевле, чем писать чаты с нуля.

Разработчики быстро освоились. Для Android-разработчиков оказались в новинку только мультиплатформенные библиотеки и настройка build-скрипта модуля. Остальное привычно и не вызывает сложностей. iOS-разработчикам было легко понять синтаксис языка, но пришлось покопаться в сборке через Gradle. Но сейчас каждый из них уже решил как минимум одну задачу в общем коде.

Главный минус технологии время сборки для iOS. Например, когда мы искали причину падения приложения, пересобирать общий код для iOS приходилось часто. При публикации общих модулей это не ощущается. С выпусками новых версий Kotlin скорость сборки растёт, что добавляет надежд на будущий комфорт разработки.

Иногда мы спотыкались о проблемы по незнанию. Когда мы начинали, информации по внедрению KMM было очень мало, поэтому набивали шишки сами. Сейчас сообщество Kotlin Multiplatform быстро развивается. Появляется всё больше статей и докладов на конференциях и митапах. Есть каналы в Slack и Telegram, библиотеки для Kotlin Multiplatform.

Оно того стоило

Первые шаги давались сложно, так как технология была новой. Поначалу казалось проще использовать нативные решения на известных команде библиотеках, чем разобраться в новых. Но мы понимали, что дальше дело пойдёт быстрее. Так и произошло. Можно сказать, что скорость разработки в общем коде и нативных проектах сравнялась.

Сейчас у нас уже 10 общих модулей разной сложности, и мы продолжаем выносить бизнес-логику в общий код. Уверен, что Kotlin Multiplatform Mobile готов к покорению мира разработки мобильных приложений.

Подробнее..

Kotlin 1.5 онлайн-мероприятие

14.05.2021 18:17:37 | Автор: admin

Недавно мы выпустили первое крупное обновление 2021 года. В честь этого события мы проведем онлайн-встречу, в ходе которой члены команды Kotlin ответят на ваши вопросы о недавних обновлениях и обо всем, что связано с Kotlin (на английском языке). Присоединяйтесь к нам 25 мая в 17:00 МСК.


Подробнее о самом мероприятии, о сессии Ask Me Anything (AMA) на Reddit, а также о том, как задать вопросы и выиграть футболку, читайте ниже.


image

Зарегистрироваться

Что мы для вас приготовили


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


Смотрите прямую трансляцию на нашем YouTube-канале. Ведущими будут два наших девелопер-адвоката: Светлана Исакова и Себастьян Айгнер. Они начнут с обзора новшеств в Kotlin 1.5.0 и поговорят с Романом Елизаровым о перспективах Kotlin. Присоединяйтесь будет интересно!


Как принять участие в сессии вопросов-ответов и Reddit AMA


Чтобы мероприятие прошло с максимальной пользой, присылайте свои вопросы команде заранее. Их можно отправить при регистрации или написать в Twitter с хештегом #kotlin15ask. Кроме того, вопросы можно будет задавать в чате прямой трансляции на YouTube если у вас нет вопросов сейчас, они могут появиться уже в ходе встречи.


Мы постараемся ответить на все вопросы: и на подготовленные заранее, и на те, что будут заданы в прямом эфире. Все, о чем мы не успеем поговорить до конца мероприятия, обязательно обсудим с 27 по 28 мая на сессии AMA на Reddit. Чтобы не пропустить уведомление об AMA, поставьте галочку в регистрационной форме или подпишитесь на сабреддит r/Kotlin.


Дополнительная информация и розыгрыш лотереи


Нет вопросов? Они появятся, когда вы посмотрите наши материалы о выходе Kotlin 1.5.0:


  1. Статья в блоге и раздел Что нового.
  2. Плейлист на YouTube с видеообзорами самых заметных новшеств в Kotlin 1.5.0. Подписывайтесь на наш канал там скоро появятся новые видео о Kotlin 1.5.0.

Все, кто пришлет нам свои вопросы заранее, примут участие в розыгрыше 10 футболок Kotlin. Победители будут объявлены во время мероприятия. Мы свяжемся с ними, чтобы организовать доставку выигранного приза.


Участвуйте в онлайн-мероприятии, посвященном Kotlin 1.5, и присылайте свои вопросы!



Ваша команда Kotlin
The Drive to Develop
Подробнее..

Перевод Почему Kotlin хуже, чем Java?

20.05.2021 18:13:57 | Автор: admin

Такой провокационный вопрос задал реддитор nenemen в сабреддите Java:

Я думаю о том, чтобы свой следующий проект сделать на Kotlin + Spring Boot, но мощь всенародной любви к Kotlin и одновременно ненависти к Java заставляют всё это походить на какой-то культ. Поэтому хотел бы услышать аргументы против.

Мы в FunCorp в своё время сделали именно такой выбор в пользу Kotlin. И сегодня соотношение Java/Kotlin у нас составляет примерно 20 на 80, продолжая уменьшаться при каждом удобном случае. Поэтому ответы на этот вопрос меня заинтересовали, и я стал листать секцию комментариев. Там наткнулся на реплику реддитора rzwitserloot, которая мне показалась настолько взвешенной, многосторонней и рациональной, что я захотел поделиться ей с нашей командой, а заодно и читателями Хабра.

Далее перевод его аргументов.

Причины использовать Java вместо Kotlin

1. Kotlin более проприетарный. Например, изрядное количество подробностей внутренней работы kotlinc скрыто внутри сгенерированных файлов классов, представляющих из себя аннотации @Metadata с бинарными данными (байтовыми массивами, разрешёнными в аннотациях) внутри. Насколько мне известно, эти данные не описаны ни в каких публичных спецификациях. Также многие типы в Kotlin жёстко закодированы. Это вполне утилитарный подход, но он означает, что без IDEA (автор, видимо, имел в виду JetBrains компанию-разработчик языка Kotlin и серии IDE для работы с разными языками программирования прим. переводчика) Kotlin немедленно умрет. Конечно, это мелкая придирка, но, возможно кому-то этот недостаток открытости будет важен.

2. Есть ощущение, что и сообщество, и IDEA продвигают Kotlin так, будто это Java, но без уродливых нашлёпок. Но что из этого следует? Останется ли Kotlin языком, который чрезвычайно легко освоить, если вы уже знаете Java и так похож на Java, что вы можете взаимозаменять Java и Kotlin, а также вызывать одно из другого почти без усилий в обозримом будущем (в этом случае я вижу проблемы, о которых расскажу ниже)? Или это был способ первоначальной популяризации и роста пользовательской базы, чтобы быстро заполучить сразу кучу Java-разработчиков и дать им возможность постепенно переводить свой код с одного языка на другой шаг за шагом, используя совместимость вызовов и двойную компиляцию? В этом случае к будущему языка тоже есть много вопросов. Мне кажется, что ребята, делающие Kotlin, думали, что оба варианта верные, но на самом деле они взаимоисключающие. Если попросить людей, которые понимают в Kotlin больше моего, объяснить, почему два следующих варианта неверны или не приведут в будущем к проблемам, то сначала решите между собой, что такое Kotlin и для чего он предназначен.

Если Kotlin всегда будет как Java, только лучше

Это реальная проблема. Лучше всего объяснить её на примере новой фичи языка.

Все фичи Kotlin делятся на три категории:

  • Что-то, что уже было в Java, когда Kotlin только проектировался. Тогда Kotlin берёт знакомые Java-разработчикам конструкции, и либо следует им дословно, либо делает по-своему, исходя из своих соображений что такое хорошо. Кстати, перестановка местами типа и имени переменной или отказ от точек с запятой мне не кажутся хорошими идеями. Но текст и так довольно длинный, плюс это в изрядной мере личные предпочтения, так что не будем об этом.

  • Что-то, чего в Java не было и нет до сих пор. Они решили, что это нечто важное и сделали полностью по-своему.

  • Что-то, чего в Java раньше не было, но теперь появилось. Если Kotlin очень повезло, то в Java это сделали достаточно похоже, и разработчику в процессе переучивания с Java на Kotlin это не создаёт проблем. Но что, если это не так?

Суть в том, что со временем всё больше и больше фич Kotlin будут попадать в третью категорию, и поэтому подход как Java, только лучше обречён.

Приведу пример: присвоение типа конструкцией instanceof. Когда она появилась в Kotlin, в Java такого функционала не было даже в проекте: не было ни JEP, ни постов в блогах или рассылках (например amber-dev@openjdk.net).Такое поведение instanceof решает типичную задачу: проверить, принадлежит ли переменная к подходящему классу, и если да то работать с данными в переменной с учётом этого.

В Java вплоть до 14-й версии это выглядело так:

if (x instanceof String) {String y = (String) x;System.out.println(y.toLowerCase());}

В Kotlin сделали примерно так:

if (x instanceof String) {// теперь x имеет тип String!System.out.println(x.toLowerCase());}

Но в Java версии 16+ стало так:

if (x instanceof String y) {System.out.println(y.toLowerCase());}

Получается, что оба языка имеют способ обработать описанный сценарий, но разными способами. Я уверен, что если бы мог вдавить огромную кнопку сброс, разработать Kotlin с нуля и снова выпустить сегодня бета-версию, то в Kotlin было бы сделано так же, как сейчас в Java. Учитывая, что синтаксис Java более мощный: мы можем сделать с ним намного больше, чем просто проверить тип (например, деконструировать типы-значения). Нам даже не нужен блок кода, а можно написать прямо так:

if (!(x instanceof String y)) return;System.out.println(y.toLowerCase());

В целом, следующие несколько релизов Java расширяли этот принцип, добавляя много типов присвоения.

Теперь Kotlin должен сделать выбор:

  1. Всё сломать, убить старый способ и переделать как в Java. Это будет катастрофой, и потому крайне маловероятно. Такой вариант сломает огромную кучу чужого кода и навсегда подорвёт доверие к языку. Кому нужен инструмент, который будет ломаться каждый год или два?

  2. Оставить всё как есть. Это значит, что аргумент перейти с Java на Kotlin легко будет всё слабее и слабее с каждым новым релизом и новой фичей Java.

  3. Оставить всё как есть и добавить новый синтаксис. Звучит хорошо, но это значит, что с каждым своим релизом Kotlin будет накапливать вдвое больше плохих решений, чем Java. Со временем Kotlin превратится в ужасную разбухшую помойку формата куча-способов-сделать-одно-и-то-же. Это приведёт к тому, что изучать Kotlin будет сложнее, а пользы это не принесёт.

  4. Более хитроумное, но сложное решение. Например, сделать оба варианта, но исходный объявить устаревшим и через какое-то время удалить. Это означает, что Kotlin навсегда останется в тени Java без единого шанса оттуда выбраться.

Ни один из этих пунктов не кажется хорошим. Что приводит к более позитивному варианту номер два: начать отходить в сторону от синтаксиса Java. Отсюда следует:

Как Java, но лучше было способом получить первоначальное ускорение и пользовательскую базу

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

Это не худший вариант, но количество историй успеха новых языков программирования (видимо, автор имел в виду языки, работающие в JVM прим. переводчика) исчезающе мало. Scala, например, практическ мёртв. Конечно, он собрал изрядно хайпа, на него перешёл Twitter, организовал кучу классных митапов для разрабов, и на сегодня у Scala меньше пользователей, чем было тогда. Это подтверждается инструментами типа TIOBE, которые оставляют желать лучшего в части точности, но давайте будем честны с собой: набирает ли Scala обороты в последнее время? Fan/Fantom зашёл в абсолютный тупик, Groovy не подаёт признаков жизни настолько, что Gradle пытается диверсифицироваться от него подальше. JRuby и Jython появились и исчезли, в том смысле, опять же, что никого они не вдохновили.

Конечно, нет никаких доказательств, что все новые языки изначально обречены. Но с другой стороны, это некая ситуация по умолчанию. Прямо сейчас Kotlin оседлал волну успеха, но со временем жизнь его будет тем тяжелее, чем шире будет зазор между ним и Java, и чем сложнее будет преодолеть этот зазор.

Ему придётся опираться только на себя и перестать рекламировать доступность всех преимуществ Java и её экосистемы. Пример с instanceof уже демонстрирует, почему я думаю, что Kotlin не будет лучше Java: почти каждая новая фича, которая появилась в Java недавно или вот-вот появится (в смысле, имеет активный JPE и обсуждается в рассылках) выглядит более продуманной, чем любая фича Kotlin. Java развивается в сторону доступа к нативным 80-битным регистрам CPU сложных типов, сохраняющих производительность и требования к памяти как у примитивов, так и в целой новой парадигме программирования, основанной на присвоении типов и деконструировании значений.

[конец перевода]

От себя: очень хочется не согласиться с этим суждением, но в комментариях на Hacker News большинство контраргументов опровергаются. Например, поддержка со стороны Google штука во многом политическая, которая в любой момент может прекратиться, когда тяжба с Oracle закончится. Да и дефицит внимания характерная вещь для Google, пример Dart отбивает охоту делать на него ставку. Нативная поддержка null, которая греет душу любого котлиниста, легко заменяется в Java на обёртку из Optional.ofNullable. Data-объекты могут быть заменены более богатым функционалом record. Скорость компиляции, да и в целом производительность в Kotlin по сравнению с Java тоже оставляют желать лучшего.

Как думаете, сохранит Kotlin свою популярность через пять лет и почему?

Подробнее..

Recovery mode Почему Kotlin лучше Java?

24.05.2021 08:13:54 | Автор: admin

Это ответ на переведенную публикацию Почему Kotlin хуже, чем Java?. Поскольку исходная аргументация опирается всего на два примера, то не теряя времени пройдем по этим недостаткам Kotlin.

Проприетарные метаданные?

изрядное количество подробностей внутренней работы kotlinc скрыто внутри сгенерированных файлов классов...без IDEA Kotlin немедленно умрет

Это не проприетарный код, а просто способ для компилятора дописать дополнительные данные в жестко заданном формате .class-файлов, который ранее был заточен только под javac. Метаданные нужны для рефлексии и их можно удалить при компиляции. Исходный код метаданных открыт и общедоступен.

Kotlin будет отставать?

Вкратце, посыл исходной статьи таков что Kotlin был иновационным, но Java добавит все те же языковые возможности, только продуманее и лучше, и уже Kotlin-вариант выпадет из мейнстрима.
В качестве примера автор приводитinstanceof:

В Kotlin сделали примерно так:
if (x instanceof String) {
// теперь x имеет тип String! System.out.println(x.toLowerCase());
}

Но в Java версии 16+ стало так:
if (x instanceof String y) {
System.out.println(y.toLowerCase());
}

Получается, что оба языка имеют способ обработать описанный сценарий, но разными способами. Я уверен, что если бы мог вдавить огромную кнопку сброс, разработать Kotlin с нуля и снова выпустить сегодня бета-версию, то в Kotlin было бы сделано так же, как сейчас в Java. Учитывая, что синтаксис Java более мощный: мы можем сделать с ним намного больше, чем просто проверить тип (например, деконструировать типы-значения).
...
Ему придётся опираться только на себя и перестать рекламировать доступность всех преимуществ Java и её экосистемы. Пример с instanceof уже демонстрирует, почему я думаю, что Kotlin не будет лучше Java: почти каждая новая фича, которая появилась в Java недавно или вот-вот появится (в смысле, имеет активный JEP и обсуждается в рассылках) выглядит более продуманной, чем любая фича Kotlin.

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

when(val v = calcValue()) {  is String -> processString(v)}

Здесь в одной области можно проверить сразу несколько типов вместе со значениями значения, без создания дополнительных переменных. Попробуйте повторить в Java c if/instanceof/switch:

when(val v = calcValue()) {  is String -> processString(v)  42 -> prosess42()  is Int -> processInt(v)  else -> processElse(v)}

Остается разобраться с аргументом, что Kotlin, якобы, придется что-то делать с изменениями вносимыми Java, адаптироваться или расходиться, что это, якобы, создает проблему развития для Kotlin.

Прямо сейчас Kotlin оседлал волну успеха, но со временем жизнь его будет тем тяжелее, чем шире будет зазор между ним и Java, и чем сложнее будет преодолеть этот зазор.

Здесь автор путает местами причину и следствие, пытаясь выдать Kotlin за догоняющий язык. На самом деле, адаптация новых возможностей это в первую очередь проблема именно для Java. Именно новые возможности таких конкурентов как C# и Kotlin подстегивают Java изменяться, и именно для Java сложнее это сделать по причине изначально тяжеловесного и сложившегося за десятилетия синтаксиса, не предусматривавшего появление новых, функциональных возможностей. В Java-парадигме для них попросту осталось не так много места, где их можно прикрутить к синтаксису, и именно поэтому они выглядят чужеродно обременительно для восприятия.

Нативная поддержка null, которая греет душу любого котлиниста, легко заменяется в Java на обёртку из Optional.ofNullable. Data-объекты могут быть заменены более богатым функционалом record.

Все догоняющие возможности Java содержат фатальные изъяны по умолчанию, все больше заставляют прибегать к солгашениям, а не к дизайну языка. Вместо Optional можно передать null, а record ничуть не более богатый чем data class.

Как думаете, сохранит Kotlin свою популярность через пять лет и почему?

Некоторые в комментариях вспомнили историю Scala и Java. Но есть и другая история, история того что сделал С++ со старым Си.
Java несомненно останется там где нужно поддерживать старые решения. Однако новые решения для все больше писаться на Kotlin, пока он не станет языком по умолчанию, как это уже произошло в Android экосистеме, и прямо сейчас происходит для backend разработки в jvm экосистеме. Kotlin не просто лучше, он дает думать в другой парадигме, открыт для новых возможностей, страхует от многих ошибок на этапе компиляции.

Подробнее..

Работа с java.time в Kotlin любовь, боль, страдания

24.05.2021 10:16:39 | Автор: admin

Микоропост о том, как можно себя обмануть при использовании фичи Котлин: возможность работы с операторами сравнения типа Comparable.

Кто юзает Котлин не могли не оценить перегрузку операторов (ну точнее как она сделана), правда я допустим жил в Java и без нее прекрасно, но да тут не об этой фичи языка, а об основанной на ней: Comparison Operations. Это когда можно применять знаки сравнения для классов, реализующих Comparable, что является сахаром, но очень приятным.

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

На Java (пишу максимально кратко и без принятых норм, просто передать идею):

class TimeIterval {  LocalDateTime from;  LocalDateTime to;}class TimeIntervalUtil {  public boolean areOverlapped(TimeInterval first, TimeInterval second) {            return (first.from.isEqual(second.to) || first.from.isBefore(second.to)) &&                (first.to.isEqual(second.from) || first.to.isAfter(second.from));  }}

т.е. ничего сложного, но на всякий случай поясню код (мне приходиться самому пояснять себе такой код, когда его встречаю): пересечение двух интервалов возможно только если дата начала одного наступила ранее или в тот же момент времени по отношению к дате окончания второго и в это же время дата окончания первого интервала наступает позднее или в момент начала второго.

Теперь то же самое но на Котлине с его сахаром, но без рейнджей:

data class TimeInterval(val from: LocalDateTime, val to: LocalDateTime)fun areOverlapped(first: TimeInterval, second: TimeInterval): Boolean =   first.from <= second.to && first.to >= second.from

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

Тут нам нужно сделать то же самое, но уже с OffsetDateTime. Он тоже Comparable, как и почти все в java.time. Следовательно мы будем использовать такие же подходы как и LocalDateTime. В частности на Java код точно не измениться и будет работать так же как и ранее, а вот с Котлином будет засада.

Если посмотреть compareTo, в вызовы которого интерпретируется код на Kotlin при использовании знаков сравнения, то окажется что для LocalDateTime в принципе получается корректный код (сравниваются дни, часы, месяца и прочее по отдельности), что вроде как нормально.

В случае с OffsetDateTime будет сравнение, не которое мы ожидаем получить, так как compareTo учитывает зону времени, т.е. при сравнении 2021-04-25 10:00+0 и 2021-04-25 11:00+1 они не будут эквиваленты. Простой пример:

val inUtc = OffsetDateTime.of(LocalDateTime.of(2021, 4, 25, 10, 0), ZoneOffset.UTC)val inUtc1 = OffsetDateTime.of(LocalDateTime.of(2021, 4, 25, 11, 0), ZoneOffset.ofTotalSeconds(60*60))println(inUtc1>=inUtc && inUtc1 <= inUtc)println(inUtc.isEqual(inUtc1))

Вот так можно легко и непринужденно нарваться на неочевидную багу.

Подробнее..
Категории: Kotlin , Java

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

24.05.2021 12:21:25 | Автор: admin

Если вы регулярно читаете Хабр, то вам попадались статьи в духе: бросайте всё и начинайте изучать Swift, Kotlin или Flutter прямо сейчас. Давайте разбираться, правда ли стоит переобуваться в мобильного разработчика. Мы попросили спикеров, программный комитет и разработчиков взглянуть на сферу мобильной разработки с разных ракурсов и приоткрыть завесу тайны грядущей конференции Мир. Труд. Мобайл. В конце приятный бонус для читателей Хабра и подробности программы.

Мобильная разработка актуальна. Это факт

В отчёте State of Mobile 2021 говорится, что рынок мобильных приложении и игр вырос на 30% за 2020 год пользователи потратили на них рекордные $111 млрд. Пандемия и изоляция внесли свой вклад.

Про начинку современных смартфонов не приходится говорить. Фотографируют лучше стареньких Кэнон, по производительности и возможностям потягаются с простенькими ноутбуками.

Вспоминаются времена, когда все хотели мобильное приложение как манну небесную. Учитывая, что за последний год среднее время просиживания в телефоне на платформе Android составило 4 часа ежедневно, спрос на приложения будет только расти.

В сторах можно найти приложение на любой случай жизни. Что говорить, когда даже у автомата по розливу воды у дома в спальном районе (за 3 /литр) есть мобильное приложение для онлайн-оплаты. Кажется, мобильщики выбрали как девиз любая идея достойна мобильного приложения! Почему? Отчасти потому что новые технологии упростили и шаблонизировали разработку.

Отчёт State of Mobile 2021 Отчёт State of Mobile 2021

Про изменения в подходах к разработке мы спросили Фёдора Цымбала из Orion Innovations. Он выступит с докладом: Android Automotive. Не путать с Android Auto.

Новые технологии бывают разные. В основном упрощается решение типовых задач. Нужно помнить, что некоторые фреймворки намеренно усложняют жизнь разработчику. Заставляют думать об аспектах, которые раньше можно было игнорировать. Например, работа с Permissions, как мне кажется, усложняется в каждой новой версии Android. Но это связано с желанием Google защитить персональные данные пользователя. Так что не всегда усложнение это плохо.

В плане UI-фреймворков сейчас очень популярен Flutter. Но Jetpack Compose вполне может его потеснить. Стоит выбрать что-то одно из этих двух опций.

Заметен уход с мобилок на другие устройства: часы, телевизоры, автомобили. Android на этих устройствах сейчас активно развивается. По моему мнению, это тоже очень интересная тема. Про Android Automotive я и буду рассказывать: Google Automotive Services, Driver Distraction Guidelines, Garage Mode и об интеграции Android с подсистемами автомобиля, такими как камера заднего вида, климат контроль или поворотники.

Павел Стрельченко из hh.ru занимается Android-разработкой с 2015 года, поэтому успел застать разработку под Android 4, первую версию Android Studio, жизнь без Jetpack, Architecture Components и Kotlin. Павел выступит с темой: Укрощая фиче-флаги. Разберем проблемы постоянных merge-конфликтов, сбора флагов в один-единственный список.

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

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

Сейчас Android-разработчикам можно порекомендовать две вещи изучать корутины и Compose. Скоро все там будем, чтобы не отстать, нужно не останавливаться в изучении современных подходов и библиотек.

Тем, кто только начинает свой путь в увлекательный мир Android, я всегда рекомендую одно и тоже посмотреть лекции Android Academy. Мои коллеги по Android-у проделали потрясающую работу, собрав в один бесплатный курс всё необходимое для успешного старта в профессии.

Все спикеры

Evelio Tarazona Cceres
Instagram / Facebook

Server Driven Cross-Platform UI/Features/Apps. At Instagram we leverage Server Driven UI approach to build once/iterate quickly and ship features to billions of users on Android/iOS and the web.

Федор Цымбал
Orion Innovations

Android Automotive. Не путать с Android Auto. Google Automotive Services, Driver Distraction Guidelines, Garage Mode и об интеграции Android с подсистемами автомобиля, такими как камера заднего вида, климат контроль или поворотники.

Ольга Сартакова
Redmadrobot

Процесс работы над дизайном цифрового продукта. Целеполагание, определение задачи, решение, запуск в стор и жизнь после запуска.

Евгений Ртищев
Sberbank

Оптимизируем процессы разработки и параметры приложения.

Андрей Малеваник
Нетологиия

Красота или функциональность. Должен ли интерфейс быть красивым?

Павел Стрельченко
hh.ru

Укрощая фиче-флаги. Разберем проблемы постоянных merge-конфликтов, сбора флагов в один-единственный список.

Екатерина Петрова
JetBrains

State of Kotlin Multiplatform Mobile. О том, что поменялось в экосистеме KMM с момента большого релиза, о трендах и планах развития.

Александр Аверин
adVentures, Mail.ru

История перезапуска музыкального приложения BOOM глазами дизайнера.

Александр Гращенков
RoadAR

Почему тормозит iPhone. От базового уровня перевода задач в бэкграунд, до ускорения отрисовки с помощью Metal.

Михаил Никипелов
Distillery

Пиктограммы 80-го уровня. Про подбор идей и отсекание лишнего: как при работе с библиотеками, так и при отрисовке своих иконок.

Дмитрий Мельников
EventSheep (ех-Yandex, ех-Mail.ru)

Как мобильному разработчику сделать стартап без команды. MVP или прототип целого проекта с сервером и фронтендом.

Андрей Чевозеров
Банк ВТБ

SwiftUI в production. Как и зачем?

Антон Назаров
Crisalix

RxSwift vc Combine. О личном опыте миграции с RxSwift на Combine, какие подводные камни есть, как облегчить процесс.

Александр Денисов
EPAM

Так ли страшен Null, как его малюют? О том, что такое Null Safety, чем она может помочь в разработке, какие сложности могут ждать при миграции и чем реализация в Dart похожа, а чем отлична от Kotlin и Swift реализации.

Антон Шилов
Badoo

Воркшоп по анимациям на Jetpack Compose. Разберем основные API и инструменты для работы с анимациями от простого к сложному.

Мария Кирдун
EPAM

Искусство коммуникаций, или как творчеству выжить в IT. Игры на коммуникацию на реальных примерах,

Евгений Сатуров
Surf

Типы модульных тестов на Flutter. О том, как писать код на Flutter, чтобы его можно было протестировать.

Павел Горшков
экс-Redmadrobot, экс-Яндекс

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

Сергей Акентьев
Кошелёк

CI на Apple M1. Жутко больно и запредельно быстро. Как работать с кластером MacMini на M1 и почему мы оказались в Дата-центре? Проблемы архитектуры arm64, билды под Apple Rosetta 2, как бороться с софтом Apple и стоит ли оно того?

Алексей Бородкин
Магнит

Рок-н-ролл среди людей в галстуках: как делать классные приложения в большой корпорации. Вам это пригодится, даже если вы не работаете в большой корпорации.

Александр Соболь
МегаФон

Многопроцессная разработка Android приложений взгляд на микросервисную архитектуру.

Приложение остаётся способом реализовать большую идею малыми силами

Мобильное приложение по-прежнему отличный вариант для старта своего продукта с небольшой затратой ресурсов. Самый сладкий и большой кусок пирога мобильные игры. Пользователи потратили на них в 2020 году $143 млрд. Доля мобильных игр в магазинах приложений в 2021 году вырастет до 20%.

Про технологии, рынок, зарплаты и перспективы вроде понятно. Вопрос сколько стоит реализовать свою идею.

Приложения комбинируют механики. Афиша DVIZZ реализована в виде свайпов мероприятийПриложения комбинируют механики. Афиша DVIZZ реализована в виде свайпов мероприятий

Резиденты Иннополиса работают над приложением DVIZZ, которое объединяет афишу городских событий и создание встреч, чтобы познакомиться с новыми людьми и хорошо провести время. Придумать новую идею становится сложнее, уже практически всё реализовано. Поэтому новые приложения активно комбинируют функции. Стремление в сторону супераппов с замкнутой экосистемой как тренд не только для IT-гигантов.

Основатель DVIZZ Михаил Иванов рассказывает, что потратил 500 000 на разработку и 3 млн на зарплаты за полгода после запуска.

Сейчас на операционные расходы уходит около 10 000 в месяц. Но это ещё не всё, потому что самое сложное продвижение. На анализ, стратегию, маркетинг, таргетинг потратили 300 000 . Уже удалось снизить стоимость установки до приятных цифр, но начальный этап действительно самый сложный.

Приложением я решаю личную проблему, а таких как я много. Нет площадок для ивентов, где каждый может рассказать про свои планы на выходные и позвать знакомых, например, как в инсте. Можно сказать, что DVIZZ Инстаграм для ивентов. В ленте можно выбрать мероприятие и купить билет, присоединиться ко встрече пользователей или создать свою.

У нас есть три стратегии монетизации: реклама больших ивентов в афише, платные мероприятия или мастер-классы больше 10 человек и доступ в бизнес-кабинет владельцев площадок для аренды, например, теннисного корта или лектория прямо из приложения. Уже понемногу зарабатываем, планируем запускать в Москве. Окупиться планируем через год.

Что будет на Мир. Труд. Мобайл

Два формата: бесплатный онлайн и офлайн на цифровой даче в Иннополисе. Всего будет 5 больших хабов:

  • Android;

  • iOS;

  • Кроссплатформа;

  • Дизайн;

  • Софтскилы.

Офлайн

Специальная программа для 200 офлайн-участников: закрытые лекции и воркшопы, дачный нетворкинг (песни под гитару у диджитал-костра), тестирование беспилотного такси, афтепати. Спикеры будут читать доклады на грядках, веранде, солнцепёке и даже на чердаке.

Ждём в гости!Ждём в гости!

Онлайн

По недавнему опыту IT Nights мы убедились, что очень важно экспериментировать с подачей и не бояться свежих решений в довольно консервативном жанре конференций. Поэтому в этот раз поднимаем планку и скрещиваем офлайн и онлайн. В офлайне будет немного больше приколов (уж извините), но онлайн точно не останется серой мышью в виде прямого эфира. Уже доступна удобная платформа: прямой эфир, опросы и голосования, беседка-клабхаус со спикерами на холиварные темы после доклада и дачный шопинг на виртуальном базаре.

Зарегистрироваться на онлайн сейчас стоит хотя бы потому, что после конференции доступ к докладам будет платным.

Увидимся на Мир. Труд. Мобайл.

27 мая

Скидка на офлайн 25% для читателей Хабра по промокоду habr

Мобайл редьки слаще!

Подробнее..

KotlinDL 0.2 Functional API, зоопарк моделей c ResNet и MobileNet, DSL для обработки изображений

25.05.2021 12:05:50 | Автор: admin

Представляем вам версию 0.2 библиотеки глубокого обучения KotlinDL.

KotlinDL 0.2 теперь доступен на Maven Central (до этого он лежал на bintray, но закатилось солнышко земли опенсорсной). Появилось столько всего нового: новые слои, специальный DSL для препроцессинга изображений, новые типы датасетов, зоопарк моделей с несколькими моделями из семейства ResNet, MobileNet и старой доброй моделью VGG (рабочая лошадка, впрочем).

В этой статье мы коснемся самых главных изменений релиза 0.2. Полный список изменений доступен по ссылке.

Functional API

Прошлая версия библиотеки позволяла описывать нейронные сети лишь при помощи Sequential API. Например, используя метод Sequential.of(..), вы могли легко описать модель как последовательность слоев и построить VGG-подобную модель.

Однако с 2014 года (эпохи взлета и расцвета подобных архитектур) много воды утекло, и было создано множество новых нейросетей. В частности, стандартным подходом стало использование так называемых остаточных нейросетей (Residual Neural Networks или ResNet), которые решают проблемы исчезающих градиентов (vanishing gradients) и, напротив, взрывающихся градиентов (exploding gradients) а значит, и проблемы деградации обучения нейросети. Подобные архитектуры невозможно описать в виде Sequential API их корректнее представлять в виде направленного ациклического графа (Directed Acyclic Graph). Для задания таких графов мы добавили в версии 0.2 новый Functional API, который позволяет нам описывать модели, подобные ResNet или MobileNet.

Ну что же, давайте построим некое подобие ResNet. Нейросеть будет обучаться на датасете FashionMnist (небольшие изображения модных вещей). Черно-белые изображения размером 28х28 отлично подойдут на старте работы с нейросетями.

val (train, test) = fashionMnist()val inputs = Input(28, 28, 1)val conv1 = Conv2D(32)(inputs)val conv2 = Conv2D(64)(conv1)val maxPool = MaxPool2D(poolSize = intArrayOf(1, 3, 3, 1),strides = intArrayOf(1, 3, 3, 1))(conv2)val conv3 = Conv2D(64)(maxPool)val conv4 = Conv2D(64)(conv3)val add1 = Add()(conv4, maxPool)val conv5 = Conv2D(64)(add1)val conv6 = Conv2D(64)(conv5)val add2 = Add()(conv6, add1)val conv7 = Conv2D(64)(add2)val globalAvgPool2D = GlobalAvgPool2D()(conv7)val dense1 = Dense(256)(globalAvgPool2D)val outputs = Dense(10, activation = Activations.Linear)(dense1)val model = Functional.fromOutput(outputs)model.use {it.compile(optimizer = Adam(),loss = Losses.SOFT_MAX_CROSS_ENTROPY_WITH_LOGITS,metric = Metrics.ACCURACY)it.summary()it.fit(dataset = train, epochs = 3, batchSize = 1000)val accuracy = it.evaluate(dataset = test, batchSize = 1000).metrics[Metrics.ACCURACY]println("Accuracy after: $accuracy")}

Перед вами вывод метода summary(), описывающий архитектуру только что созданной нами модели.

Некоторые не любят сухие отчеты и предпочитают диаграммы. В нашем случае диаграмма типична для всех представителей славного семейства ResNet.

Если вы знакомы с фреймворком Keras, то без особого труда сможете перенести модели, описанные при помощи Functional API, в Keras, используя KotlinDL.

Коллекция предварительно тренированных моделей ResNet и MobileNet

Начиная с релиза 0.2, в Kotlin DL появляется зоопарк моделей (или Model Zoo). По сути, это коллекция моделей с весами, полученными в ходе обучения на большом датасете изображений (ImageNet).

Зачем нужна такая коллекция моделей? Дело в том, что современные сверхточные нейросети могут иметь сотни слоев и миллионы параметров, обновляемых многократно в течении каждой итерации обучения. Тренировка моделей до приемлемого уровня точности (7080%) на таком большом датасете, как ImageNet, может занимать сотни и тысячи часов вычислительного времени большого кластера из видеокарт.

Зоопарк моделей позволяет вам пользоваться уже готовыми и натренированными моделями (вам не придется тренировать их с нуля каждый раз, когда они вам нужны). Вы можете использовать такую модель непосредственно для предсказаний. Также вы можете применить ее для дотренировки части модели на небольшой порции входных данных это весьма распространненная техника при использовании переноса обучения (Transfer Learning). Это может занять десятки минут на одной видеокарте (или даже центральном процессоре) вместо сотен часов на большом кластере.

Доступны следующие модели:

  • VGG16

  • VGG19

  • ResNet50

  • ResNet101

  • ResNet152

  • ResNet50v2

  • ResNet101v2

  • ResNet152v2

  • MobileNet

  • MobileNetv2

Для каждой модели из этого списка доступны функции загрузки конфигурации модели в JSON-формате и весов в формате .h5. Также для каждой модели можно использовать специальный препроцессинг, применявшийся для ее обучения на датасете ImageNet.

Ниже вы видите пример загрузки одной из таких моделей (ResNet50):

// specify the model type to be loaded, ResNet50, for exampleval loader =ModelZoo(commonModelDirectory = File("cache/pretrainedModels"), modelType = ModelType.ResNet_50)// obtain the model configurationval model = loader.loadModel() as Functional// load class labels (from ImageNet dataset in ResNet50 case)val imageNetClassLabels = loader.loadClassLabels()// load weights if required (for Transfer Learning purposes)val hdfFile = loader.loadWeights()

Ну что же, теперь у вас есть сама модель и веса вы можете использовать их по вашему усмотрению.

Внимание! К изображениям, которые вы подаете на вход модели для предсказаний, необходимо применять специальный препроцессинг, о котором мы говорили ранее. Иначе вы получите неверные результаты. Для вызова препроцессинга используйте функцию preprocessInput.

Если вам не нужны предобученные веса, но вы не хотите описывать многослойные модели а-ля VGG или ResNet с нуля, у вас есть два пути: а) просто загрузить конфигурацию модели либо б) взять за основу полный код конструирования модели, написанный на Kotlin, он доступен для каждой из моделей через вызов функции высшего порядка, лежащей в пакете org.jetbrains.kotlinx.dl.api.core.model.

Ниже мы приводим пример использования функции, строящей облегченную версию ResNet50:

val model = resnet50Light(imageSize = 28,numberOfClasses = 10,numberOfChannels = 1,lastLayerActivation = Activations.Linear)

Если вы хотите узнать больше о переносе обучения и использовании зоопарка моделей, советуем этот туториал: вы увидите, как загружается модель VGG19, затем у нее удаляется последний слой, добавляются новые Dense-слои, после чего их веса инициализируются и дообучаются на небольшом датасете, состоящем из изображений кошек и собак.

DSL для предобработки изображений

Python-разработчикам предлагается огромное количество библиотек визуализации и предобработки изображений, музыки и видео. Разработчикам экосистемы языков программирования JVM повезло меньше.

Большинство библиотек для предобработки изображений, найденные на просторах Github и имеющие разную степень заброшенности, так или иначе используют класс BufferedImage, оборачивая его более понятным и согласованным API. Мы решили упростить жизнь Kotlin-разработчиков, предложив им простой DSL, построенный на лямбда-выражениях и объектах-приемниках.

На данный момент доступны следующие функции преобразования изображений:

  • Load

  • Crop

  • Resize

  • Rotate

  • Rescale

  • Sharpen

  • Save

val preprocessing: Preprocessing = preprocess {   transformImage {       load {           pathToData = imageDirectory           imageShape = ImageShape(224, 224, 3)           colorMode = ColorOrder.BGR       }       rotate {           degrees = 30f       }       crop {           left = 12           right = 12           top = 12           bottom = 12       }       resize {           outputWidth = 400           outputHeight = 400           interpolation = InterpolationType.NEAREST       }   }   transformTensor {       rescale {           scalingCoefficient = 255f       }   }}

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

Если, экспериментируя с DSL, вы поймете, что некоторых функций вам не хватает, не стесняйтесь написать об этом в наш баг-трекер.

Новые слои

В релизе 0.2 появилось много новых слоев. В основном, это обусловлено тем, что они используются в архитектурах ResNet и MobileNet:

  • BatchNorm

  • ActivationLayer

  • DepthwiseConv2D

  • SeparableConv2D

  • Merge (Add, Subtract, Multiply, Average, Concatenate, Maximum, Minimum)

  • GlobalAvgPool2D

  • Cropping2D

  • Reshape

  • ZeroPadding2D*

* Спасибо Anton Kosyakov за имплементацию нетривиального ZeroPadding2D!

Кстати, если вы хотите добавить новый слой, вы можете самостоятельно реализовать его и создать пул-реквест. Список слоев, которые мы хотели бы включить в релиз 0.3, представлен набором тикетов в баг-трекере с пометкой good first issue и может быть использован вами как точка входа в проект.

Dataset API и парочка наследников: OnHeapDataset & OnFlyDataset

Типичным способом прогона данных через нейросеть в режиме прямого распространения (forward mode) является последовательная загрузка батчей в оперативную память, контролируемую языком, а затем в область нативной памяти, контролируемую вычислительным графом модели TensorFlow.

Мы также поддерживаем подобный подход в OnFlyDataset. Он последовательно, батч за батчем, загружает датасет в течений одной тренировочной эпохи, применяя препроцессинг данных (если вы его заранее определили) и аугментацию (если вы ее добавили).

Этот метод хорош, когда оперативной памяти мало, а данных много. Но что, если оперативной памяти более чем достаточно? Это не такой уж редкий случай для задач переноса обучения: датасеты для дообучения могут быть не такими большими, как при тренировке моделей. Также можно получить некоторый прирост в скорости за счет того, что препроцессинг будет применен лишь один раз на этапе формирования датасета, а не при каждой загрузке батча. Если у вас достаточно оперативной памяти, используйте OnHeapDataset. Он будет держать все данные в оперативной памяти не нужно будет повторно считывать их с диска на каждой эпохе.

Набор встроенных датасетов

Если вы только начинаете путешествие в удивительный мир глубокого обучения, мы настоятельно рекомендуем вам строить и запускать ваши первые нейросети на широко известных датасетах, таких как MNIST (набор рукописных цифр), FashionMNIST(набор изображений модных вещей от компании Zalando), Cifar10 (подмножество ImageNet, насчитывающее 50 000 изображений) или коллекцию изображений кошек и собак со знаменитого соревнования Kaggle (по 25 000 изображений каждого класса различных размеров).

Все эти датасеты, как и модели из зоопарка моделей, вы можете загрузить в папку на вашем диске при помощи функций высшего порядка, таких как mnist() и fashionMnist(). Если датасет уже был загружен, заново по сети он грузиться не будет, а будет взят с диска.

Как добавить KotlinDL в проект

Чтобы начать использовать KotlinDL в вашем проекте, просто добавьте дополнительную зависимость в файл build.gradle:

repositories {    mavenCentral()}dependencies {    implementation 'org.jetbrains.kotlinx:kotlin-deeplearning-api:0.2.0'}

KotlinDL можно использовать в Java-проектах, даже если у вас нет ни капли Kotlin-кода. Здесь вы найдете пример построения и тренировки сверточной сети, полностью написанный на Java.

Если вы думаете, что в вашем проекте будет полезен Java API, напишите нам об этом или создайте PR.

Полезные ссылки

Мы надеемся, что вам понравилась наша статья и новые возможности KotlinDL.

Хотите узнать больше о проекте? Предлагаем ознакомиться с Readme или со страничкой проекта на GitHub. А этот туториал поможет вам создать вашу первую нейросеть на Kotlin.

Если вам интересно, как устроен KotlinDL, как он появился и в каком направлении развивается, почему он так похож на Keras, и планируется ли поддержка PyTorch, посмотрите свежее видео от Алексея Зиновьева.

Также мы ждем вас в Slack-канале #kotlindl (инвайт можно получить тут). В нем вы можете задавать вопросы, участвовать в дискуссиях и первыми получать информацию о превью-релизах и новых моделях в зоопарке моделей.

Ваша обратная связь, ваши описания багов и краш-репорты, идеи и комментарии все это очень важно для нас. Мы ждем новых пользователей и контрибьюторов, как начинающих, так и опытных исследователей всех, кому интересны Deep Learning и Data Science на Kotlin, Java и Scala!

Подробнее..

Как я хотел поработать нативным Android разработчиком, но устроился Flutter разрабом

25.05.2021 18:15:50 | Автор: admin

Небольшое вступление

После праздничных каникул (это было в январе) я решил немного поработать Android разработчиком на пол ставки, дабы совмещать работу с учебой.

Ещё в декабре я познакомился с главным программистом IT-компании, которая находится в Сочи.

Я не буду оглашать имя компании в целях корпоративной тайны, это не суть. Компания довольно молодая, и поэтому использует более новые технологии. Я был удивлен, когда мне ответили, что им нужен Flutter разработчик, а не Java/Kotlin.

Так я и познакомился с Flutter.

Первые впечатления

Я был совершенно поражен когда впервые увидел сгенерированный проект мобильного приложения на Flutter. Все совершенно отличалось от обычной нативной разработки под Android.

Первое что бросалось в глаза - это совершенно другой язык, Dart.

Я сразу начал штудировать этот раздел и узнал, что Flutter - это Framework с декларативным стилем написания UI.

Мне никогда не был понятен данный стиль написания кода. Когда-то в прошлом я решил освоить React JS, но не смог его одолеть и забросил (в основном из-за глупости и лени). Зачем вообще декларативный стиль программирования? Есть же интуитивно понятно императивный: создал объект кнопки, добавил в родительский элемент и т.д.

Когда я увлекся Flutter, то осознал и понял главные преимущества такого подхода:

  • Меньше кода

  • Интуитивно понятный

  • Ускоренная разработка

  • Мощность

Возможно это произвучит чересчур громко. Все эти преимущества в той или иной мере правдивы.

Вот так, к примеру, выглядит разметка UI приложения, сгенерированного Android Studio:

Scaffold(      appBar: AppBar(        title: Text("Counter App"),      ),      body: Center(child: Column(        mainAxisAlignment: MainAxisAlignment.center,        children: [      Text("You have pushed the button this many times: "),      SizedBox(height: 10),        Text("$counter",           style: Theme.of(context).textTheme.headline4,          )    ],      ),),  floatingActionButton: FloatingActionButton(        onPressed: () { setState(() => counter = counter + 1); },    child: Icon(Icons.add),      ),);

Первая важная особенность такого кода: большое количество скобок и запятых, как бы смешно это не звучало)))

Вторая: большая гибкость и мощность, вы можно по-разному вкладывать друг в друга элементы и комбинировать их, создавая более сложный UI

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

И наконец, Dart имеет интересную фишку: можно поставить запятую в конце последнего параметра функции или конструктура класса, поэтому вы можете не париться по поводу последней запятой в вашем коде, когда делает разметку UI или передаете параметры функции.

Как вы уже догадались в любой технологии найдется уязвимое место. Какие оптимизации бы не сделал Flutter разработчик, его приложение все равно будет проигрывать в скорости работы приложения, написаного на Java / Kotlin - это 100% очевидно (данная проблема проявляется не во всех ситуациях).

Первое приложение

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

Я начал курить flutter.dev, прочитал довольно много полезных материалов на данную тему.

В результате, я решил использовать обертку sqlite для Android и iOS - sqflite.

Сразу стоит отметить, что подключение большинства библиотек (pub-пакетов) осуществляется через специальный файл pubspec.yaml, в отличие от build.gradle (Android).

Все пакеты Dart (включая подмножество Flutter) располагаются на сайте pub.dev

Как я позже узнал, Flutter позволяет использовать нативный код Android и iOS, что меня очень сильно порадовало.

Дальнейшие разработки

C февраля я был переведен на первый рабочий проект.

Я все больше стал понимать синтаксис языка Dart. Для тех, кто хочет довольно быстро с ним познакомиться покурите Dart Tour

В многих проектах, даже довольно маленьких всегда есть какие-либо тяжелые задачи, которые могут затормозить отрисовку UI приложения (кнопок, меню и т.д.). Например: загрузка файла из сети, или запрос в базу данных и т.д.

Поэтому необходимо использовать либо Thread'ы (Java), либо Coroutines (Kotlin) в нативной разработке под Android

В Flutter это решается довольно просто, использованием асинхронных функций:

fun getArticles() async {  final response = await http.get("https://xxx.ru/rest/getArticles");  final List<Article> articles = decodeArticles(response.body);setState(() {    this.articles = articles;  });}

Возможно не совсем понятно для незнакомых с Flutter и декларативным стилем написания кода, вызов функции setState.

setState является функцией высшего порядка (Dart поддерживает функциональное программирование) и принимает другую функцию, как входной параметр.

Логика setState довольно простая: сначала выполняем функцию, которая была передана в качестве параметра, а затем перерисовываем все компоненты Flutter приложения. (на самом деле не все, Flutter сам решает, что нужно перерисовать, а что нет, дабы обеспечить эффективность работы).

В этом и состоит один из важнейших принципов декларативного подхода Flutter - принципа состояние.

Более подробно о состоянии: flutter.dev

При реализации моего первого рабочего приложения на Flutter я впервые столкнулся с проблемой архитектуры.

По большей части все данные Flutter приложения - это состояние (на момент выполнения приложения несомненно).

И поэтому в проектировании архитектуры Flutter приложения нужно руководствоваться одним из подходов по управления состоянием.

Я выбрал provider и не пожалел об этом. Данный подход довольно простой и изящный.

В апреле мой первый более менее рабочий проект был опубликован в Google Play и Apple Store

Мое личное мнение о Flutter

Я считаю, что Flutter - довольно неплохой кроссплатформенный framework для мобильной разработки, по моему мнению он не уступает своим конкурентам, таким как React Native например.

Большинство коммерческих проектов вполне могут быть реализованы на Flutter.

Основные преимущества Flutter по моему мнению:

  • Довольно мощный UI framework, позволяет сильно кастомизировать внешний вид приложения. Это также является важнейшим преимуществом по отношению к нативной Android разработке, т.к. создание кастомных View и написание дополнительного кода является не одной из самых простых задач;

  • Быстрая разработка - т.к. Flutter является кроссплатформенным инструментом для разработки, вам не нужно писать отдельно код для iOS и Android, что действительно повышает скорость разработки, но не во всех случаях работу самого приложения :)

  • Декларативный стиль обладает некоторыми преимуществами над императивным, как было отмечено выше

  • Функциональность - Flutter имеет огромное количество полезных компонентов, а также pub-пакетов, которые не раз меня выручали). Сейчас Flutter продолжает расти, в марте прошел Flutter Engage 2021

Причины по которым вы не должны использовать Flutter:

  • Высокая производительность - если каждая доля миллисекунды вам дорога, то несомненно в таком случае вам не стоит использовать Flutter

  • Кастомная отрисовка компонентов

  • Какие-либо нестандарные решения

  • Низкоуровневая работа с компонентами мобильной ОС

Заключение

Статья носит субъективное мнение и поэтому я могу ошибаться в некоторых, а может и во всех отношениях.

Моей главной целью было выразить мой скромный опыт разработки на Flutter и поделиться своим мнением и некоторыми замечаниями.

В конце, хотелось бы отметить, что Flutter имеет все нужные обстоятельства для будущего развития и, возможно даже перерастет другие подходы кроссплатформенной разработки (React Native), если ещё не перерос.

Полезные ссылки:

Подробнее..

Получаем результат правильно(Часть2). FragmentResultAPI

25.05.2021 20:20:29 | Автор: admin

Мы продолжаем рассказ о новинках библиотеки Jetpack, призванных упростить обмен данными между компонентами Android приложения. Первая часть была посвящена передаче данных из Activity и новому Api Activity Result.

На этот раз посмотрим, какое решение Google предлагает для Fragment. Ввиду популярности паттерна Single Activity работа с фрагментами представляет большой практический интерес для многих Android-разработчиков.

Как передать данные между двумя фрагментами? - частый вопрос на собеседованиях. Ответить на него можно по-разному: создание общей ViewModel, имплементация интерфейса в Activity, использование targetFragment и другие способы.

С появлением Fragment Result Api в этот список добавился простой способ передачи небольшого объема информации из одного фрагмента в другой. Например, возвращение результата какого-либо пользовательского сценария. Мы разберем, как применять новый Api на практике, но сначала немного теории.

Теория

Начиная с версии 1.3.0-alpha04, FragmentManager реализует интерфейс FragmentResultOwner. Это означает, что FragmentManger является диспетчером для результатов, которые отправляют фрагменты. Благодаря этому фрагменты могут обмениваться информацией, не имея прямых ссылок друг на друга.

Таким образом, всё взаимодействие происходит через FragmentManager:

  • Если фрагмент ожидает получить некоторые данные от другого фрагмента, он должен зарегистрировать слушатель во FragmentManger с помощью метода setFragmentResultListener().

  • Если фрагменту необходимо вернуть результат другому фрагменту, он передает FragmentManger объект Bundle, содержащий информацию. Для этого вызывается метод setFragmentResult().

  • Чтобы FragmentManger знал, как соотнести Bundle с нужным слушателем, необходимо указывать строковый ключ при регистрации слушателя и при передаче результата.

Упрощенно данную схему можно представить так:

FragmentB передает данные в FragmentA . FragmentManager выполняет роль диспетчераFragmentB передает данные в FragmentA . FragmentManager выполняет роль диспетчера

Достоинством Fragment Result Api является lifecycle-безопасность - результат передается во фрагмент, только когда тот достиг состояния STARTED, но еще не находится в состоянии DESTROYED.

Под капотом FragmentManger хранит все зарегистрированные слушатели и все отправленные результаты в потокобезопасных реализациях Map:

  • Map<String, Bundle> для результатов, отправленных фрагментами

  • Map<String, LifecycleAwareResultListener> для зарегистрированных слушателей

Когда фрагмент регистрирует FragmentResultListener, FragmentManager добавляет его в Map, а при уничтожении фрагмента, слушатель удаляется из Map. Для того, чтобы учитывать жизненный цикл фрагмента, FragmentResultListener оборачивается в LifecycleAwareResultListener.

При отправке результата, FragmentManager ищет зарегистрированный с тем же ключом слушатель и передает ему результат. Если слушатель не найден, то результат сохраняется в Map в ожидании дальнейшего использования.

А теперь практика.

Практика

В качестве примера возьмем следующий кейс: ProductsFragment содержит список товаров, которые можно сортировать по различным критериям, а SortFragment позволяет указать нужную сортировку. Информация о выбранной сортировке будет передаваться с помощью Fragment Result Api.

Так выглядит итоговая реализация, которую можно найти по ссылке нижеТак выглядит итоговая реализация, которую можно найти по ссылке ниже

В коде все выглядит довольно просто. Чтобы наладить передачу результата, необходимо выполнить всего два шага.

Шаг 1

В ProductsFragment, который ожидает получить результат, мы должны зарегистрировать слушатель с помощью FragmentManager. Для этого воспользуемся экстеншен-функцией setFragmentResultListener из fragment-ktx, которая принимает строковый ключ и слушатель, обрабатывающий результат.

Регистрацию слушателя можно произвести в колбеке onCreate():

override fun onCreate(savedInstanceState: Bundle?) {   super.onCreate(savedInstanceState)   setFragmentResultListener("request_key") { key, bundle ->        val selectedSort = bundle.getParcelable<Sort>("extra_key")        // применение полученной сортировки   }}

Шаг 2

Когда SortFragment будет готов отправить результат, вызывается метод setFragmentResult, в который передается тот же строковый ключ и заполненный объект Bundle.

applyButton.setOnClickListener {   setFragmentResult(      "request_key",       bundleOf("extra_key" to getSelectedSort())   )}

Вот и всё, что требуется для передачи результата с помощью Fragment Result Api.

Важно

Хотя Api довольно прост, стоит разобрать некоторые нюансы его работы, связанные с правильным выбором FragmentManager и жизненным цикломфрагментов.

Выбор FragmentManager

FragmentManager выполняет основную работу в передаче результата от одного фрагмента к другому. Но каждому фрагменту доступен выбор из нескольких вариантов: parentFragmentManager, childFragmentManager и FragmentManager у активити-хоста. Разберемся, в каких случаях стоит выбирать тот или иной FragmentManager.

Сначала представим так называемую master-detail конфигурацию. Активити содержит два фрагмента, FragmentA и FragmentB, между которыми требуется передать результат.

Активити является хостом для FragmentA и FragmentBАктивити является хостом для FragmentA и FragmentB

В таком случае передавать результат между фрагментами может FragmentManager активити-хоста, т.к. доступ к нему имеют оба фрагмента. Получить данный FragmentManager можно путем вызова requireActivity().supportFragmentManager либо parentFragmentManager.

Следующая ситуация характерна, например, для открытия DialogFragment или в случае, если FragmentA размещает внутри себя FragmentC.

FragmentA является хостом для FragmentСFragmentA является хостом для FragmentС

При таком сценарии, передать результат из FragmentС в FragmentA можно двумя способами:

  • Через FragmentManager активити с помощью requireActivity().supportFragmentManager

  • Через дочерний FragmentManager у FragmentA. Чтобы получить на него ссылку, FragmentA должен обращаться к childFragmentManager, а FragmentС к parentFragmentManager.

Особенности Lifeсycle

Как уже сказано, Fragment Result Api обеспечивает lifecycle-безопасность - результат доставляется, только если фрагмент находится на экране. Рассмотрим несколько примеров.

Представим стандартный случай - фрагмент подписывается в колбеке onCreate, затем переходит в состояние STARTED, и как только другой фрагмент передает во FragmentManager результат, фрагмент-подписчик его получает.

Фрагмент получит лишь bundle3, так как он был отправлен последнимФрагмент получит лишь bundle3, так как он был отправлен последним

Если еще до перехода фрагмента в состояние STARTED, во FragmentManager было передано несколько результатов, то фрагмент получит лишь последний из них (так как FragmentManager хранит результаты в Map<String, Bundle>, то каждый последующий перезаписывает предыдущий).

Автоматическая отписка фрагментов происходит при достижении состояния DESTROYEDАвтоматическая отписка фрагментов происходит при достижении состояния DESTROYED

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

Если фрагмент фрагмент-подписчик был закрыт до отправки результата, он получит его при повторном открытии.Если фрагмент фрагмент-подписчик был закрыт до отправки результата, он получит его при повторном открытии.

В том случае, когда фрагмент не закрыт окончательно, а лишь находится в бэкстеке (в таком случае он в состоянии CREATED), то результат будет доставлен, как только пользователь вернется к этому фрагменту.

Сценарий при нахождении фрагмента в бэкстеке в момент передачи результатаСценарий при нахождении фрагмента в бэкстеке в момент передачи результата

Все рассмотренные ситуации объединяет то, что фрагмент подписывался по уникальному строковому ключу. Но что если сразу несколько подписчиков будут использовать один и тот же ключ? Напомним, что FragmentManager сохраняет информацию о подписках в Map<String, LifecycleAwareListener>, следовательно не может содержать несколько записей с одним и тем же ключом. Именно поэтому результат будет доставлен в тот фрагмент, который зарегистрировал слушатель последним.

Результат получает только последний подписчикРезультат получает только последний подписчик

Заключение

Подводя итог, отметим достоинства нового способа передачи результата между фрагментами:

  • Fragment Result Api является стабильным, можно не бояться использовать его в продакшене. Тем, кто использует targetFrament особенно стоит присмотреться, ведь targetFrament стал Deprecated.

  • Api прост в использовании и не требует написания большого количества кода

  • Учитывает жизненный цикл фрагментов - при получении результата, можно сразу работать со view фрагмента

  • Позволяет пережить изменение конфигурации и даже смерть процесса (FragmentManager умеет сохранять данные о переданных результатах в Parcelable)

Но присутствуют и недостатки:

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

  • так как результат передается в Bundle, отсутствует его типизация. При неаккуратном обращении, можно получить ClassCastException.

В целом, Fragment Result Api оставляет положительное впечатление, и точно стоит того, чтобы его опробовать, а наглядный пример можно найти по ссылке.

Подробнее..

C vs Kotlin

01.06.2021 08:12:59 | Автор: admin

Когда речь заходит о сахаре и модных фичах в языках программирования, среди первых вариантов на ум приходят C# и Kotlin. Поскольку эти два языка занимают схожие ниши, то есть, строго типизированы, обладают сборкой мусора, кроссплатформенны, применяются как на бекенде, так и в мобильной разработке, то сегодня мы попытаемся сравнить их синтаксические возможности и устроить небольшое голосование. Чтобы сравнение прошло честно, будем рассматривать последние версии обоих языков. Оговорюсь о своей непредвзятости: мне одинаково нравятся оба языка, они находятся в непрерывном развитии и не отстают друг от друга. Эта статья является сравнительной, а не обучающей, поэтому некоторые заурядные синтаксические возможности могут быть опущены.

Начнем с точки входа

В C# эту роль играет статический метод Main или top-level entry point, например

using static System.Console;WriteLine("Ok");

В Kotlin нужна функция main

fun main() = println("Ok")

По этим небольшим двум примерам в первую очередь заметно, что в Kotlin можно опускать точку с запятой. При более глубоком анализе видим, что в C#, несмотря на лаконичность показательного entry point, статические методы в остальных файлах по прежнему требуется оборачивать в класс и явно импортировать из него (using static System.Console), а Kotlin идет дальше и разрешает создавать полноценные функции.

Обьявление переменных

В C# тип пишется слева, а для создания экземпляра используется ключевое слово new. В наличии есть специальное слово var, которым можно заменить имя типа слева. При этом переменные внутри методов в C# остаются подвержены повторному присваиванию.

Point y = new Point(0, 0); var x = new Point(1, 2);x = y; // Нормально

В Kotlin типы пишутся справа, однако их можно опускать. Помимо var, доступен и val который не допускает повторного присваивания. При создании экземляров не нужно указывать new.

val y: Point = Point(0, 0)val x = Point(1, 2)x = y // Ошибка компиляции!

Работа с памятью

В C# нам доступны значимые (обычно размещаются на стеке) и ссылочные (обычно размещаются в куче) типы. Такая возможность позволяет применять низкоуровневые оптимизации и сокращать расход оперативной памяти. Для объектов структур и классов оператор '==' будет вести себя по разному, сравнивая значения или ссылки, впрочем это поведение можно изменить благодаря перегрузке. При этом на структуры накладываются некоторые ограничения связанные с наследованием.

struct ValueType {} // структура, экземпляры попадут на стекclass ReferenceType {} // ссылочный тип, экземпляры будут в куче

Что до Kotlin, то у него нет никакого разделения по работе с памятью. Сравнение '==' всегда происходит по значению, для сравнения по ссылке есть отдельный оператор '==='. Объекты практически всегда размещаются в куче, и только для некоторых базовых типов, например Int, Char, Double, компилятор может применить оптизмизации сделав их примитивами jvm и разместив на стеке, что никак не отражается на их семантике в синтаксисе. Складывается впечатление что рантайм и работа с памятью это более сильная сторона .NET в целом.

Null safety

В C# (начиная с 8ой версии) есть защита от null. Однако ее можно явно обойти с помощью оператора !

var legalValue = maybeNull!;// если legalValue теперь null, // то мы получим exception при первой попытке использования

В Kotlin для использования null нужно использовать два восклицания, но есть и другое отличие

val legalValue = maybeNull!! // если maybeNull == null, // то мы получим exception сразу же

Свойства классов

В C# доступна удобная абстракция вместо методов get/set, то есть всем известные свойства. При этом традиционные поля остаются доступны.

class Example{     // Вычислено заранее и сохранено в backing field  public string Name1 { get; set; } = "Pre-calculated expression";    // Вычисляется при каждом обращении  public string Name2 => "Calculated now";    // Традиционное поле  private const string Name3 = "Field"; }

В Kotlin полей нет вообще, по умолчанию доступны только свойства. При этом в отличие от C# public это область видимости по умолчанию, поэтому это ключевое слово рекомендукется опускать. Для разницы между свойствами, допускающими set и без него, используются все те же ключевые var/val

class Example {    // Вычислено заранее и сохранено в backing field  val name1 = "Pre-calculated expression"    // Вычисляется при каждом обращении  val name2 get() = "Calculated now"}

Классы данных

В C# достаточно слова record чтобы создать класс для хранения данных, он будет обладать семантикой значимых типов в сравнении, однако по прежнему остается ссылочным (будет размещаться в куче):

class JustClass{  public string FirstName { init; get; }  public string LastName { init; get; }}record Person(string FirstName, string LastName);...   Person person1 = new("Nancy", "Davolio");Person person2 = person1 with { FirstName = "John" };

В Kotlin нужно дописать ключевое слово data к слову class

class JustClass(val firstName: String, val lastName: String)data class Person(val firstName: String, val lastName: String)...val person1 = Person("Nancy", "Davolio")val person2 = person1.copy(firstName = "John")

Расширения типов

В C# такие типы должны находиться в отдельном статическом классе и принимать вызывающий первым аргументом, помеченным this

static class StringExt{  public static Println(this string s) => System.Console.WriteLine(s)      public static Double(this string s) => s + s}

В Kotlin расширямый тип должен находиться слева от метода, который можно разместить в любом месте. При этом расширить тип можно не только методом, но и свойством

fun String.println() = println(this)fun String.double get() = this * 2

Лямбда выражения

В C# для них есть специальный оператор =>

numbers.Any(e => e % 2 == 0);numbers.Any(e =>   {    // объемная логика ...    return calculatedResult;  })

В Kotlin лямбды органично вписываются в Си-подобный синтаксис, кроме того во многих случаях компилятор заинлайнит их вызовы прямо в используемый метод. Это позволяет создавать эффективные и красивые DSL (Gradle + Kotlin например).

numbers.any { it % 2 == 0 }numbers.any {  // объемная логика ...  return calculatedResult}

Условия и шаблоны

У C# есть очень мощный pattern matching c условиями (пример из документации)

static Point Transform(Point point) => point switch{  { X: 0, Y: 0 }                    => new Point(0, 0),  { X: var x, Y: var y } when x < y => new Point(x + y, y),  { X: var x, Y: var y } when x > y => new Point(x - y, y),  { X: var x, Y: var y }            => new Point(2 * x, 2 * y),};

У Kotlin есть аналогичное switch выражение when, которое, несмотря на наличие возможности сопоставления с образцом, не может одновременно содержать деконструкции и охранных условий, но благодаря лаконичному синтаксису можно выкрутиться:

fun transform(p: Point) = when(p) {  Point(0, 0) -> Point(0, 0)  else -> when {    x > y     -> Point(...)    x < y     -> Point(...)    else      -> Point(...)  }}// или такfun transform(p: Point) = when {  p == Point(0, 0) -> Point(0, 0)  p.x < y          -> Point(p.x + y, p.y)  p.x > y          -> Point(p.x - p.y, p.y)  else             -> Point(2 * p.x, 2 * p.y)}

Подводя итоги

Уложить в одной статье все отличия обоих языков практически нереально. Однако кое какие выводы сделать уже можем. Заметно что Kotlin-way скорее в том чтобы минимизировать количество ключевых слов, реализуя весь сахар поверх базового синтаксиса, а C# стремится стать более удобным увеличивая количество доступных выражений на уровне самого языка. У Kotlin преимущество в том что его создатели могли оглядываться на удачные фичи C# и лаконизировать их, а C# выигрывает за счет мощной поддержки в лице Microsoft и лучшего рантайма.

Подробнее..

Null safety of Kotlin. Мысль про киллер фичу

02.06.2021 16:12:10 | Автор: admin

Познакомившись впервые с языком Котлин после продолжительной работы с Java меня воротило от одной мысли, что null-safety может быть полезен и вообще переменная без null - примитив, но я сам этого не осознавал.

Как это проявлялось:

  1. Не удобно работать с переменными и филдами, у которых не может быть null. Ну просто даже не понимал, как что-то не может быть null.

  2. Не удобно работать с nullable типами. Ну блин ?: !!

Вообщем шло время, потихоньку начинал привыкать к тому, что null может не существовать, даже пытался сделать что-то на мой тот взгляд идиоматичное: дефолтные значения в виде объекта.... Вообщем все это меня вгоняло в тоску и я очень хотел опять писать на Java, так как привык жить с null. Прошло время я уже нормально начал жить с котлиновским not nullable, как в друг в один прекрасный день я вернулся в Java... И через время осознал одну вещь:

Когда я работаю с легаси кодом (ну это весь код, который вообще пишется), то я не понимаю, где у может быть null, а где нет, что заставляет меня делать переключение контекста от реализации бизнес логики на выяснение: а может ли тут быть null?

Рассмотрим синтетику: У нас есть сущность номер телефона с филдами: номер, международный код города и префикс (ну +7, +3 и т.д.), написана она до нас, есть мэппинг в базу, вообщем все по канонам кровавого. По бизнесу все 3 поля номера телефона обязаны быть.

Если я в Java, то при работе с этой сущностью у меня есть сколько вариантов как ее использовать:

  1. Использовать как есть, не думая о том, что там с null-ам (продакшен разберется, если что баг пофиксим)

  2. Дойти до базы данных и посмотреть на констрейнт, тем самым убедившись, что null там не может быть и можно спокойно юзать эту сущность без проверок.

  3. Если расставлены аннотации @NotNull, нажать Ctrl+Q, чтобы увидеть описание.

  4. Обработать все поля, предполагая, что везде может быть null.

  5. Написать свой код, потом разобраться с потенциальным null.

  6. Можно еще придумать варианты....

Все, кроме первого варианта, заставляют меня приложить усилия на переключение контекста и отвлечься от основной мысли над которой я работаю, ну да пункт 5 может сохранить мое внимание какое-то время, но потом прийдется потратить еще кучу времени на переключение контекста. Первый в принципе не плох, но потом будет весело разбираться с NPE.

Какие у нас есть варианты в Котлин:

  1. Значение филда может быть null и я могу сразу обработать такой кейс.

  2. Значение не может быть null.

То есть оба варианта не сбивают меня, не заставляют меня переключить свое внимание.

Конечно тут можно возразить, что есть совместимость с Java и там все плохо в этом плане, на что есть такой ответ: совместимость это круто, но как он сделан, это трейд офф, чтобы не плодить море конструкций вида !! ?:

Ну и еще раз про само свойство null safety: это не совсем техническая штука, скорее это про бизнес, если у меня в бизнесе какое-либо значение не может быть null, так пусть оно упадет на ранних этапах, чем с каким-то null жить.

Подробнее..
Категории: Kotlin , Java , Null

Распознавание команд

03.06.2021 20:19:56 | Автор: admin

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

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

/** Правило проверяет лексему на соответствие */typealias Rule = (String) -> Boolean/** Нормализованное семантическое представление */open class Semnorm(vararg val rules: Rule)/** Правило задает стемы для семантических представлений */fun stem(vararg stems: String): Rule = { stems.any(it::startsWith) }/** Правило задает точные соответствия для семантических представлений */fun word(vararg words: String): Rule = { words.any(it::equals) }/** Проверяем слово на соответствие семантике */fun String.matches(norm: Semnorm) = norm.rules.any { it(this) }

Теперь у нас появилась возможность задавать предопределенные нормализованные семантические представления в виде объектов:

object Day : Semnorm(stem("day", "суток", "сутк", "дня", "ден", "дне"))

Фреймворк ставит их в соответствие лексемам входящих фраз, и предложение начинает выглядеть, например так:

assertThat(  "забань васю на 5 минут".tokenize(),   equalTo(   listOf(     Token("забань", Ban),      Token("васю", null),     Token("на", null),      Token("5", Number),     Token("минут", Minute)   )  ))

С распознаванием речи мы разобрались. Код токенизатора приложен в репозитории, доступном в конце статьи. Перейдем к исполнению команд из речи. А вот здесь и начинается самое интересное: фреймворк позволяет для каждого семантического представления навесить заданное поведение. Снова простейший пример, как распознать запрос справки на двух языках:

object Help : ExecutableSemnorm(stem(  "помощ", "справк", "правил", "help",   "rule", "faq", "start", "старт",)) {  override fun execute(bot: Botm: Message) {    val faq = message.from.relatedFaq()    bot.sendMessage(m.chat.id, faq)  }}

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

object Ban : DurableSemonrm(stem(  "ban", "block", "mute", "бан", "блок",  "забан", "завали", "замьют",)) {  override fun execute(    bot: Bot, attackerMessage: Message, duration: Duration) {    val victimMessage = attackerMessage.replyToMessage    val victimId = victimMessage.from.id    val untilSecond = now().epochSecond + duration.inWholeSeconds    bot.restrictChatMember(      attackerMessage.chat.id, victimId, untilSecond)  }}

Откуда это семантическое представление знает о своей продолжительности? Дело в том, что ему вовсе не обязательно парсить всю цепочку токенов целиком. Достаточно задать только минимально необходимое поведение для каждого представления, например для времени:

object Week : Semnorm(stem("week", "недел")) {  override fun toDuration(number: Long) =     days(number) * 7}

Или для любых команд, зависящих от времени:

class DurableSemnorm(vararg rules: Rule) : ExecutableSemnorm(*rules) {  final override fun execute(    token: Iterator<Token>, bot: Bot, m: Message) =       execute(bot, message, token.parseDuration())  abstract fun execute(bot: Bot, m: Message, duration: Duration)}

Благодаря такой архитектуре, нам больше не приходится думать о запутанной логике работы интерпретатора. Достаточно просто определить желаемые атрибуты для семантических представлений и наслаждаться результатом. Пример бота, использующего эту концепцию, можно посмотреть на Github: https://github.com/demidko/timecobot

Подробнее..

Перевод Миграция с LiveData на Kotlins Flow

08.06.2021 16:18:29 | Автор: admin

LiveData была нужна нам еще в 2017 году. Паттерн наблюдателя облегчил нам жизнь, но такие опции, как RxJava, в то время были слишком сложными для новичков. Команда Architecture Components создала LiveData: очень авторитетный класс наблюдаемых хранилищ данных, разработанный для Android. Он был простым, чтобы облегчить начало работы, а для более сложных случаев реактивных потоков рекомендовалось использовать RxJava, используя преимущества интеграции между ними.

DeadData?

LiveData по-прежнему остается нашим решением для Java-разработчиков, новичков и простых ситуаций. В остальном, хорошим вариантом является переход на Kotlin Flows. Flows (потоки) все еще имеют крутую кривую обучения, но они являются частью языка Kotlin, поддерживаемого Jetbrains; кроме того, на подходе Compose, который хорошо сочетается с реактивной моделью.

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

В этом посте вы узнаете, как отобразить Flows в представление, как собирать их, и как точно настроить под конкретные нужды.

Flow: простые вещи труднее, а сложные легче

LiveData выполняла одну вещь и делала это хорошо: она показывала данные, одновременно кэшируя последние значения и учитывая жизненные циклы Android. Позже мы узнали, что она также может запускать корутины и создавать сложные преобразования, но это требовало немного больше усилий.

Давайте рассмотрим некоторые паттерны LiveData и их эквиваленты Flow:

#1: Показ результата однократной операции с модифицированным держателем данных

Это классический паттерн, в котором вы мутируете держатель состояния с результатом выполнения корутины:

Показ результата однократной операции с модифицированным (Mutable) держателем данных (LiveData)Показ результата однократной операции с модифицированным (Mutable) держателем данных (LiveData)
<!-- Copyright 2020 Google LLC.   SPDX-License-Identifier: Apache-2.0 -->class MyViewModel {    private val _myUiState = MutableLiveData<Result<UiState>>(Result.Loading)    val myUiState: LiveData<Result<UiState>> = _myUiState    // Load data from a suspend fun and mutate state    init {        viewModelScope.launch {             val result = ...            _myUiState.value = result        }    }}

Чтобы сделать то же самое с потоками, мы используем модифицированный StateFlow:

Показ результата однократной операции с модифицированным держателем данных (StateFlow)Показ результата однократной операции с модифицированным держателем данных (StateFlow)
class MyViewModel {    private val _myUiState = MutableStateFlow<Result<UiState>>(Result.Loading)    val myUiState: StateFlow<Result<UiState>> = _myUiState    // Load data from a suspend fun and mutate state    init {        viewModelScope.launch {             val result = ...            _myUiState.value = result        }    }}

StateFlow это особый вид SharedFlow (который является особым типом Flow), наиболее близкий к LiveData:

  • У него всегда есть значение.

  • У него только одно значение.

  • Он поддерживает несколько наблюдателей (поэтому поток является общим).

  • Он всегда воспроизводит последнее значение при подписке, независимо от количества активных наблюдателей.

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

#2: Показ результата однократной операции

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

В LiveData мы использовали для этого конструктор корутин liveData:

Показ результата однократной операции (LiveData)Показ результата однократной операции (LiveData)
class MyViewModel(...) : ViewModel() {    val result: LiveData<Result<UiState>> = liveData {        emit(Result.Loading)        emit(repository.fetchItem())    }}

Поскольку держатели состояния всегда имеют значение, хорошей идеей будет обернуть наше UI-состояние в какой-нибудь класс Result, который поддерживает такие состояния, как Loading, Success и Error.

Эквивалент Flow немного сложнее, потому что вам придется выполнить некоторую настройку:

Показ результата однократной операции (StateFlow)Показ результата однократной операции (StateFlow)
class MyViewModel(...) : ViewModel() {    val result: StateFlow<Result<UiState>> = flow {        emit(repository.fetchItem())    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000), // Or Lazily because it's a one-shot        initialValue = Result.Loading    )}

stateIn это оператор Flow, который преобразует его в StateFlow. Давайте пока доверимся этим параметрам, так как позже нам понадобится более сложная информация для правильного объяснения.

#3: Однократная загрузка данных с параметрами

Допустим, вы хотите загрузить некоторые данные, которые зависят от ID пользователя, и вы получаете эту информацию от AuthManager, который показывает Flow:

Однократная загрузка данных с параметрами (LiveData)Однократная загрузка данных с параметрами (LiveData)

С помощью LiveData можно сделать примерно следующее:

class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: LiveData<String?> =         authManager.observeUser().map { user -> user.id }.asLiveData()    val result: LiveData<Result<Item>> = userId.switchMap { newUserId ->        liveData { emit(repository.fetchItem(newUserId)) }    }}

switchMap это преобразование, тело которого выполняется, а результат подписывается при изменении userId.

Если нет причин для того, чтобы userId был LiveData, лучшей альтернативой этому является объединение потоков с Flow и окончательное преобразование полученного результата в LiveData.

class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }    val result: LiveData<Result<Item>> = userId.mapLatest { newUserId ->       repository.fetchItem(newUserId)    }.asLiveData()}

Выполнение этого действия с помощью Flows выглядит очень похоже:

Однократная загрузка данных с параметрами (StateFlow)Однократная загрузка данных с параметрами (StateFlow)
class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<UserId> = authManager.observeUser().map { user -> user.id }    val result: StateFlow<Result<Item>> = userId.mapLatest { newUserId ->        repository.fetchItem(newUserId)    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.Loading    )}

Обратите внимание, что если вам нужна большая гибкость, вы также можете использовать transformLatest и emit элементы в явном виде:

val result = userId.transformLatest { newUserId ->        emit(Result.LoadingData)        emit(repository.fetchItem(newUserId))    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.LoadingUser // Note the different Loading states    )

#4: Наблюдение за потоком данных с параметрами

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

Продолжая наш пример: вместо вызова fetchItem на источнике данных мы используем гипотетический оператор observeItem, который возвращает Flow.

С помощью LiveData вы можете преобразовать поток в LiveData и все обновления emitSource:

Наблюдение за потоком с параметрами (LiveData)Наблюдение за потоком с параметрами (LiveData)
class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: LiveData<String?> =         authManager.observeUser().map { user -> user.id }.asLiveData()    val result = userId.switchMap { newUserId ->        repository.observeItem(newUserId).asLiveData()    }}

Или, лучше всего, объединить оба потока с помощью flatMapLatest и преобразовать только выход в LiveData:

class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<String?> =         authManager.observeUser().map { user -> user?.id }    val result: LiveData<Result<Item>> = userId.flatMapLatest { newUserId ->        repository.observeItem(newUserId)    }.asLiveData()}

Имплементация Flow похожа, но в ней нет преобразований LiveData:

Наблюдение за потоком с параметрами (StateFlow)Наблюдение за потоком с параметрами (StateFlow)
class MyViewModel(authManager..., repository...) : ViewModel() {    private val userId: Flow<String?> =         authManager.observeUser().map { user -> user?.id }    val result: StateFlow<Result<Item>> = userId.flatMapLatest { newUserId ->        repository.observeItem(newUserId)    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.LoadingUser    )}

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

#5 Объединение нескольких источников: MediatorLiveData -> Flow.combine

MediatorLiveData позволяет вам наблюдать за одним или несколькими источниками обновлений (наблюдаемыми LiveData) и что-то делать, когда они получают новые данные. Обычно вы обновляете значение MediatorLiveData:

val liveData1: LiveData<Int> = ...val liveData2: LiveData<Int> = ...val result = MediatorLiveData<Int>()result.addSource(liveData1) { value ->    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))}result.addSource(liveData2) { value ->    result.setValue(liveData1.value ?: 0 + (liveData2.value ?: 0))}

Эквивалент Flow намного проще:

val flow1: Flow<Int> = ...val flow2: Flow<Int> = ...val result = combine(flow1, flow2) { a, b -> a + b }

Можно также использовать функцию combineTransform или zip.

Настройка открытого StateFlow (оператор stateIn)

Ранее мы использовали stateIn для преобразования регулярного потока в StateFlow, но это требует некоторой настройки. Если вы не хотите вдаваться в подробности прямо сейчас и вам просто нужно копировать-вставлять, я рекомендую использовать эту комбинацию:

val result: StateFlow<Result<UiState>> = someFlow    .stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.Loading    )

Однако если вы не уверены в этом, казалось бы, случайном 5-секундном параметре started, читайте дальше.

StateIn имеет 3 параметра (из документации):

@param scope the coroutine scope in which sharing is started.@param started the strategy that controls when sharing is started and stopped.@param initialValue the initial value of the state flow.This value is also used when the state flow is reset using the [SharingStarted.WhileSubscribed] strategy with the `replayExpirationMillis` parameter.

started может принимать 3 значения:

  • Lazily: начать, когда появится первый подписчик, и остановить, когда scope будет отменен.

  • Eagerly: начать немедленно и остановить, когда scope будет отменен.

  • WhileSubscribed: Это сложно.

Для одноразовых операций вы можете использовать Lazily или Eagerly. Однако, если вы наблюдаете за другими потоками, вам следует использовать WhileSubscribed для выполнения небольших, но важных оптимизаций, как описано ниже.

Стратегия WhileSubscribed

WhileSubscribed отменяет восходящий поток, когда нет коллекторов. StateFlow, созданный с помощью stateIn, передает данные в View, но он также наблюдает за потоками, поступающими из других слоев или приложения (восходящий поток). Поддержание этих потоков активными может привести к напрасной трате ресурсов, например, если они продолжают считывать данные из других источников, таких как подключение к базе данных, аппаратные датчики и т.д. Когда ваше приложение переходит в фоновый режим, будет хорошо, если вы остановите эти корутины.

WhileSubscribed принимает два параметра:

public fun WhileSubscribed(    stopTimeoutMillis: Long = 0,    replayExpirationMillis: Long = Long.MAX_VALUE)

Таймаут остановки

Из документации:

stopTimeoutMillis настраивает задержку (в миллисекундах) между исчезновением последнего абонента и остановкой восходящего потока. По умолчанию она равна нулю (остановка происходит немедленно).

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

Решение в конструкторе корутины liveData заключалось в добавлении задержки в 5 секунд, после которой корутина будет остановлена, если нет подписчиков. WhileSubscribed(5000) делает именно это:

class MyViewModel(...) : ViewModel() {    val result = userId.mapLatest { newUserId ->        repository.observeItem(newUserId)    }.stateIn(        scope = viewModelScope,         started = WhileSubscribed(5000),         initialValue = Result.Loading    )}

Этот подход отвечает всем требованиям:

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

  • Последнее значение все еще будет кэшироваться, чтобы, когда пользователь вернется к нему, представление сразу же получило данные.

  • Подписки перезапускаются, и новые значения будут поступать, обновляя экран, когда они доступны.

Истечение срока воспроизведения

replayExpirationMillis настраивает задержку (в миллисекундах) между завершением работы программы совместного доступа и сбросом кэша воспроизведения (что делает кэш пустым для оператора shareIn и возвращает кэшированное значение к исходному initialValue для stateIn). По умолчанию он равен Long.MAX_VALUE (кэш воспроизведения сохраняется постоянно, буфер никогда не сбрасывается). Используйте нулевое значение для немедленного истечения срока действия кэша.

Наблюдение StateFlow из представления

Как мы уже видели, для представления очень важно сообщить StateFlows во ViewModel, что они больше не прослушиваются. Однако, как и во всем, что связано с жизненными циклами, все не так просто.

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

  • Activity.lifecycleScope.launch: запускает корутину немедленно и отменяет ее при завершении активности.

  • Fragment.lifecycleScope.launch: немедленно запускает корутину и отменяет ее при завершении фрагмента.

LaunchWhenStarted, launchWhenResumed...

Специализированные версии launch, называемые launchWhenX, будут ждать, пока lifecycleOwner находится в состоянии X, и приостановят выполнение корутины, когда lifecycleOwner упадет ниже состояния X. Важно отметить, что они не отменяют выполнение программы до тех пор, пока жизненный цикл не будет закончен.

Сбор потоков с помощью launch/launchWhenX небезопасенСбор потоков с помощью launch/launchWhenX небезопасен

Получение обновлений, когда приложение находится в фоновом режиме, может привести к сбоям, что решается приостановкой сбора в View. Однако восходящие потоки остаются активными, пока приложение находится в фоновом режиме, что может привести к трате ресурсов.

Это означает, что все, что мы делали до сих пор для настройки StateFlow, было бы совершенно бесполезно; однако в нашем распоряжении есть новый API.

lifecycle.repeatOnLifecycle на помощь

Этот новый конструктор корутин (доступный в lifecycle-runtime-ktx 2.4.0-alpha01) делает именно то, что нам нужно: он запускает корутины в определенном состоянии и останавливает их, когда уровень жизненного цикла опускается ниже этого состояния.

Различные методы сбора потокаРазличные методы сбора потока

Например, во Фрагменте:

onCreateView(...) {    viewLifecycleOwner.lifecycleScope.launch {        viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) {            myViewModel.myUiState.collect { ... }        }    }}

Сбор начнется, когда представление фрагмента будет STARTED, продолжится до RESUMED и остановится, когда оно вернется к STOPPED. Читайте об этом в статье Более безопасный способ сбора потоков из пользовательских интерфейсов Android.

Сочетание API repeatOnLifecycle с приведенным выше руководством по StateFlow обеспечит вам наилучшую производительность при рациональном использовании ресурсов устройства.

StateFlow выставляется с помощью WhileSubscribed(5000) и собирается с помощью repeatOnLifecycle(STARTED)StateFlow выставляется с помощью WhileSubscribed(5000) и собирается с помощью repeatOnLifecycle(STARTED)

Предупреждение: Поддержка StateFlow, недавно добавленная в Data Binding, использует launchWhenCreated для сбора обновлений, и она начнет использовать repeatOnLifecycle` вместо этого, когда достигнет стабильности.

Для Data Binding вы должны использовать Flows везде и просто добавить asLiveData(), чтобы отобразить их в представлении. Привязка данных будет обновлена, когда lifecycle-runtime-ktx 2.4.0 станет стабильным.

Резюме

Лучшим способом предоставления данных из ViewModel и сбора их из представления является:

  • Выставить StateFlow, используя стратегию WhileSubscribed, с таймаутом. [пример]

  • Собирать с помощью repeatOnLifecycle. [пример].

Любая другая комбинация будет поддерживать восходящие потоки активными, расходуя ресурсы:


Перевод материала подготовлен в рамках курса "Android Developer. Basic". Если вам интересно узнать о курсе больше, приходите на день открытых дверей онлайн. На нем вы сможете узнать подробнее о программе и формате обучения, познакомиться с преподавателем.

Подробнее..

Разгоняем REACTOR

12.06.2021 18:20:44 | Автор: admin

Кому будет интересно?

Реактор сегодня - это стильно, модно, молодежно. Почему многие из нас практикуют реактивное программирование? Мало кто может ответить однозначно на этот вопрос. Хорошо - если Вы понимаете свой выигрыш, плохо - если реактор навязан организацией как данность. Большинство аргументов "ЗА" - это использование микросервисной архитектуры, которая в свою очередь обязывает микросервисы часто и много коммуницировать между собой. Для коммуникации в большинстве случаев выбирают HTTP взаимодействие. Для HTTP нужен легковесный веб-сервер, а что первое приходит на ум? Tomcat. Тут появляются проблемы с лимитом на максимальное количество сессий, при превышении которого веб-сервер начинает реджектить запросы (хотя лимита этого не так уж и легко достичь). Здесь на подмогу приходит реактор, который подобными лимитами не ограничен, и, например, Netty в качестве веб-сервера, который работает с реактивностью из коробки. Раз есть реактивный веб-сервер, нужен реактивный веб-клиент (Spring WebClient или Reactive Feign), а раз клиент реактивный, то вся эта жуть просачивается в бизнес логику, Mono и Flux становятся Вашими лучшими друзьями (хотя по началу есть только ненависть :))

Среди бизнес задач, очень часто встречаются серьезные процедуры, которые обрабатывают большие массивы данных, и нам приходится применять реактор и для них. Тут начинаются сюрпризы, если реактор не уметь готовить, можно и проблем схлопотать очень много. Превышение лимита файловых дескрипторов на сервере, OutOfMemory из-за неконтролируемой скорости работы неблокирующего кода и многое многое другое, о чем мы сегодня поговорим. Мы с коллегами испытали очень много трудностей из-за проблем с пониманием как держать реактор под контролем, но всё что нас не убивает - делает нас умнее!

Блокирующий и неблокирующий код

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

Лидер здесь - HTTP взаимодействие, вариантов масса, выбирай любой. Я предпочитаю Reactive Feign от Playtika, в комбинации со Spring Boot + WebFlux + Eureka мы получаем очень годную сборку для микросервисной архитектуры.

Давайте по-простому: НЕблокирующий код, это обычно всё, в названии чего есть reactive, а блокирующий - все оставшееся :) Hibernate + PostgreSQL - блокирующий, отправить почту через JavaMail - блокирующий, скинуть сообщение в очередь IBMMQ - блокирующий. Но есть, например, реактивный драйвер для MongoDB - неблокирующий. Отличительной особенностью блокирующего кода, является то, что глубоко внутри произойдет вызов метода, который заставит Ваш поток ждать (Thread.sleep() / Socket.read() и многие подобные), что для реактора - как нож в спину. Что же делать? Большинство бизнес логики завязано на базу данных, без нее никуда. На самом деле достаточно знать и уметь делать 2 вещи:

  • Необходимо понимать где блокирующий код. В этом может помочь проект BlockHound или его аналоги (тут тема для отдельной статьи)

  • Исполнение блокирующего кода необходимо переключать на пулы, готовые его выполнять, например: Schedulers.boundedElastic(). Делается это при помощи операторов publishOn & subscribeOn

Разгоняемся сами

Перед тем, как продолжить, необходимо немного размяться!

Уровень 1

    @Test    fun testLevel1() {        val result = Mono.just("")            .map { "123" }            .block()        assertEquals("123", result)    }

Начнем с простого, такой код обычно пишут начинающие reactor программисты. Как начать цепочку? Mono.just и ты на коне :) Оператор map трансформирует пустую строку в "123" и оператор block делает subscribe.

Обращаю особенное внимание на оператор block, не поддавайтесь соблазну использовать его в Вашем коде, исключение составляют тесты, где это очень удобно. При вызове block внутри метода Вашего RestController, Вы сразу получите исключение в рантайме.

Уровень 2

    fun nonBlockingMethod1sec(data: String)     = data.toMono().delayElement(Duration.ofMillis(1000))    @Test    fun testLevel2() {        val result = nonBlockingMethod1sec("Hello world")            .flatMap { nonBlockingMethod1sec(it) }            .block()        assertEquals("Hello world", result)    }

Усложняем наш код, добавляем неблокирующий метод nonBlockingMethod1sec, все что он делает - ожидает одну секунду. Все что делает данный код - дважды, по очереди, запускает неблокирующий метод.

Уровень 3

    fun collectTasks() = (0..99)    @Test    fun testLevel3() {        val result = nonBlockingMethod1sec("Hello world")            .flatMap { businessContext ->                collectTasks()                    .toFlux()                    .map {                        businessContext + it                    }                    .collectList()            }            .block()!!        assertEquals(collectTasks().toList().size, result.size)    }

Начинаем добавлять самое интересное - Flux! У нас появляется метод collectTasks, который собирает массив из сотни чисел, и далее мы делаем из него Flux - это будет наш список задач. К каждой задаче мы применяем трансформацию через оператор map. Оператор collectList собирает все результаты в итоговый список для дальнейшего использования.

Здесь наш код начинает превращаться в рабочий паттерн, который можно использовать для массового выполнения задач. Сначала мы собираем некий "бизнес контекст", который мы используем в дальнейшем для выполнения задач.

Уровень 4

    fun collectTasks() = (0..100)        @Test    fun testLevel4() {        val result = nonBlockingMethod1sec("Hello world")            .flatMap { businessContext ->                collectTasks().toFlux()                    .flatMap {                        Mono.deferContextual { reactiveContext ->                            val hash = businessContext + it + reactiveContext["requestId"]                            hash.toMono()                        }                    }.collectList()            }            .contextWrite { it.put("requestId", UUID.randomUUID().toString()) }            .block()!!        assertEquals(collectTasks().toList().size, result.size)    }

Добавляем немного плюшек. Появилась запись данных (15) в реактивный контекст, а также чтение (10) из него. Мы почти у цели. Постепенно переходим к итоговому варианту

Уровень 5

    fun collectTasks() = (0..1000)        fun doSomethingNonBlocking(data: String)        = data.toMono().delayElement(Duration.ofMillis(1000))        fun doSomethingBlocking(data: String): String {        Thread.sleep(1000); return data    }    val pool = Schedulers.newBoundedElastic(10, Int.MAX_VALUE, "test-pool")    private val logger = getLogger()    @Test    fun testLevel5() {        val counter = AtomicInteger(0)        val result = nonBlockingMethod1sec("Hello world")            .flatMap { _ ->                collectTasks().toFlux()                    .parallel()                    .runOn(pool)                    .flatMap {                        Mono.deferContextual { _ ->                            doSomethingNonBlocking(it.toString())                                .doOnRequest { logger.info("Added task in pool ${counter.incrementAndGet()}") }                                .doOnNext { logger.info("Non blocking code finished ${counter.get()}") }                                .map { doSomethingBlocking(it) }                                .doOnNext { logger.info("Removed task from pool ${counter.decrementAndGet()}") }                        }                    }.sequential()                    .collectList()            }            .block()!!        assertEquals(collectTasks().toList().size, result.size)    }

Вот мы и добрались до итогового варианта! Часть с реактивным контекстом была опущена для более наглядной демонстрации того, зачем мы здесь собрались. У нас появились два новых метода: doSomethingNonBlocking (3) & doSomethingBlocking (6) - один с неблокирующим ожиданием в секунду, второй с блокирующим. Мы создали пул потоков для обработки задач (10), добавили счетчик активных задач в реакторе (15). У нас появился оператор parallel (19) и обратный ему sequential (29). Задачи мы назначили на свежесозданный пул (20). Для понимания, что же происходит внутри, добавили логирование внутри операторов doOnRequest (вызывается перед исполнением метода), doOnNext (вызывается после исполнения метода). Основная задумка - на примере, определить сколько задач одновременно выполняется в реакторе и за какое время цепочка завершит свою работу.

Такой "паттерн", мы с коллегами очень часто применяем для выполнения сложных задач, таких как отправка отчетов или массовая обработка транзакций. Первым делом собирается бизнес контекст - это некая структура, содержащая в себе информацию, полученную в результате вызовов других микросервисов. Бизнес контекст необходим нам для выполнения самих задач, и собирается он заранее, чтобы не тратить время в процессе обработки. Далее мы собираем список задач, превращаем их во Flux и скармливаем реактору на параллельную обработку.

И вот здесь начинается самое интересное. Попробуйте ответить на несколько вопросов. Как Вы считаете, сколько времени будет выполнятся данная цепочка? В ней 100 задач, в каждой задаче неблокирующее ожидание в 1 секунду, блокирующее ожидание в 1 секунду, и у нас в наличии пул из 10 потоков? (Вполне годная задачка на собеседование senior reactor developer :))

Правильный ответ

Около 12 секунд. Рассуждаем от блокирующего :) Блокирующее ожидание никуда не деть, и тут имеем 100 блокирующих секунд на 10 потоков, итого 10 секунд. Неблокирующее ожидание заметно нам лишь в первый раз, далее оно незаметно запускается в передышках между блокирующим. Не забываем про одну секунду сбора "бизнес контекста" перед запуском задач.

А теперь уберем строку (26) .map { doSomethingBlocking(it) } . Освободим наш реактор от блокирующего кода, интересно, сколько теперь времени займет выполнение цепочки?

Правильный ответ

2 секунды! 1 на сбор "бизнес контекста" и 1 на выполнение всех задач. Реактор запустит 100 задач одновременно. Но ведь у нас пул из 10 потоков? Как так? Первый разрыв шаблона.

Мы идем до конца и увеличиваем количество задач в методе collectTasks() до ... 1000? а может быть сразу до 15000? Как долго реактор будет выполнять столько задач?

Правильный ответ

2 секунды! 1 на сбор "бизнес контекста" и 1 на выполнение всех задач. Реактор запустит ВСЕ задачи одновременно. Второй разрыв шаблона. Где предел?

А это вообще легально?

Как же так и как это контролировать? Почему это опасно? Что если внутри параллельной обработки Вы решите вызвать другой микросервис? Если у вас 30000 задач, и по завершению каждой, Вам нужно отправлять запрос соседнему микросервису, Вы с удивлением можете обнаружить, что реактор непременно постарается выполнить все вызовы одновременно (Вы ведь используете реактивный web-client или реактивный feign, верно?) Открытие такого большого количества сокетов повлечет за собой превышение лимита открытых файловых дескрипторов в системе, что как минимум создаст проблемы с невозможностью создания новых сокетов в системе и помешает другим сервисам, а как максимум повалит Вам на сервере SSH и Вы потеряете доступ к серверу. Сомневаюсь, что в этот момент, программист будет кричать "зато смотри как быстро работает".

Разрыв шаблона. Thread Pool & Reactor

Основная проблема начинающего реактор программиста - это образ мышления, если есть медленный процесс - добавь X потоков, будет быстрее в X раз, а если слишком быстро - сократи количество потоков. Как всё просто было раньше? :) С реактором это не работает.

Классический thread pool - двери. Больше дверей - больше пропускная способность, все работает быстрее.

Теперь встречайте reactor! Вы видите двери? Нет никаких дверей

Реактор это большой мешок с подарками, или воздушная труба, задачи в которую валятся и летают там пока не выполнятся. А кто эти люди в желтом? Это наши epoll реактивные потоки, которые ни в коем случае нельзя нагружать блокирующими задачами. Можно провести аналогию с прорабами или инженерами. Они здесь, чтобы управлять процессом, а не чтобы выполнять тяжелую работу. Займите одного инженера тяжелой задачей, и когда к нему придет следующий рабочий с вопросом "что делать дальше?", он не сможет ответить, потому что был занят. Вот так и появляются таймауты в реактивном коде. Казалось бы микросервис стоит без нагрузки, выполняет какие-то задачки, а один из 500 запросов к нему падает с тайм-аутом, и непонятно почему. Велика вероятность что инженер был занят блокирующей задачей! Заботьтесь о своих инженерах и поручайте тяжелую работу специально обученным рабочим, например, Schedulers.boundedElastic().

Как контролировать эту "трубу", в которую валится всё без контроля? Вот мы и подошли к кульминации

Конфигурируем реактор!

В своей дефолтной конфигурации, параллельная обработка в реакторе зависит от количества ядер процессора сервера, на котором запускается код, поэтому, к своему удивлению, Вы получите разные результаты, проверяя работу реактора в тесте на локальной машине с 4-8 ядрами и production сервере с 32 ядрами.

Парад настроек открывает parallel с его аргументом parallelism

Меняя parallelism, мы можем регулировать количество запускаемых rails (это местное понятие реактора, которое похоже на корутины, но по сути является количеством одновременно выполняемых неблокирующих задач). Prefetch мы рассмотрим более подробно в следующем разделе.

Но одного parallelism недостаточно, реактор все еще будет нагребать задач как не в себя.

Мало кто обращал внимание что у оператора flatMap (только того что запускается на Flux) есть перегрузки с интересными аргументами, а именно maxConcurrency

maxConcurrency очень важен, по дефолту значение стоит Integer.MAX_VALUE (определяет сколько неблокирующих задач может выполняться одновременно на одной рельсе. Понимаете теперь откуда аппетит у реактора?

Также, не стоит забывать, что если цепочка будет запущена несколько раз (вызов одного http метода контроллера несколько раз), то все помножится! Никакой пул не спасет.

Количество запусков цепочки напрямую влияет на количество одновременно выполняемых задач.

Подведем небольшой итог:

  • parallel (parallelism)

  • flatMap (maxConcurrency)

  • Количество запусков цепочки

Эти три параметра являются множителями, для расчета количества одновременных задач.

По дефолту это Кол-во ядер * Integer.MAX_VALUE * Количество запусков цепочки

Напротив же, запустив данный код для 5 задач длительностью в секунду мы получим цепочку работающую 5 секунд. Теперь всё под контролем!

        val result = nonBlockingMethod1sec("Hello world")            .flatMap { _ ->                collectTasks().toFlux()                    .parallel(1)                    .runOn(pool, 1)                    .flatMap({                        Mono.deferContextual { _ ->                            doSomethingNonBlocking(it.toString())                        }                    }, false, 1, 1)                    .sequential()                    .collectList()            }            .block()!!

Стоп, или не всё?

Thread Pool

Зачем же нужен пул потоков в реакторе? Думайте о нем как о двигателе для Вашего автомобиля. Чем пул мощнее - тем блокирующие задачи будут разбираться быстрее, а если потоков мало, то и блокирующие задачи задержатся у вас надолго! А куда же мы без блокирующих вызовов? На количество одновременно выполняемых задач в реакторе он не влияет, вот это поворот :)

Надеюсь, Вы не пробовали использовать Schedulers.parallel() для исполнения Вашего блокирующего кода? =) Несмотря на свое подходящее название (ну называется он parallel, значит и нужен для параллельной обработки) использовать этот пул можно только для неблокирующего кода, в доке указано что он живет с одним воркером, и содержит в себе только особенные, реактивные потоки.

Распределение задач по рельсам

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

Зеленые прямоугольники это наши задачи, которые распределяются в реакторе по алгоритму round-robin, что в случае с синтетическими данными дает красивую картинку.

Хорошо загруженный реактор (задачи равномерно распределены). 54 блокирующих задачи (каждая по 1сек), round-robin распределение по 6 рельсамХорошо загруженный реактор (задачи равномерно распределены). 54 блокирующих задачи (каждая по 1сек), round-robin распределение по 6 рельсам

Но запуская код в production среде, мы можем встретиться с долгим запросом в базу, сетевыми задержками, плохим настроением микросервиса да и чего только не бывает.

Плохо загруженный пул (задачи распределены не равномерно)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсамПлохо загруженный пул (задачи распределены не равномерно)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсам

Оператор collectList() вернет нам результат только после завершения последней задачи, и как мы видим, наш пул будет простаивать пока 1 поток трудится разгребая очередь накопившихся задач. Это создает неприятные задержки, когда Вы знаете что можно быстрее, но быстрее не происходит.

Бороться с этим можно несколькими способами

  • concatMap вместо flatMap (посмотрите в профилировщик на ваш пул, передумаете)

  • правильно планировать задачи, чтобы исключить аномалии (почти невозможно)

  • дробить каждую задачу на много мелких, и также запускать их в параллельную обработку чтобы нивелировать проблемы с распределением (вполне рабочий вариант)

  • prefetch (наш выбор!)

Параметр prefetch у flatMap & runOn позволяет определить, сколько задач будет взято на одну рельсу на старте, а затем при достижении некоторого порога выполнения задач, реквесты будут повторяться с этим количеством. Значение по умолчанию - 256. Сменив значение на 1, можно заставить реактор использовать механизм "work stealing", при котором, рельсы и потоки, которые освободились, будут забирать задачи себе на выполнение и картина получится гораздо более приятная.

Хорошо загруженный пул (задачи равномерно распределены)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсамPrefetch !Хорошо загруженный пул (задачи равномерно распределены)54 блокирующих задачи (каждая по 1сек кроме 2ух), round-robin распределение по 6 рельсамPrefetch !

На этом у меня всё. Будет интересно прочесть Ваши замечания и комментарии, на 100% истину не претендую, но все результаты подкреплены практическими примерами, на Spring Boot + Project Reactor 3.4. Всем спасибо!

Подробнее..
Категории: Kotlin , Java , Concurrency , Parallel , Reactor , Parallelism , Mono , Threads , Flux , Pool , Prefetch

Мультивселенная и задачи о переправе

16.06.2021 04:11:19 | Автор: admin

Как-то прочел на Хабре статью Перевозим волка, козу и капусту через реку с эффектами на Haskell, которая так понравилась, что решил написать фреймворк для всего класса задач о переправах, используя мультипарадигменное проектирование. Наконец удалось найти время, и вот, спустя почти год, фреймворк готов. Теперь персонажи, их взаимодействия и описание искомого результата задаются через domain-specific language, который позволяет решать любые головоломки подобного рода с пошаговым выводом. Ниже приводится поэтапный разбор реализации DSL. Статья подойдет тем кто изучает язык Kotlin или просто интересуется примерами его использования. Некоторые малозначимые детали (вроде импортов и вывода) для кратости опущены.

Персонажа легко можно описать открытым для наследования классом:

open class Person(private val name: String)

Также просто определим понятие берега, как набора персонажей задачи:

typealias Riverside = Set<Person>

Дальше построим лодку. Лодка будет знать о населенности обоих берегов, но находится в состоянии квантовой неопределенности между ними, с возможностью инвертировать свое положение:

abstract class QuantumBoat(  val left: Riverside, val right: Riverside) {    abstract fun invert(): List<QuantumBoat>    fun where(    condition: Riverside.() -> Boolean,     selector: QuantumBoat.() -> Boolean  ) = Multiverse(this, condition).search(selector)}

Лодка также снабжена высокоуровневым методом where, для поиска необходимого состояния через N шагов по реке. Условие (condition) определяет валидность берегов в процессе, а селектор (selector) задает искомое конечное состояние. Обратите внимание, что при использовании этого метода лодка на самом деле не двигается с места, а перебирает альтернативные вселенные, пока не обнаружит подоходящую :)
Но об этом мы поговорим позже, а пока что перейдем к простой имплементации лодки для перемещения слева направо:

class LeftBoat(left: Riverside, right: Riverside) : QuantumBoat(left, right) {  override fun invert() =    left.map {      RightBoat(left - it - Farmer, right + it + Farmer)    } + RightBoat(left - Farmer, right + Farmer)}

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

Лодку мы реализовали, персонажей тоже, теперь опишем то ради чего все затевалось, а именно, код для записи этапов перемещения. Поскольку мы планируем добавлять данные в конец, опишем их как псевдоним связанного списка состояний лодки:

typealias History = LinkedList<QuantumBoat>  fun Sequence<History>.fork() = sequence {  for (history in this@fork) {    for (forked in history.last.invert()) {      yield((history.clone() as History).apply {        add(forked)      })    }  }}

Заодно описали функцию форка мультиверсума (историй перемещений) в следующий набор состояний (шаг). Чтобы все это добро не забивало лишний раз память, используем ленивые последовательности и yield.

Теперь нам осталось всего лишь описать мультиверсум (а код для поиска состояний у нас уже есть):

/** * Мультиверсум для лодки * @param boat исходное состояние лодки * @param condition валидатор промежуточных состояний */class Multiverse(boat: QuantumBoat, val condition: Riverside.() -> Boolean) {  /**   * Все смоделированные истории передвижений лодки   */  private var multiverse = sequenceOf(historyOf(boat))  /**   * Найти историю подходящей нам лодки   * @param selector нужное состояние берегов и лодки   * @return все найденные варианты достижения состояния   */  tailrec fun search(selector: QuantumBoat.() -> Boolean): List<History> {    multiverse = multiverse.fork().distinct().filter {      it.last.left.condition()        && it.last.right.condition()    }    val results = multiverse.filter { it.last.selector() }.toList()    return when {      results.isNotEmpty() -> results      else -> search(selector)    }  }}

Здесь мы заиспользовали оптимизацию хвостовой рекурсии, благодаря чему kotlinc сгенерирует импертивный цикл для повышения производительности. Что здесь происходит: на каждом шаге мы делаем форк всех состояний мультиверсума перемещая все возможные объекты на другой берег в параллельных вселенных. Затем отбрасываем дубликаты и невалидные состояния (коза и капуста например), а оставшиеся последовательности и будут ответами к задаче. Вуаля!

Наконец, пример использования DSL на всем известной задачке про волка, козу и капусту:

object Wolf : Person("")object Goat : Person("")object Cabbage : Person("")fun Riverside.rule() =  contains(Farmer) ||    (!contains(Wolf) || !contains(Goat)) &&    (!contains(Goat) || !contains(Cabbage))fun main() {  val property = setOf(Wolf, Goat, Cabbage)  // стартовали с левого берега  LeftBoat(property)     // отбросили все невалидные состояния    .where(Riverside::rule)    // выбрали из оставшихся те варианты,    // где все имущество оказалось на правом берегу    { right.containsAll(property) }     // выводим на экран пошаговое решение    .forEach(History::prettyPrint)}

Вот что получилось, вставляю скриншотом, потому что смайлики хабр не переваривает:

Всем удачного дня и побольше времени на написание собственных DSL :)

Исходный код здесь: demidko/Wolf-Goat-Cabbage
Приветствуется критика и предложения как сделать лучше.

Подробнее..

JetBrains Academy платформенные обновления, любимые проекты пользователей и годовая подписка

17.06.2021 14:15:17 | Автор: admin

Привет, Хабр!

Cо времен нашего последнего поста в JetBrains Academy изменилось довольно много: мы выпустили важные обновления платформы, опубликовали множество новых проектов и тем, а также ввели годовую подписку! Кроме того, мы проанализировали наши проекты и составили рейтинг лучших из них по мнению наших пользователей. Ниже мы расскажем обо всем этом подробнее.

Если вы еще не зарегистрировались на JetBrains Academy, но уже изучаете или планируете изучать программирование, начните бесплатную пробную неделю прямо сейчас! Выполнив первую стадию проекта в течение недели, вы дополнительно получите месяц бесплатного доступа к JetBrains Academy. А закончив свой первый проект в течение этого времени, вы продлите бесплатный доступ еще на один месяц. Таким образом, помимо пробной недели, у вас есть возможность получить до двух месяцев бесплатного обучения, в течение которых вы сможете создавать реальные приложения, способные усилить ваш GitHub-профиль и резюме.

Платформенные обновления

Обновления в JetBrains Academy происходят практически ежедневно от небольших изменений в интерфейсе для более комфортной и эффективной работы до новой функциональности. Ниже мы описываем наиболее важные изменения с точки зрения пользователя JetBrains Academy.

  • Появилось два подготовительных трека для пользователей, только начинающих свое знакомство с программированием: Python for Beginners и Preparing for the AP Computer Science. Первый поможет сделать первые шаги в изучении Python, второй в изучении Java. После их прохождения вам будет легче перейти на треки Python Developer и Java Developer. Обратите внимание, что трек Preparing for the AP Computer Science находится на ранней стадии разработки и доступен бесплатно.

  • Мы создали два новых трека для более подробного изучения отдельных концепций Java и Python. Java Desktop Application Developer подойдет для совершенствования навыков написания десктопных приложений на Java. Natural Language Processing научит работе с текстовыми данными с помощью Python. Обратите внимание, что треки находятся на ранней стадии разработки и доступны бесплатно.

  • Мы улучшили технологию интервального повторения для эффективного усвоения пройденного материала. Она заключается в повторении учебного материала по определенным постоянно возрастающим интервалам.

  • Для детального изучения своего прогресса в учебе вы теперь можете воспользоваться более полным анализом вашего обучения. Узнайте число дней, в течение которых вы активно учились на платформе, пройденных тем, решенных задач, опубликованных вами комментариев, подсказок, решений задач и полезных ссылок. Посмотреть статистику можно в профиле пользователя.

    Аналитика обучения пользователя JetBrains Academy Аналитика обучения пользователя JetBrains Academy
  • Появились персональные бейджи виртуальные награды за успешное обучение и помощь пользователям. Среди них: Committed Learner, Brilliant Mind, Helping Hand, Sweetheart и многие другие.

    Персональные бейджи пользователя JetBrains AcademyПерсональные бейджи пользователя JetBrains Academy
  • Мы обновили карту знаний. Теперь она поддерживает еще больше уровней абстракции знаний. В отличие от предыдущей версии новая карта предлагает иерархию абстракций на более высоких уровнях, а также детализацию учебных материалов на более низких уровнях. Это даст вам четкое понимание того, на каком этапе обучения вы находитесь и куда двигаться дальше.

    Карта знаний. Раздел Computer science > Programming languages > Java > Code organization > Code styleКарта знаний. Раздел Computer science > Programming languages > Java > Code organization > Code style
  • Для ознакомления с JetBrains Academy теперь предлагается экскурсия, разъясняющая новым пользователями термины и концепции, которые важно знать при обучении на платформе.

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

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

    Возможные действия при работе с комментариями пользователей JetBrains AcademyВозможные действия при работе с комментариями пользователей JetBrains Academy

Любимые проекты пользователей

Со времен нашего прошлого поста мы добавили более 50 новых проектов, 300 тем и получили более 280 тысяч комментариев пользователей! Мы проанализировали любимые проекты наших пользователей и составили из них рейтинг тех, которые оказались наиболее полезными, увлекательными и простыми в понимании среди более чем 110 других проектов.

Java Developer

  1. Проект Simple Banking System. Работая над упрощенной версией банковской системы, вы изучите основы SQL и познакомитесь с алгоритмом Луна, помогающим избежать ошибок при вводе номера банковской карты.

    Что отмечают пользователи: практика ООП и структурирования кода, знакомство с базами данных через JDBC.

  2. Проект Tic-Tac-Toe with AI. Напишите собственную игру в крестики-нолики и сыграйте в нее с компьютером! Работая над этим проектом, вы узнаете о планировании и разработке сложных программ с нуля, научитесь использовать классы, объекты и методы в Java, обрабатывать ошибки и осуществлять ввод данных.

    Что отмечают пользователи: интересное применение рекурсии, углубленное ООП, знакомство с алгоритмом Минимакс.

  3. Проект Maze Runner. Предлагаем вам написать программу по созданию лабиринтов и нахождению выходов из них! Вы научитесь работать с классом Random и многомерными массивами, а также сохранять результат работы программы в файл.

    Что отмечают пользователи: увлекательная разработка алгоритмов и структур данных.

Python Developer

  1. Проект Simple Banking System. Работая над упрощенной версией банковской системы, вы изучите основы SQL и познакомитесь с алгоритмом Луна, помогающим избежать ошибок при вводе номера банковской карты.

    Что отмечают пользователи: практика ООП и структурирования кода, работа с базами данных через SQLite.

  2. Проект Rock-Paper-Scissors.Предлагаем вам создать усовершенствованную версию игры камень-ножницы-бумага камень-ножницы-бумага-ящерица-спок а затем сыграть в нее против компьютера! Это хорошая тренировка навыков написания алгоритмов, работы с массивами, модулем random и форматированием строк.

    Что отмечают пользователи: работа с файлами и коллекциями.

  3. Проект To-Do List.Создайте свой список дел для управления повседневными задачами! Создавая эту программу, вы потренируетесь работать с циклами и условными операторами, а также изучите основы SQLAlchemy.

    Что отмечают пользователи: знакомство с базами данных через SQLAlchemy.

Kotlin Developer

  1. Проект Parking Lot. В рамках этого проекта предлагаем вам создать собственную программу по управлению парковкой, которая будет сообщать о количестве свободных и занятых мест. Вы узнаете и закрепите основы программирования на Kotlin: ввод и вывод консоли, циклы и условные операторы. Также вы научитесь использовать списки и применять к ним различные операции.

    Что отмечают пользователи: практика ООП, особенно классов данных.

  2. Проект Tic-Tac-Toe. Напишите собственную игру в крестики-нолики и сыграйте в нее с компьютером! Работая над этим проектом, вы узнаете о планировании и разработке сложных программ с нуля, научитесь использовать классы, объекты и методы в Java, обрабатывать ошибки и осуществлять ввод данных.

    Что отмечают пользователи: отличная проработка основ Kotlin.

  3. Проект Smart Calculator. Предлагаем вам реализовать свой калькулятор, который не только выполняет стандартные функции сложения, вычитания, умножения и деления, но и запоминает все предыдущие вычисления. Проект дает возможность поработать с базовыми структурами данных, такими как стек и очередь, а также с классом BigInteger.

    Что отмечают пользователи: детальное знакомство со структурами данных и регулярными выражениями.

Годовая подписка

Прошлым летом мы объявили, что ранний доступ к JetBrains Academy завершается, а позже ввели ежемесячную подписку. После этого мы получили большое количество запросов на годовую подписку от пользователей, которые предпочитают учиться в своем темпе, предпочитая длинные дистанции ежемесячным спринтам.

В декабре мы объявили о введении годовой подписки на JetBrains Academy. Ее стоимость равна пяти месяцам ежемесячной подписки! Теперь, в зависимости от целей и сроков обучения, вы можете воспользоваться наиболее удобной для вас формой подписки. Оформить ее можно на странице подписки вашего профиля на JetBrains Academy или на нашем сайте.

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

Мы всегда рады вашим комментариям здесь, в Twitter, Facebook и на Reddit! Они помогают нам развивать функциональность JetBrains Academy и делать проекты более качественными, полезными и увлекательными.

Обращаем ваше внимание, что материалы JetBrains Academy, включая теоретические разделы и описания проектов, сейчас доступны только на английском языке.

Ваша команда JetBrains Academy

Подробнее..

Аналог R.string в android приложении

20.06.2021 12:09:43 | Автор: admin

Всем привет! Меня зовут Владимир, я Android-разработчик в компании Альфа-Капитал. Наверняка любое мобильное приложение в процессе развития нуждается в гибкой настройке текстовой информации за счет серверной части. В этой статье я поделюсь мыслями и решениями нашей команды. Также я покажу пример генерации кода с помощью gradle скрипта, сильно упростивший жизнь android команде.

С чего всё начиналось

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

Сразу оговорюсь: в текущем виде (и в ближайшем будущем) бизнес ориентирован на Россию, поэтому в проекте нет необходимости поддерживать несколько языков в приложении.

Столкнувшись с проблемой обновления информации, мы стали задумываться о хранении текстов (которые в перспективе придется корректировать) не в приложении, а на сервере. Часть (например, большой список дисклеймеров для юристов) удалось вытащить. Но оставалось еще множество строк, которые сложно как-то объединить и для которых не хотелось каждый раз делать сетевой запрос и экран загрузки.

Сначала мы попробовали держать тексты на Firebase. По функциональности такое решение вполне подходило, к тому же оно добавляло версионирование и возможность создания a/b тестов. Вскоре стало ясно, что это все-таки не то, что нам нужно. Тогда мы сформулировали свои требования:

  1. Удобный и единый источник текстов для всех мобильных платформ (android/ios);

  2. Обновление текстов в рантайме при старте приложения (для обновления важных мест без выпуска фиксов/релизов);

  3. В приложении мы не должны страдать от необходимости выполнения сетевого запроса или показа лоадинга ради загрузки лексем;

  4. Обновление текстов должно быть доступно без вмешательства разработчиков (т.е. чтобы условный аналитик / тестировщик смог спокойно обновить тексты при необходимости);

  5. Максимально простое создание дефолтных значений, которые будут лежать в приложении.

Firebase Remote Config не подошел слишком хороший функционал для простых текстов. У нас быстро получился большой список необходимых лексем, а их добавление / редактирование становилось слишком сложным. Нелегкой задачей была и установка дефолтных значений в приложении. Нам хотелось чего-то попроще.

Мы решили, что самым оптимальным будет объединение необходимых текстов в JSON файл. Почему именно JSON, а не XML, который кажется более нативным для Android? Так показалось удобней для обеих команд (Android и iOS). JSON понятный формат данных, его легко разберет любая платформа. Этот файл можно легко скачать, положить в проект и получить дефолтные данные.Схема работает и в обратную сторону. Пришла задача с новым текстом? Нужно добавить новые строки в проект, закинуть этот же JSON c ключами на сервер.

Пример json файла:

{ "screen1_text1": "Text 1", "screen1_text2": "Text 2 \nnext line", "screen1_text3": "Text 3", "screen1_text4": "Text 4"}

Первая реализация

В итоге мы получили JSON файл с текстами на сервере, этот же файл храним в проекте в папке assets. Сначала мы создали объект Lexemator, у которого можно по ключу запросить какой-то текст. При старте приложение подкачивает тексты с сервера в Lexemator, а если что-то пошло не так, берет дефолтные текста из папки assets.

object Lexemator {fun getString(key: String): String}

Использование в коде выглядит следующим образом:

class MainActivity : Activity() {   override fun onCreate(savedInstanceState: Bundle?) { ...       val textView = findViewById<TextView>(R.id.text1)       textView.text = Lexemator.getString("screen1_text1")   }}

По сравнению с Firebase стало лучше, но был один существенный недостаток: достаточно одной ошибки в ключе, и мы получаем не тот текст. Нам хотелось получить статическую поддержку, похожую на R.string, где Android Studio подсказывала бы константы и проект не компилировался бы при ошибке.

Это была предыстория, теперь переходим к коду.

Gradle - наше всё

Раз в проекте имеется JSON файл с текстами, значит, уже на этапе сборки мы понимаем, какие есть ключи для разных текстов. Если каких-то ключей нет, то они либо не нужны, либо их всё равно надо добавить для дефолтных значений. Выходит, на этапе сборки можно сгенерировать код, который будет содержать ключи для текстов. Мы решили сделать это с помощью gradle task.

Ниже представлен получившийся скрипт

import groovy.json.JsonSlurper/*** Таска ищет файл с текстами с названием strings.json и создает объект LL.* Для каждого текста из strings.json создает переменную LL.key внутри объекта** Если файла strings.json не существует - скрипт кинет Exception.** Чтобы сгенерить текста заново, достаточно перебилдить проект, или изменить файл strings.json*/def classFileName = "LL"def stringsFileName = "strings.json"def filePath = project.rootProject.getProjectDir().path + "/app/src/main/assets/json"def outputPath = project.rootProject.getProjectDir().path + "/app/build/generated/strings"def inputFile = new File(filePath + "/${stringsFileName}")def outputFile = new File(outputPath + "/${classFileName}.kt")task createStrings {   /**    * Если что-то изменится в inputFile, то при следующей сборке будет заново сгенерирован    * outputFile.    * Если ничего не изменилось, и outputFile уже есть, таска будет помечена "UP-TO-DATE" и    * не будет выполняться лишний раз.    */   inputs.file(inputFile)   outputs.file(outputFile)   doLast {       if (!inputFile.exists()) {           throw RuntimeException("файл ${inputFile} не найден")       }       println("Начало создания файла ${outputFile.path}")       outputFile.delete()       outputFile.createNewFile()       /**        * Тройные кавычки нужны для того, чтобы перевод строки (\n) в strings.json        * не ломал строки в созданном LL.kt файле.        */       def s1 = """package com.obolonnyy.lexemator//<!--Этот файл создан автоматически gradle скриптом из create_strings.gradle -->object ${classFileName} {"""       def s2 =               """      fun addLexems(map: Map<String, String>) {       map.forEach { k, v -> addLexem(k, v) }   }   fun addLexem(key: String, value: String) {       when(key) {"""       def json = new JsonSlurper().parse(inputFile)       assert json instanceof Map       json.each { entry ->           s1 += "    var ${entry.key} = \"\"\"${entry.value}\"\"\"\n        private set\n"           s2 += "            \"${entry.key}\" -> ${entry.key} = value\n"       }       def result = s1 + "\n\n" + s2 + """        }   }}"""       outputFile.write(result)       println("файл ${outputFile.path} успешно создан.")   }}/*** Показываем, что созданный файл теперь тоже является частью проекта.* Без этого мы не сможем использовать созданный LL.kt класс в своих классах.*/android {   sourceSets {       main {           java {               srcDirs += outputPath           }       }   }}

Скрипт создает object LL, у которого есть список ключей (поля типа String, с приватным сеттером) с дефолтными значениями, и две функции для обновления значения ключей. При старте приложения мы запрашиваем с сервера текста и обновляем значения через функцию addLexems().

Комментарий про название объекта LL: сначала мы думали назвать L (от слова Lexemator), чтобы было привычно как с R, но мешала константа android.icu.lang.UCharacter.GraphemeClusterBreak.L.Поэтому, мы не придумали ничего лучше, чем назвать класс LL.

Сгенерированный объект LL выглядит следующим образом:

//<!--Этот файл создан автоматически gradle скриптом из create_strings.gradle -->object LL {   var screen1_text1 = """Text 1"""       private set   var screen1_text2 = """Text 2next line"""       private set   var screen1_text3 = """Text 3"""       private set   var screen1_text4 = """Text 4"""       private set     fun addLexems(map: Map<String, String>) {       map.forEach { k, v -> addLexem(k, v) }   }   fun addLexem(key: String, value: String) {       when(key) {           "screen1_text1" -> screen1_text1 = value           "screen1_text2" -> screen1_text2 = value           "screen1_text3" -> screen1_text3 = value           "screen1_text4" -> screen1_text4 = value       }   }}

Пример использования объекта LL в коде выглядит следующим образом:

class MainActivity : Activity() {   override fun onCreate(savedInstanceState: Bundle?) {...       val textView = findViewById<TextView>(R.id.text1)       textView.text = LL.screen1_text1   }}

Получилось довольно просто и привычно.

Итоги

Мы сделали механизм управления текстами в приложении без необходимости перевыпуска релиза. Тексты хранятся на сервере, обновляются через git репозиторий. Для бизнеса планируется создать админку для управления текстами. Для Android команды мы сделали удобный механизм работы с этими текстами и статическую поддержку текстов в коде. Сейчас наш JSON файл насчитывает 180 различных строк, и найденное решение всех устраивает.

Рабочий пример можно найти по ссылке.

Подробнее..

Категории

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

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