Старый Мобильщик
74 subscribers
34 photos
1 video
1 file
118 links
Разработка мобильных приложений, дедлайны и все, что вы любите в IT.

Будни. Сниппеты. Заметки.

Когда-то были AsyncTasks ... Android 2.3.3 и ни одной вакансии в городе-миллионнике

Обсудить что-либо: @activitynotfound
Download Telegram
Channel photo updated
Пост 0. О себе и индексе массива

Представим, что вы в новой кампании. Примерно так выглядело бы ваше представление (немного утрированное) другим коллегам.
Звать вас Александром.
Вам 33 года уже (привет, Age-изм и все вытекающие?), с 2011 года вы занимаетесь только разработкой под Android. Полтора года вы руководили Android-командой удаленно из 5+2 человек и опыт этот даже был не плохой.
Повидали, как говорится, много разного Г. Ну и вы не из Москвы. Есть такой славный город Ростов-на-Дону.
Спросят: а что заканчивал? А вы гордо (нет) скажите Донской Государственный Технологический Университет, по специальности Компьютерная Безопасность. Хотя учились на коммерческой основе почти два года, пока не стукнуло заняться обучением и перейти на бюджетную основу. Одна из немногих ваших гордостей.
Обучились Android разработке сами, когда еще и загуглить-то по теме было особо нечего. Исходники и дедукция вам в помощь.
Начинали с Android 2.1 на эмуляторе. То еще, кстати. Удовольствие было.
Но все равно все хорошо, потому что была жажда новых знаний и желание писать качественный код. Тогда и без каки-либо курсов можно прекрасно справиться. Но хорошие коллеги - самое ценное, как ни крути.
Работали и работаете удаленно, хотя и в офисе потрудились.
Хороший опыт, хорошая ЗП (действительно, грех жаловаться) - казалось бы, что еще нужно?
Видимо писульки тут писать еще. Возможно, они кому-то будут полезными.
С этого и начнем, с нулевого. Вы же все знаете, что в нормальных языках индекс в коллекции всегда с нуля?
Старый Мобильщик pinned «Пост 0. О себе и индексе массива Представим, что вы в новой кампании. Примерно так выглядело бы ваше представление (немного утрированное) другим коллегам. Звать вас Александром. Вам 33 года уже (привет, Age-изм и все вытекающие?), с 2011 года вы занимаетесь…»
Пост 1. Charles Proxy

Charles - очень удобная штука для тех, кто работает с API. Для многих это не секрет!
Во-первых, легко можно просмотреть ответ от сервера, особенно, это удобно когда он довольно весомый и Logcat часть обрезает.
Во-вторых, виден запрос и все нужные параметры (заголовки, тело) - удобно отлаживать, когда вы пишите классы для работы с API.
В-третьих, ответ форматирован и его удобно просматривать.
В четвертых, все ответы сгруппированы по папкам. Это просто прекрасно.
В-пятых, запрос который выполняется подсвечивается цветом.
И самый весомый плюс (о котором мы поговорим в отдельном посте) - возможность мокать ответ от сервера (подменять на свои данные). Таким образом без единой строчки кода можно тестировать приложение.
И это далеко не все плюсы. Их гораздо больше.
Единственная засада - тулза платная и нужен ключик (про другие пути его использования упоминать не хочется)

Раньше было довольно муторно его настраивать, но теперь с Android Emulator можно все настроить за 3 минуты.
Поможет отличный гайд: https://mdapp.medium.com/the-android-emulator-and-charles-proxy-a-love-story-595c23484e02
Пост 2. Сказ о том, как Алеши в личке о смене дизайна договорились.

Друзья! Никогда не делайте так, как наши герои, если вы заинтересованы в долгой, продуктивной работе в команде и кампании в целом. 
Дело было так.
Утренний дейлик.
iOS-разработчик начал возмущаться на тему цвета и дизайнер сказал, что напишет ему в личку.
У меня эта задача уже была сделана. Но кто бы мог подумать, что дизайнер втихаря поменяет цвета?! Во время спринта. Без обновления Фигмы. И без сообщения в чате для всех заинтересованных разработчиков.
Потом ко мне в личку приходит тестеровщик и говорит "что-то цвета не совпадают с тем, что в дизайне".
Шикардос.
Сюжет прекрасный? И, наверное, классический для многих команд.
Так вот.
НЕ ДЕЛАЙТЕ ТАК НИКОГДА. ЕСЛИ ВЫ ОБСУЖДАЕТЕ ЧТО-ТО, ЧТО КАСАЕТСЯ ЕЩЕ КОГО-ЛИБО В ВАШЕЙ КОМАНДЕ - ОБСУЖДАЙТЕ ЭТО В ОБЩЕМ РАБОЧЕМ ЧАТЕ, ТЕГАЯ НУЖНОГО ЧЛЕНА КОМАНДЫ.
Такая простая истина, а на деле часто не выполняется и даже в 2021-ом от нее подгорает капитально.
Пост 3. ViewBindingPropertyDelegate

Неплохая обертка для удобной инициализации ViewBinding-а:
https://github.com/kirich1409/ViewBindingPropertyDelegate
для тех кому не нравится гугловое решение из доки, которое довольно многословно:

private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

Ccылка на доку откуда взят код: https://developer.android.com/topic/libraries/view-binding

Единственное, лучше использовать делегат без рефлексии (в либе отдельные зависимости на делегаты с рефлексией и без).
А если не хотите зависеть от дополнительной библиотеки, что сейчас почему-то становится трендом - скопируйте нужный делегат к себе в проект и поддерживайте как вам нравится.
Пост автора: https://proandroiddev.com/make-android-view-binding-great-with-kotlin-b71dd9c87719
Пост 4. Пахнущий код.

Что не так с этим кодом? 

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

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

Что еще тут плохо?
Конечно же, название интерфейса. Что за DataConverter? Название, которое не говорит вообще ничего. Какие данные мы конвертируем. Куда? Во что? 

И напоследок еще несколько замечаний.
Зачем проверять типы таким вот странным образом (цикл + continue)? Возможно, код сконвертирован из Java, но в Kotlin есть метод filterIsInstance и другие более лаконичные варианты. В данном случае же, код выглядит довольно запутанным.

Следующий нюанс - это строка:
val list: MutableList<Product> = ArrayList()

Есть функция mutableListOf(), которая во всех статьях только и мелькает. Еще красивее можно было б сделать через listOf() и apply() / run().

Что мне еще не нравится здесь - это использование Locale. Зависимость, которая не передается (не инжектится через DI) в конструктор, а используется напрямую. При чем зависимость вообще, которой тут быть не должно.

Такой маленький кусочек кода, а набрали столько проблем. Зато все эти принципы спрашивают и очень очень ждут, чтобы рассказали их смысл. Может чтобы самим разобраться заодно?
Пост 5. Способы ускорить сборку проекта

Печальнее ничего нет, чем сидеть и ждать пока соберется проект 5-10, а то и более минут.
Но процесс можно ускорить! Это сэкономит кучу вашего времени, а еще кучу денег кампании в который вы работаете.

Самый простой вариант - нормальное железо мы опустим в 2021-ом году. Хотя цены на комплектующие значительно выросли.
Рассмотрим только способы, которые можно применить здесь и сейчас. Как говорится с тем, что имеем.
Самый лучший вариант - гульнуть на почти все ОЗУ (на самом деле 4гб вполне достаточно даже для средних по величине проектов) и дать Gradle и самой Android Studio больше памяти.

Как это сделать?
В корне проекта есть файлик gradle.properties.
Нужно добавить строку:
org.gradle.jvmargs=-Xmx1536m //4g, 2g тоже будет работать
Обычно, эта строка и так есть - можно просто раскомментировать и добавить нужное количество ОЗУ.

Дальше стоит посмотреть на настройки Android Studion (см скриншот) и выставить максимальные, если позволяет ОЗУ. Если не позволяет, то билдить проекты вы будете долго, хотя наличие быстрого SSD тоже прекрасно помогает в этом.

Общие советы:
1. Используйте векторные ресурсы
2. Используйте формат WebP для графики, которую нельзя конвертировать в вектор (в Android Studio, кстати, есть опция Convert to WebP)
3. Обновите все плагины (чем новее тем лучше): Gradle Plugin, Android Plugin, Kotlin
4. Не забывайте обновлять библиотеки
5. Оптимизируйте ресурсы (часто в проекте много неиспользуемых ресурсов и ресурсов для каждой плотности экрана)
6. Используйте Gradle Cache
7. Используйте offline сборку (на любителя, потому что иногда бывают проблемы с либами, но обычно тоже ускоряет немного процесс сборки)
8. Если не можете поменять железо - можно присмотреться к внешнему SSD (даже они помогут ускорить сборку)
9. Используйте R8
10. Обновите ваш JDK. У JDK 9 версии более модный Garbage Collector, который еще и параллелится при выполнении. Чем версия JDK новее тем лучше (совместимая с Android SDK). У нас в проекте используется далеко не новая и это сильно влияет на скорость сборки.
11. Периодически чистите ваш код от мусора и старых, неиспользуемых классов.
12. Не используйте или используйте минимум рефлексию и библиотеки, основанные на ней

Не плохой конфиг для gradle.properties:
org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.caching=true
android.enableBuildScriptClasspathCheck=false
Есть еще опция: org.gradle.workers.max=2
Но пока не могу сказать насколько она помогает. Кому помогло - делитесь опытом.

Отличная сборная солянка советов здесь (on English):
https://medium.com/linedevth/build-your-android-app-faster-and-smaller-than-ever-25f53fdd3cdc
Не все советы отсюда мне нравятся (например Instant Run), но поэкспериментировать с настройками можно.
Пост 6. Полезные extensions (часть 1)

Немного полезных Kotlin extensions, которыми мы активно пользовались в предыдущем проекте.

//Context.kt
inline val View.layoutInflater
get() = context.layoutInflater

inline val Context.layoutInflater: LayoutInflater
get() = LayoutInflater.from(this)

inline fun Context.copyToClipboard(data: String, label: String?) {
val clipData = ClipData.newPlainText(label, data)
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(clipData)
}

inline fun Context.isAppAvailable(appName: String?): Boolean {
return try {
packageManager.getPackageInfo(appName, PackageManager.GET_ACTIVITIES)
true
} catch (e: Exception) {
false
}
}

//DialogFragment.kt
inline fun DialogFragment.show(fragmentManager: FragmentManager?, vararg args: Pair<String, Any?>) {
if (args.isNotEmpty()) {
arguments = bundleOf(*args)
}
fragmentManager?.let {
val tag = this::class.java.simpleName
if (it.findFragmentByTag(tag) == null) {
show(it, tag)
}
}
}

inline fun DialogFragment.show(fragment: Fragment?, vararg args: Pair<String, Any?>) {
show(fragment?.parentFragmentManager, *args)
}

//View.kt
inline fun View.showKeyboard(requestFocus: Boolean = false, forceShow: Boolean = false) {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
if (imm != null) {
if (requestFocus) requestFocus()
val flags = if (forceShow) InputMethodManager.SHOW_FORCED else InputMethodManager.SHOW_IMPLICIT
imm.showSoftInput(this, flags)
}
}

fun View.hideKeyboard() {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(windowToken, 0)
}
inline fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View {
return LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
}

fun View.visibleIf(condition: Boolean) {
this.visibility = if (condition) View.VISIBLE else View.GONE
}

inline fun View.visible() {
this.visibility = View.VISIBLE
}

inline fun View.invisible() {
this.visibility = View.INVISIBLE
}

inline fun View.gone() {
this.visibility = View.GONE
}


Как этим пользоваться?
Очень просто. Любое расширение становится как бы методом класса для которого оно написано. Особенно удобно, когда расширение пишется для базового класса иерархии.
Пару примеров вызова:

someEditTextView.showKeyboard()

// например так мы показываем BottomSheet
fun showNow(fragment: Fragment) = SomeSheetDialog().apply {
setTargetFragment(fragment, R.id.request_code)
show(fragment) // наш extensions
}

Если еще не перешли на Kotlin (да, такое бывает и в 2021-ом), то вот вам дополнительный стимул. Код гораздо короче и лаконичнее.
RedMadRobot решили проверить "хаотичное" обучение с нуля на примере Kotlin Coroutines:
https://www.youtube.com/watch?v=cHERit7LNGM

Пробежались по основам и немного рассказали про StateFlow и SharedFlow.
В целом полезное видео для тех, кто собирается переходить на корутины. Есть сравнение корутиновских флоу с Rx флоу. И особенно радует, что ребята рассказали на каком уровне использовать скоупы и диспатчеры в контексте современной архитектуры на базе ViewModel в качестве presentation-слоя.

Впрочем это можно почитать самостоятельно из главной статьи, которая постоянно упоминается в видео:
https://developer.android.com/kotlin/coroutines/coroutines-best-practices

Мне как человеку, которому довелось поработать в команде RedMadRobot всегда приятно смотреть за их развитием. И именно в RMR я начал использовать впервые Котлин, кажется в 2017ом! Так что ребята всегда следят за трендами и самое главное - применяют их на практике.

Ждем вторую часть!
Давайте проведем небольшой интерактивчик.
Сколько лет вы занимаетесь Android-разработкой?
Anonymous Poll
64%
Только начинаю / начинающий (менее года)
24%
1-3 года
6%
3-5 лет
6%
Более 5 лет
Пост 7. Неявное в Data-классах Kotlin

Data-классы - идеальный инструмент для создания моделей на Domain и Data-слоях (по сути любых моделей данных).
Если вы знакомы только с Java, то самая простая аналогия - это Java-класс с геттерами и сеттерами, для которого реализован стандартный паттерн hashCode() - equals() и toString(). Kotlin реализовывает это все за нас, что делает код невероятно компактным!

Но нужно помнить одну важную вещь.
Только поля, которые переданы в конструктор Data-класса будут добавлены в дефолтную реализацию паттерна hashCode() - equals().
То бишь если объявить класс так - все будет хорошо - все поля попадут в реализацию:

data class User(val name: String, val age: Int)


Но если тот же класс объявить так:

data class User(val name: String) {
var age: Int = 0
}

В реализацию hashCode() и equals() от Kotlin не попадет поле age. Об этом нужно помнить. Особенно во время ревью Pull Requests ваших коллег.

Еще у Data-классов есть классный метод copy(), который очень часто выручает. Делает по-прежнему работу с данными immutable, но позволяет менять какие-то данные при создании копии объекта:

recipeData = recipeData?.copy(sourceName = sourceName, sourceDisplayName = sourceName, sourceUrl = sourceUrl)

Говоря простым языком: если у вас есть объект, поля которого заполняются поэтапно (например, большая форма/экран, которую(ый) пользователь может заполнить частично) - copy() прекрасный вариант сделать это безопасно и элегантно.

Про Data-классы в sealed-классах поговорим отдельно.

Ссылка на оф доку: https://kotlinlang.org/docs/data-classes.html
Старый Мобильщик pinned «Давайте проведем небольшой интерактивчик.
Сколько лет вы занимаетесь Android-разработкой?
»
Что новенького в рассылках #1
Добрался до своих еженедельных рассылок и решил поревьювить - что вообще интересного есть.
Спойлер: не особо много.

Разве что...

Любопытная либа-надстройка над Retrofit от Slack:
https://github.com/slackhq/EitherNet
Позволит упростить процесс обработки ошибок от бекенда, используя sealed-классы Kotlin вместо обработки исключений. Особых проблем с обработкой исключений вроде не замечено, но с sealed-классами более красиво выходит.

и...

Любопытная статья на тему настройки CI поверх GithubActions:
https://proandroiddev.com/continuous-integration-delivery-for-android-with-github-actions-part-1-b232ed2b1740
Прогонет тесты и сбилдит apk-шку после пуша в репу. Может пригодится если ваша репа на Github.

Было еще пару примеров очередных архитектур, хайпов на тему Kotlin Multiplatform и примерчик игры "Змейка" на Compose for Desktop - вообщем, все как обычно.
Пост 8. Моки в Charles

Я в восторге от Charles Proxy!
Работая в такой, местами очень не гибкой сфере (мягко говоря), как банковская - постоянно сталкиваешься с какими-то трудностями при отладке. Всегда может внезапно отвалиться бекенд (довольно часто в момент вашего багфикса) и как раз в таких ситуациях моки приходят нам на помощь.

Настроить их очень просто.
1. Сохраните себе в текстовый файл ответ от сервера, который будете подставлять в Charles (лучше сделать несколько заготовок с разными данными)
2. Выбираете Tools - Rewrite. Откроется окно в котором нужно будет добавить новое правило и поставить галочку Enable Rewrite.
3. При создании правила нужно выбрать тип Body (мокаем ответ от сервера), ставим галочку Response и в поле Value подставляем сохраненный ответ от сервера.
4. В окне Location нужно добавить URL-адрес вашего бекенда и путь к вызову метода.
5. По сути все. Дважды нажимаем Ок, в эмуляторе делаем нужный нам запрос (в моем случае обновляем данные главного экрана) и мок будет подставляться.
Даже эмулятор перезапускать не нужно! Всего три минуты и можно тестировать и отлаживать.
Главное, не забудьте потом убрать галочку Enable Rewrite, чтобы снова вернуть реальный ответ от сервера.

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

Чуть подробнее можно почитать, например, здесь:
https://medium.com/@migueloruiz/charles-tips-and-tricks-mocking-dfddf0ffaadd
Пост 9. Полезные extensions (часть 2)

Сегодня у нас будет целая пачка мелких, но полезных на практике extensions. В основном по работе с разными ресурсами.

inline fun Fragment?.dimenPx(@DimenRes resId: Int): Int {
return this?.context?.resources?.getDimensionPixelSize(resId) ?: 0
}

inline fun Fragment?.dimen(@DimenRes resId: Int): Float {
return this?.context?.resources?.getDimension(resId) ?: 0f
}

inline fun View.dimen(@DimenRes resId: Int): Float = resources.getDimension(resId)

inline fun View.dimenPx(@DimenRes resId: Int): Int = resources.getDimensionPixelSize(resId)

inline fun View.color(@ColorRes resId: Int): Int = ContextCompat.getColor(context, resId)

inline fun View.drawable(@DrawableRes resId: Int): Drawable? = ContextCompat.getDrawable(context, resId)

inline fun Context.dimen(@DimenRes resId: Int): Float = resources.getDimension(resId)

inline fun Context.dimenPx(@DimenRes resId: Int): Int = resources.getDimensionPixelSize(resId)

inline fun Context.color(@ColorRes resId: Int): Int = ContextCompat.getColor(this, resId)

inline fun Context.drawable(@DrawableRes resId: Int?): Drawable? = resId?.let { ContextCompat.getDrawable(this, it) }

inline fun View.getString(@StringRes resId: Int): String = context.getString(resId)

inline fun Fragment.color(@ColorRes resId: Int): Int =
context?.let { ContextCompat.getColor(it, resId) } ?: Color.TRANSPARENT

inline fun Fragment.drawable(@DrawableRes resId: Int): Drawable? =
context?.let { ContextCompat.getDrawable(it, resId) }

inline fun Fragment.font(@FontRes resId: Int): Typeface? = context?.font(resId)

inline fun View.font(@FontRes resId: Int) = context.font(resId)

inline fun Context.font(@FontRes resId: Int): Typeface? = ResourcesCompat.getFont(this, resId)
inline fun Context.fontSpan(@FontRes resId: Int): TypefaceSpanCompat? = font(resId)?.getSpan()

inline val Context?.screenWidth: Int; get() = this?.resources?.displayMetrics?.widthPixels ?: 0
inline val Context?.screenHeight: Int; get() = this?.resources?.displayMetrics?.heightPixels ?: 0

Обратите внимание на аннотации. Лучше всегда помечать нужные типы ресурсов подобными аннотациями, чтобы и среда и различные линтеры (вроде Lint, KtLint) различали в каком случае у вас именно int, а в каком случае Int ссылается на ресурс цвета. Подобные утилиты помогут пройтись по коду и составить список потенциальных проблем, а среда их подсветит в виде Warnings при сборке проекта.

А аннотации Nullable / NotNull пригодятся еще и в смешанном проекте (в котором и Kotlin и Java).

Почитать подробнее про аннотации можно здесь:
https://developer.android.com/studio/write/annotations
Пост 10. О полезных иногда EventBus-ах

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

Думаю многие уже знают или со временем столкнуться с шинами или EventBus-ами. Сейчас, по большому счету - это антипаттерн.

Если совсем упрощать, то суть вот в чем: кто-то кидает события, а кто-то на них подписывается.

https://telegra.ph/Post-10-O-poleznyh-inogda-EventBus-ah-04-16-2