Javanese Online

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

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

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

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

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

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

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

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

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

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

Как работать с асинхронностью? У AsyncTask неудобный интерфейс и нерешённые проблемы с жизненным циклом. Loader полны багов и неудобны. ThreadPoolExecutor ничего не знают о жизненном цикле. Rx или корутины несложно адаптировать для работы с Android (в частности, отписываться в onDestroyView), но их нужно приносить с собой, т. к. фреймворк их не содержит.

Аналогично с JSON: я бы не стал использовать этот формат добровольно, но к этому принуждает, например, Google Billing Library. org.json, который есть в Android, хорош лишь в качестве академической реализации; практические реализации — Gson, Jackson, Moshi, Klaxon — нужно приносить с собой.

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

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 у фрагментов в тех случаях, которые не описаны (и никогда не были описаны) в документации как недопустимые.

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

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

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

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

Баги

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

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

Javanese.Online в GitHub

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

RSS-лента