Javanese Online

Фундаментальные проблемы Android

Android — это плохой DI-контейнер

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

Все компоненты Android-приложений — активити, фрагменты, сервисы — создаются через no-arg конструктор (данные можно передать лишь посредством extras/arguments). Как только компонент создан, он оторван от внешнего мира и нежизнеспособен: уже позже вызываются setArguments, attachBaseContext/onAttach, onCreate и т. п.. Соответственно, такие объекты, подобно плохим синглтонам, вынуждены разрешать зависимости самостоятельно: лезть в Application или static и забирать нужные объекты.

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

Такой подход становится феерической помехой повторному использованию кода: при добавлении новой функциональности она не делегируется объектам, которые передаются в конструктор, а реализуется прямо внутри фрагмента (активити, презентера, вьюмодели) в if-else/switch-case. Впоследствии такой класс становится типичным customizable object, подчиняющимся десяткам условий — а такой код крайне сложно поддерживать.

Ложку дёгтя добавляет жизненный цикл. Знание того, что Activity/Fragment — обычный Java-объект, оказывается немного неверно: после пересоздания ту же задачу выполняет уже другой объект, которому достаётся Bundle с сохранённым состоянием предшественника. Люди, не знающие Java, часто неверно интерпретируют пересоздание как «GC вычистил мне все ссылки!»

Иерархии типов

Нужно постоянно наследовать классы. Если какая-нибудь библиотека, например, хочет предоставить свои подтипы фрагментов, приходится учитывать, что есть нативные фрагменты (android.app.Fragment) и их бэкпорт (android.support.v4.app.Fragment), а также несколько особых случаев: DialogFragment, BottomSheetDialogFragment и т. п..

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

Чтобы поселить фактический сервис (просто объект, представляющий определённую функциональность) в Bound Service и передать его в Activity, нужно унаследовать как минимум Service и Binder (а также реализовать ServiceConnection). Пример заворачивания объекта в сервис

Принеси с собой свои best practices

Как работать с асинхронностью?
AsyncTask
Работа с JSON

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

HttpURLConnection — это вообще издевательство. Конечно же, большинство использует OkHttp/Retrofit, пока Google пытается делать припарки библиотеке Volley.

Всё осложняется полнейшим отсутствием пакетного менеджера. В некоторых дистрибутивах Linux есть apt-get, apt или aptitude, на Mac OS можно установить brew. В Java-мире есть системы сборки Gradle, Maven и множество других. Все вышеперечисленные умеют скачивать пакеты, их зависимости, зависимости их зависимостей и т. д.. В Android нет ничего подобного — Gradle скачивает всё на многострадальный компьютер разработчика, ProGuard давится десятками библиотек, dx конвертирует их в Android-совместимый байт-код, программист загружает в маркет, пользователь — из маркета, рантайм верифицирует, загружает, компилирует все эти классы, собирает о них статистическую информацию для наиболее оптимального выполнения (profile-driven compilation). Всё это происходит снова и снова, даже если множество приложений используют одни и те же зависимости одинаковых версий, в том числе для таких крупных библиотек как AppCompat/Support, Google Mobile Services, ExoPlayer, Realm, FFMpeg. Последние две содержат нативный (машинный) код, что заставляет разработчиков собирать по несколько APK для разных архитектур (APK splits).

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

См. также: Graviton Browser.

Context

Контекст — это god object. View, например, нужен не контекст, а тема и ресурсы. registerReceiver мог бы быть методом не Context, а Application (для локального броадкаста) и, допустим, AndroidSystem (выдуманный класс) для броадкаста по всей системе.

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

non-configuration instance

Retain-фрагменты переживают смену конфигурации. Слышали что-нибудь о retain-фрагментах без View? Это костыль, который позволяет хранить объекты в таком фрагменте. В Activity есть похожий механизм — non-configuration instance (custom non-configuration instance у AppCompatActivity). Отсюда вопрос: почему нельзя сделать Activity живучей, как retain-фрагмент, и почему у фрагмента, наоборот, нет non-configuration instance?

Ресурсы

У Android собственный механизм для доступа к ресурсам. Стандартные для Java ресурсы из classpath работают, но медленно и расточительно. Та же проблема затрагивает механизм ServiceLoader, т. к. META-INF/services — тоже ресурсы classpath.

XML — основной и официальный способ описания ресурсов — от вёрстки, меню, переводов и графики до конфигураций, примитивов и строк. Он хорошо подходит для случая, когда ОС/лончеру/шторке нужно достать ресурсы, не пробуждая приложение, но мешает всяческим попыткам написать лаконично, гибко и без дублирования. Присутствует (неудавшаяся) попытка исправить XML статической типизацией.

Каждый ресурс формально находится в определённом пакете (package) — изолированном пространстве имён. На практике таковых всего два — android и пакет текущего приложения; Android Gradle Plugin сливает ресурсы изо всех AAR-библиотек в пакет приложения, провоцируя потенциальные конфликты имён, а также страшные костыли с перегенерацией R.java.

AttributeSet — это интерфейс, но реализовывать его бесполезно: obtainStyledAttributes кастит его к XmlBlock.Parser — а это package-private класс. По сути, AttributeSet играет роль маркер-интерфейса, что есть антипаттерн.

Сама абстракция Drawable кажется мне очень удачной, а разнообразие коробочных реализаций радует глаз. Но атрибуты темы не работают на четвёрках, названия XML-тегов (selector, shape) отличаются от имён классов (StateListDrawable, GradientDrawable), возможность использовать в XML свои классы доступна аж с SDK 24, а Drawable paddings влияют на View paddings по-разному, в зависимости от версии Android.

В векторных картинках можно задавать путям цвета из темы (например, ?colorPrimary). Но темы нельзя создавать из кода!

Нет никакой гарантии, что указанный ресурс вообще существует. Он может иметь значение только для определённых квалификаторов или не иметь его вовсе (<item name="..." type="..." />).

shrinkResources удаляет неиспользуемые ресурсы, но как-то несерьёзно; оставшиеся ресурсы не переименовываются.

Remote views нельзя создать из кода, только XML. Что вносит множество ограничений.

LayoutParams можно создать только из одного XML-тэга. <include> с layout_gravity, в котором находится <ViewStub> с layout_margin, который содержит вью с layout_width и layout_height — нельзя, получайте непереиспользуемый код.

В XML-вёрстку нельзя передавать параметры, будь это хоть LayoutInflater#inflate, хоть <include> или <ViewStub> (а вот в любом шаблонизаторе такая возможность есть).

В XML-вёрстке нельзя поставить точку останова (breakpoint), что делает отладку ошибок вроде Binary XML file line #0: Error inflating class ... чрезвычайно увлекательной. LayoutInflater работает рекурсивно, поэтому многие стектрейсы не вмещаются и обрезаются: 86 more...

findViewById — нечто абсолютно противоположное лаконичности и типобезопасности. Количество различных костылей, с этим связанных, превышает все мыслимые пределы.

Идеологически TypedArray — это массив TypedValue. Но функциональность у них разная: TypedArray предоставляет человеческий интерфейс из методов getText, getBoolean, getInt, getFloat, getColor, ..., а TypedValue ничего из этого не умеет. Зато у TypedValue есть поле changingConfigurations, а вот TypedArray#getChangingConfigurations возвращает одно общее значение для всех элементов (за O(n)) и доступно только для 21+. В итоге приходится использовать TypedArray даже для единственного атрибута и прибегать к помощи TypedValue, даже когда имеется «человеческий» TypedArray.

changingConfigurations у классов ColorStateList, Theme, TypedArray, Drawable, TypedValue расскажут, при смене каких конфигураций ресурс инвалидируется. Казалось бы, чтобы обновлять ресурсы вовремя, нужно придержать айдишники всех ресурсов, которые могут инвалидироваться в течение жизни данного компонента (т. е. (component.configChanges & resource.configChanges) != 0) и переопределить onConfigurationChanged данного Activity или View, где и можно перезагрузить протухшие ресурсы. Но метод Activity#getChangingConfigurations предназначен для другого и возвращает осмысленное значение, только когда активити уничтожается, а у вью вообще нет подобного метода. Чтобы узнать, какие конфигурации обрабатывает текущая активити, нужно спросить ActivityInfo у PackageManager. Хотя нужный объект ActivityInfo уже есть у Activity. В приватном поле.

Parcel

Писать собственную сериализацию — это всегда весело и задорно. Parcel очень напоминает DataInput и DataOutput из JDK, а Parcelable — это подобие Externalizable, но реализациями этих интерфейсов они не являются, что заставляет писать платформозависимый код.

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

Асинхронные события и фрагменты

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

Даже если транзакция выполняется в ответ на действие пользователя (например, непосредственно в OnClickListener), Activity вполне может быть на паузе в этот момент. Банальное нажатие кнопки «назад» может привести к падению, приложению при этом вообще не обязательно использовать фрагменты.

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

Обратная совместимость

APK, собранные с использованием gradle plugin 3 и build-tools 26, ломают лончер и файловый менеджер Cyanogenmod.

В AppCompat 26 сломали target у фрагментов в тех случаях, которые не описаны (и никогда не были описаны) в документации как недопустимые.

В javadoc не проставлены @since. (На сайте они откуда-то всё же берутся.)

Слабые контракты

Intent может содержать Action (строка), Uri, Extras (Bundle, т. е. Map<String, ?>). Для популярных extras есть заранее заготовленные ключи, например, Intent.EXTRA_EMAIL. Но нет типов. Так, можно попытаться открыть почтовый клиент, передав в качестве темы письма картинку, или открыть браузер, не передавая адреса веб-страницы. Чтение интента в видимой всей системе активити — поле непаханное для крэшей. Некоторые популярные приложения валились у меня на глазах при попытке поделиться картинкой. И дело не только в кривизне рук разработчиков, но и в изначальной проблемности такого механизма.

Кроме как в Extras, Bundle используется ещё и в аргументах фрагмента (Fragment.setArguments). Как всегда, нет гарантий, что по нужному ключу передан объект нужного типа.

Положим, у вас есть фрагмент, который принимает какой-то объект (выдумаем, например, ParcelUser) через аргументы. И вы осознаёте, что не нужно передавать объект целиком, достаточно передать идентификатор пользователя (ParcelUuid). Счастливого рефакторинга!

Профилирование

Взамен Android Monitor сделали полурабочий Android Profiler. Да, я посвятил открыванию хип-дампов целый тред.

Взамен Android Device Monitor не сделали ничего. Благо, его не удалили по-настоящему, а просто убрали из меню в IDE.

Единственный профайлер, способный предоставить полезную информацию, — это стороннее решение. Method tracing бесполезен, т. к. выбрасывает скомпилированный (и даже интринсифицированный!) код и использует интерпретатор. Systrace помогает найти медленные места, но очень приблизительно, т. к. фреймворковые методы инструментировать нельзя.

Баги

Ясное дело, баги есть в любом софте. Только здесь их, как мне кажется, никто не правит.

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

{"type":"articleComments","id":"f33da80f-db37-4111-93ab-30a62b1bcddd","comments":[]}

Javanese.Online в GitHub

Чаты и каналы в Telegram

RSS-лента