Саша Аскеров

Саша Аскеров

Неделя
Aug 23, 2021 → Aug 29, 2021
Темы

Архив недели @alex_askerov

Понедельник


Всем привет! Меня зовут Саша и я лид андроид команды во Wrike. Есть твиттер @alex_askerov, в который я не пишу, но может быть это поменяется после недели тут. Телега: AlexAskerov

План на неделю такой. Пн: Знакомство. Немного про андроид приложение Wrike. Вт: Реализация оффлайн режима. Ср: Философское про качество. И немного конкретики про автотесты. Чт: Интервью. Пт: Процессы. Сб, Вс: Дни без обязательств.

Отношения с андроидом у меня начались в 2010 году. Я на последнем курсе универа и чуть-чуть умею в джаву. Было 2 варианта: андроид или бекенд. Андроид победил

Первым местом работы был аутсорс Unreal Mojo. Напомню, это 2011 год и мобильная разработка только зарождается. Были конечно ребята, которые что-то делали для что-то для Pocket PC, Symbian etc, JavaME, но это не то и не та массовость.

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

Было весело. Что-то получалось, что-то не очень. При этом в клиентах сразу появились всякие банки, яндексы и т.д. Которые через какое-то забирали разработку себе.

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

В 2014 я начал работать во Wrike. И практически все, о чем я буду рассказывать связано с работой тут. Далее о Wrike и нашем андроид приложении.

Wrike это SaaS приложение для управления работой. Система большая, сложная, в большей степени ориентированная на крупных клиентов.

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

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

Все это одной небольшой андроид командой (3-5 разработчиков в разное время). Главный плюс маленькой команды в том, что ты привыкаешь к людям, легко их понимаешь, знаешь от кого чего ждать.

Главный минус: редко приходят новые люди. А новые люди всегда приносят что-то полезное. Сейчас мы понемногу становимся больше. В один из дней будет тред про наш подход к собеседованиям.

Немного о технической стороне нашего приложения. Ходят слухи, что мобильное приложение у нас Flutter. Но нет, и предположу, что не будет :)

Апп появился в 2013 году. Тогда у нас было примерно следующее: UI на фрагментах. Внутри фрагментов лоадеры. Лоадеры подписаны на апдейты базы данных по URI через ContentResolver. Обновления в базу записываются через ContentProvider и идут на сервер через SyncAdapter.

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

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

Но сидели мы на этом довольно долго и перешли только на ViewModel/LiveData, когда они появились. Стали использовать всякие MediatorLIveData и NetworkBoundResource. Это была плохая идея :) Удивительно, но RxJava прошла мимо нас.

Сейчас у нас нас почти MVVM. ViewModel с юзкейсами внутри, когда они нужны. Наружу из вью моделей торчит Flow с состоянием.

Кстати, когда я тут говорю ViewModel, это не совсем андроидная ViewModel. По крайней мере мы от нее стараемся зависеть поменьше. Зачем? А вот medium.com/wriketechclub/…

Так что у нас есть легаси из лоадреов, легаси из логики на MediatorLiveData и NetworkBoundResource, и еще некоторе. Звучит страшно? Похоже на монстра франкенштейна?

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

Иногда мы пишем велосипеды. Есть например своя реализация навигации, своя библиотека для работы с базой данных поверх SqliteDatabase на Kotlin. О чем-то из этого интересно подробней?

Читатели, а какое у вас отношения к велосипедам, для всяких штук, для которых есть некоторе стандартное решение: от гугла, или стороннее, но популярное.
🤔 54.2% У нас такие есть
🤔 45.8% Всегда возьму готовое

На сегодня все. Завтра будет про то, как мы делаем поддержку оффлайна. А в приложении, котором вы занимаетесь есть оффлайн режим?
🤔 43.9% Нет.
🤔 27.1% Да, но только чтение
🤔 29.0% Да, с редактированием.

🔥Тред (Саша Аскеров)

Вторник


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

Зачем оффлайн? Причина очевидная: отсутсвие интернета или нестабильность интернета. И менее очевидная: качественный UX. Нам не нужно заставлять пользователя ждать пока сервер что-то ответит, блокировать его ошибками про соединение и т.п.

На сегодня все. Завтра будет про то, как мы делаем поддержку оффлайна. А в приложении, котором вы занимаетесь есть оффлайн режим?
А еще, толстый клиент это интересно. Немножко сложнее, чем отправил запрос -> показал результат. Кстати, удивлен результатами вчерашнего опроса. Думал первый и второй варианты будут сильно популярнее, а там примерно поровну. twitter.com/mobileunderhoo…

Во первых для оффлайна нужен single source of truth. В нашем случае это локальная sqlite база данных. Не нужно выбирать источник данных в зависимости от наличия коннекта.

Чтение: прочитали данные из базы, параллельно пошли в интернет, обновили данные в базе. Запись: записали в базу -> прогнали синк через ворк менеджер UI подписан на изменения в нужных таблицах, например на уровне ViewModel

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

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

Тут есть 2 варианта. Первый: на каждый апдейт сохранять данные только в истории операций. Начнем с него.

Заведем таблицу "Operations": "entity_type" и "entity_id" — поля ссылки на основную сущность, которую изменяем "field" — название поля, которое изменяем "new_value" — новое значение для этого поля

При таком подходе у нас получаются иммутабельные сущности с точки зрения локальных изменений. Любое локальное обновление — это простая вставка записи в таблицу операций.

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

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

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

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

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

Но чтение в таком случае простое: таблица операций нас не интересует, мы просто читаем основную таблицу сущности.

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

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

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

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

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

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

Впрочем, некоторые запросы вообще нет смысла повторять. Если сервер ответил Bad Request, то вряд ли получится добиться иного. Напротив, всякие ситуации с недостатком разрешений на редактирование часто решаемы.

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

В заключении оставлю ссылку на серию статей разработчиков из Trello на эту же тему. tech.trello.com/sync-architect… Я ее читал уже после того, как мы сделали свою реализацию. Как это ни странно, у нас одинаковые проблемы :) А вот решали мы их уже по разному.

🔥Тред (Саша Аскеров)

Среда


Сегодня про качество. Для начала, бессмысленный в своей категоричности опрос. Какое "качество" важнее. (ясное дело, добавь сюда вариант "я за все хорошее" он победит, поэтому его не будет)
🤔 14.3% Кода
🤔 85.7% Продукта

The correlation principle says that our productivity is tightly correlated with the internal quality of software. The two go up together, and they go down together, and you can't trade away the one to get more of the other. Let's talk it over.
Большинство выбирает продукт. Очевидный выбор на первый взгляд. Но можно и поспорить.То что я тут напишу дальше моя вольная и короткая интерпретация мыслей из этого треда.twitter.com/GeePawHill/sta…

Качество приложения можно разделить на внешнее и внутреннее. Внешнее это все то, что видимо пользователю. Его определяют UX, скорость работы, баги. Внутреннее это про то, с чем сталкиваемся мы как разработчики. Это качество нашего кода. Далеко не всегда одно означает друге.

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

Какое качество важнее? Каким из них жертвовать при релизах если мы хотим уменьшить time-to-market или делаем какой-то MVP?

Это не очевидно, но практически всегда разумно пожертвовать внешним качеством: например сделать попроще UX, не упариваться в оптимизации производительности.

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

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

Вывод из этого: всегда лучше вести переговоры с условным PO, когда он говорит: “а давай сейчас вот эту новую фичу как-нибудь по быстрому, а потом там поправите у себя все как надо”

“Потом” может и не наступить, так как потом захотят развивать, а не рефакторить. И убедить, что сначала надо что-то там внутри сделать хорошо будет не проще чем вначале.

🔥Тред (Саша Аскеров)
UI тесты. У нас они появились около года назад. Начинали их мы по большей части силами разработчиков. Сейчас больше ими занимаются уже QA.

Используем Espresso и пишем тесты на Kotlin. Тесты разбиты на 2 слоя. Внутренний — работа с вьюшками и именно он у нас завязан на эспрессо. Внешний: собственно код самого теста. Тут никаких зависимостей на сторонние фреймворк.

Так как в Kotlin есть такая штука как lambda with receiver код теста можно сделать максимально простым и легким для восприятия. kotlinlang.org/docs/lambdas.h…

Вот примерно так, как описано в статье. medium.com/android-bits/e…

У разделения на слои очевидный плюс: не нужно переписывать конечный тест, при не значительных изменениях дизайна. Или при переписывании вьюшки с xml на compose :)

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

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

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

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

Сделать тесты Espresso стабильными бывает не просто. Вот набор неплохих советов на тему medium.com/stepstone-tech…

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

Есть у вас UI тесты? Espresso (если речь про андроид)? Пишут сами разработчики или QA? На чём гоняются? Расскажите тоже что-нибудь из опыта :)
🤔 52.2% Есть!
🤔 47.8% Нет :(

🔥Тред (Саша Аскеров)

Четверг


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

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

В первую очередь нужно ответить на вопрос, а какой человек нужен. И речь не про уровни senior/middle. Кому-то в кайф всякие технические внутренности, кто-то любитель продуктовых задач. У всех разные скиллы и предпочтения. Редко, когда в одном человеке сочетаются все качества.

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

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

Но знаю, что многие тестовые не любят. Станете делать если предложат? Пишите почему.
🤔 31.4% Да
🤔 27.6% Нет
🤔 41.0% Зависит от задачи

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

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

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

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

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

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

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

Интервью не ограничивается обсуждением наших задач. Обсуждаем прошлые задачи кандидата, которые ему были интересны. Минимизируем вопросы, в духе “А что означает xxx в котлин”. Совсем без такого нельзя, но чем серьезней кандидат, тем меньше нужно таких прямых вопросов.

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

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

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

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

Кто-то, кто не попал в компанию "A", попали в компанию "Б" и вполне успешно там работает. Может все эти наши сложные вопросы просто способ потешить самолюбие интервьюера? Ваши мысли?

🔥Тред (Саша Аскеров)

Пятница


Тут у меня по плану было что-то про процессы. Но пятница и что-то я процессами сегодня уже наелся. Лучше пару твитов про Jetpack Compose.

Наконец-то руки добрались попробовать. Вроде даже нравится. Хотя мозг с непривычки сопротивляется и говорит: "да зачем тебе это все, иди ты уже тут constraint layout и recycler view сделай, быстрее будет".

Некоторая недоделанность присутствует. В списки на компоузе скроллбары не завезли почему-то.

А еще, если по какой-то странной причине у вас имеется класс, который наследует MediatorLiveData и переопределен setValue, то код не скомпилится issuetracker.google.com/issues/1957014…

Что у вас с компоузом? Как впечатления? На баги натыкались?
🤔 16.2% Уже в продакшене
🤔 40.5% Изучаем
🤔 43.2% Без него обойдемся

🔥Тред (Саша Аскеров)

Суббота


Хитрые моменты с SqlCipher. Очень специфичная мало кому нужная тема. Как раз для вечера субботы. Если ваш апп с лютыми секюрити требованиями и какой-то работой с локальными данными, то вы скорее всего в курсе про SqlCipher github.com/sqlcipher/andr…

В нем все хорошо. И всего-то надо заменить android.database.sqlite.* на net.sqlcipher.database.* и все должно заработать. И заработает. Но несколько по другому.

Все дело в том, что все что лежит в net.sqlcipher.database это форк какой-то очень древней android версии этих классов. И самое неприятное, что там есть, это по умолчанию включенный лок на все операции.

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

Лок на операции регулируется методом setLockingEnabled. В андроидном SQLiteDatabase этот метод deprecated уже много лет и ничего не делает, а SQLiteDatabase сам заботится об оптимальных блокировках.

А вот в sqlcipher он используется и по умолчанию включен. Нужно отключить, но не просто так.

Во первых включаем WAL. Что это такое есть в документации sqlite.org/wal.html Но главное, что он позволяет операциям записи и транзакциям не блокировать чтение. А операциям чтения — запись.

Во вторых, позаботимся о том, чтобы не писать в базу конкурентно. Заботится нужно только об update, insert, delete, так как транзакции идут через форсированный лок и метод setLockingEnabled на них не действует.

Как именно? Например выделить на запись в базу отдельный single thread executor и пустить все через него. Или сделать все записи через транзакции. Да, оверхед, но не очень большой.

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

SqlCipher поддерживает Room. Им никогда не пользовался, так что про него ничего сказать не могу. Но я думаю, что там та же история, так как SQLiteDatabase просто реализует SupportSQLiteDatabase.

🔥Тред (Саша Аскеров)

Воскресенье


Время прощаться. Мне было ново и интересно. Надеюсь, что для кого-то было полезно. Спасибо!

Планы. Осталось неопубликованными пара заметок: про процессы и впечатления от перехода из разработчика в лиды — получилось сумбурно и мне не понравилось. Доведу до ума и выложу в @alex_askerov

В работе сейчас есть планы на Kotlin Multiplatform Mobile. Пока больше как эксперимент над одним из экранов. Когда закончим обязательно напишем о результатах. Текущее состояние: отвязываемся от андроидных зависимостей и вытаскиваем код в мультиплатформенный модуль.

А на этой неделе тут было:

Сегодня о том, как можно делать поддержку оффлайн режима приложения. Такого, в котором данные можно просматривать и изменять, а потом все синхронизируется. Плюс к этому, с одними и теми же данными могут работать несколько клиентов.
Полноценный оффлайн twitter.com/mobileunderhoo…

Сегодня про качество. Для начала, бессмысленный в своей категоричности опрос. Какое "качество" важнее. (ясное дело, добавь сюда вариант "я за все хорошее" он победит, поэтому его не будет)

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

Поговорим про собеседования. Последние месяца четыре мы понемногу расширяем нашу команду. Поделюсь нашим подходом.
Собеседования twitter.com/mobileunderhoo…

Хитрые моменты с SqlCipher. Очень специфичная мало кому нужная тема. Как раз для вечера субботы. Если ваш апп с лютыми секюрити требованиями и какой-то работой с локальными данными, то вы скорее всего в курсе про SqlCipher github.com/sqlcipher/andr…
SqlCipher и его отличия от андроидной реализации twitter.com/mobileunderhoo…

🔥Тред (Саша Аскеров)

Ссылки