Javanese Online

Hype-driven Android-development, или как инженерная специальность превращается в маркетинг

Я привык считать программирование инженерной специальностью. То есть такой, где нужно проектировать, рассчитывать, измерять, а выбор обосновывать составленными списками плюсов и минусов. Где лучший специалист — тот, кто пишет максимально простой для понимания и внесения изменений код; кто способен спроектировать надёжную и производительную систему, разработать уникальную функциональность, проработать все угловые случаи; сделать пользователя довольным. При таком подходе основной критерий при выборе инструментов — баланс проблем, которые он решает, против неудобств, которые приносит.

Разберём в деталях, что происходит вместо этого в экосистеме Android-разработки.

ConstraintLayout

Теперь в свежесозданном проекте сразу подключена эта библиотека, а корневым контейнером заготовки вёрстки стал ConstraintLayout вместо RelativeLayout. Constraint — отличный нишевый инструмент для решения очень узкого круга задач: позволяет плоско сверстать сложные отношения, расставив вью, например, по окружности, в вершинах звезды или на кончиках усов котёнка. Только это не значит, что он везде должен собой заменить RelativeLayout, FrameLayout и LinearLayout. Многим проектам он не нужен вовсе, потому что способен улучшить вёрстку одного редко посещаемого экрана, ухудшив остальные. Самое неприятное, что RelativeLayout теперь переехал во вкладку Legacy в палитре компонентов визуального редактора (очевидно, в Android SDK ничего нет ему на замену).

Преимущества

Недостатки

RxJava 2

Библиотека для «реактивщины» — так сейчас модно называть асинхронную обработку событий. Является одной из реализаций парадигмы ФРП (функциональное реактивное программирование) (на самом деле нет).

Решает лишь те проблемы, которые уже давно решены с помощью java.util.stream и java.util.concurrent, от Executors до CompletableFuture.

Проблемы реализации

stream
    .filter(it -> it > 10)
    .map(it -> 10*it)
    .reduce((a, b) -> a + b)

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

stream
    .compose(filter(it -> it > 10))
    .compose(map(it -> 10*it))
    .compose(reduce((a, b) -> a + b))

такой стиль склоняет к более модульному коду и слабому связыванию.

Проблемы применения

Future<A> aFuture = ioExecutor.submit(() -> {
    Thread.sleep(500); // типа, сходили в интернет
    return new A(/* типа, вернули данные */);
});
Future<B> bFuture = ioExecutor.submit(() -> {
    Thread.sleep(500);
    return new B(...);
});
ioExecutor.execute(() -> {
    doSomethingWith(aFuture.get(), bFuture.get());
});

Здесь get — блокирующее ожидание результата. В UI-потоке, конечно, так делать нельзя, ждать придётся на бэгкраунде:

/**
 * Executes {@param supplier} on the {@param worker},
 * passes returned value to {@param consumer} on the {@param target}.
 * Ignores error handling problem.
 */
public static <R> Future<?> executeAsync(
        final ExecutorService worker, final Callable<R> supplier,
        final Handler target, final Consumer<R> consumer
) {
    return worker.submit(() -> {
        final R value = supplier.call();
        target.post(() -> consumer.accept(value));
        return null; // conform Callable
    });
}

void sample() {
    Future<A> aFuture = ...;
    Future<B> bFuture = ...;
    executeAsync(
            ioExecutor, () -> combineSomehow(aFuture.get(), bFuture.get()),
            new Handler(Looper.getMainLooper()), result -> {
                someTextView.setText(result.textValue());
            }
    );
}

Более современный и похожий на pipeline путь — CompletableFuture, который появился в Java 1.8 (2014), но был бэкпортирован как минимум дважды: stefan-zobel/streamsupport, retrostreams/android-retrofuture, (заметка о лицензии).

supplyAsync(() -> new A(...), ioExecutor)
.thenCombineAsync(
        supplyAsync(() -> new B(...), ioExecutor),
        ::combineSomehow,
        ioExecutor
)
.thenAcceptAsync(
        result -> someTextView.setText(result.textValue()),
        uiExecutor
)

В качестве uiExecutor не удастся передать экземпляр Handler, а вот лямбду runnable -> handler.post(runnable) — вполне.

public final class Debounce extends SimpleTextWatcher
        implements Runnable, View.OnAttachStateChangeListener {

    /**
     * Invokes {@param consumer} on {@param handler} passing text from {@param source}
     * at most one time in {@param delayMillis}
     * and only if {@param source} view is attached to window.
     */
    public static void debounced(
            Handler handler, TextView source, int delayMillis, Consumer<String> consumer
    ) {
        Debounce d = new Debounce(handler, source, delayMillis, consumer);
        source.addTextChangedListener(d);
        source.addOnAttachStateChangeListener(d);
    }

    private final Handler handler;
    private final TextView source;
    private final int delayMillis;
    private final Consumer<String> consumer;

    private Debounce(
            Handler handler, TextView source, int delayMillis, Consumer<String> consumer
    ) {
        this.handler = handler;
        this.source = source;
        this.delayMillis = delayMillis;
        this.consumer = consumer;
    }

    @Override public void afterTextChanged(@NotNull Editable s) {
        if (source.isAttachedToWindow()) {
            handler.removeCallbacks(this);
            handler.postDelayed(this, delayMillis);
        }
    }

    @Override public void run() {
        consumer.accept(source.getText().toString());
    }

    @Override public void onViewAttachedToWindow(View v) {
    }
    @Override public void onViewDetachedFromWindow(View v) {
        handler.removeCallbacks(this);
    }

}

В этом коде примечательно, что не нужно отписываться, — Debounce станет недостижимым вместе с иерархией вью. А при необходимости несложно добавить пару усовершенствований: отправку первого события сразу же при создании; отправку события при onAttachedToWindow, если предыдущая отправка была отменена из-за onViewDetachedFromWindow

Преимущества

Только вот разработчикам библиотек стоит учитывать, что если они хотят поддержать реактивность, нужно добавлять в зависимости не RxJava, а Reactive Streams, чтобы позволить клиенту выбирать реализацию самостоятельно. Ведь есть ещё как минимум Project Reactor.

DI-фреймворки

Из-за фундаментальных ошибок проектирования Android даже такая простая задача, как создание графа объектов и удовлетворение их зависимостей, превращается в кошмар. Что могут предложить нам DI-контейнеры и Service Locator'ы вроде Dagger 2, Toothpick, Koin, KoDeIn?

Решаемые проблемы

Привносимые проблемы

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

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

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

MVP

Model-View-Presenter — порт неудачного server-side паттерна Model-View-Controller на client-side. Презентер отличается от контроллера тем, что не только передаёт данные во View, но и слушает события из View и реагирует на них.

Решаемые проблемы

Собственные проблемы

Moxy

Moxy — реализация MVP, которая сохраняет презентер между сменами конфигурации, а после пересоздания View восстанавливает его состояние, передоставляя события.

Восстановление View работает только при смене конфигурации (что довольно бесполезно, ведь в Activity можно поставить configChanges="orientation|screenSize" и не терять вью, а в retain-фрагментах можно хранить состояние в полях, и его никто не украдёт). В случае рестарта процесса никак не поможет. Даже наоборот: рекомендуется «очищать» состояние вью в onFirstViewAttach(), потому что после пересоздания процесса создастся новый презентер, а состояние View восстановится встроенными средствами Android.

То есть единственная задача, с которой справляется Moxy, — протаскивание багов мимо тестирования в релиз (ну, пока отдел тестирования не прочитает про Don't Keep Activities). Всё это — ценой замедленной компиляции (apt/kapt), добавления больших объёмов сгенерированного кода, замусоривания результатов поиска классов и символов в IDEA/AS.

AAC ViewModel

Не предлагает вообще ничего, не помогает никак. Положить HashMap<Class<T>, T> в nonConfigurationInstance можно и без помощи библиотеки.

Не имеет никакого отношения к ViewModel из MVVM, правильное название — RetainObject или NonConfigurationInstance.

Отнимает у разработчика доступ к конструктору (ну, как обычно).

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

Retrofit

Retrofit представляет удалённые HTTP-эндпоинты в виде Java-методов. Считаю, что это очень уместная абстракция. Однако, имеются проблемы:

На пересечении Retrofit и ProGuard возникает ещё одна проблема: т. к. ProGuard не предоставляет возможности сохранить определённые аннотации, вырезав остальные, приходится писать -keepattributes *Annotation*, сохраняя все. Когда добавляется Kotlin, в конечное приложение попадает ощутимое количество мусора в виде @kotlin.Metadata.

Clean architecture

Вероятно, здесь я буду критиковать не саму clean architecture Дядюшки Боба, а распространённую в русскоязычном сообществе интерпретацию. В качестве примера разберём теоретический материал по основам архитектуры на ломаном русском.

Решаемые проблемы

Привносимые проблемы

В продолжение этой темы некоторые разделяют проект на модули. Но, опять же, вместо того чтобы отделять фичи (что бывает полезно), они отделяют слои. Смысл этого костыля в том, что kapt меньше тормозит, если Room, Moxy, Dagger находятся каждый в своём модуле.

Конференции

Множество докладов заключается в пересказе документации очередной модной библиотеки или изложении очередного модного подхода. Главный вопрос — какая при этом решается проблема — остаётся без ответа.

Трудоустройство

Казалось бы: пусть сходят с ума как хотят. Кому какое дело? Проблема в том, что мусорные библиотеки стали стандартом и насквозь пронизали индустрию. Хорошую работу со своевременной «белой» зарплатой и адекватным руководством найти непросто, а тут ещё и технологии, которые делят индустрию на несколько лагерей.

Я вижу такой (неутешительный) вывод: на рынке труда лучше себя чувствует тот разработчик, который владеет модными инструментами, может найти им применение там, где это не нужно, сумеет разобраться в «современном» коде. А инженерные вопросы стали своего рода «вопросами религии» и могут спровоцировать неиллюзорные «холивары». Я уже предвижу в комментариях религиозных фанатиков, адептов превеликого Dagger и святой RxJava, которые абсолютно необходимы в любом современном CRUD-приложении.

Вместо завершения

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

{"type":"articleComments","id":"8adb9cd1-df2a-496b-98ea-4247c04686ca","comments":[{"id":"c7e64a17-3a27-4937-8cda-29c8a4e4fbf5","authorSrc":"GitHub","authorId":"Nik-Gleb","text":"Miha! :)\nБольшое тебе спасибо за такую содержательную статью, такой глубокий анализ.\nНа самом деле твои мысли - имеют схожее отражение у 25% наших коллег, и \nвозможно еще у 25% - это молчаливый глас совести. \nОчень рад что кто-то взялся за оформление этих позиций в тезисы и словесную форму.\nБолее того - статья получилась, по-настоящему техническая, беспристрастная, с анализом, \nплюсами и минусами. Приятно, что она в итоге ни к чему не клонит, не призывает, \nне снабжена приглашением на очередной доклад или репозиторий на гитхабе.\nАбсолютно согласен с общим посылом в целом и особенно с абзацем-заключением \nпро \"влияние на трудоустройство и рынок труда\". Всё прям точно сказано! \nЯ бы сказал - \"как с уст снял)\"","added":1554100924807,"answers":[]},{"id":"6d2c7c23-1d8d-4529-829b-b110a9f2ba02","authorSrc":"GitHub","authorId":"andrewgrow","text":"Отличная статья, спасибо! Вот прямо мои мысли оформил в текст. Сохранил в закладки, а то в спорах я быстро думать не умею и потому проигрываю обсуждения. А тут вот прямо готовый меч-кладенец ) ","added":1554193913732,"answers":[]},{"id":"d399eebc-e724-416a-85d5-31afcca9ae26","authorSrc":"GitHub","authorId":"Semper-Viventem","text":"Миша, спасибо за статью! Только про Rx хочу уточнить.\n\n> Решает лишь те проблемы, которые уже давно решены с помощью java.util.stream и java.util.concurrent, от Executors до CompletableFuture.\n\nТак реактивные стримы вроде CompletableFeature и появились только в Java 8 (2014 год), а первый коммит в RxJava был в 2012 (https://github.com/ReactiveX/RxJava/commit/697fd66aae9beed107e13f49a741455f1d9d8dd9), а это несколько раньше получается. Затем вышла вторая версия RxJava, которая является идейным продолжением, и исправляет некоторые проблемы, и предлагает новые решения для работы с реактивными потоками.\n","added":1554205646602,"answers":[{"id":"f0a68fac-6615-487b-b7eb-f8ba3d7f45da","authorSrc":"GitHub","authorId":"Miha-x64","text":"Думаю, что создатели `CompletableFuture` вдохновлялись именно идеями RxJava.\nНо сейчас уже нет большой разницы, что появилось раньше: просто есть разные инструменты и выбор между ними, а CF — один из примеров возможного выбора.\n\n(А мне обычно хватает `Executors`.)","added":1554207690287,"answers":[]}]},{"id":"127b77a7-7ae8-4ad1-8a00-2729b75f6003","authorSrc":"GitHub","authorId":"alaershov","text":"Было бы классно предложить альтернативы по каждому из перечисленных вами пунктов. Вы назвали популярные решения для некоторых задач. Да, решения не идеальные, но ничего ведь не происходит на ровном месте, в том числе и перечисленные вами библиотеки. Если вы не используете ни одно из них, и утверждаете, что это всё не нужно, и при этом нашли свой подход, который решает все задачи лучше, чем \"хайповые\" решения, то, может быть, поделитесь этим подходом с массами?","added":1554208053912,"answers":[{"id":"9bd3b450-12d7-4628-9234-55864171dbe9","authorSrc":"GitHub","authorId":"Miha-x64","text":"Часть проблем считаю совсем надуманными. Часть решаю «коробочными» инструментами. Ещё для части разрабатываю [библиотеку](http://github.com/Miha-x64/reactive-properties/), про которую буду [рассказывать на AppsConf](http://appsconf.ru/moscow/2019/abstracts/4652).","added":1554212932412,"answers":[{"id":"43f29f84-62ef-41d8-879c-54960ccfda52","authorSrc":"GitHub","authorId":"alaershov","text":"Круто, ждём доклада)","added":1554268273476,"answers":[]}]}]},{"id":"cbaa06ff-bd6e-442c-b473-47ec1a4e2c21","authorSrc":"GitHub","authorId":"Yukooo","text":"Автор, не путай людей, применения любого инструменты в работе требует ясного понимание, что это за инструмент и как с ним правильно обращаться.\nPS. Наберись опыта и поучаствуй в больших продуктовых проектах (от 10 человек).","added":1554218059313,"answers":[{"id":"69d90bb2-113c-428e-8808-ebeb95b65435","authorSrc":"GitHub","authorId":"Miha-x64","text":"Аргументов, конечно, не будет.","added":1554226187779,"answers":[]}]},{"id":"3a5fd027-93d6-40f3-a935-b6f83d961bd3","authorSrc":"GitHub","authorId":"techyourchance","text":"В деталях я не согласен по нескольким пунктам (особенно с тем что касается Dependency Injection), но в целом, конечно, все верно, и от этого очень грустно. Особенно огорчает ситуация с конфами (хотя мне кажется, что в русскоязычном сообществе проблема не так остра как везде) и трудоустройством.\nСпасибо за статью. ","added":1554263769543,"answers":[]},{"id":"ca0e15d7-542e-4af0-a093-a10d4cba9fc0","authorSrc":"GitHub","authorId":"ilyamodder","text":"TL;DR: все хайповые инструменты мне непонятны, а значит, не нужны. Что мне привычнее, то и надо использовать","added":1554981786869,"answers":[{"id":"4a599abf-36e4-4896-b8fb-aca1050e208e","authorSrc":"GitHub","authorId":"Miha-x64","text":"tl;dr в статью не вникал, аргументов не будет.","added":1555239707481,"answers":[]}]},{"id":"0e508c79-b4e8-40c9-90ef-9dc4b2d7fc8e","authorSrc":"GitHub","authorId":"syndarin","text":"Похоже на очередной плач Ярославны, если честно.\n\n1. ConstraintLayout - возражу с помощью всего лишь двух аргументов: Chains & Guidelines. Сделать интерфейс, элементы которого равномерно расположены по экрану, либо же занимают фиксированную его долю всегда было большой головной болью, которую эти фичи успешно решают. Тем паче, что костыли с LinearLayout & weights работают далеко не во всех случаях.\n\n- \"неудобство переиспользования/композирования отдельных частей вёрстки\" - что, include уже запретили? Если Вы не об этом, то тогда вообще не вполне понятно, как Вы привыкли её переиспользовать (копипаст не считается).\n\n2. RxJava:\n- \"невозможность передачи null-значений\" - строго говоря, воспринимая любой rx-овый тип как поток значений, не вполне понятно, зачем нужны отсутствующие? Если они таки нужны, то, имхо, это проблемы с дизайном Вашей архитектуры. Ну, в крайнем случае, используйте Optional, кто мешает? \n- \"обилие разных интерфейсов\" - обусловлено разным поведением, не находите?.\n- \"необходимость ручной отписки (плохо ложится на парадигму автоматического управления памятью)\" - за отсутствием memory leaks тоже приходится следить вручную, Вас это не напрягает?\n- \"возможность возникновения OnErrorNotImplementedException\" - простите, как разумный аргумент воспринимать не могу.\n\nИ да, StreamAPI не замена Rx. По крайней мере, если не использовать Rx исключительно для операций над коллекциями. Из часто используемых примеров - zip, combineLatest, withLatestFrom.\n\n3. DI-фрейморки\n\nВступление уже прекрасно: \"Из-за фундаментальных ошибок проектирования Android даже такая простая задача, как создание графа объектов и удовлетворение их зависимостей, превращается в кошмар.\"\n\nЭто действительно причина рассматривать DI-фреймворки как просто очередной хайповый инструмент?\n\n\n","added":1554982596216,"answers":[{"id":"8ab8cfde-015e-4de0-a844-11c1fc81106a","authorSrc":"GitHub","authorId":"Miha-x64","text":"> Сделать интерфейс, (...) всегда было большой головной болью, которую эти фичи успешно решают.\n\nК такому применению никаких претензий нет. Я же говорю о ситуации, когда констреинт используется вместо всего.\n\n> что, include уже запретили?\n\nЕсли не рассматривать вариант инклуда констреинта в констреинт, то все include-вёрстки должны содержать layout-параметры именно для констреинта. Тогда такие «вставки» гораздо меньше похожи на самостоятельные компоненты и сильно зависят от родительского контейнера.\n\n> Если они [null-значения] таки нужны, то, имхо, это проблемы с дизайном Вашей архитектуры.\n\nСогласен. Мне вообще приятно писать на языках без нуллов. [А кому-то это зачем-то нужно](https://stackoverflow.com/a/45371201/3050249)\n\n> в крайнем случае, используйте Optional, кто мешает?\n\nВ Java/Kotlin встроен бесплатный `Optional`. `null` называется.\n\n> (...) OnErrorNotImplementedException\" - простите, как разумный аргумент воспринимать не могу.\n\nСогласен, я плохо сформулировал свою идею. В JVM-мире нет подходящего решения проблемы, лучше удалю этот пункт.\n\n> И да, StreamAPI не замена Rx.\n\nДа, ничто не замена Rx, кроме Rx. Для разных применений я предложил разные замены.\n\n > Это действительно причина рассматривать DI-фреймворки как просто очередной хайповый инструмент?\n\nНет, это вступление, в котором я утверждаю, что проблема зависимостей в Android стоит действительно остро. А вот DI-фреймворки — неудачное её решение.","added":1555245167482,"answers":[]}]},{"id":"d2e05e88-ca21-4a20-9a68-da535cd07c3e","authorSrc":"GitHub","authorId":"KuVaUgU","text":"Почти со всем согласен. Вы работу не ищите?","added":1554982985611,"answers":[{"id":"22c75e8b-9698-4840-96c4-7f22c843f0ea","authorSrc":"GitHub","authorId":"Miha-x64","text":"Короткий ответ — всё сложно. Можно [обсудить в Телеграме](https://t.me/Harmonizr)","added":1555247541204,"answers":[]}]}]}

Javanese.Online в GitHub

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

RSS-лента