Javanese Online

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

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

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

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

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

Такой подход становится феерической помехой повторному использованию кода: при добавлении новой функциональности она не делегируется объектам, которые передаются в конструктор, а реализуется прямо внутри фрагмента (активити). Впоследствии такой класс становится типичным 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 или Volley.

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?

Ресурсы

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

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

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

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

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

Parcel

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

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

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

У фрагментов может случиться state loss. И всё бы ничего, но в стек трейсе не будет вашего кода, т. к. исключение бросается в асинхронном коде.

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

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). Счастливого рефакторинга!

Баги

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

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

Javanese.Online в GitHub

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

RSS-лента