Dev Easy Notes
3.17K subscribers
123 photos
1 video
147 links
Работаю в IT уже 8 лет. Рассказываю про разработку простым языком. Полезность скрыта под тупыми шутками и слоем мата. Лучший underground канал про разработку, который вы сможете найти.

По сотрудничеству писать @haroncode
Download Telegram
Итак, у нас всего 4 типа разрешений. Все эти 4 типа можно разделить на две основные группы:

 👉 Install-time - которые даются во время установки 
 👉 Runtime - которые нужно запросить во время работы приложения

Перед тем, как идти дальше важно понимать, что помимо разрешений, которые уже встроены в систему (как например интернет или локация) можно создавать свои разрешения. И свои разрешения могут быть как Install-time, так и Runtime. Зачем это нужно мы разберем далее, главно держать это в голове.

В группу Install-time входят 🤙Normal и ✍️ Signature Permissions. Первые это самые обычные разрешения, которые не дают доступа к критичным данным, и список таких разрешений мы можем увидеть в google play. Со вторыми, которые Signature уже интереснее. Эти разрешения система дает автоматически, для приложений которые подписаны одним ключом. Допустим есть приложение A которое создало разрешение типа Signature и есть приложение B, которое прописало в своем манифесте это самое разрешение. Если оба этих приложения подписаны одним и тем же ключом, значит приложение B автоматически получает это разрешение, если же ключи разные, то соответственно это разрешение не дается. 

✍️ Для чего нужен Signature Permissions? Этот механизм позволяет создавать экосистемы из приложений, когда на всех один и тот же аккаунт. Яркий пример тут Яндекс со своим Яндекс плюсом, логинясь в одном приложении, вы автоматически логинитесь во всех остальных. Другими словами, Signature Permissions один из механизмов, как можно расшарить данные вашего приложения только тем, кому вы доверяете.

В группе Runtime тоже есть два вида разрешений: ❗️Danger и 🦚 Special Permissions. Формально Special не относится к Runtime, но мне показалось что они стоят очень близко и так проще. 

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

🦚 И последние это Special Permissions. Это уже довольно экзотические разрешения, которые используют всякие приложения утилиты. Сюда относится разрешение рисовать UI поверх других приложений, что например используют приложения для записи экрана. Получить их можно также только с позволения пользователя, и причем довольно не просто. Дело не обойдется одним системным диалогом, тут нужно отправить пользователя в конкретное место в настройках, да еще и рассказать чего он там должен сделать. С некоторых пор и отправка Pending Intent в точно заданное время тоже теперь относится к специальным разрешениям. Насколько я понимаю это попытка гугла убрать с рынка сторонние будильники. 

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

Каждый раз, когда пользователь дает приложению разрешение на те или иные данные, UID этого приложения добавляется в соответствующую группу, у которой есть права на эти данные. Иии собственно все, когда вы запрашиваете данные о локации, в сервисе проводится проверка, входит ли UID вашего приложения в нужную группу, и если нет, то отпинывает с ошибкой.
🔥38👍13👏3🥰1
Куда можно навесить разрешения и как запрашивать?
👇
😁18👍4🔥4
{1/2} Теперь погнали к более практической части работы с разрешениями. Раньше запрос разрешения выглядел так, проверяем дано ли оно вообще, если нет то сначала при помощи специальной функции запускаем запрос разрешения. Результат такого запроса приходил в метод onActivityResult. В котором нужно было проверить есть ли результат, а тот ли id запроса и самому раскрасить данные, геморройная схема.

С появлением Result API все стало проще, теперь мы просто описываем лямбду куда придет результат. Хоть я и не в восторге от Result API, запрашивать разрешения просто сказка. Скорее всего вы и так это знаете, ведь метод onActivityResult задеприкейтили и result API сейчас это единственный вариант. 

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

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

📱Мобильные системы устроены таким образом, чтобы у клиента было ощущение, что он сидит в одном приложении. Об этом я писал с одной из своих недавних статей. Android так и устроен, мы всегда видим Activity какого-то приложения. Даже когда вы переходите в лаунчер, Activity вашего приложения просто меняется на Activity лаунчера. 

Я клоню к тому, что если так подумать то компоненты, которые вы объявляете в манифесте, становятся достоянием всей системы. Вот такой вот софтверный социализм. Однако мы можем указать системе, что некоторые компоненты можем запускать и использовать только мы и никто больше.  

Есть такой атрибут, указываемый при объявлении компонента в манифесте, и он показывает, можно ли этим компонентом пользоваться кем-то кроме нашего приложения, называется exported. Делаем exported как false и забываем про какие-либо проблемы с безопасностью, ведь этот компонент можем запустить только мы и никто больше. 

Однако если у компонента настроен intent-filter сделать его НЕ exported, уже не получится. Самый частый кейс использования intent-filter это когда вам нужно сделать deeplink или функциональность для отправки email. В таком случае ваш компонент сможет запустить любое приложение в системе. Однако возможно, вы не хотите, чтобы вас мог использовать любой (двояко получилось согласен). 

🔐И тут уже на сцену выходят кастомные permission. В кейсе с intent-filter все довольно просто. Объявляем кастомное разрешение, ставим ему protectionLevel=“signature” (из прошлого поста) и все. Теперь только ваши приложения, подписанные одним ключом смогут использовать ваши компоненты.

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

Зачем это нужно? Ну смешно жи, представьте в какой растерянности будет пользователь, когда увидит системный диалог с текстом “Разрешите доебаться?”. В реальности разумеется так никто не делает, ибо зачем? Если мы открываем свою Activity мы наоборот всеми правдами и неправдами должны привлекать пользователя и другие приложения нас вызвать, для увеличения конверсии и все такое.
👍29😁5
{2/2} Как упоминал ранее, кастомные разрешения можно повесить на все что угодно, поэтому все что в посте было рассказано про Acitivity работает и с другим компонентами. Еще интересно тут с Broadcast Receiver. Когда вы отправляете событие, можно еще приложить строку в которой будет указано разрешение. Другое приложение обязано иметь это разрешение, чтобы вообще получить это событие. Так можно защитить еще и рассылку событий м/у вашими приложениями, потому как когда вы отправляете событие, его потенциально может получить не только ваше приложение.

Короче совет. Если тема интересна, попробуйте поиграться с кастомными разрешениями, так вы точно поймете как работает эта система и на тех. собесах вам не будет равных по этой теме.
👍38
Что за сложности с разрешениям для ContentProvider?
👇
👍25🔥9
Если вы думали что я пропал, то нет, я просто хренову тучу времени убил на эксперименты с Content Provider, чтобы вам тут не напиздеть не ввести в заблуждение. Теперь у меня есть куча всего интересного чего я могу рассказать, что будет интересно даже прожженными сеньорам.

Content Provider я выделил отдельно вот по какой причине. У него в отличие от других компонентов есть несколько интересных моментов касательно установки разрешения. Так как Content Provider это основной компонент на него, как и на другие можно навесить обычное разрешение. Устанавливаем такое, и теперь только приложение с нужным разрешением сможет получить от нас какие-либо данные. Помимо этого можно exported сделать false и Content Provider получится приватным и только ваше приложение сможет его использовать. Тут все как у всех других компонентов ничего интересного.

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

И самое интересное и запутанная хрень в Content Provider это grantUriPermissions. Лучше всего объяснить на примере. Смотрите, возможно вам когда-либо выпадала задача показать pdf файл в любом другом приложении. Этот файл доступен только вашему приложению, т.е он не лежит в открытой публичной папке. И вот каким образом дать разрешение другому приложению конкретно вот на этот файл?

Если вы пойдете на stackoverflow, там будет дикий запад с костылями про то, как это сделать. Поискав чуть лучше, можно будет найти FileProvider. Суть проста, FileProvider это частный случай Content Provider который работает только с файлами. А нужен он для того, чтобы вы могли раздавать свои приватные файлы другим приложениям. 

Идея тут должна быть понятна, мы не можем перегнать весь файл в bundle и отправить его в другое приложение, мы можем другому приложению передать только путь до нашего файла. Однако, что другое приложение сможет сделать с этим путем? Да ничего, система пошлет его лесом как только он попробует по этому пути перейти. А вот FileProvider позволяет легально обойти это ограничение. 

Если вы зайдете в доку FileProvider, там будет сказано, что вам нужно сделать этот провайдер как exported=false. И тут внимательный читатель задастся вопросом: “Если exported=false то как он вообще что-то может передать другому приложению, ведь провайдер становится приватным“. Все так и есть, FileProvider делается приватным, однако мы также указываем grantUriPermissions=true и вот тут, начинается самое интересное. 

GrantUriPermissions это настройка, которая позволяет другим приложениям выдавать временный доступ до конкретного Uri (если говорим про FileProvider то условно путь до файла). Однако работает она не сама по себе, мы должны явно сообщить об этом системе, и тут два пути:
👉 Первый это когда мы формируем Intent на показ файла, нужно добавить флаг ‘FLAG_GRANT_READ_URI_PERMISSION’  и приложение, которое получит этот Intent получить также право временно(до перезагрузки устройства) обращаться к переданному Uri(к нашему файлу). 
👉 Второй, мы можем программно вызывать метод grantUriPermission, передать в него наш Uri а также пакет конкретного приложения, которому мы это права даем. Ну и есть зеркальный метод revokeUriPermission, который это разрешение отзывает.

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

Однако это не все самое прикольное. Я обнаружил интересную толи багу, толи фичу связанную с FLAG_GRANT_READ_URI_PERMISSION, о которой расскажу в следующем посте.
🔥58👍20
Интересное поведение ACTION_SEND

👇
🔥5
В прошлом посте я описал штуку под названием FLAG_GRANT_READ_URI_PERMISSION, которая позволяет выдать временное разрешение на какой-либо файл когда его нужно зашарить с другим приложением. Я разбирал его на примере кейса, когда нам нужно просто открыть файл. Создаем Intent с ACTION_VIEW, добавляем Uri, закидываем флаг для прав и погнали. Все работает достаточно логично и очевидно, однако есть другой action, который творит магию, которая мне не доступна для понимания. 

Вот смотрите, такой кейс: нам нужно не просто показать файл, а отправить его по email. Для этого в Android есть специальный action ACTION_SEND. В него можно также передать данные о том, на какой email мы отправляем сообщение, задать тему, текст и вот это все. Самое интересное, что можно также прикрепить файл для отправки. Как вы помните мы не можем просто взять и передать в Intent ссылку на файл, нужно еще проставить FLAG_GRANT_READ_URI_PERMISSION.

Когда я проверял поведение, решил, что, ну надо же проверить как будет вести себя система без передачи этого флага. Обнаружил весьма забавную вещь. Если в Intent с ACTION_SEND передаем Uri на файл, разрешение выдается автоматически. Серьезно я пробовал это и на эмуляторе и на реальном устройстве. Все работает без указания флага FLAG_GRANT_READ_URI_PERMISSION. И так работает только с ACTION_SEND, в кейсе с ACTION_VIEW, если самому не выдать разрешение, другое приложение ничего не откроет.

Для проверки я накидал мини приложеньку, которая просто перехватывает Intent с ACTION_SEND и действительно, можно просто так получить файл. Однако если при этом приложение попытается позже в другой Activity обратиться к этому Uri, система пошлет приложение лесом. 

Вывод какой, вероятно где-то под капотом, если вы передаете Intent с ACTION_SEND и кладете в него Uri, система к этому Intent проставляет разрешение на чтение. Видимо это из соображение бизнес логики. Если вы даете такой функционал пользователю, значит вы точно хотите расшарить файл и система любезно проставляет разрешения за вас. Спорный момент, как мне кажется было бы очевиднее если бы такими флагами мы управляли сами. Ну а по пока получается, что ACTION_SEND это аналог сына депутата, который может работать и без прав.

Также в инете по всюду пишут, что разрешение FLAG_GRANT_READ_URI_PERMISSION выдается до тех пор пока не вызывали revokeUriPermission либо пока устройство не перезагрузится. Это не совсем правда, возможно так было на раних версиях Android. Сейчас же я тестировал на Android 11+ и там разрешение выдается исключительно на время жизни Intent который мы передали, т.е пока живет та Activity которая открылась этим Intent.

В итоге, даже несмотря на всю эту магию, если у вас есть функциональность отправки email с файлов, я бы советовал проставлять вручную флаг FLAG_GRANT_READ_URI_PERMISSION, чисто на всякий случай, мало ли как там другие прошивки работают.
👍28🤔3
Как раздаем права в UI тестах?

👇
🔥4
Теперь совсем немного поговорим о том, как раздавать права в тестах. Смотрите, когда мы говорим про разрешения в тестах, очень вероятно речь идет именно о UI тестах, в других тестах мы обычно не завязываемся на Android. В UI тестах у нас есть два основных компонента это Driver и Runner.

🛞Driver – то, с помощью чего мы вообще можем тыкать по кнопкам из теста и проверить, что там отрисовалось на экране. В Android их два Espresso и UIAuthomator, все остальные это либо производные этих двух либо всеми забытая технология.

🏃‍♂️Runner – штука которая запускает тесты на эмуляторе или нескольких. Помимо этого Runner берет на себя работу по установке нужного окружения, перезапуска тестов, сбора статистики, сбор результатов с устройства и т.д. Самые распространенные Runner для тестов это Fork и Maraphon. Есть еще куча других, просто эти самые популярные. Смотрите, тут может возникнуть вопрос, а для чего нужны сторонние Runner ведь у Android фреймворка есть свой. Cтандартный Runner фреймворка не умеет параллелить тесты м/у несколькими эмуляторами, и не умеет перезапускать флакающие тесты. Есть хотите узнать чуть больше про Runner и Driver, про них можно почитать в этой статье.

Раздача прав для тестов, может быть реализована как на уровне 🛞 Driver так и на стороне 🏃‍♂️Runner. Оба тупо используют команды adb. Runner при установке приложения, а Driver в момент выполнения теста. Сама возможность вызывать команды adb нам доступна потому, что у нас тесты всегда запускаются на эмуляторе или устройстве в котором включена настройка разработчика и отладка по usb. Благодаря этому мы можем использовать adb как нам захочется.

Почти во всех Runner есть настройка, позволяющая выдать все возможные runtime права сразу при установке. И вся эта магия работает путем просто прибавления к команде adb install флага -g.

Если говорим про выдачу прав во время выполнения теста, т.е через Driver. Иногда такое нужно если вы хотите протестить какой-то функционал связанный с разрешениями. Можно использовать специальную TestRule которая идет вместе с тестовым фреймворком GrantPermissionRule. Если заглянуть внутрь, то все что она делает, это берет UIAutomator у которого есть доступ к adb, и выполняет команду ‘grand permission’. Помимо этого, можно самим взять UIAutomator и раздать или отобрать права у приложения.

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

А теперь навалю кликбейта. Если хотите серию постов про продробное описание того, как устроена инфраструктура тестирования в больших компаниях, наберите 100 лайков под постом)
👍86🔥10👏6
Гребаные дженерики

👇
🥰45🤯6
Знаете такое выражение, научишься кататься на велосипеде и всю жизнь умеешь. Однако есть три вещи, с которыми все работает наоборот. Каждый раз как первый. Это регулярные выражения, bash и дженерики. Все знают как это использовать на практике, но когда речь заходит о том, чтобы рассказать про них, начинаются проблемы. 

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

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

Чтобы понять суть дженериков, достаточно понять как они были придуманы. А придуманы они были вот так – сидел программист и думал: “вот у меня есть функция, я хочу чтобы она работала с любыми типами, впадлу копипастить для каждого типа вручную”. Конечно если мы говорим про Java, то можно просто поставить тип параметров Object, однако не во всех языка есть Object или Any. 

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

В чем нюанс дженериков на Java? В том, что они появились только в 5-й версии. При этом создатели Java сильно придерживаются обратной совместимости. Другими словами вы можете написать код с использованием Java 1, и он скомпилируется при использовании Java версии 17. Поэтому нужно было сделать дженерики таким образом, чтобы код ДО дженериков тоже мог компилироваться. 

Из-за этого разработчикам пришлось сделать костыль. Когда вы используете дженерики, компилятор стирает к чертям все ваши дженерики и ставит туда Object, а в местах использования сам добавляем приведение к нужному типу. Для сравнения в C++, дженерики (там они называются template) работают путем генерации кучи других классов и методов на каждый используемый тип. Вы пишите List<Integer>, компилятор C++ это видит и генерирует код List именно под Integer. 

Самый частый вопрос на собесах, можно ли узнать тип дженерика в runtime. Краткий ответ – нет, нельзя. Более развернутый: можно, но нужно заняться сексом с рефлексией в плохом смысле этого слова. Короче, если вы пишете List<Integer>, то в результате будет просто List без указания дженерика или List<Object>. 

Kotlin появился гораздо позже Java, и  в нем уже нельзя создать коллекции без указания дженерика. Однако из-за того, что код на Kotlin должен быть совместим в Java, в нем дженерики также стираются.
👍758🥰2
Инвариантность, ковариантность и контравариантность

👇
👍45🔥2
Идем дальше, есть три понятия инвариантность, ковариантность и контравариантность. Не пугайтесь названий, сейчас все раскидаем, что поймет каждый. Буду объяснять на примере List и двух классов Developer и MobileDeveloper . Как вы понимаете MobileDeveloper наследует Developer, т.е Developer стоит выше по иерархии наследования. Это значит что мы любую ссылку на MobileDeveloper можем привести к Developer. Типо такого:

MobileDeveloper mobileDeveloper = MobileDeveloper()
Developer developer = mobileDeveloper 


Прежде чем пойдем дальше стоит понять такую штуку, как производный тип. Производный тип получается когда один класс, является одним из компонентов другого типа. Проще на примере, для наших классов это может быть – List<Developer>. List<Developer> – отдельный тип, однако в него входит наш Developer. Думаю тут суть ясна.

Значит, инвариантность это когда List<Developer> и List<MobileDeveloper> это два абсолютно разных типа. Другими словами мы не можем один тип привести к другому. Уже нельзя взять и привести ссылку на список List<MobileDeveloper>, к List<Developer> вас не пропустит компилятор. Все дженерики в Java и в Kotlin по дефолту инвариантные.

Ковариантность это сохранение иерархии в производных типах. Или проще, ковариантность это когда List<MobileDeveloper> является наследником List<Developer>. Раз он наследник, значит можно приводить одну ссылку в другой. Примерно так: 

List<MobileDeveloper> mobileDevelopers = new ArrayList<>();
List<? extends Developer> developers = mobileDevelopers;


Как вы заметили для этого нужно было использовать ? extends Developer. Это и есть синтаксическая реализация ковариантности в Java. Работает это примерно так, когда мы используем строку ? extends Developer мы говорим комплятору, в текущем списке, лежат объекты, которые 100% можно привести к Developer, ведь они или и есть Developer или ниже по иерархии наследованния. 

Для чего это нужно? Очень удобно теперь делать функцию, которая итерируется по списку List<? extends Developer>. Ведь теперь не имеет значение какой именно наследник внутри этого списка. Помимо этого, мы теперь не можем в списке использовать модифицирующие операторы. А вот почему не можем, расскажу в следующем посте.

И последний компонент это Контравариантность. Он противоположен ковариантности. Другими словами контравариантность это когда List<Developer> является наследником List<MobileDeveloper>. Иерархия в производных типах поворачивается на 180 градусов. А пример вот такой:

List<Developer> developers = new ArrayList<>();
List<? super MobileDeveloper> mobileDevelopers = developers;


Синтаксически контравариантность реализуется при помощи ? super MobileDeveloper. Этим мы говорим компилятору, что в списке mobileDevelopers либо MobileDeveloper, либо выше по иерархии. 

Нужно это для того, чтобы делать функции заполнения. Аля такой, вот у меня метод fill(), он принимает вот такой список List<? super MobileDeveloper>. Теперь я могу передавать в этот метод как List<Developer> так и List<MobileDeveloper>, и он отработает одинаково хорошо. Ровно как и с Ковариантностью, у нас минус одна операция. Когда используем ? super MobileDeveloper нельзя использовать операции чтения. Если попробовать использовать get, компилятор упадет. 

Откуда такие ограничения на чтение и запись при контравариантности и ковариантности расскажу в следующем посте. Это одна из самых сложных вещей в дженериках, поймете это, и больше у вас никогда не возникнет проблем.
👍74🔥7👏21
Значит, смотрите ковариантность или ? extends Developer у нас накладывает ограничение на запись, и вообще всех методов, где дженерик это один из аргументов. Контравариантность накладывает ограничения на чтение, или всех методов, где джереник это тип возвращаемого элемента. Почему так?

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

Начнем с ковариантности. Причина этому такая, ведь когда ставим ? extends Developer мы говорим компилятору, что нам нужна гарантия, что объекты внутри будут либо Developer, либо ниже по иерархии. Однако, во время вставки компилятор не может понять какой именно тип дженерика нужно использовать в методе set. Мы не можем указать тип ссылки вставляемого объекта как ? extends Developer, это синтаксически невозможно. И раз так, мы не можем использовать любые методы, где аргумент это дженерик. Вот поясняющий пример:

List<? extends Number> list = new ArrayList<Double>();
list.add(new Integer(1));

Ссылка на список у нас ? extends Number и как бы кажется, что мы можем безопасно вставлять Number и все что ниже. Однако объект, на который ссылается list это список именно Double. И если бы компилятор тут позволил сделать вставку, то после при чтении из такого листа мы бы просто упали с ClassCastException

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

List<? super Double> list = new ArrayList<Number>();
list.get(0);

У нас ссылка ? super Double, что означает что в объекте списка, на который ссылается эта ссылка, либо объекты Double, либо выше по иерархии, вообще хз. И как в таком случае компилятору кастить объект на выходе? Правильно, никак, поэтому если мы читаем из такого листа, то можем получить только Object, т.к все объекты ниже по иерархии.

Я понимаю, что это может звучать супер запутанно, чтобы понять, еще раз перечитайте прошлый пост и повторите примеры. Потрогать эту штуку руками это самый рабочий метод понять эту теорию. Помимо этого, очень сильно рекомендую прочитать про правило PECS. После прочтения этого правила у вас все встанет на свои места.
👍448👎1
В предыдущем посте был коммент что у начинающих может возникнуть вопрос, вроде: ну вот ковариантность и контравариантность, а кроме собесов то, где это использовать? Вопрос хороший, постараюсь ответить. 

Первое, что тут посоветую пойти, найти книгу “Эффективная Java”, затем прочитать главу про правило PECS (Producer extend, Consumer super). В этой главе полностью и на достаточно глубоком уровне дается ответ на этот вопрос. Вообще на мой взгляд это лучшая книга по Java которую я читал, многие вещи правда уже не актуальны, однако большая часть советов просто топ. Поэтому крайне рекомендую прочитать полностью.

Если же хочется более краткого и простого ответа, то… его не будет. Серьезно, я тут даю вам зонтик от душнил, а не пытаюсь объяснить досконально каждую вещь в Java, на это и жизни не хватит) Для этого есть книги, вроде той, что описал выше и видосы наших друзей из Индии.
👍24😁8
Отличия в дженериках Kotlin и Java

👇
👍19
На самом деле по большей части отличий довольно мало. Первое отличие синтаксическое ? super заменили на in, ? extends заменили на out:

List<? extends Developer> developers = new ArrayList<MobileDeveloper>();
val developers: List<out Developer> = ArrayList<MobileDeveloper>();

List<? super Double> numbers = new ArrayList<Number>();
val numbers: List<in Double> = ArrayList<Number>();


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

Первое, теперь можно в объявлении класса указать ковариантность и контравариантность. Смотрите, в Java мы эти вещи могли указывать только на уровне ссылок. В Kotlin эту вещь упростили, и например если мы хотим ковариантность сразу на всех объектах нашего класса, достаточно в объявлении указать out возле дженерика. На практике это работает так:

val numbers: List<Int> = listOf<Number>();
 
Видите в чем прикол. В Kotlin у List дженерик помечен как out на уровне интерфейса. Это автоматически дает свойство ковариантности всем объектам этого List. Аналогично работает и свойство контравариантности, для этого нужно поставить in

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

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

inline fun <reified T : Any> fn() {
    val kClass = T::class
    println("Тип дженерика $kClass")
}


Подводя итог, основных отличия три:
Нельзя создавать класс без указания дженерика
Свойство ковариантность и контравариантность можно сразу задать всем объектам класса 
Через reified можем получить тип дженерика в runtime
🔥405
Два друга @JvmWildcard и @JvmSuppressWildcards.

В комментах накинули вопрос про @JvmSuppressWildcards. Тоже довольно неочевидная штука, но порой приходится разбираться. Суть в чем, Java и Kotlin полностью совместимы, что означает что из Java можно вызывать Kotlin, и наоборот. На стыке этих двух языков начинаются приколы, например в Kotlin все типы делятся на Nullable и NotNullable, а в Java такого разделения нет. 

Работа дженериков тоже немного отличается. Буквально в предыдущем посте я описывал, что ковариантность и контравариантность в Kotlin может работать на уровне классов, а не ссылок. И вот пример:

class Box<out T>(val value: T)

fun box(value: Integer): Box< Integer> = Box(value)
fun unbox(box: Box<Number>): Number = box.value


Как видите в Box дженерик помечен как out, что означает ковариантность на уровне всех объектов. Только это работает на уровне Kotlin. Если попытаемся вызвать метод box из Java, то он вернет Box без дженерика. Это как бы немного нарушает логику ковариантности которую обещает Kotlin. При этом в методе unbox автоматически появится wildcard. В итоге в Java получим вот это:

Box box(Integer value)
Number unbox(Box<? extend Number>)


В целом все не так страшно, однако иногда охото подкрутить это поведение. Например, мы не хотим, чтобы в методе unbox автоматически проставлялся wildcard. Или же напротив проставлялся в результатах функции, в методе box. И вот для этого, нужны наши волшебные аннотации. Как всегда, лучше на примере.

Делаем в Kotlin:  

fun box(value: Integer): Box<@JvmWildcard Integer> = Box(value)

Получаем в Java:

Box<? extend Integer> box(Integer value)

Делаем в Kotlin:  

fun unbox(box: Box<@JvmSuppressWildcards Number>): Number = box.value

Получаем в Java:

Number unbox(Box<Number> box)

Сравните эти две полученные функции с дефолтным поведением и суть станет ясна.

Где это нужно на практике? У меня такое было только при работе с Dagger. Фишка в чем, в том что Dagger работает на базе Kapt. Если вдруг не знали, то kapt работает в два приседания. Сперва он перегоняет весь код в Java Stub и только потом генерит нужный код. Поэтому он такой долгий и по этой причине иногда нужно делать подсказки компилятору, когда пишем модули на Kotlin. В противном случае кодогенератор начинает чудить и падать.
🔥23👍4🤔1
Соглашение по конфигурации

👇
😁91👍1
Все же хотя бы краем уха слышали про Ruby On Rails. Пожалуй самый знаменитый из всех веб фреймворков, которые есть в индустрии. Он получился настолько удачным, что в принципе язык Ruby редко рассматривается без Ruby On Rails. Фреймворк крут тем, что принес много интересных концепций. 

Одна из таких концепций – соглашения по конфигурации (Convention over configuration). Суть в том, что когда мы затаскиваем какую-то либу, или настраиваем фреймворк, или билд систему нам нужно сделать конфигурацию, хотя бы минимальную. И когда вы делаете это в первый раз, то особых проблем это не вызывает. Но на 5-10 раз начинаешь бомбить из-за копипасты. И вот концепция соглашения вместо конфигурации позволяет решить эту проблему и уменьшить количество дублируемого кода.

Работает это так: мы просто делаем систему такой, чтобы все работало сразу из коробки без минимальной конфигурации. И только в том случае, если тебе нужно подкрутить дефолтное поведение, ты уже делаешь конфигурацию. Ruby On Rails для примера запускает целый сервак без какой-либо настройки. Из-за этого кстати его все боялись, когда фреймворк только вышел. Потому как всем казалось, что не может быть так просто и ты явно за это где-то заплатишь. 

На практике в Android разработке этот подход можно использовать в Gradle. Когда у вас в проекте 2-3 модуля, можно просто копировать конфигурацию. Однако когда их переваливает за 500, копипастой уже не отделаешься. Простое изменение параметра может привести к тому, что придется переделывать везде, да и новые модули создавать запарно. Эту проблему и помогает решить наша концепция. 

Соглашения в Gradle можно реализовать в виде своих плагинов, у Gradle даже есть дока для этого. Просто выносим конфигурацию в такой плагин, и все что остается сделать это в новом подключаемом модуле просто заиспользовать этот самый плагин. Все работает как нужно, прям из коробки, а если нужно в отдельном модуле подкрутить поведение, делаешь это только в этом конкретном модуле.
👍312🤔1