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

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

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

Обсудить что-либо: @activitynotfound
Download Telegram
А вот и часть 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 всегда нужно быть осторожным!
Шаг вправо, шаг влево - краш релизной сборки!
Небольшой пример как сделать 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