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

Разработка под android

Упаковка приложения в F-Droid

10.09.2020 12:19:55 | Автор: admin


tl;dr: упаковываю и отправляю приложение без троянов для управления своими лампами в F-Droid без каких-либо знаний в разработке для Android.

У меня есть политическая позиция, я сторонник СПО во многом согласен со Столлманом. Как это часто бывает, такая позиция дополняется лёгкой паранойей. Я стараюсь избавляться от проприетарных приложений, особенно, если они работают через сторонние сервера.

А ещё у меня есть умные лампы Xiaomi Yeelight, которые контролируются приложением, работающим через сервера Xiaomi. Но в нём есть возможность включить в лампе API, работающее внутри локалки. Чтобы чувствовать себя спокойней, я пошёл искать приложение в Github и Gitlab и теперь намереваюсь продвинуть его в F-Droid, чтобы поддержать наше параноидальное сообщество.

Сборка и проверка


Для того, чтобы приложение попало в каталог, оно должно хотя бы собираться и, по-хорошему, работать. Мой выбор оказался скудным, а на Gitlab ничего не нашлось ни одного приложения. Из доступных приложений я смог собрать лишь два, а запустилось в итоге только одно из них. Я очень далёк от разработки под Android, за несколько дней я освоил только простую сборку с помощью Gradle, её дальше и опишу.

Нам понадобится: git, Java Runtime Environment, Android SDK, Android Debugging Bridge и свежий Gradle. JRE, ADB и git для Debian Testing можно установить из пакетов apt install git adb openjdk-11-jre-headless.

Android SDK обычно устанавливается вместе с Android Studio, но я воспользовался консольной утилитой sdkmanager:

unzip commandlinetools-linux-6609375_latest.zipexport PATH=$PATH:$PWD/tools/bin/mkdir android-sdkexport ANDROID_SDK_ROOT=$PWD/android-sdk/

Если вам показалось, что я пропустил установку собственно Android SDK, то вам не показалось. Потом объясню. В репозиториях Debian лежит протухшая версия Gradle, сборка с ней не работает, свежую тоже придётся устанавливать с сайта:

wget https://services.gradle.org/distributions/gradle-6.6.1-bin.zipunzip gradle-6.6.1-bin.zipexport PATH=$PATH:$PWD/gradle-6.6.1/bin/

Репозиторий я форкнул к себе и почистил автосгенерированный мусор. Собирается всё так:

yes | sdkmanager --licenses --sdk_root=$ANDROID_SDK_ROOTgit clone https://github.com/asz/OpenLight.gitcd OpenLight/gradle wrapper./gradlew assemble

Первой командой принимаем оптом принимаем все лицензионные условия, как того требует sdkmanager для неинтерактивной установки. При автогенерации обёртки (gradle wrapper) Gradle сам распарсит и установит все необходимые зависимости. Скачивание нужной версии Android SDK, сборочного инструментария и других специфичных для Android зависимостей выполняется именно с помощью sdkmanager, поэтому руками это делать не обязательно, а лицензии придётся принять заранее.

image

Самое время включить API в официальном приложении. Включите в смартфоне отладку по USB, подключите его к компьютеру и устанавливайте сгенерированный отладочный пакет: adb install app/build/outputs/apk/debug/app-debug.apk. Не забудьте разрешить отладку с вашего компьютера во всплывающем окне на телефоне. Если приложение на телефоне демонстрирует признаки жизни, то можно продолжать.

Подготовка патча в F-Droid


У F-Droid есть правила для включения приложения в каталог. Основные довольно простые: никаких несвободных зависимостей сборки, проблемы с приватностью и любые несвободные зависимости самого приложения должны быть помечены. Я не стал проверять зависимости самостоятельно, ведь у F-Droid есть CI и собственная система сборки, это позволяет просто прогнать коммит через пайплайн.

Теперь пойдём на GitLab, где ведётся разработка F-Droid. Сначала обязательно проверьте, что вашим приложением ещё никто не занимается. Подобная активность сосредоточена в запросах на упаковку и в запросах на слияние. Форкайте репозиторий Data и клонируйте его из своего профиля.

Для того, чтобы приложение оказалось в F-Droid, достаточно одного YML-файла. Найдите любой подходящий YML-файл в подкаталоге metadata/ вашего репозитория и скопируйте его в аналогичном формате: applicationId.yml. Значение applicationId для вашего приложения можно достать из какого-нибудь build.gradle его же репозитория, в моём случае из app/build.gradle. Не помню, какой из файлов я взял в качестве референса, покажу лишь итоговый файл metadata/grmasa.com.open_light.yml:

AntiFeatures:
- NonFreeDep
Categories:
- Connectivity
License: GPL-2.0-or-later
AuthorWebSite: https://github.com/grmasa
SourceCode: https://github.com/grmasa/Open_light
IssueTracker: https://github.com/grmasa/Open_light/issues
Changelog: https://github.com/grmasa/Open_light/tags

AutoName: Open Light
Summary: Control Xiaomi Yeelight WiFi smart bulbs
Description: |-
Control Xiaomi Yeelight smart bulbs within your Local Area Network.
Only fits WiFi controlled bulbs.

This app requires enabled LAN control for bulbs: open the official app, set up
all the bulbs, go to LAN control in the menu, and enable it for every device.

RepoType: git
Repo: https://github.com/grmasa/Open_light.git

Builds:
- versionName: 1.1.2
versionCode: 1
commit: v1.1.2
subdir: app
gradle:
- yes

AutoUpdateMode: Version v%v
UpdateCheckMode: Tags
CurrentVersion: 1.1.2
CurrentVersionCode: 1


Мне было сложно выбрать конкретную AntiFeature, но доскональная точность не требуется, указал NonFreeDep. Категория Connectivity подходит для приложений-компаньонов устройств. Ключ Build описывает, из какого коммита/тега собирать приложение. versionCode также можно найти в build.gradle. Если вы не хотите обновлять приложение вручную, то можно заполнить UpdateCheckMode и AutoUpdateMode (v%v описывает формат тега).

Для базовой проверки файла потребуется утилита fdroid из репозитория F-Droid Server, его зависимости и переменная ANDROID_HOME:

git clone https://gitlab.com/fdroid/fdroidserver.gitexport PATH=$PATH:$PWD/fdroidserverapt -y install python3-git python3-pyasn1 python3-pyasn1-modules python3-yaml python3-requestsexport ANDROID_HOME=$ANDROID_SDK_ROOT

Можно вернуться в директорию fdroiddata и проверить, рабочие ли метаданные:

fdroid readmetafdroid lint grmasa.com.open_lightfdroid build -v -l grmasa.com.open_light

Первые две команды проверяют синтаксис, третья собирает приложение. На выходе у вас должен появиться APK: unsigned/grmasa.com.open_light_1.apk.

Можно на всякий случай запустить fdroid checkupdates grmasa.com.open_light для проверки обновлений и fdroid rewritemeta grmasa.com.open_light для корректного перезаполнения файла с метаданными, после чего перепроверить сборку.

Почти счастливый конец


Теперь можно коммитить и проверять пайплайн Gitlab CI. У меня всё прошло!

Я заполнил Merge Request, его проверили и попросили уведомить автора оригинального приложения. Если автор будет игнорировать меня больше двух недель, я просто перепишу ссылки в метаданных на свой форк и тогда его примут.

image

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

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



Подробнее..

Google не узнает, что вы делали прошлым летом (ну почти)

18.09.2020 14:13:41 | Автор: admin

Google (или его родительский холдинг Alphabet) на данный владеет самым популярным одноименным поисковым сервисом, самым популярным видеохостингом YouTube, самым популярным сервисом электронной почты с Gmail, самой популярной мобильной операционной системой Android и целым рядом популярных облачных приложений для работы с документами Google Docs. По крайней мере восемь продуктов корпорации имеют более миллиарда пользователей. Бородатая шутка из середины нулевых о том, что скоро мы все будем ездить на работу в Гугле на Гугле, чтобы заработать немного Гугла, сегодня оказалась близка к реальности как никогда.

Материнская компания Google Alphabet (GOOGL, GOOG) в январе 2020 года превысила рыночную оценку в $1 трлн., а за 12 месяцев 2019 года ее акции выросли на 25%. За весь 2019 год технический гигант получил доход в $161,857 млрд. Что же является источником таких баснословных доходов? Таргетированная реклама!

В начале февраля 2020, публикуя свои финансовые результаты за 4 квартал и весь 2019 год, Alphabet впервые раскрыл более подробную информацию о двух своих основных сегментах прибыли: Google и Другие статьи. Сегмент рекламы Google advertising обеспечил более 83% всех доходов Alphabet, по итогам 2019 года сумма составила $134,811 млрд., рост составил 16% по сравнению с значением 2018 года. Основная часть доходов (а именно $98,115 млрд.) от рекламы была получена за счет ее размещения в поисковых сервисах Google.

Цена использования бесплатных приложений


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

Google собирает личные данные, когда пользователи дают очевидное согласие (авторизуются в любом из сервисов компании YouTube, Gmail) и когда пользователи не дают очевидное согласие и чаще всего даже не подозревают, что компания в тот или иной момент собирает личные данные.

Так называемые пассивные методы сбора данных (без явного согласия пользователя) действуют, например, на платформах Android и Chrome, в приложениях Поиск, YouTube, Карты), инструментах издателя Google Analytics, AdSense и инструментах рекламодателя AdMob, AdWords.

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

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

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

Прецеденты на на государственном уровне


21 января 2019 года Национальная комиссия по делам информационных технологий и правам человека (CNIL) Франции оштрафовала Google на 50 млн евро.

Штраф стал результатом расследования, начатого в мае 2018, когда в CNIL поступили коллективные жалобы от правозащитных ассоциаций None Of Your Business (NOYB) и La Quadrature du Net (LQDN), представляющих интересы более 10 000 человек.

В этих двух жалобах ассоциации обвиняли Google в отсутствии надлежащей правовой базы для обработки персональных данных пользователей, в том числе для персонализации рекламы.
В ходе расследования Нацкомиссия Франции установила два нарушения европейского Регламента о защите данных (GDPR).

В Сентябре 2019 года YouTube обязали выплатить $170 млн за сбор персональной информации детей без согласия родителей и использование ее для таргетированной рекламы. Штраф стал рекордным с начала существования закона о конфиденциальности детей в США.

Хватит это терпеть!


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

  1. Вы испытываете дискомфорт от самого факта, что каждое ваше действие логируется, анализируется и подвергается дата-майнингу.
  2. Вы хотели бы избежать передачи собранной о Вас информации по запросу от государственных компетентных органов и силовых ведомств.
  3. Вы боитесь, что в случае взлома вашего аккаунта или кражи\утери девайса, злоумышленники получат доступ сразу ко всей хронологии ваших перемещений, действий и переписок.
  4. Вам не нравится, что средства мониторинга Google вызывают повышенный расход энергии и трафика вашим мобильным устройством, а его программное обеспечение обновляется без вашего ведома и контроля.
  5. Ваш смартфон спустя некоторое время после покупки стал страшно тормозить, хотя вы на него даже не успели ничего толком поставить.

Согласны хотя бы с одним из пунктов? Тогда пришла пора принять меры, чтобы не дать Гуглу узнать, что вы делали этим летом!

Уходим в отказ


Для разминки откажемся от услуг Google на вашем настольном компьютере. В этом случае рецепт счастья довольно прост.

Локальный аккаунт Windows, установка Vivaldi или Mozilla Firefox браузером по умолчанию с активацией всех функций защиты приватности, установка и настройка браузерных расширений AdBlock и NoScript, установка DuckDuckGo поисковиком по умолчанию, выбор альтернативного почтового сервиса. Важно понимать, что эти меры не сделают вас неуязвимыми для слежки, но довольно радикально уменьшат ее размах

Теперь возьмемся за ваш смартфон на Android. Первым делом устанавливаем независимый маркетплейс открытого программного обеспечения для Android F-droid, синхронизатор контактов DAVx5, меняем браузер по умолчанию на более приватный Vivaldi или Mozilla Firefox, ставим альтернативный картографический сервис OsmAnd и почтовое приложение K9 и можно жить? Не тут-то было

Андроид не лыком шит, а сервисами Гугла


Оказывается, что даже если вы поменяете все основные приложения андроид-смартфона на альтернативные, и отключите в настройках предустановленные с завода, это почти не уменьшит объем передаваемой информации о вас в Google. А дело все в том, что на подавляющем большинстве смартфонов кроме самого Андроида предустановлены Сервисы Google для мобильных устройств.

Вот что говорит он них сам поисковый гигант: Сервисы Google для мобильных устройств (GMS) это набор приложений и API, которые реализуют дополнительные возможности на устройствах Android. Приложения не требуют специальных настроек и взаимодействуют друг с другом, обеспечивая непревзойденное удобство для пользователей.

Звучит как очень важное и полезное ПО, если бы не два НО полностью отключить или удалить GMS просто так невозможно, и именно через механизмы GMS осуществляется львиная доля слежки за пользователем смартфона.

Что представляют представляют собой Google сервисы?


Google Mobile Services (GMS) это проприетарные или частично проприетарные приложения от Google, такие как Google Play Store, Google Chrome, Youtube и другие, которые часто предустановлены на устройствах Android. GMS не является частью свободного Android Open Source Project (AOSP), что означает, что производителю Android необходимо получить лицензию от Google, чтобы законно предустановить GMS на устройстве Android. И ставят их почти все производители.

Одним из основных компонентов GMS являются Google Play Services это проприетарный фоновый сервис и пакет API для устройств Android от Google. Сервисы Google Play автоматически и незаметно обновляются через Google Play на устройствах с этим приложением, установленным на Android 4.1 или новее. Это означает, что Google может доставлять обновления без необходимости обновлять прошивку Android производителям, что позволяет избежать фрагментации платформы, из-за которой ранее она получила печальную известность. Именно на Google Play Services завязаны ключевые функции взаимодействия с облаком Google и одновременно сбора данных о пользователе.

Почему почему пользователю смартфона следует отказаться от GMS\Play сервисов?

  • Постоянно передают данные в Google.
  • Обновляются самостоятельно вне вашего ведома и контроля.
  • Со временем новые версии GMS начинают потреблять все больше ресурсов.
  • Работа GMS расходует дополнительную энергию батареи и мобильный трафик.
  • Занимают значительный объем оперативной и постоянной памяти на основном разделе.

Путем выполнения специальных манипуляций GMS и play services все-таки можно удалить, и использовать устройство уже без них, однако из-за намеренной глубокой интеграции сервисов в работу смартфона вас могут поджидать большие сложности с запуском и использованием многих популярных приложений.
Последствия из-за отказа от GMS и play services, которые не решить простой установкой альтернативных приложений могут быть такими:

  • Проблемы с определением местоположения в картографических приложениях.
  • Проблемы с запуском фирменных приложений Google и авторизации в них.
  • Отсутствие доставки пуш-уведомлений на смартфон.
  • Отсутствие облачной синхронизация календаря и телефонной книги.

Избавляемся от Google на смартфоне правильно


Так как же удалить гугл со смартфона и не потерять большую часть функциональности? Еще несколько лет назад это была практически неразрешимая задача и пользователям приходилось просто мирится с серьезными неудобствами и ограничениями. Пока однажды не появился проект microG Services Core, который позволяет заменить собой функции проприетарных Google Play Services.

MicroG свободная альтернативная реализация (по сути протез) Google Play Services Framework с открытым исходным кодом, предоставляющая все основные функции сервисов Google и сохраняющая конфиденциальность.

Возможности, которые предоставляет MicroG:

  • Низкий расход батареи, малое потребление оперативной памяти и ресурсов процессора.
  • Малый размер занимаемого места на системном разделе по сравнению с сервисами Google (5-10 mb против 400-500 mb).
  • Авторизация на серверах Google и широкая поддержка приложений.
  • Онлайн и оффлайн сервисы навигации полноценно функционируют.
  • Отсутствие сомнительных компонентов.
  • Работает как на реальных устройствах, так и в эмуляторах Android.
  • Открытый исходный код (лицензия Apache 2.0).

Но путь к замене Google Play Services на microG для заводской прошивки довольно тернист и включает в себя приличное количество шагов.

Универсальная инструкция, актуальная для смартфонов на базе Android 9\10


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

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

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

  1. Потребуется установить кастомное recovery.

    Recovery это образ аварийной среды восстановления, который обычно присутствует на смартфонах, однако заводские рекавери обычно сильно ограничены в функциях, поэтому все ставят себе доработанные кастомные образы.Лучше всего подойдет наиболее популярное кастомное рекавери TRWP. Способы установки зависят от конкретной модели телефона, их можно посмотреть на официальном сайте проекта twrp.me.

  2. Следующий шаг установка утилиты Magisk

    Это опенсорсная утилита, которая патчит boot образ системы, позволяет получить root-права (MagiskSU), устанавливать различные системные приложения и моды в режиме systemless, т. е. без модификации основного системного образа Android. Скачиваем zip-архива Magisk с официального сайта в корневую папку смартфона.



    Перезагружаемся в рекавери TRWP и выполняем прошивку zip-архива. Загружаем телефон снова, скачиваем и устанавливаем приложение Magisk Manager для управления Magisk и его модулями.

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

    Устанавливаем модуль Magisk manager for recovery mode (mm) из репозитория Magisk. Он нужен для отключения Xposed в случае цикличной перезагрузки устройства.

    Устанавливаем архивы EdXposed-SandHook-v0.4.6.2.4529.-release.zip и magisk-riru-v21.3.zip как модули Magisk. Перезагрузите смартфон!

    Устанавливаем приложение-менеджер EdXposedManager-4.5.7-45700-org.apk

    Проверяем статус Xposed Framework в Xposed Installer.
  4. Из репозитория Xposed устанавливаем модуль FakeGapps. Он необходим для подделки подписи, чтобы MicroG смогли полноценно притворятся Play services. Перезагрузите смартфон!
  5. Установка пакета NanoLX NanoDroid то, ради чего выполнялись все предыдущие действия. NanoDroid это автоматическая пакетная система удаления Google Play Services и установки и настройки компонентов microG и дистрибутив популярных опенсорсных программ.

    Скачайте архив NanoDroid-22.9.20200910.zip с официального сайта и установите этот архив через Magisk Manager или TRWP. Обязательно перед его установкой перезагрузите смартфон и создайте файл конфигурации .nanodroid-setup по инструкции. Незнакомые параметры не меняем. Также в этом файле можно выбрать, какое дополнительное свободное программное обеспечение нужно предустановить.

    Нас интересуют эти параметры (другие при желании тоже)
    nanodroid_microg=* вариант установки microG
    0 = не устанавливать
    1 = установить GmsCore, GsfProxy, DroidGuard Helper и бекенд Nominatim Geocoder Backend (оптимальный вариант)
    2 = установить GmsCore и бекенд Nominatim Geocoder Backend (не будут работать push-сообщения)
    nanodroid_gsync=* Файлы синхронизации
    0 не устанавливать
    1 устанавливать (оптимальный вариант)
    nanodroid_overlay=* Удаление Google Play services и части предустановленных приложений или скрытие средствами Magisk, при наличии Magisk (Pseudo-debloat)
    0 нет
    1 да (оптимальный вариант)
    nanodroid_play=21 Тип магазина приложений
    21 = установить Aurora Store (вместе с аддоном Aurora Services) и Fake Store

    После установки перезагружаем смартфон!

Можно начинать пользоваться!


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

  • Aurora Store. альтернативный клиент для Play Store, выполняет установку и обновление бесплатных приложений.
  • Yalp Store, это практически то же самое, но без красивого интерфейса и с еще более скромными системными требованиями
  • F-Droid каталог открытого ПО для Android. Можно использовать как официальный репозиторий, так и сторонние.

P.S. Не любите длинные и сложные инструкции?
Можно установить комбинированную прошивку LineageOS for MicroG и получить полнофункциональный Андроид-смартфон без гугл сервисов сразу из коробки. Самый простой и удобный способ из множества доступных, настоятельно рекомендуется, если ваше устройство есть в списке поддерживаемых.



Подробнее..

Особенности обновлений прошивки мобильных устройств

18.09.2020 14:13:41 | Автор: admin
Обновлять или не обновлять прошивку на личном телефоне каждый решает самостоятельно.
Кто-то ставит CyanogenMod, кто-то не чувствует себя хозяином устройства без TWRP или jailbreak.
В случае с обновлением корпоративных мобильных телефонов процесс должен быть относительно единообразным, иначе IT-шникам даже Рагнарёк покажется забавой.

О том, как это происходит в корпоративном мире, читайте под катом.

Особенности обновлений прошивки мобильных устройств рисунок 1

Краткий ЛикБез


Мобильные устройства на базе iOS получают регулярные обновления аналогично устройствам на Windows, но при этом:

  • обновления выходят реже;
  • обновления получает большинство устройств, но не все.

Apple выпускает обновление iOS сразу для большинства своих устройств, кроме тех, которые снимаются с поддержки. При этом Apple поддерживает свои устройства достаточно долго. Например, обновление iOS 14 получат даже iPhone 6s, вышедшие в 2015 году. Конечно, не обходится без косяков, типа принудительного замедления старых аппаратов, которое, как утверждается, было сделано не с целью вынудить купить новый телефон, а для продления срока службы старого аккумулятора Но в любом случае это лучше, чем ситуация с Android.

Android это по сути своей франшиза. Оригинальный Android от Google встречается только на устройствах линейки Pixel и бюджетных устройствах, которые участвуют в программе Android One. На других устройствах встречаются только производные от Android EMUI, Flyme OS, MIUI, One UI и т.д. Для безопасности мобильных устройств такое разнообразие большая проблема.
Например, сообщество находит очередную уязвимость в Android или системных компонентах, которые лежат в его основе. Далее уязвимости присваивается номер в базе CVE, нашедший получает вознаграждение по одной из баунти-программ от Google, а уж потом Google выпускает заплатку и включает её в очередной релиз Android.

Получит ли её ваш телефон, если он не Pixel или не участник программы Android One?
Если вы купили новое устройство год назад, то, наверное, да, но не сразу. Производителю вашего устройства ещё нужно будет включить патч Google в свою сборку Android и протестировать её на поддерживаемых моделях устройств. Топовые модели поддерживают чуть дольше. Всем остальным остаётся смириться и не читать по утрам базу CVE, чтобы не портить себе аппетит.

Ситуация с мажорными обновлениями Android, как правило, ещё хуже. В среднем новая мажорная версия докатывается до мобильных устройств с кастомными Android не меньше, чем за квартал, а то и больше. Так обновление Android 10 от Google вышло в сентябре 2019, а устройства разных производителей, которым повезло заслужить возможность обновления, получали его вплоть до лета 2020.

Производителей можно понять. Выпуск и тестирование новых прошивок это затраты и не малые. А так как устройства мы уже купили, то дополнительных денег с нас не получить.
Остаётся вынуждать нас покупать новые устройства.

Особенности обновлений прошивки мобильных устройств, Краткий ЛикБез, картинка 2

Дырявость сборок Android отдельных производителей стала причиной того, что Google изменил архитектуру Android, чтобы доставлять критичные обновления самостоятельно. Проект получил название Google Project Zero, около года назад о нём писали на Хабре. Фича относительно новая, но она встроена во все устройства с 2019 года, где есть сервисы Google. Многие знают, что эти сервисы платные для производителей устройств, которые платят за них роялти в Google, но мало кто знает, что дело не ограничивается коммерцией. Чтобы получить разрешение использовать сервисы Google на конкретном устройстве, производитель должен отдать свою прошивку в Google на проверку. При этом Google не принимает на проверку прошивки с древними Android. Это позволяет Google навязывать рынку свой Project Zero, что, надеемся, сделает Android устройства более безопасными.

Рекомендации корпоративным пользователям


В корпоративном мире используются не только публичные приложения, доступные в Google Play и App Store, но и приложения собственной разработки. Иногда жизненный цикл таких приложений прекращается в момент подписания акта приёма-передачи и оплаты услуг разработчика по договору.

В этом случае установка нового мажорного обновления ОС часто приводит к тому, что такие job-is-done приложения перестают работать. Бизнес-процессы останавливаются, а разработчиков нанимают повторно до возникновения следующего косяка. То же самое случается, когда корпоративные разработчики не успевают вовремя адаптировать свои приложения под новую ОС или новая версия приложения уже доступна, но пользователи её ещё не установили. В том числе для решения таких проблем предназначены системы класса UEM.

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

UEM системы могут удалённо заблокировать или отложить обновление прошивки мобильных устройств. Поведение зависит от платформы и производителя устройств. На iOS в режиме supervised (о режиме читайте в нашем FAQ) можно отложить обновление до 90 дней. Для этого достаточно настроить соответствующую политику безопасности.

На Android устройствах производства Samsung можно бесплатно запретить обновление прошивки или воспользоваться дополнительным платным сервисом E-FOTA One, с помощью которого можно указать какие обновления ОС устанавливать на устройства. Это даёт администраторам возможность предварительно проверить поведение корпоративных приложений на новых прошивках своих устройств. Понимая трудоёмкость этого процесса, мы предлагаем своим заказчикам сервис на базе Samsung E-FOTA One, включающий услуги проверки работоспособности целевых бизнес-приложений на используемых у заказчика моделях устройств.

На Android устройствах других производителей аналогичной функциональности, увы, нет.
Запретить или отложить их обновление можно, разве что, с помощью страшилок, типа:
Уважаемые пользователи! Не обновляйте свои устройства. Это может привести к неработоспособности приложений. При нарушении этого правила ваши обращения в службу технической поддержки рассматриваться/выслушиваться НЕ БУ-ДУТ!.

Ещё одна рекомендация


Следите за новостями и корпоративными блогами производителей операционных систем, устройств и платформ UEM. Буквально в этом году Google решил отказаться от поддержки одной из возможных мобильных стратегий, а именно fully managed device with work profile.

За этим длинным названием скрывается следующий сценарий:

До Android 10 UEM-системы полноценно управляли устройством И рабочим профилем (контейнером), в котором содержатся корпоративные приложения и данные.
Начиная с Android 11, возможен полноценный функционал управления только ИЛИ устройством ИЛИ рабочим профилем (контейнером).

Google объясняет нововведения заботой о конфиденциальности данных пользователей и своём кошельке. Если есть контейнер, то данные пользователя должны находиться вне зоны видимости и управления со стороны работодателя.

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

Google утверждает, что такой доступ к личному пространству отпугивал 38% процентов пользователей от установки UEM. Теперь UEM-вендорам остаётся кушать что дают.

Особенности обновлений прошивки мобильных устройств, Рекомендации корпоративным пользователям, картинка 3

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

Малоизвестные факты


В завершение еще несколько малоизвестных фактов об обновлении мобильных ОС.

  1. Прошивки на мобильных устройствах иногда можно откатить. Как показывает анализ поисковых фраз, фразу как восстановить Android ищут чаще, чем обновление Android. Казалось бы, фарш нельзя провернуть назад, но иногда всё-таки можно. Технически защита от отката базируется на внутреннем счётчике, который увеличивается не с каждой версией прошивки. В рамках одного значения этого счётчика откат становится возможен. Это то, что касается Android. В iOS ситуация чуть отличается. С сайта производителя (или бесчисленного числа зеркал) можно скачать образ iOS конкретной версии для конкретной модели. Чтобы установить его по проводу с помощью iTunes, Apple должен подписать прошивку. Обычно в первые несколько недель после выхода новой версии iOS Apple подписывает предыдущие версии прошивок, чтобы пользователи, чьи устройства после обновления глючат, могли вернуть себе более стабильный билд.
  2. Во времена, когда jailbreak сообщество ещё не разбежалось по крупным компаниям, можно было изменить версию отображаемую версию iOS в одном из системных plist. Так можно было, например, сделать iOS 6.2 из iOS 6.3 и обратно. Зачем это было нужно, расскажем в одной из следующих статей.
  3. Очевидна всеобщая любовь производителей к программе для прошивки смартфонов Odin. Лучшего инструмента для прошивки ещё не сделали.

Пишите, обсудим, может и поможем.
Подробнее..

Знакомство с App Gallery. Создаем аккаунт разработчика

22.09.2020 18:18:39 | Автор: admin


Что происходит, кто виноват и что делать


Недавно Google прекратил сотрудничество с Huawei. Это привело к тому, что Huawei на своих новых девайсах уже не может использовать сервисы Google (магазин приложений, геолокация, карты, пуши, аналитика etc), что для пользователя превращает девайс в кирпич. Если бы это не была китайская компания, то, скорее всего, на этом её бизнес, связанный с Android, просто бы прекратился. Но компания китайская, большая и они пошли по пути импортозамещения, в кратчайшие сроки реализовав функционал, аналогичный Google сервисам.


В этой серии статей мы хотим поделиться своим опытом использования Huawei Mobile Services в уже готовом приложении, использующем Google Mobile Services для аналитики (Firebase Analytics), карт и геолокации. Текста получилось довольно много и о сильно разных сервисах, засим статей будет несколько. Начнём мы с основ регистрации аккаунта разработчика и базовых вещей в коде.


  1. Создаём аккаунт разработчика, подключаем зависимости, подготавливаем код к внедрению. вы тут
  2. Встраиваем Huawei Analytics.
  3. Используем геолокацию от Huawei.
  4. Huawei maps. Используем вместо Google maps для AppGallery.

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


Что нужно для успешного внедрения


Всё было бы просто, если бы приложение писалось с нуля и не нужно было бы поддерживать как Google так и Huawei. Но мы живём в реальном мире и без сложностей не обойтись. Однако дело сильно упростится, если соблюдён ряд условий.


Но перед перечислением условий надо составить ТЗ. Оно у нас получилось такое:


  1. Нам нужно получить 2 версии APK одну для Google Play, с библиотеками от Google, другую для AppGallery, с библиотеками от Huawei.
  2. В приложении уже используется Firebase Analytics. Надо его заменить на аналог от Huawei.
  3. Есть определение местоположения пользователя. Аналогично заменяем на аналог.
  4. Есть карты. Нужно также заменить на аналог, по максимуму сохранив функционал, т.к. в реализации от Huawei некоторые вещи ещё не сделаны.

А вот что сильно сократит прикладываемые усилия:


  1. Код должен быть написан хорошо. И быть без багов (хотя это само собой разумеется зачем код с багами писать?). Под хорошо будем подразумевать более-менее стандартную архитектуру, мимикрирующую под Clean.
  2. Если код из Google библиотек размазан ровным слоем по всему проекту, то у меня для вас плохие новости. Например у вас может не быть абстракции над аналитикой и/или над полученными от Google координатами. В этом случае придётся её завести, чтобы почистить код от импортов гугловых классов, которые будут недоступны, когда мы уберём их из сборки.
  3. Использование DI. Очень упрощает абстрагирование над аналитикой и геолокацией. Используем интерфейсы, через DI передавая нужную реализацию.
  4. Карты не слишком сильно кастомизированы. В частности, основная сложность будет с абстрагированием над кластеризацией маркеров.

Подготовка к внедрению


Как и в случае с Google, надо зарегистрироваться, создать проект приложения, получить файл конфигурации.


  1. Регистрируемся на https://developer.huawei.com. Тут понадобится паспорт/права + пластиковая карта. День-два вас будут проверять, потом аккаунт заработает. Если вдруг что-то пойдёт не так (забудете что-то указать или укажете неправильно) вам напишут и подробно объяснят. После общения с Google Play всё выглядит очень круто русскоязычная техподдержка отвечает быстро и по делу.
  2. Принимаем всякие соглашения об обработке персональных данных. Внимательно читая, конечно же)
  3. Создаём проект приложения, указывая пакет (он же ApplicationId).
  4. Если вам нужно ещё и встроенные покупки реализовать то надо: а) Заполнить данные банковского счёта б) Распечатать и заполнить заявление о трансграничной передаче персональных данных в КНР в) Отправить скан оного вместе с данными из пункта а г) Отправить заявление из пункта б по почте в Москву. Когда заявление дойдёт вам придёт e-mail и останется только активировать сервис в настройках проекта. На почте бывают накладки возможно, придётся подождать. Я пару недель ждал, потом позвонил ответственному за это в Huawei уверили, что проблему решат. И решили. На русском тоже всё общение очень круто)
  5. Включаем сервис аналитики. В отличие от геолокации и карт, включённых по умолчанию, это нужно сделать вручную.
  6. Добавляем SHA-256 для всех ключей, которыми будет подписано приложение. Т.е. дебажные ключи и релизный ключ.
  7. Скачиваем аналог google-services.json, в случае Huawei называемый agconnect-services.json
  8. Создаём разные flavors для Google и Huawei. Наконец-то можно перейти к коду:

В build.gradle (module app) создаём flavors и указываем, что в папках src/google/kotlin, src/google/res, src/huawei/kotlin, src/huawei/res также находиться будет наш код.


android {  ...  sourceSets {      google.java.srcDirs += 'src/google/kotlin'      google.res.srcDirs += 'src/google/res'      huawei.java.srcDirs += 'src/huawei/kotlin'      huawei.res.srcDirs += 'src/huawei/res'  }  flavorDimensions "store"  productFlavors {      google {          dimension "store"      }      huawei {          dimension "store"      }  }}

Также создаём папки src/huaweiDebug и src/huaweiRelease. В них помещаем наш файл конфигурации agconnect-services.json


И добавляем apply plugin: 'com.huawei.agconnect' в конец build.gradle (module app).


И наконец, добавляем в build.gradle проекта:


buildscript {    ...    repositories {        ...        maven {url 'https://developer.huawei.com/repo/'}    }    dependencies {        ...        classpath 'com.huawei.agconnect:agcp:1.2.1.301'    }}allprojects {    repositories {        ...        maven {url 'https://developer.huawei.com/repo/'}    }}

В следующей части встраиваем аналитику


Теперь мы полностью готовы. У нас есть 2 разных варианта сборки для Huawei и Google. У нас подключены необходимые зависимости. Созданы папки, где будет наш код. Создан аккаунт разработчика и выполнены необходимые действия по созданию проекта приложения. У нас даже какое-то ТЗ есть. И мы уже выполнили первый пункт из ТЗ! Отличный повод на этом статью закончить. И уже в следующей встроить аналитику не от Google, а от Huawei.


Весь код, который есть в этом цикле статей вы можете посмотреть в репозитории на GitHub. Вот ссылка.

Подробнее..

Перевод 20 инструментов Android-разработчика, о которых вы могли не знать

14.09.2020 18:23:30 | Автор: admin

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

Работая над статьями о 30 лучших библиотеках и проектах Android 2019 г. и 25 лучших библиотеках и проектах Android 2020 г., я наткнулся на множество замечательных инструментов и проектов, которые могут пригодиться в разработке приложений для Android ниже они приведены в случайном порядке. Пользуйтесь.

1. AinD Android (Anbox) в Докере

AinD запускает приложения Android, помещая контейнеры Anbox в Докер.

В отличие от аналогичных проектов на основе виртуальных машин, AinD может выполняться на экземплярах IaaS без поддержки вложенной виртуализации. Docker Hub: aind/aind.

Предназначение:

2. Booster

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

Booster это набор модулей для оценки производительности, оптимизации многопоточности, встроенного индекса ресурсов, сокращения числа избыточных ресурсов, сжатия ресурсов, исправления системных ошибок и т. д. Booster позволяет повысить стабильность приложения на 1525% и снизить размер пакета на 110 МБ.

Документация очень хорошая, лицензия Apache 2.0.

3. Shake

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

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

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

4. Scabbard

Scabbard помогает с визуализацией и анализом графика зависимостей Dagger 2.

Scabbard визуализирует точки входа, схемы зависимостей, взаимосвязи компонентов и области действия. Добавить этот инструмент в проект очень легко: он хорошо интегрирован с Gradle, а также с Android Studio и IntelliJ (нажав значок на левом поле в редакторе, можно просмотреть схему для @Component или @Subcomponent).

Документирован проект отлично: есть множество примеров и подсказок.

Лицензия Apache 2.0.

5. Can I Drop Jetifier?

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

Всё больше и больше библиотек переходят на AndroidX, поэтому в какой-то момент необходимость включать этот инструмент отпадает. Этот плагин определяет, какие из используемых библиотек нужно перенести на AndroidX или избавиться от них, если уже вышла новая версия, Can I Drop Jetifier?

Документация понятная, проект выпущен под лицензией Apache 2.0. Очень рекомендую!

6. ADB Event Mirror

ADB Event Mirror позволяет отражать события касания экрана, нажатия клавиш и кнопок одного устройства на одно или несколько других устройств в реальном времени.

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

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

7. Android Emulator Container Scripts

Android Emulator Container Scripts набор небольших сценариев для запуска эмулятора в контейнере для различных систем (например, для Докера) с целью внешнего использования. Сценарии совместимы с Python версий 2 и 3. Этот репозиторий довольно популярен и пригодится, если нужно запускать много эмуляторов на удаленных машинах.

Проект выпущен под лицензией Apache 2.0 и хорошо документирован.

8. Autoplay

Autoplay это плагин для Gradle, предназначенный для публикации артефактов Android в Google Play.

Его можно считать очень простой альтернативой Gradle Play Publisher или Fastlane. Опубликовать приложение можно как apk или набор App Bundle.

Особенности Autoplay:

  • Оптимизирован для использования в CI/CD.

  • Удобен для разработчиков.

  • Надежен и перспективен.

У проекта хорошая документация, версия на момент написания статьи 1.3.0, лицензия Apache 2.0.

9. Плагин Gradle для статического анализа

Плагин статического анализа Gradle комплексная замена для всех значимых инструментов статического анализа кода, включает в себя следующее:

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

10. AndroidUtilCode

AndroidUtilCode функциональная и простая в использовании библиотека для Android, которая инкапсулирует функции, обычно используемые при разработке Android с демонстрационными версиями и модульными тестами. Инкапсулированные API позволяют значительно повысить эффективность разработки.

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

Версия проекта 1.29.0, лицензия Apache 2.0.

11. Hijckr

Hijckr вмешивается в инфляцию макета Android и перенаправляет названные элементы в другие классы.

Это довольно интересный инструмент. Например, если файл макета содержит TextView, Android обычно загружает android.widget.TextView, но вместо этого можно перехватить xml-теги и загрузить com.myapp.TextView.

Описание проекта довольно подробное и позволяет быстро начать работу с инструментом (который полностью написан на Java).

12. Roomigrant

Roomigrant это вспомогательная библиотека для автоматической генерации миграций библиотеки Android Room с использованием формирования кода во время компиляции. Она использует созданные библиотекой Room файлы схемы и генерирует миграции на основе разницы между ними то есть, создание схемы Room должно быть включено в файле build.gradle, что хорошо описано в README.

Проект выпущен под лицензией MIT, версия 0.1.7.

13. RoomExplorer

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

Инструмент хорошо документирован, лицензия Apache 2.0.

14. Android Framer

Инструмент android-framer добавляет рамки и заголовки к скриншотам в Google Play. Источник вдохновения fastlane frameit.

Инструмент написан на Python и использует ImageMagick. Настроить рамки (фоны) можно, например, с помощью Facebook Design. Также можно менять шрифт, кегль, размер рамки и т. д.

Лицензия Apache 2.0.

15. Dependency Tree Diff

Dependency Tree Diff это интеллектуальный инструмент сравнения для вывода задачи dependencies Gradle, который всегда показывает путь к корневой зависимости.

Можно установить инструмент через brew или просто использовать jar-файл.

Лицензия Apache 2.0.

16. Gradle Doctor

Gradle Doctor это плагин для сканирования сборки Gradle. Функциональность: настраиваемые предупреждения о проблемах со скоростью сборки, измерение временных затрат на инструменты обработки аннотаций Dagger, установка переменной JAVA_HOME и проверка ее соответствия JAVA_HOME в IDE, простое отключение кеширования тестов, остановка сборки в случае, если найдены пустые каталоги src (поскольку это может быть причиной несовпадений в кеше), и многое другое.

У инструмента отличная документация, проект выпущен под лицензией Apache 2.0.

17. GloballyDynamic

GloballyDynamic это набор инструментов, направленных на обеспечение всеобщей доступности Dynamic Delivery, независимо от магазина приложений или платформы распространения, которые также предоставляют единый унифицированный клиентский API для Android и простой интерфейс для разработчиков.

Поддерживаются:

Рекомендую прочитать README и подробнее ознакомиться с этим инструментом.

Лицензия Apache 2.0.

18. Dagger Browser

Dagger Browser еще один инструмент (прогрессивное веб-приложение) для удобной навигации по схеме Dagger в проекте.

Данные схемы заполняются с помощью SPI-плагина Dagger, а средство просмотра написано с помощью CRA (create-react-app) и TypeScript, Dagger Browser

Есть и пример приложения, в котором показано, как инструмент работает. Всё это выпущено под трехпунктовой лицензией BSD.

19. Wormhole

Wormhole путешествующий во времени инструмент преобразования байт-кода, добавляющий в android.jar будущие API-интерфейсы, которые можно десахаризовать на все уровни API с помощью D8 и R8.

Wormhole обеспечивает обратную совместимость с более новыми API. Приведу пример.

В Android R есть новые методы из Java 9 например, List.of. Благодаря D8 и R8 они не являются эксклюзивными для API 30 и мгновенно превращаются в совместимые с API 1. В D8 и R8 есть набор методов десахаризации для API, которых еще нет в android.jar. И можно не ждать, пока они появятся этот проект дает возможность использовать их сразу же.

20. MNML

MNML (произносится как minimal минимальный) простое бесплатное приложение для записи экрана в Android.

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

Лицензия Apache 2.0.

Заключение

Вот и всё. Надеюсь, список вам понравился и какие-то инструменты смогли вас вдохновить. До встречи!

О переводчике

Перевод статьи выполнен в Alconost.

Alconost занимается локализацией игр, приложений и сайтов на 70 языков. Переводчики-носители языка, лингвистическое тестирование, облачная платформа с API, непрерывная локализация, менеджеры проектов 24/7, любые форматы строковых ресурсов.

Мы также делаем рекламные и обучающие видеоролики для сайтов, продающие, имиджевые, рекламные, обучающие, тизеры, эксплейнеры, трейлеры для Google Play и App Store.

Подробнее..

Дайджест интересных материалов для мобильного разработчика 360 (31 августа 6 сентября)

06.09.2020 16:13:36 | Автор: admin
В новом выпуске разбираемся со скруглением иконок (два раза!), с UI-тестами и MVI, мультиплатформенными приложениями и CI/CD, проектированием интерфейсов, самыми зарабатывающими приложениями и многим другим.


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

Этот дайджест доступен в виде еженедельной рассылки. А ежедневно новости мы рассылаем в Telegram-канале.

iOS

(+66) Секрет формы иконок iOS: это сквиркл? Разбор
(+3) Формулы переводов: хитрая локализация для iOS и не только
(+3) Swift Best Practices которые не стыдно знать
Apple откладывает защиту конфиденциальности в iOS 14 до следующего года
Особенности реализации календаря с горизонтальным скролом
В App Store запускаются промо-коды для подписок
10 советов по разработке виджетов для iOS 14
Apple не будет задерживать исправление ошибок в App Store
Apple делает свой поисковый сервис?
Мои любимые приложения для iOS-разработки в 2020 году
Как создать интерактивный Bottom Sheet в Swift 5
Встроенные покупки и StoreKit в iOS 14
Привязка к прокрутке элемента
Как создать навигатор для перехода на любую страницу в Swift
Ваш первый UITest на Swift
Что нового в Xcode 12.0?
Обработка видео в SwiftUI в реальном времени с использованием Core Image
Не делайте этого с помощью Swift Optionals
Ассемблер в iOS
Apple Silicon для разработчиков приложений
Swifty Guitar Chords: гитарные аккорды на Swift
MultiplatformApp: мультиплатформенное приложение на SwiftUI

Android

(+10) На чем писать Android UI-тесты
(+4) Быть или не быть: дискуссии о тестировании в мобильной разработке
(+3) Загрузка и сборка AOSP
(+2) Так для чего же нам все таки нужен MVI в мобильной разработке
(+2) Как встроить голосового помощника в любое мобильное приложение. Разбираем на примере Habitica
(+1) Анализ сервисов приема SMS для Android против сайтов-сервисов и опыт разработки нового функционала под Android
Android Broadcast: Dagger Hilt: Deep Dive / No more Koin
Android Broadcast: Kotlin 1.4: быстрее и лучше
Вышла альфа Kotlin Multiplatform Mobile
Thermal в Android
Приватные библиотеки в Android почему вы должны рассмотреть это
Извлекаем взаимодействий из ViewModel
Распознавание касания, двойного касания, панорамирования и щипка в Android
Заставьте ваш (Kotlin) код выражать самого себя
Распространенные ошибки разработчиков при создании RecyclerViews и способы их устранения с помощью класса ViewRepresentation
Отформатируйте код Kotlin с помощью Ktlint
Shape Drawables самый мощный инструмент для Android UI
Исследуем Kotlin Multiplatform
Настраиваем Logcat в Android Studio
20 инструментов Android-разработки о которых вы, вероятно, никогда не слышали
Добавляем ленту на иконку приложения в Android (снова))
Предпочитаемое хранение данных в Jetpack DataStore
Jetpack Release Tracker: отслеживание обновлений AndroidX
TrackerControl: отслеживание сбора данных на Android
Jetpack Compose Samples: примеры работы с Compose

Разработка

(+43) Ваши квадрокруги неправильные
(+18) Создаем разрушаемые объекты в Unreal Engine 4 и Blender
(+17) Ну, покати! или CI/CD мобильных приложений на основе контракта
(+9) Как прошел открытый Demo Day в Райффайзенбанке
(+7) Тестирование Flutter-приложений: инструменты, преимущества, проблемы
(+7) Как работают мобильные кошельки на примере приложения Mir Pay
(+4) Улучшаем работу со сценами с помощью ScriptableObject
(+4) Продвинутое велосипедостроение или клиент-серверное приложение на базе C# .Net framework
(+3) Совет инженерам по тестированию 1: Докеризируйте ваш Selenium Grid
(+2) Победитель Apple Design Awards: статистически усреднённый портрет
Podlodka #179: рациональность
Как создать свой игровой бизнес
Дизайн приложений: примеры для вдохновения #15
Google и Apple разработали упрощенную систему предупреждения о COVID-19
Pokemon GO прекращает поддержку старых смартфонов
Unity Distribution Portal позволит работать сразу со многими магазинами приложений
Проектирование игры в Sketch: интервью с создателем King Rabbit
Не проектируйте для мобильных устройств (Mobile-first)
С чего начать проектирование мобильного приложения. Основы UI дизайна
Что такое Actions Builder? Actions Builder и Actions SDK для Google Assistant
Реверс-инжиниринг: разработка эмулятора сервера для Marvels Avengers
10 лучших и самых популярных пакетов Flutter
5 советов для лучшего дизайна кнопок
Улучшаем Flutter-приложения с помощью автозаполнения
Чистый код для ведущих разработчиков
Автоматизация жизненного цикла Flutter-проекта с помощью GitHub Actions
Да, TDD вас замедляет
Разработка дневника настроения на Flutter за 2 часа
6 шагов в поиске подходящей среды автоматизации тестирования (с примером из практики)

Аналитика, маркетинг и монетизация

Avo: аналитика нового поколения
Самые зарабатывающие приложения в августе 2020
Индия забанила PUBG и еще сотню приложений
Google запустил Growth Academy для Украины и Беларуси
App Annie представляет отчет по геймингу за первое полугодие
myTracker анализирует доходы от рекламы в приложениях
Локализация: как выйти на международный рынок и увеличить количество установок?
Модель монетизации: меняем и дешево тестируем новую
Почему удержание лучший способ роста

AI, Устройства, IoT

(+20) Малиновый киноцентр или как сделать неубиваемый смарт-ТВ
(+18) Как игры стали движущей силой двух школ исследований ИИ
(+1) Микроволновка, знающая о тебе всё: что такое Интернет вещей (IoT)?
Apple iPhone 11 стал самым продаваемым смартфоном в первой половине 2020

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

Дайджест интересных материалов для мобильного разработчика 361 (7 13 сентября)

13.09.2020 18:19:09 | Автор: admin
На этой неделе Google выпустил Android 11, а Huawei представил Harmony 2.0, Apple продолжила биться с Epic в суде, мы продолжили исследование Kotlin в 1.4 и новых веяний неоморфизма, стагнации машинного обучения и правил создания иконок. Все это и многое другое в нашей новой подборке!


Упаковываю и отправляю приложение без троянов для управления своими лампами в F-Droid без каких-либо знаний в разработке для Android. Корпорация Google опубликовала релиз мобильной ОС Android 11. Главный акцент в новой версии операционной системы сделан на упрощении работы с различными мессенджерами, управлении smart-устройствами и улучшенной конфиденциальностью пользователя.

Этот дайджест доступен в виде еженедельной рассылки. А ежедневно новости мы рассылаем в Telegram-канале.

iOS

Apple подала встречный иск к Epic
Микровзаимодействия: анимированная волна
7 вариантов AlertView в SwiftUI
Тестирование производительности Xcode на большом проекте Swift. Сравнение iMac, MacBook, iMac Pro
Преобразование платного iOS-приложения в подписное
Пишем первый виджет для iOS
7 основных инструментов iOS-разработчика
Создание 3D анимации прокрутки карт в SwiftUI
Как сделать iOS-приложение безопасным?
SwiftUI 2.0: будущее декларативно
Swiftagram: клиент для Instagram
Velik: отслеживание поездок на велосипеде

Android

(+11) Navigation Component-дзюцу, vol. 1 BottomNavigationView
(+6) Полируем UI в Android: StateListAnimator
(+4) Превращаем EditText в SearchEditText
Google выпустил Android 11 Go
Huawei представил Harmony 2.0
JetBrains проводит конференцию по Kotlin 1.4
Android Broadcast: новый компилятор Kotlin в 1.4
20 инструментов Android-разработчика, о которых вы, вероятно, никогда не слышали
Google показал зависимость Firebase от GMS
Шесть лет споров: зачем Microsoft сделала Android-смартфон с двумя экранами в мире, где все устройства одинаковые
Подход чистой архитектуры при рассмотрении Модели
Не изобретайте колесо заново, делегируйте его!
Базовая инъекция зависимостей с помощью Hilt
Магические функции Kotlin все, что вам нужно знать
Полируем UI в Android: StateListAnimator
Сборка Android: как уменьшить время с 5 минут до 15 секунд
Разработка сложного пользовательского интерфейса с использованием Android ConstraintLayout
22 расширения Kotlin для более чистого кода
Простая библиотека настроек создаем экран настроек за секунды
TDD в Android
Современная безопасная Android-разработка
Неисправный AndroidX FragmentFactory
Исследуем Jetpack DataStore
Biometric Auth: биометрическая аутентификация в Kotlin
Blue Pair: работа с Bluetooth в Android

Разработка

(+25) Неоморфизм и его проблемы
(+11) Домофоны, СКУД И снова здравствуйте
(+10) Как захватить новую страну за 3 недели
(+6) Flutter.dev: Простое управление состоянием приложения
(+4) Локализуем приложение на React Native
Podlodka #180: PHP
C++ стал самым быстрорастущим языком программирования рейтинга TIOBE
Яндекс запускает новый сезон стажировок
Дизайн приложений: примеры для вдохновения #16
Мотивация разработчиков и других людей творческих профессий руководство для компаний
Илкка Паананен: Игры, как бизнес, не должны управляться процессами
Инструкция: как создать приложение для просмотра погоды на Flutter
Сетки, принципы и правила создания интерфейсных иконок, iOS и Android
Год на воде и хлебе: как делать приложение на свои и не сдаваться
5 советов по улучшению дизайна кнопок. Основы UI дизайна
Действительно ли Firebase так хорош, как кажется?
Создаем веб-приложение Flutter с нуля и размещаем его с помощью Continuous Deployment
4 типа разработчиков, с которыми вы (к сожалению) будете работать
Использование шаблона BLoC для чистых Flutter-приложений: теория и практический пример
Анатомия превосходного дизайна
Советы, как стать более эффективным ревьювером кода
Duofolio: ридер со словарем

Аналитика, маркетинг и монетизация

(+21) Российские пасхалки в мобильных приложениях. Какие они?
(+17) Ошибки в дизайне A/B тестов, которые я думала, что никогда не совершу
(+4) Как понять, что новая фича принесет пользу продукту, а не навредит ему?
(+2) Apple Grace Period и Billing Retry статусы при обработке чеков пользователей
AppsFlyer запускает Xpend платформу для агрегации данных о расходах на рекламу
Руководство по продуктовой аналитике от Mixpanel
Самые скачиваемые приложения в августе 2020
Mustard: скаутинг на основе ИИ
Два типа стратегий роста: стратегии искры (kindle) и стратегии пламени (fire)
Как итерации помогают в поисковой оптимизации приложений

AI, Устройства, IoT

(+31) Стагнация машинного обучения. Многие задачи не будут решены никогда?
(+12) Автоматизируем работу системы отопления в квартире без переделки интерьера умный дом z-wave
(+9) Будни OEMщика (Часть 1)
(+3) Интернет автомобилей: первые шаги к беспилотной езде
Яндекс выпустит ТВ-приставку с Алисой
Представлен новый протокол Z-Wave Long Range
Как стать экспертом в области искусственного интеллекта: пошаговое руководство
Planet-Scale AR Alliance готовит дополненную реальность для 5G
Relativty VR-гарнитура с открытым исходным кодом за 200 долларов

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

Дайджест интересных материалов для мобильного разработчика 362 (14 20 сентября)

20.09.2020 16:05:00 | Автор: admin
В этом дайджесте презентация Apple, инструменты и антипаттерны Android-разработки, ARM против x86 и кроссплатформа против нативной разработки, искусство рассказывания историй, секреты улучшения дизайна и многое другое!


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

Этот дайджест доступен в виде еженедельной рассылки. А ежедневно новости мы рассылаем в Telegram-канале.

iOS

(+22) О чем нам рассказали на ежегодной сентябрьской презентации Apple
(+5) Формальные грамматики на службе мобильного клиента
Подготовка к iOS 14
Что означает последнее обновление правил конфиденциальности Apple для вашего приложения
Apple представляет совершенно новый iPad Air с A14 Bionic, iPad 8-го поколения, Apple Watch Series 6 и Apple Watch SE
В App Store разрешили стриминг игр, но очень ограниченно
Социальная сеть на Swift UI
iOS 14 UISplitViewController: 5 проблем, с которыми вы можете столкнуться
Объяснение Диапазонов в Swift на примерах
Декодирование JSON в Swift с помощью Codable: практическое руководство
10 Pod-ов для использования в новом iOS-проекте
Улучшите свой UX с помощью Core Animations
Как обезопасить iOS-приложение от скриншотов и записи экрана?
WidgetKit: продвинутая разработка
10 мощных@Атрибутов в Swift
DTTextField: поле ввода с подсказками
ContainerController: выезжающая панель

Android

(+15) 20 инструментов Android-разработчика, о которых вы могли не знать
(+8) Navigation Component-дзюцу, vol. 2 вложенные графы навигации
(+2) Антипаттерн Репозиторий в Android
(+1) Обзор HMS Core 5.0: ещё больше возможностей для ML на мобильных устройствах и новые инструменты для аудио и видео
(0) Как с помощью возможности распознавания текста HUAWEI ML Kit реализовать функцию автоматического ввода номеров
Microsoft запускает Android-приложения в Windows 10
Android 11 вызвал проблемы в работе с Android Auto
Привет DataStore, пока SharedPreferences
Объяснение жизненного цикла Android Fragment
Изучение Jetpack Compose: модификатор отступа
Управление несколькими приложениями в одном проекте Android (Studio)
Распознавание жестов поворота в Android
Как определить обновление Android-приложения
Просто добавьте MVI с Orbit 2
Адаптируйте свое приложение к последним рекомендациям по обеспечению конфиденциальности
Как корутины формируют новые способы разработки
Автоматизация Code Review
Почему я решил написать свой собственный инструмент для тестирования UI
Понимаем внутреннее устройство Lottie рендеринг файла анимации
JetInstagram: Instagram на Jetpack Compose

Разработка

(+19) ARM против x86: В чем разница между двумя архитектурами процессоров?
(+9) Когда имеет смысл писать кроссплатформенные приложения: появление и исчезновение React Native в Lingualeo
(+8) Вставка реальных объектов в Unity с помощью Meshroom
(+7) UXD Реальность и будущее в дизайне или человек во главе всего
(+7) Crash-crash, baby. Автоматический мониторинг фатальных ошибок мобильных приложений
(+3) Как документ на мобильнике распознается: от простого к сложному
Podlodka #181: хантинг
Искусство рассказывания историй в разработке программного обеспечения
Дизайн приложений: примеры для вдохновения #17
Секрет улучшения дизайна: 4 способа сторителлинга
Исследование. Какую иконку выбрать для обозначения аккордеонов?
Искусство сторителлинга в разработке программного обеспечения
Руководство по минималистическому дизайну
Автоматизация публикации ваших приложений Flutter в Google Play с помощью GitHub Actions
Создаем приложения для чата на Flutter с помощью Firebase
Жизненный цикл разработки программного обеспечения: как мы создали новый Dropbox Plus
Барьеры на пути к разработке игр устранены
12 основных инструментов для разработчика мобильных приложений на Flutter
Начинаем работать с дополненной реальностью с помощью Unity AR Foundation Framework
Действительно ли я знаю программирование?
Mixin: мессенджер, кошелек и клиент для децентрализованной сети

Аналитика, маркетинг и монетизация

(+1) Как представить игру издателям и инвесторам
Почему следующая фаза роста Китая будет определяться потребителями и что это означает для рекламодателей
Bunch получил $20 млн. на социальный слой для игр
Министерство финансов США изучает безопасность игр Riot Games и Epic Games
make sense: О выборе фреймворков приоритизации, подходах к принятию решений и командной осознанности
Зачем бизнесу заказывать разработку приложения?
Отчет О состоянии рынка рекламы приложений для шоппинга в 2020 году
Как студия Donut Lab закрыла раунд инвестиций на $1.6M
Маркетинг приложений в апокалипсис: как работать с тревожными трендами?
Как я получил 200 000 загрузок приложений без платного маркетинга

AI, Устройства, IoT

(+29) Подключем новый Xiaomi Gateway 3 к Home Assistant без паяльника и смс
(+24) Как за два месяца пройти путь от начинающего питониста до сертифицированного TensorFlow-разработчика
(+12) ИК датчик движения на STM32
(+3) Автомобильное ПО: варианты стратегического развития
Facebook анонсировал Oculus Quest 2
Facebook выпустит смарт-очки вместе с Ray-Ban
Gameloft оживляет игрушки Kinder с помощью дополненной реальности
Nvidia покупает ARM
8 лучших No-Code платформ машинного обучения, которые вы должны использовать в 2020 году

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

Обзор HMS Core 5.0 ещё больше возможностей для ML на мобильных устройствах и новые инструменты для аудио и видео

14.09.2020 18:23:30 | Автор: admin


Привет, Хабр! Вместе с Harmony OS мы представили пятую версию HMS Core набора инструментов, с помощью которых можно разрабатывать приложения для экосистемы Huawei. Мы добавили новые возможности для работы с контентом, сделали акцент на безопасности данных, взаимодействии между устройствами и расширили возможности для AI-инструментов обо всём этом мы детально поговорили на нашей конференции HDC.Together, а в этой статье дадим обзор новых возможностей HMS.

Инструменты для работы с ML и AI


Основные сервисы для работы с AI входят в ML Kit и позволяют работать с текстом, голосом, картинками, AR/VR-технологиями. В HMS Core 5.0 мы увеличили количество поддерживаемых языков до 50 и можем выполнять перевод между 20 языками, при этом на вход принимаются как текстовые записи, так и голосовые. Также ML Kit может быть использован для отсеивания спама и всяких неприличных картинок.

С ML Kit можно конвертировать голосовые записи в текстовый формат и обратно, и мы расширили этот функционал с помощью Video Course Creator, который автоматически создаёт образовательные курсы: он берёт на вход образовательную программу, комментарии учителя и предустановленные аудио- и видеопараметры во время трансляции учитель может обращаться к нужным материалам и все записанные материалы компонуются в единый курс.



Особый акцент мы сделали на распознавание лица, жестов и поз. Благодаря этому с помощью ML Kit можно проводить аутентификацию по лицу система подключается через API и может быть использована для быстрого доступа к приложениям без каких-либо дополнительных действий.

Работать с инструментами AI и ML можно на разных уровнях:
  • Платформа для работы с технологиями искусственного интеллекта на мобильных устройствах HiAI позволяет обучать нейросети, создавать модели и конвертировать их в бинарный файл, который уже можно загрузить на NPU-чип.
  • Платформа Ability Gallery предоставляет разработчикам готовые сценарии использования AI и позволяет работать с большими данными в своих приложениях.

AR/VR


Наш AR-движок анализирует информацию об освещении, плоскости, форме объектов, типе поверхности, умеет искать заданные объекты в пространстве. Отдельно система может строить 3D-схему с помощью опорных точек и отслеживать человеческие движения, жесты и мимику. Например, для определения положения руки выделяется 21 точка, а для положения тела 23 точки. Сейчас система может распознавать 6 поз и анализировать сразу 2 человек.


С пятой версии HMS Core CameraKit обеспечивает различные режимы съёмки: широкую диафрагму, портретный режим, HDR, размытие фона, суперночной режим и иже с ними. Также появилась возможность использовать AI в фото- и видеосъемке для предварительного выбора фильтров и цветокоррекции.

Совместная работа устройств


В новой версии HMS Core появилось несколько новых движков для совместной работы разных устройств. Мы упростили взаимодействия между устройствами и умными вещами с помощью DeviceVirtualization-движка. Он преобразует периферийные устройства в виртуальные объекты и позволяет телефону работать с ними так же, как с обычными функциями.

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

Ещё один новый движок OneHop Kit работает с NFC и позволяет безопасно передавать данные между устройствами в одно касание. С его помощью можно синхронизировать устройства Huawei между собой, передавать файлы и открывать приложения на других устройствах с теми же настройками, что и на основном. Пока он работает только между телефонами и планшетами Huawei, поэтому для связи с другими устройствами мы предоставляем Share-движок, который обеспечивает скорость до 80 Мб/c по Bluetooth.

Видео и аудио



В новой версии мы разработали несколько новых SDK-инструментов для работы с аудио и видео. Audio Kit и Video Kit поддерживают основные форматы файлов и кодеки и могут легко встраиваться в приложение и выполнять чтение, сохранение и удаление списков, последовательностей воспроизведения, регулировать скорость воспроизведения. Всё аудио и видео защищается алгоритмами шифрования.

В Audio Kit теперь есть аудиодвижок для записи звука в высоким качестве с функциями оптимизации задержки и других инструментов. Видеодвижок, в свою очередь, поддерживает основные протоколы HTTP, HTTPS, HLS, DASH. Также он позволяет организовывать стриминг со сторонних сервисов с помощью Video Kit WisePlayer SDK.

На основе нашего сервиса Huawei Meetime появился Caas Kit для видеозвонков, который обеспечивает воспроизведение и передачу HD-изображения даже при нестабильном соединении. Также движок позволяет расшаривать экран, рисовать дудлы и с помощью технологий виртуализации передавать изображения на любые другие устройства, что можно использовать для бизнес-конференций.

Картинки и 3D-рендеринг


Для обработки изображений появился Image Kit он предлагает более 20 фильтров и визуальных эффектов, включая анимацию с рендерингом. В него входят 2 SDK: Image Vision SDK для работы с цветовыми фильтрами и Image Render SDK для использования эффектов анимации.

Scene Kit предназначен для работы с 3D-объектами: он использует метод физически корректного рендеринга (PBR) и позволяет приложениям подключаться через API и получать 3D-модели сложных объектов. Движок предлагает три сценария работы: SceneView для общих сцен (не-AR), ARView для общих сцен AR и FaceView для работы с лицами в сценах AR.

Аналитика и безопасность


В Сore 5.0 Huawei мы запустили систему тегов Dynamic Tag Manager (DTM) для отслеживания маркетинговой активности пользователей: она интегрируется как с самими сервисами Huawei, так и со сторонними платформами для отправки и обработки данных. С помощью DTM можно динамически обновлять теги в пользовательском веб-интерфейсе, отслеживать определённые события и отправлять данные на сторонние аналитические платформы. В наших системах главный акцент сделан на безопасность, поэтому движок DTM также используется как antifraud-система для отслеживания подозрительной активности.

Одной из фишек новой версии HMS стала аутентификация по лицу с помощью LocalAuthentication Engine. Он работает с инфракрасной камерой, которая строит модель по опорным точкам и производит аутентификацию с помощью ML Kit.

Наконец, мы уделили внимание и защите контента: для аудиофайлов используется шифрование кеша, для видео антилитическая система и аутентификация при воспроизведении, а для работы с камерой сквозное шифрование. Все данные HMS хранит в доверенной среде TEE, в рамках которой и выполняется шифрование.



Где узнать подробности?


Все записи конференции HDC.Together доступны по ссылке. Здесь можно послушать доклады о новинках платформы, основных сценариях работы с инструментами HMS и задать технические вопросы на Huawei Developer Forum или Stackoverflow c тегом huawei-mobile-services.
Подробнее..

Как мы автоматизировали разработку WL-приложений

21.09.2020 12:22:00 | Автор: admin
White Label это мобильные приложения, которые можно кастомизировать под любой бренд: оформить в фирменных цветах, выбрать необходимые блоки и функционал, добавить описание. Мы их выпускаем на основе Рамблер/кассы с 2015 года и в этой статье хотим рассказать, как у нас получилось автоматизировать и ускорить разработку WL.



Рамблер/касса онлайн-сервис и приложение для продажи билетов на концерты, в театр, кино, спортивные и другие мероприятия. Также мы разрабатываем другие B2B-предложения для партнеров: мобильный SDK, встроенные виджеты для сайтов и соцсетей, CRM-систему для аналитики продаж и аудитории, а также ряд технологических решений. Но сегодня мы остановимся только на WL.

Что было раньше


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



В идеальных условиях всего на разработку нового WL должно уходить минимум 2-3 дня по одному на разработчиков iOS и Android, плюс тестирование. Если сотрудник первый раз сталкивается с созданием WL-приложения, то выполнение задачи у него может занять до трех дней, что увеличивает общее время работы. Таким образом, для Рамблер/кассы создание WL-приложения это рутинная задача, которая ложилась на плечи разработчиков и отнимала у них ценное время.

Какие были варианты решений


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

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

Как работает решение


Мы запустили сайт с административной панелью в виде микрослужб новый раздел в существующей админке (CMS) по управлению продажами и витринами Рамблер/кассы. Мы разработали скрипты для iOS и Android, которые локально создают в проекте новое приложение и подают на него все нужные параметры.

В качестве брокера сообщений используется RabbitMQ, а все настройки сохраняются в архив и публикуются в рамблеровский Artifactory. После этого используется API GitLab, чтобы запустить процесс сборки в мобильных репозиториях.

На стороне бэкенда формируется архив с файлами в формате JSON, содержащими информацию, которую ввели в административной панели, и графикой. Триггер Gitlab CI вызывает pipeline, в параметрах к которому передает ссылку на архив из Artifactory. Скрипт, настроенный на билд машине и лежащий в корне проекта запускается с входным параметром-ссылкой.

Скрипт для iOS

Ruby-скрипт распаковывает архив, рассылает нужные данные по всему проекту, а после этого передает управление следующей задаче, которая коммитит изменения, обновляет поды и собирает сборку с последующей выкладкой в App Store.

Скрипт для Android

Джоба подтягивает нужные библиотеки jq и unzip. Библиотека unzip распаковывает архив, скрипт парсит json с помощью jq, создает новую папку в app модуле и проверяет наличие .jks файла для данного приложения.

Если приложение новое, то создается данный файл, после собирается релизное APK и скрипт отправляет в его firebase обновляет приложение в Маркете. Далее задача проверяет появился ли новый .jks файл, и, если он есть, то пушит его в GitLab.

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

На практике автоматизация создания WL-приложений выглядит так


  1. Партнёр Рамблер/кассы заполняет и передает заполненный единообразный бриф и брендбук компании, в которых собраны все тексты, изображения, шрифты, иконки, контактные данные и параметры будущего приложения.
  2. Менеджер проекта или сотрудник поддержки формирует и уточняет требования.
  3. Дизайнер на основе брендбука или сайта партнёра предлагает свое решение по цветам и иконкам.
  4. В админке менеджер проекта самостоятельно заполняет все нужные параметры для нового приложения (ID сервисов, цвета, конфигурационные файлы, иконки и т.д.).
  5. После заполнения необходимых параметров менеджер проекта нажимает кнопку Создать приложение, а затем готовая сборка передается на тестирование.
  6. Тестировщик тестирует приложение и публикует его в App Store и Google Play с помощью CI.



Что получилось


Мы максимально автоматизировали создание WL-приложений. Раньше сам процесс разработки занимал 2-3 дня и отнимал ресурсы программистов, а теперь менеджер за 15 минут вбивает все данные и через примерно 20 минут сборка автоматически создается и передается QA на тестирование. Наши партнёры получают все возможности, которые есть в Рамблер/кассе, а мы экономим время, ресурсы и минимизируем ошибки.
Подробнее..

Navigation Component-дзюцу, vol. 3 Corner-кейсы

23.09.2020 10:08:04 | Автор: admin


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


Это третья и заключительная статья в цикле про различные кейсы навигации с Navigation Component-ом. Вы также можете ознакомиться с первой и второй частями



Если вы работаете с большим приложением, вероятно, вы уже разбили его на модули. Неважно, как именно. Может быть, вы создаёте отдельные модули для логики и UI, а может храните всю логику фичи (от взаимодействия с API до логики presentation-слоя) в одном модуле. Главное у вас могут быть кейсы, когда требуется осуществить навигацию между двумя независимыми модулями.


Где на схеме приложения кейсы с навигацией?


На картинке мы видим, что у нас есть как минимум два модуля: модуль :vacancy с одним экраном и модуль :company с двумя экранами вложенного flow. В рамках моего примера я построил навигацию из модуля :vacancy в модуль :company, которые не связаны друг с другом.


Существует три способа как это сделать, разберём их один за другим.


App-модуль + интерфейсы


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


Структура вашего приложения в этом способе


Структура приложения будет стандартной: есть app-модуль, который знает обо всех feature-модулях, есть feature-модули, которые не знают друг о друге. В этом способе ваши feature-модули пребывают в священном неведении о Navigation Component, и для навигации они будут определять интерфейсы примерно вот такого вида:


// ::vacancy moduleinterface VacancyRouterSource {    fun openNextVacancy(vacancyId: String)    // For navigation to another module    fun openCompanyFlow()}

А ваш app-модуль будет реализовывать эти интерфейсы, потому что он знает обо всех action-ах и навигации:


fun initVacancyDI(navController: NavController) {  VacancyDI.vacancyRouterSource = object : VacancyRouterSource {      override fun openNextVacancy(vacancyId: String) {          navController.navigate(              VacancyFragmentDirections                .actionVacancyFragmentToVacancyFragment(vacancyId = vacancyId)          )      }      override fun openCompanyFlow() {          initCompanyDI(navController)          navController.navigate(R.id.action__VacancyFragment__to__CompanyFlow)      }  }}

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


  • дополнительную работу в виде определения интерфейсов, реализаций, организации DI для проброса этих интерфейсов в ваши feature-модули;
  • отсутствие возможности использовать использовать Safe Args плагин, делегат navArgs, сгенерированные Directions, и другие фишки Navigation Component-а в feature-модулях, потому что эти модули ничего не знают про библиотеку.

Сомнительный, в общем, способ.


Графы навигации в feature-модулях + диплинки


Второй способ вынести отдельные графы навигации в feature-модули и использовать поддержку навигации по диплинкам (она же навигация по URI, которую добавили в Navigation Component 2.1).


Структура вашего приложения в этом способе


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


Но теперь ваш app-модуль не обязан содержать весь граф навигации приложения, он может содержать только его часть. А остальные кусочки будут содержать именно feature-модули.


<navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:id="@+id/company_flow__nav_graph"    app:startDestination="@id/CompanyFragment">    <fragment        android:id="@+id/CompanyFragment"        android:name="company.CompanyFragment">        <deepLink app:uri="companyflow://company" />        <!-- Or with arguments -->        <argument android:name="company_id" app:argType="long" />        <deepLink app:uri="companyflow://company" />        <action            android:id="@+id/action__CompanyFragment__to__CompanyDetailsFragment"            app:destination="@id/CompanyDetailsFragment" />    </fragment>    <fragment        android:id="@+id/CompanyDetailsFragment"        android:name="company.CompanyDetailsFragment" /></navigation>

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


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


// ::vacancy modulefragment_vacancy__button__open_company_flow.setOnClickListener {  // Navigation through deep link  val companyFlowUri = "companyflow://company".toUri()  findNavController().navigate(companyFlowUri)}

Плюс этого метода в том, что это самый простой способ навигации между двумя независимыми модулями. А минус что вы не сможете использовать Safe Args, или сложные типы аргументов (Enum, Serializable, Parcelable) при навигации между фичами.


P.S. Есть, конечно, вариант сериализовать ваши сложные структуры в JSON и передавать их в качестве String-аргументов в диплинк, но это как-то Странно.


Общий модуль со всем графом навигации


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


Структура вашего приложения в этом способе


У нас по-прежнему есть app-модуль, но теперь его задача просто подсоединить к себе все feature-модули; он больше не хранит в себе граф навигации. Весь граф навигации теперь располагается в специальном модуле, который ничего не знает о feature-модулях. Зато каждый feature-модуль знает про common navigation.


В чём соль? Несмотря на то, что common-модуль не знает о реализациях ваших destination-ов (фрагментах, диалогах, activity), он всё равно способен объявить граф навигации в XML-файлах! Да, Android Studio начинает сходить с ума: все имена классов в XML-е горят красным, но, несмотря на это, все нужные классы генерируются, Safe Args плагин работает как нужно. И так как ваши feature-модули подключают к себе common-модуль, они могут свободно использовать все сгенерированные классы и пользоваться любыми action-ами вашего графа навигации.


Плюс этого способа наконец-то можно пользоваться всеми возможностями Navigation Component-а в любом feature-модуле. Из минусов:


  • добавился ещё один модуль в critical path каждого feature-модуля, которому потребовалась навигация;
  • отсутствует автоматический рефакторинг имён: если вы поменяете имя класса какого-нибудь destination-а, вам нужно будет не забыть, что надо поправить его в common-модуле.

Выводы по навигации в многомодульных приложениях


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

Работа с диплинками


Практически каждое большое приложение должно уметь поддерживать диплинки. И практически каждый Android-разработчик мечтал о простом способе работы с этими глубокими ссылками. Окей, я мечтал. И казалось, что Navigation Component ровно то, что нужно.


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


Какую именно часть?


У меня было три кейса с диплинками, которые я хотел реализовать с помощью Navigation Component.


  • Открытие определённой вкладки нижней навигации допустим, я хочу через диплинк открыть вторую вкладку на главном экране после Splash-экрана

Посмотреть на картинке

Допустим, я хочу через диплинк открыть вкладку Favorites нижней навигации на главном экране после Splash-экрана:



  • Открытие определённого экрана ViewPager-а внутри конкретной вкладки нижней навигации

Посмотреть на картинке

Пусть я хочу открыть определённую вкладку ViewPager-а внутри вкладки Responses:



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

Посмотреть на картинке

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



Сразу скажу, что у меня не получилось нормально реализовать ни один из этих кейсов. И работа с диплинками в Navigation Component стала для меня самым большим разочарованием.



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


<navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:id="@+id/app_nav_graph"    app:startDestination="@id/SplashFragment">    <fragment        android:id="@+id/SplashFragment"        android:name="ui.splash.SplashFragment" />    <fragment        android:id="@+id/MainFragment"        android:name="ui.main.MainFragment">        <deepLink app:uri="www.example.com/main" />    </fragment></navigation>

Затем я, следуя документации, добавил граф навигации с диплинком в Android Manifest:


<manifest xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    package="com.aaglobal.jnc_playground">    <application android:name=".App">        <activity android:name=".ui.root.RootActivity">            <nav-graph android:value="@navigation/app_nav_graph"/>            <intent-filter>                <action android:name="android.intent.action.MAIN" />                <category android:name="android.intent.category.LAUNCHER" />            </intent-filter>        </activity>    </application></manifest>

А потом решил проверить, работает ли то, что я настроил при помощи простой adb-команды:


adb shell am start \  -a android.intent.action.VIEW \  -d "https://www.example.com/main" com.aaglobal.jnc_playground

И-и-и нет. Ничего не завелось. Я получил краш приложения с уже знакомым исключением IllegalStateException: FragmentManager is already executing transactions. Дебаггер указывал на код, связанный с настройкой нижней навигации, поэтому я решил просто обернуть эту настройку в очередной Handler.post:


// MainFragment.kt  fragment with BottomNavigationViewoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) {    super.onViewCreated(view, savedInstanceState)    if (savedInstanceState == null) {        safeSetupBottomNavigationBar()    }}private fun safeSetupBottomNavigationBar() {    Handler().post {        setupBottomNavigationBar()    }}

Это исправило краш, но приложение всё равно работало неправильно: запустив диплинк, мы пропустили Splash-экран, он просто не запускался. А это означает, что не отрабатывал код, который отвечал за инициализацию моего приложения.


Это произошло, потому что в нашем случае путь диплинка был таким: мы запустили приложение, запустилась его единственная Activity. В вёрстке этой activity мы инициализировали первый граф навигации. В этом графе оказался элемент, который удовлетворял URI, мы отправили его через adb-команду вуаля, он сразу и открылся, проигнорировав указанный в графе startDestination.


Тогда я решил перенести диплинк в другой граф внутрь вкладки нижней навигации.


<navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:id="@+id/menu__search"    app:startDestination="@id/SearchContainerFragment">    <fragment        android:id="@+id/SearchContainerFragment"        android:name="tabs.search.SearchContainerFragment">        <deepLink app:uri="www.example.com/main" />        <action            android:id="@+id/action__SearchContainerFragment__to__CompanyFlow"            app:destination="@id/company_flow__nav_graph" />        <action            android:id="@+id/action__SearchContainerFragment__to__VacancyFragment"            app:destination="@id/vacancy_nav_graph" />    </fragment></navigation>

И, запустив приложение, я получил ЭТО:


Посмотреть на ЭТО


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


И что самое неприятное во всей этой истории это не баг, а фича.


Если почитать внимательно документацию про работу с диплинками в Navigation Component, можно найти следующий кусочек:


When a user opens your app via an explicit deep link, the task back stack is cleared and replaced with the deep link destination.

То есть наш back stack специально очищается, чтобы Navigation Component-у было удобнее работать с диплинками. Говорят, что когда-то давно, в бета-версии библиотеки всё работало адекватнее.


Мы можем это исправить. Корень проблемы в методе handleDeepLink NavController-а:


Кусочек handleDeepLink
public void handleDeepLink(@Nullable Intent intent) {    // ...    if ((flags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {        // Start with a cleared task starting at our root when we're on our own task        if (!mBackStack.isEmpty()) {            popBackStackInternal(mGraph.getId(), true);        }        int index = 0;        while (index < deepLink.length) {            int destinationId = deepLink[index++];            NavDestination node = findDestination(destinationId);            if (node == null) {                final String dest = NavDestination.getDisplayName(mContext, destinationId);                throw new IllegalStateException("Deep Linking failed:"                        + " destination " + dest                        + " cannot be found from the current destination "                        + getCurrentDestination());            }            navigate(node, bundle,                    new NavOptions.Builder().setEnterAnim(0).setExitAnim(0).build(), null);        }        return true;    }}

Чтобы переопределить это поведение, нам потребуется:


  • почти полностью скопировать к себе исходный код Navigation Component;
  • добавить свой собственный NavController с исправленной логикой (добавление исходного кода библиотеки необходимо, так как от NavController-а зависят практически все элементы библиотеки) назовём его FixedNavController;
  • заменить все использования исходного NavController-а на FixedNavController.

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


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


Покажи гифку


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


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



Если у вас будет свой собственный NavController, корректно обрабатывающий диплинки, реализовать этот кейс будет просто.


В NavController-е есть специальный булевский флажок isDeepLinkHandled, который говорит нам, что текущий NavController успешно обработал диплинк. Вы могли бы добавить диплинк, ведущий на фрагмент, который содержит в себе ViewPager, затем написать примерно вот такой код, чтобы перейти на нужную вкладку:


if (findMyNavController().isDeepLinkHandled && requireActivity().intent.data != null) {    val uriString = requireActivity().intent.data?.toString()    val selectedPosition = when {        uriString == null -> 0        uriString.endsWith("favorites") -> 0        uriString.endsWith("subscribes") -> 1        else -> 2    }    fragment_favorites_container__view_pager.setCurrentItem(selectedPosition, true)}

Но, опять же, это будет доступно только в случае, если вы уже добавили к себе в кодовую базу свою реализацию NavController-а, ведь флаг isDeepLinkHandled является private-полем. Ок, можно достучаться до него через механизм reflection-а, но это уже другая история.



Navigation Component не поддерживает диплинки с условием из коробки. Если вы хотите поддержать такое поведение, Google предлагает действовать следующим образом:


  • через диплинк открыть экран, который требует авторизацию;
  • на этом экране проверить, авторизован ли пользователь, если нет открыть флоу авторизации поверх нужного экрана;
  • пройти auth flow, вернуть результат из вложенного графа и т.д., и т.п.

Возможности глобально решить мою задачу средствами Navigation Component-а я не нашёл.


Выводы по работе с диплинками в Navigation Component


  • Работать с ними больно, если требуется добавлять дополнительные действия или условия.
  • Объявлять диплинки ближе к месту их назначения классная идея, в разы удобнее AndroidManifest-а со списком поддерживаемых ссылок.

Бонус-секция кейсы БЕЗ проблем


Чтобы разбавить негатив от предыдущей секции, я покажу пару кейсов, с которыми вообще не было проблем. А я их ждал. Но их не было.



Допустим, у вас есть экран вакансий, с которого вы можете перейти на другую вакансию.


Где на схеме приложения этот кейс?


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


<fragment  android:id="@+id/VacancyFragment"  android:name="com.aaglobal.jnc_playground.ui.vacancy.VacancyFragment"  android:label="Fragment vacancy"  tools:layout="@layout/fragment_vacancy">  <argument      android:name="vacancyId"      app:argType="string"      app:nullable="false" />  <action      android:id="@+id/action__VacancyFragment__to__VacancyFragment"      app:destination="@id/VacancyFragment" /></fragment>

И этого оказалось достаточно новый экран открывался поверх старого, при нажатии на кнопку Back навигация была корректной. Если бы я захотел, чтобы каждый новый экран открывался вместо текущего, было бы достаточно добавить атрибут popUpTo к моему action-у.



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


Где на схеме приложения этот кейс?


Я добавил контейнер для будущего фрагмента со списком в вёрстку вкладки нижней навигации:


<LinearLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical">    <TextView        android:id="@+id/fragment_favorites_container__text__title"        style="@style/LargeTitle"        android:text="Favorites container" />    <androidx.fragment.app.FragmentContainerView        android:id="@+id/fragment_favorites_container__container__recommend_vacancies"        android:layout_width="match_parent"        android:layout_height="match_parent" /></LinearLayout>

А затем в runtime-е добавил нужный мне фрагмент в этот контейнер:


class FavoritesContainerFragment : Fragment(R.layout.fragment_favorites_container) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        childFragmentManager.attachFragmentInto(          containerId = R.id.fragment_container_view,          fragment = createVacancyListFragment()        )    }}

Метод attachFragmentInfo на childFragmentManager это extension-метод, который просто оборачивает всю работу с транзакциями, не более того.


А вот как я создал фрагмент:


class FavoritesContainerFragment : Fragment(R.layout.fragment_favorites_container) {    // ...    private fun createVacancyListFragment(): Fragment {        return VacancyListFragment.newInstance(          vacancyType = "favorites_container",          vacancyListRouterSource = object : VacancyListRouterSource {              override fun navigateToVacancyScreen(item: VacancyItem) {                  findNavController().navigate(                      R.id.action__FavoritesContainerFragment__to__VacancyFragment,                      VacancyFragmentArgs(vacancyId = "${item.name}|${item.id}").toBundle()                  )              }        }     }}

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



Пусть у меня есть несколько BottomSheetDialog-ов, между которыми я хочу перемещаться с помощью Navigation Component.


Где на схеме приложения этот кейс?


Год назад с таким кейсом были какие-то проблемы, но сейчас всё работает как надо. Можно легко объявить какой-то dialog в качестве destination-а в вашем графе навигации, можно добавить action для открытия диалога из другого диалога.


<navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:id="@+id/menu__favorites"    app:startDestination="@id/FavoritesContainerFragment">   <dialog        android:id="@+id/ABottomSheet"        android:name="ui.dialogs.dialog_a.ABottomSheetDialog">        <action            android:id="@+id/action__ABottomSheet__to__BBottomSheet"            app:destination="@id/BBottomSheet"            app:popUpTo="@id/ABottomSheet"            app:popUpToInclusive="true" />    </dialog>    <dialog        android:id="@+id/BBottomSheet"        android:name="ui.dialogs.dialog_b.BBottomSheetDialog">        <action            android:id="@+id/action__BBottomSheet__to__ABottomSheet"            app:destination="@id/ABottomSheet"            app:popUpTo="@id/BBottomSheet"            app:popUpToInclusive="true" />    </dialog></navigation>

Диалоги создавались как нужно, закрывались вовремя, навигация по кнопке Back отрабатывала как ожидалось.


Выводы по бонус-секции


Кейсы без проблем существуют.


Подведём итоги


На данный момент нет никакой причины переводить большое приложение на Navigation Component. Слишком много проблем, слишком много костылей, постоянно нужно выдумывать что-то для осуществления не самых сложных кейсов навигации. Сам факт, что я ухитрился написать так много текста про проблемы с Navigation Component-ом, что-то да говорит.


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


Пример приложения на Github-е лежит здесь.


Полезные ссылки по теме


Подробнее..

Чем опасен postDelayed

24.09.2020 08:13:47 | Автор: admin

Часто из-за особенностей работы android системы и sdk, нам необходимо подождать, когда определённая часть системы будет сконфигурирована или произойдёт какое-то необходимое нам событие. Зачастую это является костылём, но иногда без них никак, особенно в условиях дедлайнов. Поэтому во многих проектах для этого использовался postDelayed. Под катом рассмотрим, чем же он так опасен и что с этим делать.


Проблема


Для начала рассмотрим как обычно используют postDelayed():


override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        view.postDelayed({            Log.d("test", "postDelayed")            // do action        }, 100)}

С виду всё хорошо, но давайте изучим этот код повнимательнее:


1) Это отложенное действие, выполнение которого мы будем ожидать через некоторое время. Зная насколько динамично пользователь может совершать переходы между экранами, данное действие должно быть отменено при смене фрагмента. Однако, этого здесь не происходит, и наше действие выполнится, даже если текущий фрагмент будет уничтожен.
Проверить это просто. Создаём два фрагмента, при переходе на второй запускаем postDelayed с большим временем, к примеру 5000 мс. Сразу возвращаемся назад. И через некоторое время видим в логах, что действие не отменено.


2) Второе "вытекает" из первого. Если в данном runnable мы передадим ссылку на property нашего фрагмента, будет происходить утечка памяти, поскольку ссылка на runnable будет жить дольше, чем сам фрагмент.


3) Третье и основное почему я об этом задумался:
Падения приложения, если мы обращаемся ко view после onDestroyView
synthitec java.lang.NullPointerException, поскольку кеш уже очищен при помощи _$_clearFindViewByIdCache, а findViewById отдаёт null
viewBinding java.lang.IllegalStateException: Can't access the Fragment View's LifecycleOwner when getView() is null


Что же делать?


1 Если нам нужные размеры view использовать doOnLayout или doOnNextLayout


2 Перенести ожидание в компонент, ответственный за бизнес-логику отображения (Presenter/ViewModel или что-то другое). Он в свою очередь должен устанавливать значения во фрагмент в правильный момент его жизненного цикла или отменять действие.


3 Использовать безопасный стиль.


Необходимо отписываться от нашего действия перед тем, как view будет отсоединено от window.


    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)         Runnable {            // do action        }.let { runnable ->            view.postDelayed(runnable, 100)            view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {                override fun onViewAttachedToWindow(view: View) {}                override fun onViewDetachedFromWindow(view: View) {                    view.removeOnAttachStateChangeListener(this)                    view.removeCallbacks(runnable)                }            })        }    }

Обычный doOnDetach нельзя использовать, поскольку view может быть ещё не прикреплено к window, как к примеру в onViewCreated. И тогда наше действие будет сразу же отменено.


Где то во View.kt:


inline fun View.doOnDetach(crossinline action: (view: View) -> Unit) {    if (!ViewCompat.isAttachedToWindow(this)) { // выполнится это условие        action(this)  // и здесь мы сразу же отпишемся от действия    } else {        addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {            override fun onViewAttachedToWindow(view: View) {}            override fun onViewDetachedFromWindow(view: View) {                removeOnAttachStateChangeListener(this)                action(view)            }        })    }}

Или же обобщим в extension:


fun View.postDelayedSafe(delayMillis: Long, block: () -> Unit) {        val runnable = Runnable { block() }        postDelayed(runnable, delayMillis)        addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {            override fun onViewAttachedToWindow(view: View) {}            override fun onViewDetachedFromWindow(view: View) {                removeOnAttachStateChangeListener(this)                view.removeCallbacks(runnable)            }        })}

В принципе на этом можно остановится. Все проблемы решены. Но этим мы добавляем ещё один тип асинхронного выполнения к нашему проекту, что несколько усложняет его. Сейчас в мире Native Android есть 2 основных решения для асинхронного выполнения кода Rx и Coroutines.
Попробуем использовать их.
Сразу оговорюсь, что не претендую на 100% правильность по отношению к вашему проекту. В вашем проекте это может быть по другому/лучше/короче.


Coroutines


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


class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes), CoroutineScope by MainScope() {    override fun onDestroyView() {        super.onDestroyView()        coroutineContext[Job]?.cancelChildren()    }    override fun onDestroy() {        super.onDestroy()        cancel()    }}

Нам необходимо отменять все дочерние задачи в onDestroyView, но при этом не закрывать scope, поскольку после этого возможно вновь создание View без пересоздания Fragment. К примеру при роутинге вперёд на другой Fragment и после этого назад на текущий.


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


Все подготовительные работы сделаны.
Перейдём к самой замене postDelayed:


fun BaseFragment.delayActionSafe(delayMillis: Long, action: () -> Unit): Job? {    view ?: return null    return launch {        delay(delayMillis)        action()    }}

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


RX


В RX за отмену подписок отвечает класс Disposable, но в RX нет Structured concurrency в отличии от coroutine. Из-за этого приходится прописывать это всё самому. Выглядит обычно это примерно так:


interface DisposableHolder {    fun dispose()    fun addDisposable(disposable: Disposable)}class DisposableHolderImpl : DisposableHolder {    private val compositeDisposable = CompositeDisposable()    override fun addDisposable(disposable: Disposable) {        compositeDisposable.add(disposable)    }    override fun dispose() {        compositeDisposable.clear()    }}

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


class BaseFragment(@LayoutRes layoutRes: Int) : Fragment(layoutRes),    DisposableHolder by DisposableHolderImpl() {    override fun onDestroyView() {        super.onDestroyView()        dispose()    }    override fun onDestroy() {        super.onDestroy()        dispose()    }}

И сам extension:


fun BaseFragment.delayActionSafe(delayMillis: Long, block: () -> Unit): Disposable? {    view ?: return null    return Completable.timer(delayMillis, TimeUnit.MILLISECONDS).subscribe {        block()    }.also {        addDisposable(it)    }}

В заключении


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

Подробнее..

Перевод Полируем UI в Android StateListAnimator

08.09.2020 18:17:15 | Автор: admin
Привет, хабр! В преддверии старта курса Android Developer. Professional мы подготовили для вас перевод еще одного интересного материала.




Большую часть времени разработки нашего Android-приложения мы тратим отнюдь не на работу над пользовательским интерфейсом мы просто накидываем вьюх и начинаем писать код. Я заметил, что большинство из нас не особо заботится о пользовательском интерфейсе. И я считаю, что это в корне неправильно. Разработчики мобильных приложений должны заботиться также и о UI/UX. Я не говорю будьте экспертом в мобильном UI, но вы должны понимать язык дизайна и его концепции.

Ранее я написал статью о тенях в материальном дизайне и получил много хороших отзывов. Я хочу поблагодарить всех вас. Освоение теней в Android рассказывает о высоте (elevation) и тени (shadow) в Android. Там же я показал, как дополнял ими свою UI библиотеку с открытым исходным кодом. (Scaling Layout).

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

Содержание


В этой статье рассматриваются следующие темы:


Состояния Drawable


В Android есть 17 различных состояний для Drawable.



Возможно, мы даже никогда не встречали некоторые из них. Я не собираюсь углубляться в каждое состояние. В большинстве случаев мы используем pressed, enabled, windows focused, checked и т. д. Если мы не объявляем состояние для drawable, то подразумевается, что это состояние по умолчанию в Android.

Нам нужно понимать эти состояния, чтобы написать наш собственный StateListDrawable.

StateListDrawable


Это, по сути, список drawable элементов, где каждый элемент имеет свое собственное состояние. Для создания StateListDrawable нам нужно создать XML-файл в папке res/drawable.

<item android:drawable="@drawable/i" android:state_pressed="true"/>


Это элемент (item). Он имеет два свойства. Drawable и State.

<selector>   <item       android:drawable="@drawable/p"       android:state_pressed="true"/>   <item       android:drawable="@drawable/default"/></selector>


Это StateListDrawable. Если мы не объявляем состояние (state) для элемента, как я уже упоминал ранее, это означает, что это состояние по умолчанию.

Могу ли я использовать ShapeDrawable?


Да. Вместо использования android:drawable вы можете добавить к своему элементу произвольную форму. Вот элемент с ShapeDrawable.


StateListDrawable

Вы можете использовать StateListDrawable с уровня API 1. Таким образом, для StateListDrawable нет ограничений уровня API.

<View   android:layout_width="50dp"   android:layout_height="50dp"   android:foreground="@drawable/state_list_drawable"   android:clickable="true"/>


Вот и все. Теперь у нашего вью есть состояние. Когда пользователь нажимает на него, его цвет будет изменен. Когда пользователь отпустит его, он будет иметь состояние и цвет по умолчанию.

Но погодите секундочку. Clickable? Зачем мы добавили этот атрибут? Нам что, еще и его добавлять нужно? Да. Но только для пользовательских вью. Чтобы это выяснить, нужно время. Кнопки отлично работают без добавления clickable, потому что они по умолчанию clickable. Но если вы хотите использовать StateListDrawable для View, ImageView, Custom View и т. д., вам необходимо добавить атрибут clickable.



StateListDrawable

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

StateListAnimator


Помните, что когда вы нажимаете FloatingActionButton, его Z значение увеличивается из-за анимации. Это StateListAnimator так сказать за кадром. У некоторых виджетов материального дизайна есть собственный StateListAnimator внутри.

Давайте проясним это с помощью вопроса на StackOverflow.



(Как удалить границу/тень с кнопок lollipop).

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


(Lollipop имеет небольшую неприятную функцию, называемую stateListAnimator, которая обрабатывает высоту кнопок, производя тени.

Удалите stateListAnimator, чтобы избавиться от теней.

У вас есть несколько вариантов как сделать это:

В коде:

button.setStateListAnimator(null);)




Итак, а как мы можем создать его?

Чтобы понять StateListAnimator, нам нужно понять анимацию свойств объекта (property animation). Я не собираюсь углубляться в анимацию свойств в этой статье. Но по крайней мере, я хочу показать вам основы.

Анимация свойств


Вот самый простой пример свойства в объекте. X это свойство.

class MyObject{    private int x;    public int getX() {       return x;   }    public void setX(int x) {       this.x = x;   }}


Система property animation это надежный фреймворк, который позволяет анимировать практически все. Вы можете определить анимацию для изменения любого свойства объекта с течением времени, независимо от того, отображается ли он на экране или нет. Анимация свойства изменяет значение свойства (поля в объекте) в течение заданного периода времени.



X это свойство. Т время. Во время анимации свойство X обновляется в заданное время. В целом так работает анимация свойств. Вместо коробки может быть вью или любой объект.

ValueAnimator это базовый класс для анимации свойств. Вы можете настроить слушатель на обновления ValueAnimator и наблюдать за изменениями свойств.

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

  • У вас есть объект (любой класс с каким-нибудь свойством).
  • Вы не хотите наблюдать за слушателем ValueAnimator.
  • Вы хотите обновлять свойство объекта автоматически.


Итак, если у нас есть вью (которое является объектом), и мы хотим обновить свойство вью (координата x, координата y, rotation, translation или любое другое свойство, для которого у вью есть геттер/сеттер), мы можем использовать ObjectAnimator. Продолжим создание StateListAnimator.

<selector>    <item android:state_pressed="true">      <objectAnimator           android:duration="200"           android:propertyName="translationZ"           android:valueTo="6dp"           android:valueType="floatType" />   </item>    <item>      <objectAnimator           android:duration="200"           android:propertyName="translationZ"           android:valueTo="0dp"           android:valueType="floatType"/>   </item> </selector>



Кнопка FAB анимирует свое свойство translationZ при нажатии и разжатии.

Как я сказал ранее, мы можем использовать свойство объекта напрямую, не наблюдая за изменениями в аниматоре. Каждый View имеет свойство translationZ. Таким образом, мы можем напрямую анимировать translationZ с помощью ObjectAnimator.

Мы можем также объединить несколько <objectAnimator>-ов в <set>. Изменим еще одно свойство View. Scale X и Scale Y.

Вот результат! Теперь она также увеличивается при нажатии пользователем. А вот коммит.



Вы также можете определить другие свойства в своем animator.xml. Здесь вы можете найти больше информации об использовании ObjectAnimator.

Вот и все. Я планирую написать еще что-нибудь о ValueAnimator и ObjectAnimator. Это отличное API для анимации объекта.

Успешного вам кодинга!
Подробнее..

Вышел Android 11 с единым разделом для мессенджеров, записью экрана и управлением smart-устройствами

09.09.2020 10:06:55 | Автор: admin


Корпорация Google опубликовала релиз мобильной ОС Android 11. Исходные тексты операционной системы размещены в Git-репозитории проекта (ветка android-11.0.0_r1).

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

Что нового в Android 11?




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



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

Наконец-то появилась встроенная функция записи экрана, которая поддерживает запись звука как от микрофона, так и с самого устройства.

Добавлена поддержка беспроводного подключения к Android Auto.



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

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

Есть и меню управления умными устройствами. Активировать его можно по долгому нажатию кнопки питания. В этом же меню разместили раздел с картами Google Pay.



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



ОС сначала обновится на смартфонах компаний Google, OnePlus, Xiaomi, Realme и Oppo. Остальные получат обновления в ближайшие месяцы.

Подробнее..

Navigation Component-дзюцу, vol. 1 BottomNavigationView

09.09.2020 12:15:30 | Автор: admin


Два года назад на Google I/O Android-разработчикам представили новое решение для навигации в приложениях библиотеку Jetpack Navigation Component. Про маленькие приложения уже было сказано достаточно, а вот о том, с какими проблемами можно столкнуться при переводе большого приложения на Navigation Component, информации практически нет.


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


Это текстовая версия моего выступления в рамках серии митапов по Android 11 в Android Academy. Само выступление было на английском, статью пишу на русском. Кому удобнее смотреть велкам.


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


Disclaimer


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


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



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


Кейсы с BottomNavigationView


Когда я только-только услышал про Navigation Component, мне стало интересно: как будет работать BottomNavigationView и как Google подружит несколько отдельных back stack-ов в разных вкладках. Два года назад с этим кейсом были некоторые проблемы, и я решил проверить, как там обстоят дела сегодня.


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


Где на схеме приложения кейсы с навигацией?


Первый опыт


Я установил Android Studio 4.1 Beta (последнюю более-менее стабильную версию на тот момент) и попробовал шаблон приложения с нижней навигацией. Начало было многообещающим.


  • Мне сгенерировали Activity в качестве контейнера для хоста навигации и нижней навигации

Вёрстка Activity из шаблона
<androidx.constraintlayout.widget.ConstraintLayout    android:id="@+id/container">    <com.google.android.material.bottomnavigation.BottomNavigationView        android:id="@+id/nav_view"        app:menu="@menu/bottom_nav_menu" />    <fragment        android:id="@+id/nav_host_fragment"        android:name="androidx.navigation.fragment.NavHostFragment"        app:defaultNavHost="true"        app:navGraph="@navigation/mobile_navigation" /></androidx.constraintlayout.widget.ConstraintLayout>

Я убрал шумовые атрибуты, чтобы было проще читать.


Стандартный ConstraintLayout, в который добавили BottomNavigationView и тэг <fragment> для инициализации NavHostFragment-а (Android Studio, кстати, подсвечивает, что вместо фрагмента лучше использовать FragmentContainerView).


  • Для каждой вкладки BottomNavigationView был создан отдельный фрагмент

Граф навигации из шаблона
<navigation    android:id="@+id/mobile_navigation"    app:startDestination="@+id/navigation_home">    <fragment        android:id="@+id/navigation_home"        android:name="com.aaglobal.graph_example.ui.home.HomeFragment"/>    <fragment        android:id="@+id/navigation_dashboard"        android:name="com.aaglobal.graph_example.ui.dashboard.DashboardFragment"/>    <fragment        android:id="@+id/navigation_notifications"        android:name="com.aaglobal.graph_example.ui.notifications.NotificationsFragment"/></navigation>

Все фрагменты были добавлены в качестве отдельных destination-ов в общий граф навигации.


  • А ещё в проект был добавлен файл-ресурс для описания меню BottomNavigationView

@menu-ресурс для описания табов
<menu xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android">    <item        android:id="@+id/navigation_home"        android:icon="@drawable/ic_home_black_24dp"        android:title="@string/title_home" />    <item        android:id="@+id/navigation_dashboard"        android:icon="@drawable/ic_dashboard_black_24dp"        android:title="@string/title_dashboard" />    <item        android:id="@+id/navigation_notifications"        android:icon="@drawable/ic_notifications_black_24dp"        android:title="@string/title_notifications" /></menu>

При этом я заметил, что идентификаторы элементов меню должны совпадать с идентификаторами destination-ов в графе навигации. Не самая очевидная связь между табами BottomNavigationView и фрагментами, но работаем с тем, что есть.


Пора запускать приложение


После создания приложения из шаблона я запустил его и сразу столкнулся с двумя проблемами.


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


А ну-ка покажи


Для проверки я добавил во вкладку Dashboard простенькую ViewModel со счётчиком. На гифке видно, как я переключаюсь со вкладки Home на вкладку Dashboard, увеличиваю счётчик до четырёх. После этого я переключился обратно на вкладку Home и вновь вернулся на Dashboard. Счётчик сбросился.


Баг с описанием этой проблемы уже два года висит в Issue Tracker-е. Чтобы решить её, Google-у потребовалось серьёзно переработать внутренности фреймворка Fragment-ов, чтобы поддержать возможность работать с несколькими back stack-ами одному FragmentManager-у. Недавно на Medium вышла статья Ian Lake, в которой он рассказывает, что Google серьёзно продвинулись в этом вопросе, так что, возможно, фикс проблемы с BottomNavigationView не за горами.


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


А ну-ка покажи


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


Не самое лучшее первое впечатление, подумал я. И начал искать фикс.


У нас есть workaround


Решение этих проблем живёт в специальном репозитории Google-а с примерами работы с Architecture Components, в проекте NavigationAdvancedSample.


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


  • Во-первых, для каждой вкладки вводится отдельный, независимый граф навигации

Граф навигации для одной из вкладок
<?xml version="1.0" encoding="utf-8"?><navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    xmlns:tools="http://personeltest.ru/away/schemas.android.com/tools"    android:id="@+id/navigation_home"    app:startDestination="@id/HomeFragment">    <fragment        android:id="@+id/HomeFragment"        android:name="com.aaglobal.jnc_playground.ui.home.HomeFragment"        android:label="@string/title_home"        tools:layout="@layout/fragment_home" /></navigation>

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


  • Во-вторых, для каждой вкладки под капотом создаётся отдельный NavHostFragment, который будет связан с графом навигации этой вкладки

Создание NavHostFragment-а для графа вкладки BottomNavigationView
private fun obtainNavHostFragment(    fragmentManager: FragmentManager,    fragmentTag: String,    navGraphId: Int,    containerId: Int): NavHostFragment {    // If the Nav Host fragment exists, return itval existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?    existingFragment?.let { return it }    // Otherwise, create it and return it.    val navHostFragment = NavHostFragment.create(navGraphId)    fragmentManager.beginTransaction()        .add(containerId, navHostFragment, fragmentTag)        .commitNow()    return navHostFragment}

FragmentManager пока что не поддерживает работу с множеством back stack-ов одновременно, поэтому пришлось придумать альтернативное решение, которое позволило ассоциировать с каждым графом свой back stack. Им стало создание отдельного NavHostFragment-а для каждого графа. Из этого следует, что с каждой вкладкой BottomNavigationView у нас будет связан отдельный NavController.


  • В-третьих, мы устанавливаем в BottomNavigationView специальный listener, который будет заниматься переключением между back stack-ами фрагментов

Listener для переключения между вкладками BottomNavigationView
setOnNavigationItemSelectedListener { item ->  val newlySelectedItemTag = graphIdToTagMap[item.itemId]  if (selectedItemTag != newlySelectedItemTag) {    fragmentManager.popBackStack(firstFragmentTag, FragmentManager.POP_BACK_STACK_INCLUSIVE)    val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)        as NavHostFragment    if (firstFragmentTag != newlySelectedItemTag) {      fragmentManager.beginTransaction()        .attach(selectedFragment)        .setPrimaryNavigationFragment(selectedFragment).apply {          graphIdToTagMap.forEach { _, fragmentTagIter ->            if (fragmentTagIter != newlySelectedItemTag) {              detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)            }          }        }        .addToBackStack(firstFragmentTag)        .setReorderingAllowed(true)        .commit()    }    selectedNavController.value = selectedFragment.navController    true  } else {    false  }}

В прикреплённом кусочке кода мы видим, как при переключении между вкладками BottomNavigationView выполняется специальная транзакция в FragmentManager-е, которая прикрепляет фрагмент выбранной вкладки и отцепляет все остальные фрагменты. По сути, так мы и переключаемся между различными back stack-ами.


  • В итоге метод настройки BottomNavigationView возвращает разработчику специальную LiveData, которая содержит в себе NavController выбранной вкладки. Этот NavController можно использовать, например, для обновления надписи на ActionBar

Настраиваем BottomNavigationView в Activity
class RootActivity : AppCompatActivity(R.layout.activity_root) {  private var currentNavController: LiveData<NavController>? = null  private fun setupBottomNavigationBar() {      // Setup the bottom navigation view with a list of navigation graphs      val liveData = bottom_nav.setupWithNavController(          navGraphIds = listOf(            R.navigation.home_nav_graph,            R.navigation.dashboard_nav_graph,            R.navigation.notifications_nav_graph          ),          fragmentManager = supportFragmentManager,          containerId = R.id.nav_host_container,          intent = intent      )      // Whenever the selected controller changes, setup the action bar.      liveData.observe(this, Observer { ctrl -> setupActionBarWithNavController(ctrl) })      currentNavController = liveData  }}

Метод для настройки BottomNavigationView вызывают в onCreate-е, когда Activity создаётся в первый раз, затем в методе onRestoreInstanceState, когда Activity пересоздаётся с помощью сохранённого состояния.


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


Посмотреть, как это выглядит в коде


Опять же, не самая очевидная связь между этими элементами, зато работает.


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


А ну-ка покажи

Первая проблема решилась:



И вторая тоже:



Адаптация workaround-а для фрагментов


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


Почему тебе нужен фрагмент?

Посмотрите внимательно на эту схему:



На ней можно увидеть, что пользователь начинает свой путь в приложении со Splash-экрана:



Google говорит, что Splash-экраны зло, ухудшающее UX приложения. Тем не менее, Splash-экраны суровая реальность большинства крупных Android-приложений. И если мы хотим использовать в нашем приложении Single Activity-архитектуру, то в качестве контейнера нижней навигации придётся использовать Fragment, а не Activity:



Я добавил вёрстку для фрагмента с нижней навигацией и перенёс настройку BottomNavigationView во фрагмент:


Посмотреть код
class MainFragment : Fragment(R.layout.fragment_main) {    private var currentNavController: LiveData<NavController>? = null    override fun onViewStateRestored(savedInstanceState: Bundle?) {        super.onViewStateRestored(savedInstanceState)        setupBottomNavigationBar()    }    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        if (savedInstanceState == null) {            setupBottomNavigationBar()        }    }}

Я добавил в свой пример Splash-экран и дополнительную вкладку для BottomNavigationView. А чтобы пример стал ещё более походить на приложение для соискателей hh.ru, я также убрал из него ActionBar.


Для этого я поменял тему приложения с Theme.MaterialComponents.DayNight.DarkActionBar на Theme.MaterialComponents.DayNight.NoActionBar и убрал код для связки NavController-а с ActionBar-ом:


Код настройки BottomNavigationView выглядел так
class MainFragment : Fragment(R.layout.fragment_main) {    private var currentNavController: LiveData<NavController>? = null    private fun setupBottomNavigationBar() {        val navGraphIds = listOf(            R.navigation.search__nav_graph,            R.navigation.favorites__nav_graph,            R.navigation.responses__nav_graph,            R.navigation.profile__nav_graph        )        val controller = bottom_navigation.setupWithNavController(            navGraphIds = navGraphIds,            fragmentManager = requireActivity().supportFragmentManager,            containerId = R.id.fragment_main__nav_host_container,            intent = requireActivity().intent        )        currentNavController = controller    }}

После всех манипуляций я включил режим Don't keep activities, запустил свой пример и получил краш при сворачивании приложения.


А ну-ка покажи


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


В чём была причина? При вызове onDestroyView активный NavHostFragment пытается отвязаться от NavController-а. Так как мой фрагмент-контейнер с нижней навигацией никак не привязывал к себе NavController, который он получил из LiveData, метод Navigation.findNavController из onDestroyView крашил приложение.


Добавляем привязку NavController-а к фрагменту с нижней навигацией (для этого в Navigation Component-е есть утилитный метод Navigation.setViewNavController), и проблема исчезает.


Кусочек кода с фиксом
class MainFragment : Fragment(R.layout.fragment_main) {    private var currentNavController: LiveData<NavController>? = null    private fun setupBottomNavigationBar() {        ...        currentNavController?.observe(            viewLifecycleOwner,            Observer { liveDataController ->                Navigation.setViewNavController(requireView(), liveDataController)            }        )    }}

Но это ещё не всё. Не выключая режим Don't keep activities, я попробовал свернуть, а затем развернуть приложение. Оно снова упало, но с другим неприятным исключением IllegalStateException в FragmentManager FragmentManager already executing transactions.


А ну-ка покажи


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


Краш происходит в методах, которые прикрепляют NavHostFragment к FragmentManager-у после их создания. Это исключение можно исправить при помощи костыля: обернуть методы attach-detach в Handler.post {}.


Фиксим IllegalStateException
// NavigationExtensions.ktprivate fun attachNavHostFragment(    fragmentManager: FragmentManager,    navHostFragment: NavHostFragment,    isPrimaryNavFragment: Boolean) {  Handler().post {    fragmentManager.beginTransaction()    .attach(navHostFragment)    .apply {      if (isPrimaryNavFragment) {        setPrimaryNavigationFragment(navHostFragment)      }    }    .commitNow()  }}

После добавления Handler.post приложение заработало, как надо.


Выводы по работе с BottomNavigationView


  • Использовать BottomNavigationView в связке с Navigation Component можно, если знать, где искать workaround-ы.
  • Если вы захотите иметь фрагмент в качестве контейнера нижней навигации BottomNavigationView, будьте готовы искать дополнительные фиксы для ваших проблем, так как скорее всего я поймал не все возможные краши.

На этом с BottomNavigationView всё, на следующей неделе расскажу про кейсы с вложенными графами навигации.

Подробнее..

Vivaldi 3.3 для Android Панельная свобода

10.09.2020 10:11:09 | Автор: admin


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

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

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

Работаем одной левой. Или правой.


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

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



Включить данную опцию можно в настройках браузера, раздел Внешний вид.

Способ гнездовой резки


После появления в мобильном браузере Vivaldi встроенного блокировщика рекламы и слежки, пользователи со всего мира облегчённо вздохнули и радостно приступили к активной резке всевозможных реклам, баннеров, подслушек и подглядок. Но очень быстро устали уж больно много сегодня всякого назойливого контента пропихивается на устройства пользователей. Что делать? Выход есть. Надо резать не поштучно, а целыми кустами. Полянами. Рощами. Гектарами. В общем, мы добавили возможность использовать в списках блокировки параметр 'document' с помощью этого волшебного слова можно резать целые веб-сайты целиком, чтобы не тратить время на индивидуальную прополку сорняков.

Также в новой версии были исправлены различные недочёты что характерно, тоже благодаря бдительности наших пользователей, оперативно сообщающих о найденных неполадках. Остаётся напомнить, что мобильный браузер Vivaldi работает на платформе Android версии 5 и выше, а также способен доставлять удовольствие от работы с ним на планшетах и Хромобуках. Загрузить новую версию браузера можно с помощью волшебной кнопки:
Подробнее..

Превращаем EditText в SearchEditText

12.09.2020 22:20:08 | Автор: admin
image

Пробовали ли Вы когда-нибудь настроить внешний вид или поведение стандартного компонента SearchView? Полагаю, да. В этом случае, я думаю что вы согласитесь, что далеко не все его настройки являются достаточно гибкими, для того, чтобы удовлетворить всем бизнес-требованиям отдельно взятой задачи. Одним из способов решения этой проблемы является написание собственного кастомного SearchView, чем мы сегодня и займемся. Поехали!

Примечание: создаваемое view (далее SearchEditText), не будет обладать всеми свойствами стандартного SearchView. В случае необходимости, вы можете без труда добавить дополнительные опции под конкретные нужды.

План действий


Есть несколько вещей, которые нам нужно сделать, для превращения EditText в SearchEditText. Если кратко, то нам нужно:

  • Унаследовать SearchEditText от AppCompatEditText
  • Добавить иконку Поиск в левом (или правом) углу SearchEditText, при нажатии на которую введённый поисковый запрос будет передаваться зарегистрированному слушателю
  • Добавить иконку Очистка в правом (или левом) углу SearchEditText, при нажатии на которую введённый текст в поисковой строке будет очищаться
  • Установить в параметре imeOptions SearchEditText-а значение IME_ACTION_SEARCH, для того, чтобы при появлении клавиатуры кнопка ввода текста выполняла роль кнопки Поиск

SearchEditText во всей красе!


import android.content.Contextimport android.util.AttributeSetimport android.view.MotionEventimport android.view.View.OnTouchListenerimport android.view.inputmethod.EditorInfoimport androidx.appcompat.widget.AppCompatEditTextimport androidx.core.widget.doAfterTextChangedclass SearchEditText@JvmOverloads constructor(    context: Context,    attributeSet: AttributeSet? = null,    defStyle: Int = androidx.appcompat.R.attr.editTextStyle) : AppCompatEditText(context, attributeSet, defStyle) {    init {        setLeftDrawable(android.R.drawable.ic_menu_search)        setTextChangeListener()        setOnEditorActionListener()        setDrawablesListener()        imeOptions = EditorInfo.IME_ACTION_SEARCH    }    companion object {        private const val DRAWABLE_LEFT_INDEX = 0        private const val DRAWABLE_RIGHT_INDEX = 2    }    private var queryTextListener: QueryTextListener? = null    private fun setTextChangeListener() {        doAfterTextChanged {            if (it.isNullOrBlank()) {                setRightDrawable(0)            } else {                setRightDrawable(android.R.drawable.ic_menu_close_clear_cancel)            }            queryTextListener?.onQueryTextChange(it.toString())        }    }        private fun setOnEditorActionListener() {        setOnEditorActionListener { _, actionId, _ ->            if (actionId == EditorInfo.IME_ACTION_SEARCH) {                queryTextListener?.onQueryTextSubmit(text.toString())                true            } else {                false            }        }    }        private fun setDrawablesListener() {        setOnTouchListener(OnTouchListener { view, event ->            view.performClick()            if (event.action == MotionEvent.ACTION_UP) {                when {                    rightDrawableClicked(event) -> {                        setText("")                        return@OnTouchListener true                    }                    leftDrawableClicked(event) -> {                        queryTextListener?.onQueryTextSubmit(text.toString())                        return@OnTouchListener true                    }                    else -> {                        return@OnTouchListener false                    }                }            }            false        })    }    private fun rightDrawableClicked(event: MotionEvent): Boolean {        val rightDrawable = compoundDrawables[DRAWABLE_RIGHT_INDEX]        return if (rightDrawable == null) {            false        } else {            val startOfDrawable = width - rightDrawable.bounds.width() - paddingRight            val endOfDrawable = startOfDrawable + rightDrawable.bounds.width()            startOfDrawable <= event.x && event.x <= endOfDrawable        }    }    private fun leftDrawableClicked(event: MotionEvent): Boolean {        val leftDrawable = compoundDrawables[DRAWABLE_LEFT_INDEX]        return if (leftDrawable == null) {            false        } else {            val startOfDrawable = paddingLeft            val endOfDrawable = startOfDrawable + leftDrawable.bounds.width()            startOfDrawable <= event.x && event.x <= endOfDrawable        }    }    fun setQueryTextChangeListener(queryTextListener: QueryTextListener) {        this.queryTextListener = queryTextListener    }    interface QueryTextListener {        fun onQueryTextSubmit(query: String?)        fun onQueryTextChange(newText: String?)    }}

В приведенном выше коде были использованы две extension-функции для установки правого и левого изображения EditText-а. Эти две функции выглядят следующим образом:

import android.widget.TextViewimport androidx.annotation.DrawableResimport androidx.core.content.ContextCompatprivate const val DRAWABLE_LEFT_INDEX = 0private const val DRAWABLE_TOP_INDEX = 1private const val DRAWABLE_RIGHT_INDEX = 2private const val DRAWABLE_BOTTOM_INDEX = 3fun TextView.setLeftDrawable(@DrawableRes drawableResId: Int) {    val leftDrawable = if (drawableResId != 0) {        ContextCompat.getDrawable(context, drawableResId)    } else {        null    }    val topDrawable = compoundDrawables[DRAWABLE_TOP_INDEX]    val rightDrawable = compoundDrawables[DRAWABLE_RIGHT_INDEX]    val bottomDrawable = compoundDrawables[DRAWABLE_BOTTOM_INDEX]    setCompoundDrawablesWithIntrinsicBounds(        leftDrawable,        topDrawable,        rightDrawable,        bottomDrawable    )}fun TextView.setRightDrawable(@DrawableRes drawableResId: Int) {    val leftDrawable = compoundDrawables[DRAWABLE_LEFT_INDEX]    val topDrawable = compoundDrawables[DRAWABLE_TOP_INDEX]    val rightDrawable = if (drawableResId != 0) {        ContextCompat.getDrawable(context, drawableResId)    } else {        null    }    val bottomDrawable = compoundDrawables[DRAWABLE_BOTTOM_INDEX]    setCompoundDrawablesWithIntrinsicBounds(        leftDrawable,        topDrawable,        rightDrawable,        bottomDrawable    )}

Наследование от AppCompatEditText


class SearchEditText@JvmOverloads constructor(    context: Context,    attributeSet: AttributeSet? = null,    defStyle: Int = androidx.appcompat.R.attr.editTextStyle) : AppCompatEditText(context, attributeSet, defStyle)

Как видите, из написанного конструктора мы передаём все необходимые параметры в конструктор AppCompatEditText. Важным моментом тут является то, что значением defStyle по-умолчанию является android.appcompat.R.attr.editTextStyle. Наследуясь от LinearLayout, FrameLayout и некоторых других view, мы, как правило, используем 0 в качестве значения по-умолчанию для defStyle. Однако в нашем случае это не подходит, иначе наш SearchEditText будет вести себя как TextView, а не как EditText.

Обработка изменения текста


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

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

Посмотрим на код слушателя:

private fun setTextChangeListener() {    doAfterTextChanged {        if (it.isNullOrBlank()) {            setRightDrawable(0)        } else {            setRightDrawable(android.R.drawable.ic_menu_close_clear_cancel)        }        queryTextListener?.onQueryTextChange(it.toString())    }}

Для обработки событий изменения текста использовалась extension-функция doAfterTextChanged из androidx.core:core-ktx.

Обработка нажатия кнопки ввода на клавиатуре


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

private fun setOnEditorActionListener() {    setOnEditorActionListener { _, actionId, _ ->        if (actionId == EditorInfo.IME_ACTION_SEARCH) {            queryTextListener?.onQueryTextSubmit(text.toString())            true        } else {            false        }    }}

Обработка нажатий на иконки


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

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

private fun setDrawablesListener() {    setOnTouchListener(OnTouchListener { view, event ->        view.performClick()        if (event.action == MotionEvent.ACTION_UP) {            when {                rightDrawableClicked(event) -> {                    setText("")                    return@OnTouchListener true                }                leftDrawableClicked(event) -> {                    queryTextListener?.onQueryTextSubmit(text.toString())                    return@OnTouchListener true                }                else -> {                    return@OnTouchListener false                }            }        }        false    })}private fun rightDrawableClicked(event: MotionEvent): Boolean {    val rightDrawable = compoundDrawables[DRAWABLE_RIGHT_INDEX]    return if (rightDrawable == null) {        false    } else {        val startOfDrawable = width - rightDrawable.bounds.width() - paddingRight        val endOfDrawable = startOfDrawable + rightDrawable.bounds.width()        startOfDrawable <= event.x && event.x <= endOfDrawable    }}private fun leftDrawableClicked(event: MotionEvent): Boolean {    val leftDrawable = compoundDrawables[DRAWABLE_LEFT_INDEX]    return if (leftDrawable == null) {        false    } else {        val startOfDrawable = paddingLeft        val endOfDrawable = startOfDrawable + leftDrawable.bounds.width()        startOfDrawable <= event.x && event.x <= endOfDrawable    }}

В функциях leftDrawableClicked и RightDrawableClicked нет ничего сложного. Возьмём, к примеру, первую из них. Для левой иконки мы сначала рассчитываем startOfDrawable и endOfDrawable, а затем проверяем, находится ли x-координата точки касания в диапазоне [startofDrawable, endOfDrawable]. Если да, то это означает, что левая иконка была нажата. Функция rightDrawableClicked работает аналогичным образом.

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

Вывод


В этой статье мы рассмотрели вариант превращения стандартного EditText в более продвинутый SearchEditText. Как уже упоминалось ранее, готовое решение не поддерживает все параметры, предоставляемые SearchView, однако вы в любой момент можете его усовершенствовать, добавив дополнительные опции на свое усмотрение. Дерзайте!

P.S:

Доступ к исходному коду SearchEditText вы можете получить из этого репозитория GitHub.
Подробнее..

Перевод Антипаттерн Репозиторий в Android

15.09.2020 14:04:30 | Автор: admin
Перевод статьи подготовлен в преддверии старта курса Android Developer. Professional.



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

В этой статье я расскажу вам о паттерне Репозиторий и объясню, почему он на самом деле является антипаттерном для Android приложений.

Репозиторий


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



Роль объекта репозитория в этой структуре такова:

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

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

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

Репозиторий в Android Architecture Blueprints v2


Около двух лет назад я рецензировал первую версию Android Architecture Blueprints. По идее они должны были реализовывать чистый пример MVP, но на практике эти блюпринты вылились в достаточно грязную кодовую базу. Они действительно содержали интерфейсы с именами View и Presenter, но не устанавливали никаких архитектурных границ, так что это по сути был не MVP. Вы можете посмотреть данный код ревью здесь.

С тех пор Google обновил архитектурные блюпринты с использованием Kotlin, ViewModel и других современных практик, включая репозитории. Эти обновленные блюпринты получили приставку v2.

Давайте же посмотрим на интерфейс TasksRepository из блюпринтов v2:

interface TasksRepository {   fun observeTasks(): LiveData<Result<List<Task>>>   suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>   suspend fun refreshTasks()   fun observeTask(taskId: String): LiveData<Result<Task>>   suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>   suspend fun refreshTask(taskId: String)   suspend fun saveTask(task: Task)   suspend fun completeTask(task: Task)   suspend fun completeTask(taskId: String)   suspend fun activateTask(task: Task)   suspend fun activateTask(taskId: String)   suspend fun clearCompletedTasks()   suspend fun deleteAllTasks()   suspend fun deleteTask(taskId: String)}


Даже до начала чтения кода можно обратить внимание на размер этого интерфейса это уже тревожный звоночек. Такое количество методов в одном интерфейсе вызвало бы вопросы даже в больших Android проектах, но мы говорим о приложении ToDo, в котором всего 2000 строк кода. Почему этому достаточно тривиальному приложению нужен класс с такой огромной поверхностью API?

Репозиторий как Божественный объект (God Object)


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

Группа 1:

fun observeTasks(): LiveData<Result<List<Task>>>   fun observeTask(taskId: String): LiveData<Result<Task>>


Группа 2:

   suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>   suspend fun refreshTasks()   suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>   suspend fun refreshTask(taskId: String)   suspend fun saveTask(task: Task)   suspend fun deleteAllTasks()   suspend fun deleteTask(taskId: String)


Группа 3:

  suspend fun completeTask(task: Task)   suspend fun completeTask(taskId: String)   suspend fun clearCompletedTasks()   suspend fun activateTask(task: Task)   suspend fun activateTask(taskId: String)


Теперь давайте определим сферы ответственности каждой из вышеперечисленных групп.

Группа 1 это в основном реализация паттерна Observer с использованием средства LiveData. Группа 2 представляет собой шлюз к хранилищу данных плюс два метода refresh, которые необходимы, поскольку за репозиторием скрывается удаленное хранилище данных. Группа 3 содержит функциональные методы, которые в основном реализуют две части логики домена приложения (завершение задач и активация).

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

У нас есть специальный термин для классов, которые объединяют так много обязанностей: Божественные объекты. Это широко распространенный антипаттерн в приложениях на Android. Activitie и Fragment являются стандартными подозреваемыми в этом контексте, но другие классы тоже могут вырождаться в Божественные объекты. Особенно, если их имена заканчиваются на Manager, верно?

Погодите Мне кажется, я нашел более подходящее название для TasksRepository:

interface TasksManager {   fun observeTasks(): LiveData<Result<List<Task>>>   suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>   suspend fun refreshTasks()   fun observeTask(taskId: String): LiveData<Result<Task>>   suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>   suspend fun refreshTask(taskId: String)   suspend fun saveTask(task: Task)   suspend fun completeTask(task: Task)   suspend fun completeTask(taskId: String)   suspend fun activateTask(task: Task)   suspend fun activateTask(taskId: String)   suspend fun clearCompletedTasks()   suspend fun deleteAllTasks()   suspend fun deleteTask(taskId: String)}


Теперь имя этого интерфейса намного лучше отражает его обязанности!

Анемичные репозитории


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

Если вы захотите извлечь, скажем, методы completeTask из TasksRepository, куда бы вы их поместили? Согласно рекомендованной Google архитектуре, вам нужно будет перенести эту логику в одну из ваших ViewModel. Это не кажется таким уж плохим решением, но как раз таким оно на самом деле и является.

Например, представьте, что вы помещаете эту логику в одну ViewModel. Затем, через месяц, ваш менеджер по работе с клиентами хочет разрешить пользователям выполнять задачи с нескольких экранов (это релевантно по отношению ко всем ToDo менеджерам, которые я когда-либо использовал). Логика внутри ViewModel не может использоваться повторно, поэтому вам нужно либо продублировать ее, либо вернуть в TasksRepository. Очевидно, что оба подхода плохи.

Лучшим подходом было бы извлечь этот доменный поток в специальный объект, а затем поместить его между ViewModel и репозиторием. Затем разные ViewModel смогут повторно использовать этот объект для выполнения этого конкретного потока. Эти объекты известны как варианты использования или взаимодействия. Однако, если вы добавите варианты использования в свою кодовую базу, репозитории станут по сути бесполезным шаблоном. Что бы они ни делали, это будет лучше сочетаться с вариантами использования. Габор Варади уже освещал эту тему в этой статье, поэтому я не буду вдаваться в подробности. Я подписываюсь почти под всем, что он сказал о анемичных репозиториях.

Но почему варианты использования намного лучше репозиториев? Ответ прост: варианты использования инкапсулируют отдельные потоки. Следовательно, вместо одного репозитория (для каждой концепции домена), который постепенно разрастается в Божественный объект, у вас будет несколько узконаправленных классов вариантов использования. Если поток зависит от сети, и от хранимых данных, вы можете передать соответствующие абстракции в класс варианта использования, и он будет проводить арбитраж между этими источниками.

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

Репозитории вне Android.


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

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

Эрик Эванс в своей книге Domain Driven Design также описывал репозитории. Он написал:

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


Обратите внимание, что вы можете заменить репозиторий в приведенной выше цитате на Room ORM, и это все равно будет иметь смысл. Итак, в контексте Domain Driven Design репозиторий это ORM (реализованный вручную или с использованием стороннего фреймворка).

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

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

Как репозиторий стал антипаттерном в Android


Итак, неужели в Google неверно истолковали паттерн репозитория и внедрили в него наивную идею абстрагироваться от доступа к сети? Я в этом сомневаюсь.

Я нашел самую древнюю ссылку на этот антипаттерн в этом репозитории на GitHub, который, к сожалению, является очень популярным ресурсом. Я не знаю, изобрел ли этот антипаттерн конкретно этот автор, но похоже, что именно это репо популяризировало общую идею внутри экосистемы Android. Разработчики Google, вероятно, взяли его оттуда или из одного из вторичных источников.

Заключение


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

Например, в другом блюпринте Google, на этот раз для архитектурных компонентов, использование репозиториев в конечном итоге привело к таким жемчужинам, как NetworkBoundResource. Имейте в виду, что образец браузера GitHub по-прежнему является крошечным ~2 KLOC приложением.

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

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

Подробнее..

Navigation Component-дзюцу, vol. 2 вложенные графы навигации

16.09.2020 12:14:05 | Автор: admin


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


Это вторая из трёх статей про реализацию кейсов навигации при помощи Navigation Component-а.


Первая статья про BottomNavigationView.


Где на схеме приложения кейсы со вложенными графами?



В этом кейсе мы говорим про следующую часть общей схемы навигации:



Представим такую ситуацию: у нас есть 4 экрана A, B, C и D. Пусть с экранов A и B вы можете перейти на экран C, с экрана C в экран D, а после D вернуться на тот экран, который начал флоу C->D.


А можно нагляднее?

В тестовом приложении, которое я приготовил для разбора Navigation Component-а, есть две вкладки BottomNavigationView (на схеме это Search и Responses но пусть они будут экранами A и B):



С обеих этих вкладок мы можем перейти на некоторый вложенный флоу, который состоит из двух экранов (C и D):



Если мы перейдём на экран C с вкладки Search (экрана A), то после экрана D мы должны вернуться на вкладку Search:



А если мы стартуем экран C со вкладки Responses, то после завершения внутреннего флоу C->D мы должны вернуться на вкладку Responses:



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


Как это реализовать? Для начала вы объявляете вашу вложенную последовательность экранов в отдельном XML-файле навигации, чтобы можно было вкладывать её в другие графы:


Объявление графа вложенной навигации
<!-- company_flow__nav_graph.xml --><navigation    android:id="@+id/company_flow__nav_graph"    app:startDestination="@id/CompanyFragment">    <fragment        android:id="@+id/CompanyFragment"        android:name="ui.company.CompanyFragment">        <action            android:id="@+id/action__CompanyFragment__to__CompanyDetailsFragment"            app:destination="@id/CompanyDetailsFragment" />    </fragment>    <fragment        android:id="@+id/CompanyDetailsFragment"        android:name="ui.company.CompanyDetailsFragment"/></navigation>

Затем следует вложить созданный граф навигации в уже существующий граф и использовать идентификатор вложенного графа для описания action-ов:


Добавление графа навигации в другой граф
<navigation    android:id="@+id/menu__search"    app:startDestination="@id/SearchContainerFragment">    <fragment        android:id="@+id/SearchContainerFragment"        android:name="ui.tabs.search.SearchContainerFragment">        <action            android:id="@+id/action__SearchContainerFragment__to__CompanyFlow"            app:destination="@id/company_flow__nav_graph" />    </fragment>    <include app:graph="@navigation/company_flow__nav_graph" /></navigation>

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


Проблема в том, что Navigation Component не позволяет нормально описывать навигацию НАЗАД, только навигацию ВПЕРЁД. Но при этом даёт возможность описывать удаление экранов из back stack-а при помощи атрибутов popBackUp и popBackUpInclusive в XML, а также при помощи функции popBackStack в NavController-е.


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


А можно на картинке?


Честно говоря, я не ожидал увидеть там два объекта, поскольку в back stack-е фрагментов точно был только один SplashFragment. Откуда взялась вторая сущность? Оказалось, что первый объект представляет собой NavGraph, который запустился в моей корневой Activity, а второй объект мой SplashFragment, который представлен классом FragmentNavigator.Destination.


И тут у меня появилась идея а что если вызвать на NavController-е функцию popBackStack и передать туда идентификатор графа? Коль скоро граф находится в back stack-е NavController-а, это должно удалить все экраны, которые были добавлены в рамках этого графа.


И эта идея сработала.


Возврат из flow при помощи popBackStack
class CompanyDetailsFragment : Fragment(R.layout.fragment_company_details) {    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        finish_flow_button.setOnClickListener {            findNavController().popBackStack(R.id.company_flow__nav_graph, true)        }    }}

Минус такого подхода к определению обратной навигации очевиден: эта навигация не отобразится в визуальном редакторе. Конечно, можно определить action в XML-е вот таким образом:


Определение action-а для закрытия графа навигации
<fragment  android:id="@+id/CompanyDetailsFragment"  android:name="ui.company.CompanyDetailsFragment"  android:label="@string/fragment_company_details__title"  tools:layout="@layout/fragment_company_details">  <action      android:id="@+id/action__finishCompanyFlow"      app:popUpTo="@id/company_flow__nav_graph"      app:popUpToInclusive="true" /></fragment>

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


findNavController().navigate(R.id.action__finishCompanyFlow)

Но есть в этом что-то семантически неправильное: странно использовать слово navigate для закрытия экранов и обратной навигации.


Возврат результата из вложенного флоу


Что ж, мы получили некоторое подобие обратной навигации. Но возникает ещё один вопрос: есть ли способ вернуть из вложенного флоу какой-нибудь результат?


Да, есть. В Navigation Component 2.3 Google представил нам специальное key-value хранилище для проброса результатов с других экранов SavedStateHandle. К этому хранилищу можно получить доступ через свойства NavControllerpreviousBackStackEntry и currentBackStackEntry. Но в своих примерах Google почему-то считает, что ваш вложенный флоу всегда состоит только из одного экрана.


Типичный пример работы с SavedStateHandle
// Flow screenfindNavController().previousBackStackEntry    ?.savedStateHandle    ?.set("some_key", "value")// Screen that waits resultval result = findNavController().currentBackStackEntry    ?.savedStateHandle    ?.remove<String>("some_key")

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


Посмотреть на фикс
fragment_company_details__button.setOnClickListener {    // Here we are inside nested navigation flow    findNavController().popBackStack(R.id.company_flow__nav_graph, true)    // At this line, "findNavController().currentBackStackEntry" means    // screen that STARTED current nested flow.    // So we can send the result!    findNavController().currentBackStackEntry      ?.savedStateHandle      ?.set(COMPANY_FLOW_RESULT_FLAG, true)}

Суть в следующем: до вызова findNavController().popBackStack вы находитесь ВНУТРИ вашего флоу экранов, а вот сразу после вызова popBackStack уже на экране, который НАЧАЛ ваш флоу! И это означает, что вы можете использовать для доступа к SavedStateHandle свойство currentBackStackEntry. Этот entry будет означать ваш стартовый экран, которому нужен результат из флоу.


В свою очередь, на на экране, который начал вложенный флоу, вы тоже используете currentBackStackEntry для доступа к SavedStateHandle. И, следовательно, читаете правильные данные:


Читаем данные из SavedStateHandle
// Read result from nested navigation flowval companyFlowResult = findNavController().currentBackStackEntry    ?.savedStateHandle    ?.remove<Boolean>(CompanyDetailsFragment.COMPANY_FLOW_RESULT_FLAG)text__company_flow_result.text = "${companyFlowResult}"

Выводы по работе с вложенным флоу


  • Для обратной навигации из вложенного флоу, состоящего из нескольких экранов, можно использовать функцию NavController.popBackStack, передав туда идентификатор графа навигации вашего флоу.
  • Для проброса какого-либо результата из вложенного флоу можно использовать SavedStateHandle.


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


Пусть у нас два графа навигации граф A и граф B. Я буду называть граф B вложенным в граф A, если мы вкладываем его через include. И, наоборот, я буду называть граф A внешним по отношению к графу B, если граф А включает в себя граф B.


Ещё немного картинок

Граф B вложенный в граф A:



Граф А внешний по отношению к графу B:



А теперь давайте разберём кейс навигации из вложенного графа во внешний граф.



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


Что? В смысле, это тот самый первый кейс, который ты уже разобрал? Разве вы не заметили, что у этой последовательности НЕТ нижней навигации?


Приблизить картинку

Смотрите, вот экран с нижней навигацией:



А вот последовательность экранов без неё:



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


Неправильный подход к такой навигации

Пусть мы вставили граф auth flow-навигации в наш граф вкладки нижней навигации и добавили action для перехода в него:


<navigation xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    android:id="@+id/menu__profile"    app:startDestination="@id/ProfileContainerFragment">    <fragment        android:id="@+id/ProfileContainerFragment"        android:name="ui.tabs.profile.ProfileContainerFragment">        <action            android:id="@+id/action__ProfileContainerFragment__to__AuthFlow"            app:destination="@id/auth__nav_graph" />    </fragment>    <include app:graph="@navigation/auth__nav_graph" /></navigation>

В этом случае первый экран auth-флоу появится в контейнере с нижней навигацией, а мы этого не хотели:



При этом не хочется переносить логику работы с нижней навигацией в Activity, писать какие-то методы по скрытию / демонстрации этого BottomNavigationView. Не зря же я адаптировал фикс из NavigationAdvancedSample для фрагмента, в конце концов.


В каком случае мы получим желаемый результат? Если каким-то образом осуществим навигацию из контейнера с BottomNavigationView (не из самой вкладки, а из контейнера, который является Host-ом для всех этих вкладок), то Auth-граф откроется без нижней навигации.


А на картинке можно?

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



Давайте введём action для навигации между MainFragment-ом и флоу авторизации:


Описание навигации
<! app_nav_graph.xml ><fragment  android:id="@+id/SplashFragment"  android:name="com.aaglobal.jnc_playground.ui.splash.SplashFragment"/><fragment  android:id="@+id/MainFragment"  android:name="com.aaglobal.jnc_playground.ui.main.MainFragment">  <action      android:id="@+id/action__MainFragment__to__AuthFlow"      app:destination="@id/auth__nav_graph" /></fragment><include app:graph="@navigation/auth__nav_graph" />

Но проблема в том, что если мы попытаемся использовать этот action прямо из вкладки нижней навигации, вот так:


fragment_profile_container__button__open_auth_flow.setOnClickListener {    findNavController().navigate(R.id.action__MainFragment__to__AuthFlow)}

то приложение упадёт с IllegalArgumentException, потому что NavController текущей вкладки ничего не знает о навигации вне своего Host-а навигации.


Ищем правильный NavController


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


В Navigation Component есть специальная утилитная функция для поиска NavController-а, который привязан к нужному вам контейнеру, Navigation.findNavController:


Открываем флоу авторизации правильно
fragment_profile_container__button__open_auth_flow.setOnClickListener {  Navigation.findNavController(    requireActivity(),    R.id.activity_root__fragment__nav_host  ).navigate(R.id.action__MainFragment__to__AuthFlow)}


Проблемы с навигацией по кнопке Back


Итак, мы смогли открыть флоу авторизации поверх открытого фрагмента с нижней навигацией. Но появилась новая проблема: если пользователь нажмёт кнопку Back, находясь на первом экране графа авторизации, приложение упадёт. Снова с IllegalArgumentException на этот раз NavController не может найти контейнер, с которого мы только что пришли, как будто мы используем неправильный NavController для обратной навигации.


Покажи гифку


Исключение, которое мы получаем:


java.lang.IllegalArgumentException: No view found for id 0x7f08009a (com.aaglobal.jnc_playground:id/fragment_main__nav_host_container) for fragment NavHostFragment{5150965} (e58fc3a2-b046-4c80-9def-9ca40957502d) id=0x7f08009a bottomNavigation#0}

Эту проблему можно решить, переопределив поведение кнопки Back. В одной из новых версий AndroidX появился удобный OnBackPressedCallback. Раз мы используем неправильный NavController по умолчанию, значит, мы можем подменить его на правильный:


Переопределяем back-навигацию для первого экрана auth-графа
class StartAuthFragment : Fragment(R.layout.fragment_start_auth) {    private var callback: OnBackPressedCallback? = null    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        callback = object : OnBackPressedCallback(true) {            override fun handleOnBackPressed() {                Navigation.findNavController(                    requireActivity(),                    R.id.activity_root__fragment__nav_host                ).popBackStack()            }        }.also {            requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, it)        }    }}

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


И это работает! Но есть одно но: чтобы это продолжало работать на протяжении всего auth-флоу, нам надо добавить точно такой же OnBackPressedCallback в каждый экран этого флоу =(


И, конечно же, придётся поправить закрытие всего auth-флоу там мы тоже должны добавить получение правильного NavController-а:


Как это выглядит?
class FinishAuthFragment : Fragment(R.layout.fragment_finish_auth) {  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {      super.onViewCreated(view, savedInstanceState)      fragment_finish_auth__button.setOnClickListener {          Navigation.findNavController(              requireActivity(),              R.id.activity_root__fragment__nav_host          ).popBackStack(R.id.auth__nav_graph, true)          findNavController().currentBackStackEntry            ?.savedStateHandle            ?.set(AUTH_FLOW_RESULT_KEY, true)      }  }}

Подведём итоги


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


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


Покажи на картинке


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


  • Когда пользователь нажмёт на кнопку Back на первом экране флоу авторизации, мы хотим не вернуться назад (потому что зачем нам второй раз показывать Splash), а закрыть приложение.
  • После завершения флоу авторизации мы не просто закрываем открытый нами граф, но и двигаемся вперёд.

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


Покажи код

Определяем флажок для StartAuthFragment:


<fragment  android:id="@+id/StartAuthFragment"  android:name="com.aaglobal.jnc_playground.ui.auth.StartAuthFragment"  android:label="Start auth"  tools:layout="@layout/fragment_start_auth">  <argument      android:name="isFromSplashScreen"      android:defaultValue="false"      app:argType="boolean"      app:nullable="false" />  <action      android:id="@+id/action__StartAuthFragment__to__FinishAuthFragment"      app:destination="@id/FinishAuthFragment" /></fragment>

А теперь используем этот флажок в OnBackPressedCallback:


class StartAuthFragment : Fragment(R.layout.fragment_start_auth) {    private val args: StartAuthFragmentArgs by navArgs()    private var callback: OnBackPressedCallback? = null    private fun getOnBackPressedCallback(): OnBackPressedCallback {      return object : OnBackPressedCallback(true) {          override fun handleOnBackPressed() {              if (args.isFromSplashScreen) {                  requireActivity().finish()              } else {                  Navigation.findNavController(                    requireActivity(),                    R.id.activity_root__fragment__nav_host                  ).popBackStack()              }          }      }    }}

Поскольку у нас Single Activity, requireActivity().finish() будет достаточно, чтобы закрыть наше приложение.


Со вторым пунктом чуть интереснее. Я вижу два способа реализовать такую пост-навигацию.


  • Первый способ: Navigation Component позволяет в runtime-е менять граф навигации, мы могли бы где-нибудь сохранить @id будущего destination-а и добавить немного логики при завершении авторизации.
  • Второй способ закрывать флоу авторизации как и раньше, а логику движения вперёд дописать в экран, который стартовал экраны авторизации, то есть в Splash.

Первый способ мне не нравится тем, что если появятся дополнительные destination-ы, которые надо открывать после экранов авторизации, появится и много лишней логики внутри флоу авторизации. Да и модифицировать граф навигации в runtime-е то ещё удовольствие.


Второй способ тоже не ахти потребуется сохранить предыдущий экран в back stack-е, чтобы, вернувшись на него и прочитав результат после авторизации, мы могли двигаться дальше. Но это всё равно приемлемый вариант: вложенный флоу будет отвечать только за свою собственную логику, а экран, который начинает подобную условную навигацию (выбор между main и auth на Splash-е, например), и так знает, как двигаться вперёд.


И реализовать это просто мы знаем, как закрыть auth-флоу, знаем, как прокинуть из него результат на экран, который стартовал экраны авторизации. Останется только поймать результат на SplashFragment-е.


Покажи код

Пробрасываем результат из auth-флоу:


// FinishAuthFragment.ktfragment_finish_auth__button.setOnClickListener {    // Save hasAuthData flag in prefs    GlobalDI.getAuthRepository().putHasAuthDataFlag(true)    // Navigate back from auth flow    Navigation.findNavController(        requireActivity(),        R.id.activity_root__fragment__nav_host    ).popBackStack(R.id.auth__nav_graph, true)    // Send signal about finishing flow    findNavController().currentBackStackEntry      ?.savedStateHandle      ?.set(AUTH_FLOW_RESULT_KEY, true)}

И ловим его на стороне SplashFragment-а:


// SplashFragment.ktoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) {    super.onViewCreated(view, savedInstanceState)    val authResult = findNavController().currentBackStackEntry        ?.savedStateHandle        ?.remove<Boolean>(FinishAuthFragment.AUTH_FLOW_RESULT_KEY) == true    if (authResult) {        navigateToMainScreen()        return    }}

Выводы по кейсам вложенной навигации


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

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

Подробнее..

Из песочницы Alt City Online. Как я в одиночку создавал Gta Online для мобильных устройств. Часть 1

23.09.2020 02:13:19 | Автор: admin
Возможно ли в здравом уме замахнуться на подобный проект в одного, и надо ли оно вообще? Спойлер: да (длинный пост с картинками и видео).



Предыстория


Разработкой программных продуктов я занимаюсь уже 6 лет, начинал с разработки приложений для iOS. После выпуска нескольких приложений, которые в общей сумме принесли около $500, решил попробовать себя в разработке сайтов и настройке рекламы. В этой сфере я проработал 3 года, и понял, что создавать сайты не то, чем мне хотелось бы заниматься в жизни.

Так как с детства я очень любил игры, решил рассмотреть геймдев как будущую нишу, где хотел бы себя попробовать. Сделал бесплатную игру на SpriteKit (фреймворк Apple для создания 2D игр), начал знакомиться с инструментами для разработки игр. Решил подробно изучать Unity, так как он мне показался оптимальным вариантом для разработки именно мобильных игр. Выпустил в AppStore и в Google Play простенькую игру на Unity, и естественно поиграли в нее условно 3 с половиной человека. Это меня не особо остановило, так как цель разработки этой игры была в основном в том, чтобы познакомиться с процессом разработки в Unity и запуском игры именно в Google Play. Эти цели были выполнены, можно было двигаться дальше. Я начал уже более тщательно изучать Unity: 3-4 часа в день стабильно проходил Advanced курсы по разработке. Думаю, что мне повезло попался действительно подробный и толковый курс по созданию RPG в Unity, и многое, особенно различные best-practices, я узнал именно из него.

В это время мне подвернулась вакансия в одной крупной компании-разработчике мобильных игр, решил попробовать. Проработал там 3-4 месяца. Это была моя первая работа по найму. До этого вполне успешно работал только на себя. После определенного времени я понял, что это не мое. Надо отдать должное компания предоставляла максимальные удобства для работы. И вообще поразило то, насколько у нас в стране (живу в России, в городе миллионнике) может быть крутое отношение к работникам. Ни в коем случае не хочу сказать, что в других компаниях плохое отношение, просто то что я наблюдал это очень круто.

В общем, после ухода с работы, пришло понимание, что хочу попробовать создать действительно интересную и уникальную игру (наверное термин уникальная игра не совсем вяжется с концепцией игры-аналога GTA Online, но об этом дальше). С детства я обожал серию GTA играл десятками часов напролет в GTA Vice City и GTA San Andreas, ставил моды, крутил параметры машин. Потом после выхода GTA IV все то же самое делал с ней. Потом GTA IV: Episodes from Liberty City, GTA V. И естественно GTA Liberty City Stories, GTA Vice City Stories, GTA Chinatown Wars для PSP. Также было потрачено куча часов в других подобных играх Saints Row, Godfather 2 и т. д. Эх, хорошие были времена

Так вот, потом я познакомился уже с различными Role Play проектами, основанными на GTA. Но знакомство с ними, к сожалению, уже происходило через YouTube, так как работа стала занимать практически все время, а найти 30 минут в день на ролик не проблема. Считаю, что RP проекты создали очень интересную нишу, и вообще переосмыслили GTA.

RP проекты создали очень интересную нишу, и вообще переосмыслили GTA

Как я уже сказал выше, свободного времени становилось все меньше, и я захотел поиграть во что-то по типу GTA Online / GTA RP на телефоне благо сегодня телефоны действительно мощные, и по идее что-то подобное можно реализовать (например тот же PUBG, который отлично работает на практических любых устройствах). Каково было мое удивление, когда в AppStore я нашел буквально 3 игры, которые хоть как-то можно было отнести к аналогам GTA Online, да и те ужасные. Вот так и появилась идея создать первый аналог GTA Online для устройств на базе iOS и Android.

ALT: City Online




Геймплей в ALT: City Online это смесь классической GTA Online и ее Role Play модификаций. В самом начале игры ты появляешься в стартовой точке (предполагается, что это будет либо вокзал, либо аэропорт). Твоя задача найти работу и начать зарабатывать деньги и опыт. По мере получения опыта, тебе будут открываться новые профессии. Список профессий будет широкий, и я сейчас работаю над тем, чтобы сделать геймплей каждой профессии максимально интересным насколько это возможно. Далее ты сможешь купить себе квартиру, мотоцикл, потом дом, машину, машину получше в общем все, кто играли в GTA RP, знакомы с этим. Фишка игры заключается в том, что в сессии, в отличии от классической GTA Online, будут сотни человек, ты сможешь взаимодействовать со всеми разговаривать, наносить урон, кооперировать, обмениваться вещами, продавать вещи.



В игре будет возможность покупать оружие, менять его скины. Также в игре будет доступен тюнинг транспортных средств.

Но главное отличие от RP проектов в ALT: City Online нет классической для таких проектов консоли, нет администраторов, не нужно отыгрывать РП. Ты можешь, но не обязан.

Площадь карты в игре около 225 квадратных километров, но примерно треть площади покрыто водой, так что эффективная площадь равна примерно 150-160 квадратным километрам. Естественно, наполнить такой большой мир контентом в одиночку достаточно сложно, поэтому игра будет наполняться контентом в последующих обновлениях.



Важно то, что игра будет работать не только на последних топовых девайсах. Например, если говорить об iOS, то минимально поддерживаемое устройство iPhone 7. Вообще, основным боттлнеком оптимизации игры стала непрозрачность потребления памяти на iOS платформе (это известный недостаток Unity, с которым на данный момент мало что можно сделать), из-за чего было достаточно сложно оптимизировать огромный открытый мир для работы на маленьком устройстве с 2 гб оперативной памяти.


Какие вопросы предстояло решить


В первую очередь надо было вообще понять, возможно ли реализовать мобильную онлайн игру в большом открытом мире на Unity? Соответственно, был куплен простенький генератор города для Unity, контроллер персонажа, скачаны бесплатные модели оружия и автомобиля и кое-как настроен клиент сервер (вопрос реализации мультиплеера решался достаточно долго, так как официального решения от Unity нет, а фреймворков много, и они очень отличаются, расскажу о сетевой архитектуре подробнее в одной из будущих технических статей). Спустя две недели был готов достаточно играбельный прототип. То есть был запущен тестовый сервер игры на виртуальном сервере, установлены клиенты на телефоны друзей. Мы подключились, поиграли все отлично работает для прототипа, все довольны. Провел стресс тест, получилось, что минимум 100 игроков сервер точно держит. Для игры это достаточный минимум, но вообще, цель 600-1000 игроков на сервере (то есть столько игроков будет одновременно в сессии). В общем, пришло понимание, что Unity очень даже подходит под этот проект.


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

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

Сделаю отступление надо понимать, что я занимаюсь проектом с декабря на фул-тайме (по 10-12 часов в день, без выходных), а практически все средства, отложенные за прошлые годы, я тратил на покупку различных инструментов и 3D моделей. Думаю, это снимет многие вопросы по поводу того, почему на некоторые этапы потрачено мало времени.

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

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

Текущее состояние проекта


На данный момент я работаю над игрой уже девятый месяц. Проект готов к альфа релизу на 85%. Уже сейчас полностью смоделирована карта, настроена работа открытого мира на телефонах, проведена работа над рендерингом (LODы, различные настройки графики, оптимизация текстур, динамический батчинг и т.д.), настроена архитектура клиент-сервера, оптимизировано потребление памяти.

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


Скриншоты










Заключение


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

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

Также подписывайтесь на Twitter, там будут выкладываться новости, а также актуальные фото и видео о разработке: @AltCityOnline

На сайте ALT: City Online вы также можете оставить свой email. На него придет оповещение, когда игра будет доступна для загрузки. Всем, кто оставил свой email на сайте, так же положен жирный бонус при запуске игры!
Подробнее..

Категории

Последние комментарии

© 2006-2020, personeltest.ru