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

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

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

Обсудить что-либо: @activitynotfound
Download Telegram
Оказывается Text в Compose не поддерживает includeFontPadding: https://issuetracker.google.com/issues/171394808?pli=1

Обходной вариант - создать composable wrapper для TextView: https://stackoverflow.com/questions/66126551/jetpack-compose-centering-text-without-font-padding
Временный костыль.

Так что, сделайте доброе дело - нажмите на звездочку в issue, чтобы Google поправил эту багу в более приоритетном режиме🙂
Пост 21. Немного о юнит-тестировании на примере Spek 2.

Устали от постов про Compose? Отлично!
Поговорим о тестировании.

У нас в банке для юнит-тестов используется фреймворк Spek: https://github.com/spekframework/spek/ который позволяет писать тесты в виде спецификаций, что вероятно, более наглядно для читающего.

Но сперва пару слов зачем вообще тестирование? В частности, юнит-тестирование.
Немного отсебятины, на примере нескольких компаний:
1. Отловить часть проблем, которые могут попасть в релиз. Условно CI собирая сборку, прогоняет тесты, и мы можем увидеть, что отвалилось или где закралась ошибка, когда тесты упадут.
2. Качество кода и в итоге продукта. Но здесь стоит учитывать code coverage - процент кода, покрытого тестами. Чем он выше - тем лучше.
3. Через тестирование ведут разработку, например TDD. Пишут сперва тесты, которые валятся и потом пишут код, который позволит пройти эти тесты.
4. Юнит-тесты прогоняются локально, на машине. Не нужно запускать эмулятор или иметь ферму устройств в облаке. Таким образом можно быстро протестировать бизнес-логику.

В этой часте мы настроим либу и напишем немного простых примеров.

Добавляем в app/build.gradle строки:


testImplementation 'org.junit.platform:junit-platform-engine:1.6.2'
testImplementation 'org.spekframework.spek2:spek-dsl-jvm:2.0.9'
testImplementation 'org.spekframework.spek2:spek-runner-junit5:2.0.9'


Добавляем плагин в секцию plugins:

id "de.mannodermaus.android-junit5"


В секцию android добавляем опции:

testOptions {
junitPlatform {
filters {
engines {
include 'spek2'
}
}
}
unitTests.all {
testLogging.events = ["passed", "skipped", "failed"]
}
}


Плагин, который мы добавили нужен для запуска тестов на jUnit 5.
Теперь нам осталось добавить classpath этого плагина в корневом build.gradle:

classpath "de.mannodermaus.gradle.plugins:android-junit5:1.8.0.0"


Для запуска тестов в Android Studio мы будем использовать официальный плагин: https://plugins.jetbrains.com/plugin/10915-spek-framework
Он позволит нам запускать тесты через контекстное меню, нажав на нужную папку. Но также добавляет зеленую кнопку запуска у каждого теста.

Ура, мы вроде все настроили.

Теперь простенький примерчик.
Допустим у нас есть утилита, которую нужно протестировать:

class Calculator {
fun add(num1:Int, num2: Int) = num1 + num2
}


Напишем нашу спецификацию (группу тестов, объединенных одной идеей):

object CalculatorTest : Spek({

val calc = Calculator()

describe("add operation tests") {
it("add test") {
assertEquals(3, calc.add(1,2))
}

it("add test failure") {
assertEquals(4, calc.add(1,2))
}

it("add test another") {
assertEquals(2+1, calc.add(2,1))
}
}
})


Довольно читабельно, как думаете?
1. describe описывает группу тестов, как в нашем случае, тесты для сложения и вычитания.
2. It описывает конкретный тест. Внутри него как раз пишут разные выражения (Spek кстати не включает какие-то свои assert-выражения, можно лишь воспользоваться jUnit-овскими),
Но сам DSL разделен на две части: specification и gherkin. У нас как раз первая.

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

Пост получился и так длинным, поэтому на этом и закончим.
Небольшой лайфхак.

У вас никогда не было задачи выпилить из релизного билда все логи?
Особенно интересной она становится, если у вас в проекте по каким-то причинам все еще не используется Timber (мало ли, проект старый).

Если нет никакой обертки вокруг android.util.Log то задачка та еще.
А вот решить ее можно довольно просто, используя Proguard и добавив в конфиг строки:

-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
}


Данный конфиг выпилит из релизного кода все вызовы Log.d / Log.v.
Это, конечно, не отменяет нужды по замене android.util.Log на Timber, но времени сэкономит много. А самое главное, этот способ будет работать и для других случаев.

Но с конфигами ProGuard всегда нужно быть осторожным!
Шаг вправо, шаг влево - краш релизной сборки!
Небольшой пример как сделать drag & drop-элементы списка в Compose с использованием библиотеки org.burnoutcrew.composereorderable:reorderable:
https://www.rockandnull.com/jetpack-compose-drag-and-drop-list-reorder/

Репозиторий либы: https://github.com/aclassen/ComposeReorderable
Ну что, богатые разработчики?
Как вам цена анонсированной сегодня топовой pro-шки?
Пора уже цену указывать в месячных ЗП, как мне кажется🙂
Пост 22. Orbit-MVI. Первое впечатление.

Попробовал https://github.com/orbit-mvi/orbit-mvi в качестве механизма управления состоянием для Compose (на самом деле можно использовать и вместе с фрагментами).
Впечатления пока приятные.
Ничего лишнего, по большому счету библиотека представляет собой контейнер с механизмом управления состоянием. Эдакий аналог того как бы вы управляли состоянием сами (через StateFlow, Flow), но все спрятано (почти все) под капотом. Код в целом становится почище.

Теперь ViewModel главного экрана выглядит так:

@HiltViewModel
class MainViewModel @Inject constructor(
private val interactor: WeatherInteractor
) : ViewModel(), ContainerHost<MainState, MainSideEffect> {

override val container = container<MainState, MainSideEffect>(MainState()) {
loadWeather()
}

private fun loadWeather(fromRefresh: Boolean = false) = intent {
val mainWeather = interactor.getMainWeatherData(fromRefresh)
val daysWeather = interactor.getDaysWeatherData(fromRefresh)
val hoursWeather = interactor.getHoursWeatherData(fromRefresh)
reduce {
state.copy(
mainWeather = mainWeather,
daysWeather = daysWeather,
hoursWeather = hoursWeather,
isRefreshing = false
)
}
}

fun pullToRefresh() = intent {
reduce {
state.copy(isRefreshing = true)
}
delay(3000) //TODO just for sample
loadWeather(fromRefresh = true)
}
}


Как вам? Мне лично нравится такая простота. Да, у нас простенький пример, но все же!

Теперь немного пояснений.
1. Все состояние хранится в переменной container, которая обычно состоит из пары State, SideEffect. Обычно под Side Effect подразумевается показ тоаста, навигация на другой экран и вот такие вещи.
2. Как видите, стейт по-прежнему представляет из себя StateFlow<YourState>. То бишь, работа со стейтом просто немного спрятана и не нужно писать кучу переменных во ViewModel.
3. Как и раньше, ViewModel у нас представляет из себя Store (в терминах Redux, сущность, которая хранит состояние), но теперь мы наследуемся от библиотечного интерфейса ContainerHost<MainState, MainSideEffect> передавая в него состояние нашего экрана и сайд эффект. Интерфейс как раз и вынуждает нас переопределять переменную container.
4. Функция container<State, SideEffect>() принимает также блок, который выполнится при первом обращении к переменной. В нашем случае, удобно поместить в этот блок первую загрузку погодных данных.
5. Намерение (в терминах Redux) - это некое действие, обычно пользовательское. Обычно каждое намерение меняет стейт (копируя его, а не переопределяя) через вызов функции redux. Собственно, у нас все также. В блоке intent мы можем сделать какие-либо действия для получения данных, в скоупе корутин, а в самой функции redux мы лишь копируем нужные нам данные в новый стейт через функцию copy, которая доступна для data-классов. Тем самым у нас immutable-стейт, что как раз по канонам Unidirectional Data Flow.

Со стороны Compose все взаимодействие с состоянием выглядит так:

@Composable
fun MainScreen() {
val viewModel: MainViewModel = hiltViewModel()
val state = viewModel.container.stateFlow.collectAsState().value
ShowWeather(
viewModel,
state.mainWeather,
state.daysWeather,
state.hoursWeather
)
}

Со стороны Compose особо ничего не поменялось. В переменной container как раз и хранится состояние, а эффекты в container.sideEffectFlow.

Так или иначе, подобные архитектуры скоро станут мейнстримом в мобайле!
А вы что думаете про это все?
Друзья, тут такое дело.
В свободное время ковыряю потихоньку iOS и еще медленнее Flutter. Но все же!
Было бы вам интересно почитать что-нибудь про них и, например, про KMM?
Final Results
74%
Да
26%
Нет, интересен только Android
Что новенького в рассылках #6

Забавная либа: https://github.com/kojofosu/SplitButton
Функционал чем-то напоминает улучшенную версию Spinner-ов. Для сложных экранов может пригодиться.

Либа для роутинга: https://github.com/Zhuinden/simple-stack/
Выглядит не так плохо, как большинство, но вцелом можно подождать и свежую библиотеку от автора Cicherone, или юзать Navigation Library из JetPack.

Вышел Realm Kotlin 0.6.0: https://medium.com/realm/realm-kotlin-0-6-0-baa26dcbbb9

Либа для конвертации разных форматов в виде DSL-ки с разными операторами: https://github.com/nomemory/mapneat
Интересная часть - конвертация POJO в JSON. Может пригодится, правда под капотом дофига всего тянет.

Демка по KMM: https://github.com/joreilly/FantasyPremierLeague
Как-нибудь ее разберем с вами.
В качества UI: Compose и SwiftUI.

Кстати, кто не в курсе. SwiftUI во многом аналогичный Compose UI-тулкит для iOS, но появился (если не путаю) аж в 2018-ом! Так что, разобравшись с Compose будет проще понимать в будущем и SwiftUI. А вот preview функций SwiftUI в XCode работает пока гораздо стабильнее, чем аналогичная история для Compose в Android Studio.

Либа для создания разных фабрик: https://github.com/bluegroundltd/kfactory
Во многом нужна для более удобного тестирования. Помогает подготовить фабрики данных для моделей, которые потом можно использовать в Unit-тестах.

Напоследок.
Roadmap по изучению Compose:
https://victorbrandalise.com/roadmap-for-jetpack-compose/
Если готовитесь к собесу - 50 вопросов с ответами:
https://code.coursesity.com/android-interview-questions

Вопросы скорее для подготовки на позицию джуна или начинающего миддла максимум.
Тут сравнили скорость компиляции Android-проектов на:
1. 2021 14" MacBook Pro — M1 Pro (10 core)— 32gb RAM
2. Desktop (Pop_OS!) — 4.2ghz AMD 2950x (16 core) — 64gb RAM
3. 2019 16" MacBook Pro — 2.4ghz Intel i9 (8 core)–32gb RAM

И M1 Pro показал себя очень и очень неплохо, проиграв ДЕСКТОПУ лишь на Clean Build!
Впечатляет!
Поигрался немного с созданием, запуском проектов на Flutter и KMM.

1. Для обеих платформ предлагается плагин под Android Studio. И нужно сказать, под Flutter он пока выглядит интереснее. Например, есть возможность открыть каждый специфичный модуль либо в Xcode, либо в отдельном экземпляре Android Studio (AS). А вот у плагина для KMM такого нет.
2. Flutter-проект запустился на обеих платформах, и даже в Chrome, что несомненно круто! (но стоит сказать, что когда AS не видит симулятор iOS, приходится запускать проект из Xcode, благо это делается быстро через контекстное меню в AS).
3. KMM-проект так и не запустился на iOS. И, к слову, почему-то при настройке проекта в мастере, не выбирается нужный тул из доки (Xcode Build phases). Вообщем, тут нужно явно посидеть и понастраивать.
4. Flutter-овский Hot Reload - это не что! Как будто вы кодите веб-приложение. Что-то поменял в коде (например, текст заголовка) и тут же изменения видны в эмуляторе/симуляторе, без всяких Clean-Build и повторных запусков. Кайф!
5. Помните open source проект на KMM - https://github.com/joreilly/FantasyPremierLeague ? Думаю, попробую импортнуть в AS и запустить. Вдруг запустится нормально на iOS. Но… проект будто не узнает эту структуру и считает его обычным проектом под Android. И на Android-эмуляторе запустился (правда крашнулся после нажатия на Поиск, но не суть). А вот то, что его можно запустить под iOS, видимо, знает только автор этого проекта. AS не видит даже подобной настройки. Возможно, он был создан по другой структуре и новый плагин для KMM ее не понимает. Вообщем, тоже не вышло - нужны пляски с бубном и здесь.
6. Но дока, кстати, для обеих платформ (по крайней мере, чтобы быстро стартануть) более-менее. Под KMM чуть хуже, но есть отдельные гайды по запуску проекта под iOS и там, так что попробуем победить эту проблему.

Вообщем, Flutter, пока выглядит интереснее. Но немного отталкивает сам язык Dart - какие никакие отличия в нем между Java / Kotlin есть, хотя с виду он не очень сложный.
У KMM общие части можно писать на Kotlin, что для нас плюс, а какие-то платформенно-специфичные уже открывать в Xcode и кодить на Swift.
Нельзя жить с разбитыми окнами!

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

Далее, интересные фрагменты из книги.

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

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

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

Как показывают исследования, безнадежность оказывается заразной.

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

Еще один совет из этой же серии: прежде всего не навредить!
Присоединившись к проекту, где код безупречен, вам следует действовать крайне осторожно, чтобы не навредить.

А вы встречали подобное в своей практике?
👍1
Telegram теперь встраивает собственные рекламные посты в каналах. Они все отмечены знаком «Спонсировано». Поэтому, если увидите такой пост (мало ли!) — знайте, влиять на спонсорский контент от Telegram авторы каналов пока никак не могут, а, следовательно, не могут отвечать за достоверность информации и ее качество.
Будьте аккуратны!
Старый Мобильщик pinned «Telegram теперь встраивает собственные рекламные посты в каналах. Они все отмечены знаком «Спонсировано». Поэтому, если увидите такой пост (мало ли!) — знайте, влиять на спонсорский контент от Telegram авторы каналов пока никак не могут, а, следовательно,…»
Небольшой пример Bottom Sheet на Compose

В сети можно встретить, например, такой вариант реализации Bottom Sheets:
https://proandroiddev.com/how-to-master-swipeable-and-nestedscroll-modifiers-in-compose-bb0635d6a760
Но, по-моему, он полезен скорее, как пример работы с разными модификаторами, а также показывает, что можно довольно просто создавать свои компоненты.
А вот для Bottom Sheets есть готовое решение, основанное на BottomSheetScaffold:

@ExperimentalMaterialApi
@Composable
fun BottomSheet(bottomSheetScaffoldState: BottomSheetScaffoldState) {
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
LazyColumn {
items(6) {
Text("$it")
}
}
},
sheetPeekHeight = 0.dp,
sheetBackgroundColor = Color.Gray
){}
}

И да, список внутри Bottom Sheet прекрасно работает. Хотя тут он довольно простой.

Теперь нам нужна кнопка, которая будет менять состояние нашего Bottom Sheet (параметр bottomSheetScaffoldState) и в зависимости от него он и будет показываться / скрываться.

val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
...
Button(onClick = {
coroutineScope.launch {
if (bottomSheetScaffoldState.bottomSheetState.isCollapsed) {
bottomSheetScaffoldState.bottomSheetState.expand()
} else {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
}) {
Text(text = "Show/Hide BottomSheet")
}

Все довольно просто.

Но самое интересное, что для Bottom Sheet можно даже указать Top Bar, Floating Action Button и кучу других вещей (например, цвет контента, цвет шита и т.д.). Все потому что его реализовали как обычный Scaffold, который также имеет заготовку так называемых слотов: https://developer.android.com/jetpack/compose/layouts/basics#slot-based-layouts.

Почему они так сделали с Bottom Sheet не очень понятно, но возможность такая есть.
Тут оказывается в августе вышло потенциально интересное чтиво от команды Android:
https://chethaase.medium.com/androids-765c803d5ff6

Chet Haase в своей книге, которую он писал 4 года, расскажет почему Android стал таким успешным, как это было, как шла разработка и о команде, которая нам подарила сие чудо.

Кстати, хороший вариант на подарок.
Правда на русском пока не встречал или таки уже есть?
Полезный пост про Inline Classes в Kotlin.

Inline (value) classes появились еще в Kotlin 1.3 и наконец-то вышли в релизной версии вместе с Kotlin 1.5.0. В некоторых моментах напоминают структуры (struct) в других языках программирования.
Ждали эту фичу или не особо?
Судя по отзывам коллег, https://proxyman.io - отличная альтернатива Charles Proxy.
По крайней мере, настроить моки получается проще и удобнее.
Надо бы попробовать.

А вы что используете для моков API?
Накидывайте альтернативы Charles Proxy, если юзаете что-то подобное.