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

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

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

Обсудить что-либо: @activitynotfound
Download Telegram
Любопытная библиотечка для дебаггинга на самом девайсе.

Показывает логи HTTP-запросов, собирает краши приложения, а еще можно смотреть shared preferences и подправлять на нужное значение для отладки.
Вообщем, может пригодится в повседневной работе.

Ссылка на либу: https://github.com/mocklets/pluto
Кстати, а вы обновили студию до Arctic Fox?

Она уже где-то неделю в stable. Я обновился и работаю в Arctic Fox и над старым (основным рабочим) проектом и над новыми обучающими с Compose на борту.

Полет пока нормальный, но есть нюансы:
1. Аккуратнее с R8 в релизных билдах. Теперь он более рьяно выпиливает неиспользуемые классы. Недавний пример есть на канале.
2. Выпилена инструкция compile и ее производные. Теперь только implementation.
3. Теперь AGP 7.0 (Android Gradle Plugin) требует Gradle 7.0 и Java 11.
4. Новый Layout Inspector теперь позволяет строить дерево composable-функций, смотреть как они отрисованы, просматривать параметры и т.д. - довольно удобно!
5. Preview для Compose. Его можно настроить довольно гибко, добавляя по нескольку превьюшек для одной функции, включать системный интерфейс, фоны и прочее. Здесь пока есть проблемы с отрисовкой (превью иногда не работает), но думаю допилят в ближайших обновлениях.
6. Теперь можно вытащить sqlite базу к себе на диск и выбрать при экспорте нужный формат. Не то чтобы вау-новость, но теперь можно быстрее вытаскивать БД и смотреть проблемы с таблицами, хотя и раньше можно было.
7. И самое любопытное для меня. В AGP 7.0 убрали build cache, при этом заявляют, что скорость сборки не просядет. Убрали задачу cleanBuildCache, свойства android.enableBuildCache, android.buildCacheDir.

Видосик: https://www.youtube.com/watch?v=-8tSZr7iMcw
Небольшая quiz-задачка по Compose, которая прекрасно раскрывает суть recomposing (рекомпозиции) - одной из основных концепций нового декларативного UI.

Грубо говоря, суть ее в том, что элемент UI будет обновлен (отрисован повторно), если изменится его состояние, а если часть UI остается в прежнем состояние, то Compose просто возьмет уже закешированное (ранее уже отрисованное состояние). Таким образом меняется лишь часть UI, а не все дерево целиком, как обычно происходит в xml.

У нас есть такой код:

@Composable
fun Foo() {
Log.d(TAG,"recompose Foo")
var text by remember { mutableStateOf("") }
Log.d(TAG,"recompose Foo text")

Button(onClick = { text = "$text\n$text" }) {
Log.d(TAG,"recompose Button's lambda")
Text(text)
}
}
И так, друзья.

Правильный ответ на наш квиз - вариант №3: recompose Button's lambda, recompose Button's lambda, recompose Button's lambda

Некоторые из вас ответили именно так!
Поздравляю!

Вся штука в переменной:

var text by remember { mutableStateOf("") }

При клике она меняется и таким образом меняется состояние элемента Text и скоуп кнопки, что заставляет Compose отрисовать ее с новым состоянием и кнопка становится все больше и больше.

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

Button(onClick = { text = "123" }) {
Log.d(TAG,"recompose Button's lambda $text")
Text(text)
}

Тогда при первом клике кнопка отобразит один раз наш текст 123 и далее стейт (состояние) при каждом клике уже меняться не будет.
Еще немного про состояние (states) в Compose, но в контексте нашего демо-приложения:
https://github.com/Djangist/ComposeArchSample

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

Чего же нам не хватает?
1. У нас есть вьюмодель, с данными, которые хранятся в StateFlow.
2. Мы подписались на эти данные и в случае их изменения, обновим UI

Вот наша главная composable-функция:

@Composable
fun MainScreen() {
val viewModel: MainViewModel = hiltViewModel()
val mainWeather = viewModel.mainWeatherData.value
val daysWeather = viewModel.daysWeatherData.value
val hoursWeather = viewModel.hoursWeatherData.value

ShowWeather(
viewModel,
mainWeather,
daysWeather,
hoursWeather
)
}

А не хватает нам всего лишь одного метода: collectAsState(),который уже добавлен как расширение для StateFlow.
Пара изменений в коде и данные по свайпу стали обновляться:

val viewModel: MainViewModel = hiltViewModel()
val mainWeather = viewModel.mainWeatherData.collectAsState()
val daysWeather = viewModel.daysWeatherData.collectAsState()
val hoursWeather = viewModel.hoursWeatherData.collectAsState()

ShowWeather(
viewModel,
mainWeather.value,
daysWeather.value,
hoursWeather.value
)


В описании к методу все наглядно (см скрин).
Каждое новое значение в StateFlow теперь будет приводить к recomposition, а это то, что нам и нужно!
Выглядит пока что это все неплохо. Поток данных можно довольно просто контролировать, обновляя лишь нужные кусочки UI.

Осталось добавить какой-то механизм, который будет управлять разными состояниями (например, Reducer в качестве части MVI-архитектуры/UI-паттерна) для полной красоты и усложнить наш пример, дабы посмотреть какие-то сложные кейсы и сложные состояния.
Еще никогда не было так просто настроить CI/CD для Android-приложения.
После появления Github Actions (у Gitlab есть аналогичная штука) это можно сделать за три минуты!
Кайф!
Пост: https://proandroiddev.com/continuous-integration-delivery-for-android-with-github-actions-part-1-b232ed2b1740
Забавный баг в Android Studio Arctic Fox (хотя, возможно, был и раньше).

Если остановить выполнение задачи (хорошо воспроизводится на задаче по сборке проекта), нажав на крестик (скрин 1), после остановки задачи, зеленая кнопка для запуска сборки проекта повторно не появляется (скрин 2) 🙂

Нужно запустить, например, Clean, чтобы кнопка появилась повторно.
А вот и часть 3 хаотичного изучения Coroutines от RMR.

В новом выпуске ребята разобрали:
- Для чего был нужен SingleLiveEvent
- Как его приготовить без LiveData
- Channel
- О трате ресурсов в бекграунде
- buffer, conflate, flowOn, shareIn
- WhileSubscribed
- и многое другое.
Получился очень полезный выпуск.
Пост 20. "Паттерн" репозиторий.

Многие уже знают о таком понятии как репозиторий. Однако, я все равно часто наблюдаю в коде проектов некое непонимание как правильно его "готовить".
Давайте разбираться.

В доках Гугла часто можно встретить примеры в которых во ViewModel напрямую ижектят репозитории, что мягко говоря, не очень правильно.
На самом деле, ViewModel или Presenter (смотря что используете) не должен ничего знать о репозиториях. Обычно с репой работает UseCase или Interactor. И именно он инжектится во ViewModel.

Но давайте о главном.
Что же такое репозиторий (Repository)?
Репозиторий можно рассматривать как коллекцию, которая работает с какой-либо моделью данных.
И только с одной моделью.
Это важно.
Если хочется работать с несколькими моделями, то самое правильное - это создать на каждую модель свой отдельный репозиторий.
Тут не стоит лениться - в дальнейшем будет меньше проблем.
Обычно репозиторий возвращает вам список моделей или конкретную модель, кеширует какие-либо локальные данные и возвращает эти данные из кеша и вот это вот все. Но в нем нет и не должно быть НИКАКОЙ другой логики (особенно бизнес-логики). Чем он тупее - тем лучше. Положили модель куда-то, отдали модель.

Теперь немного про слои.
Самые первые реализации Clean Arctitecture под Android делили слои репозиториев так:
1. Интерфейс репозитория мы помещаем на уровне domain.
2. Реализацию (класс, наследующий интерфейс репозитория) на уровне data.
3. UseCases / Interactors инжектит в конструктор именно интерфейс репозитория, а не реализацию! Это тоже очень важно. Реализацию подставит DI-фреймворк.
Сейчас же, когда проекты стали делить на features (фичи), а еще большие проекты на features в отдельных модулях, принцип разделения на слои остался прежним (в каждом модуле фичи своя структура domain - data - presentation).

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

class UserRepositoryImpl @Inject constructor(private val source: DataSource): ...

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

По большому счет это все основы, которые стоит четко для себя понимать при работе с репозиторием.
Главное следуем картинке ниже, чтобы Dependency Rule не нарушался и будет вам счастье.

Кстати, стоит ли отдельно рассказать про этот самый Dependency Rule?
Пишите в комменты.
Забавные новости про GitHub Copilot - AI-помощник программиста.

Первые пользователи поигрались и нашли ряд недостатков, но самое забавное в этом вот что: периодически Copilot вместо нескольких строк кода генерирует цитаты и комментарии из проектов с открытым исходным кодом. Кроме того, он вытаскивает валидные ключи API из репозиториев с открытым исходным кодом разных проектов и выдаёт их пользователям.
С одной стороны хранить открытые ключи - такое, с другой - норм так скрипт получился 🙂
Пока что выдыхаем и работаем дальше - мы все еще нужны.

Детали: https://habr.com/ru/news/t/576228/
Играемся с анимациями в Compose

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

Для простоты добавим анимацию исчезновения / появления температуры при свайпе (обновлении данных).

Добавить ее можно поместив анимируемый элемент (компонент) внутрь блока Composable-функции (в нашем случае AnimatedVisibility):

AnimatedVisibility(
visible = !isRefreshing,
) {
Text(
text = temprature.toString(),
style = Typography.h3
)
}

Анимация в Compose завязана на состояния (как и все остальное, что логично), поэтому мы добавляем поле isRefreshing, которое меняется при swipe to refresh,

По-умолчанию анимация fadeIn() + expandIn() для enter и shrinkOut() + fadeOut() для exit, но в параметрах можно задать любые другие из списка в доке, а через оператор + их еще и можно миксовать, как вы уже догадались.

Пока этот API нужно помечать специальной аннотацией, как экспериментальный. Видимо API еще будет немного меняться, но не до конца понятно, как например, сделать свою длительность анимации (обычно есть какое-нибудь поле duration).
Что ж, будем ковырять дальше.
Ощущение, что возможностей море, но описано это все пока не очень подробно.

Дока: https://developer.android.com/jetpack/compose/animation
Больше примеров здесь: https://medium.com/wizeline-mobile/jetpack-compose-animations-i-f46024bcfa37
и здесь: https://proandroiddev.com/animate-with-jetpack-compose-animate-as-state-and-animation-specs-ffc708bb45f8
С праздником, друзья!
Продолжаем понемногу копать анимации в Compose.

Один из вариантов, как можно установить длительность - это задать свою анимацию появления или исчезновения (параметры enter / exit).

Зададим, например, анимацию появления:

AnimatedVisibility(
visible = !isRefreshing,
enter =
fadeIn(animationSpec = tween(1000))
)

Длительность определяется функцией tween(). В нашем случае - 1 секунда.
Вообще, в animationSpec может быть все что угодно, что реализует интерфейс AnimationSpec (неожиданно, да?) и скорее всего намиксовать в этом параметре опций можно много.

Такие дела.
Уже пару недель привыкаю / изучаю / назовите как угодно MVI архитектуру presеentation-слоя, которая ляжет на наш новый чудо-UI на базе Compose.

Лучшие статьи скину отдельным постом (посмотрел уже порядка двух десятков, в том числе и для iOS).
Толковых статей не так много, а многие другие обычно состоят из кальки документации по Redux с какими-то своими авторскими особенностями.

Ковыряя существующие решения, буквально вчера случайно увидел этот пост: https://appmattus.medium.com/top-android-mvi-libraries-in-2021-de1afe890f27
Автор сравнивает почти все уже существующие либы для MVI и возможно кому-то будет полезно, когда будете подбирать библиотеку себе.
Но в статье есть два нюанса:
1. В сравнении нет, наверное, самой популярной либы - либы от Badoo - MVICore.
2. Автор в начале утверждает, что свое решение для MVI делать сложно и не очень правильно. Но как по мне, стоит это принимать на веру не до конца, ведь исходя из общей концепции и той же самой доки по Redux - сложного в архитектуре не так много. На первых парах наоборот видится лучшим решением попробовать самостоятельно разобраться в основах и накидать примеры, добавив условно всего несколько интерфейсов, а потом уже перейти на нормальную библиотеку, если это вообще будет нужно. Основная проблема - правильное управление состоянием и разбухание этого самого состояния.
Но об этом позже.

Немного посмотрев на все эти либы, MVICore, Orbit-MVI пока выглядят наиболее адекватными из всех решений.
Есть еще MVIFlow, но она пока еще сыровата.
Поглядим.

Кстати, свои небольшие наброски по MVI сейчас делаю в отдельной ветке: https://github.com/Djangist/ComposeArchSample/tree/feature/udf_mvi
Заглядывайте, пишите под этим постом - пообщаемся.

А чуть позже еще появятся веточки для указанных выше двух либ - для сравнения.
Либ для MVI / Unidirectional Data Flow становится все больше.
Вот, например, наши любимые ребята из Square, которые подарили нам множество прекрасных библиотек, выпустили свою: https://github.com/square/workflow/

Но прелесть ее в том, что она и для iOS и для Android, в отдельных зависимостях под каждую платформу. Кто-то захочет пойти в кроссплатформу и использовать обе, кто-то возьмет только версию под конкретную платформу. Такое разделение нравится.

Кроме того, либа не выглядит перегруженной своими абстракциями, хотя здесь они тоже чуть отличаются от принятых в том же Redux. Видится, что ее можно использовать как отдельно докрутив свои нужные слои (например, добавив свой Middleware), либо с другими либами.

Вообщем, выглядит любопытно.
Оказывается 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 всегда нужно быть осторожным!
Шаг вправо, шаг влево - краш релизной сборки!