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

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

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

Обсудить что-либо: @activitynotfound
Download Telegram
Что новенького в рассылках #5

Ура, уже вышла RC2 для Compose, а значит релиз уже совсем скоро.
Уже начали что-то пробовать и изучать?

В связи с этим многие начали смотреть в сторону MVI-архитектуры presеentation-слоя, ибо Compose должен прекрасно на нее "ложиться".
Признаюсь, мне это особенно интересно. Хочется выработать новую архитектуру на ближайшие годы, дабы все новые приложения разрабатывать на ней.

В рассылках как раз множество статей на эту тему.
Например, вот:
https://medium.com/google-developer-experts/jetpack-compose-missing-piece-to-the-mvi-puzzle-44c0e60b571

Или вот сравнение LiveData vs SharedFlow в MVVM и MVI:
https://proandroiddev.com/livedata-vs-sharedflow-and-stateflow-in-mvvm-and-mvi-architecture-57aad108816d

Советы как улучшить свою продуктивность в Android Studio и немного продуктивность самой Studio:
https://proandroiddev.com/android-studio-tips-for-faster-development-cb9a17c123f3

Наличие качественных скриншотов у приложения в Google Play довольно важная для скачивания и просмотров вещь. Автор рассказывает какие средства и сервисы в этом могут помочь. Особенно актуально если вы все делаете сами и не хочется долго искать дизайнера:
https://proandroiddev.com/how-i-made-beautiful-screenshots-for-google-play-developer-experience-61ce108fa6b4

На неделе прошла небольшая онлайн-конфа Google на тему Gamedev на которой анонсировали Android Game Development Kit:
https://android-developers.googleblog.com/2021/07/introducing-android-game-development-kit.html

На десерт.
Pacman на Compose: https://github.com/danielmbutler/Pacman_Compose

Если пропустил что-то интересное - пишите в комментарии. Обсудим.
Эксперименты с Compose. Часть 3.

Продолжаем вникать в тонкости Compose.
Темы становятся сложнее и местами ощущаешь, что все непривычно после классического подхода.

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

Теперь в активность мы добавляем NavHost, который будет выступать роутером для наших двух экранов. Да, да, у Compose есть привязка к Navigation Component.

setContent {
ComposeAppArchitectureTheme {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "splash") {
composable("splash") {
SplashScreen(splashViewModel, navController)
}
composable("main") {
MainScreen(viewModel)
}
}
}
}

Все относительно просто. Ставим сплеш скрин как стартовый экран (startDestination). А далее уже в зависимости от обычного вызова navController.navigate(«route») произойдет переход на нужную Composable функцию, которая является для макета стартовой.

А вот так выглядит макет нашего сплеш-скрина:

@Composable
fun SplashScreen(viewModel: SplashViewModel, navController: NavController) {
val state = remember { viewModel.state }
if( state.value is UIState.NavigateTo ){
Log.d(TAG,"navigate to main")
navController.popBackStack()
navController.navigate("main")
}

Log.d(TAG,"splash recompose ${state.value}")
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.background(SplashColor),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(id = R.drawable.splash),
contentDescription = null,
alignment = Alignment.Center
)
}
}

Здесь основная магия происходит в переменной state. В качестве начального состояния мы ставим UIState.Showing (будет чуть нагляднее в коде вьюмодели) и после того как наш стейт (состояние) меняется на NavigateTo мы вызываем navigateTo("main") для перехода на главный экран.
Чтобы понять лучше как это все работает, стоит вникнуть в так называемую концепцию recomposition, которая является основной для Compose. Суть в том, что определенные composable-функции будут перерисовываться в зависимости от изменения состояния. Чуть подробнее мы поговорим об этом отдельно (нужно еще самому вникнуть как следует).

И последний пазл нашего примера - код SplashViewModel:

@HiltViewModel
class SplashViewModel @Inject constructor() : ViewModel() {
private val _state = mutableStateOf<UIState>(UIState.Showing)
val state
get() = _state

init {
viewModelScope.launch {
delay(3000)
_state.value = UIState.NavigateTo("main")
}
}
}

Ждем три секунды и меняем стейт на NavigateTo, что позволит нашей composable-функции отрисоваться заново, с новым состоянием, которое мы ловим и переходим на главный экран.
И самое интересное - у нас нет никаких фрагментов.

Как вам? Звучит все это сложно?
Местами, пожалуй, да.
Вникнуть будет проще тем, кто уже работал с разными либами на подобном реактивном подходе (redux, Flutter и прочие) с MVI в основе.

Код примера здесь: https://github.com/Djangist/ComposeArchSample

Далее, мы отдельно поговорим о рекомпозиции, remember и mutableStateOf.
Скоро мы все будем не нужны...
Отчасти шутка, конечно, но насколько долго она будет именно шуткой - вопрос интересный.

Уже видели?
https://copilot.github.com
Немного изменений в демке. Времени пока не так много на нее, увы.
На полноценную часть про Compose не тянет, но пару интересных изменений о которых хочется рассказать все же есть.

1. Звучит как шутка, но наконец-то победил SwipeRefresh и сам свайп теперь работает из любой части макета, без краша. Пришлось добавить во все нужные функции ниже корневой модификатор verticalScroll(rememberScrollState()). Если добавлять в корневой Column - краш!
2. Теперь экземпляр вьюмодели получаем прямиком из Composable-функции через hiltViewModel()
Эта функция доступна в отдельной либе androidx.navigation:navigation-compose:2.4.0-alpha04
Кстати, на более свежую версию пока обновляться не советую. Вся навигация ломается на хрен! Начинается бесконечный recompose сплеш-скрина.
3. Убрал не нужные элементы, вроде вложенных Column. Стало немного чище.

Из интересного еще вот что.
hiltViewModel() стоит использовать если у вас есть навигация через NavComponent и инжекты через Hilt. В MainScreen эта функция как раз и выручает, а если будем использовать обычный метод viewModel() - угадайте что? Правильно! Краш.
Хотя в SplashScreen прекрасно работает и viewModel(), но на всякий случай решил использовать и там hiltViewModel(), для консистентности.

Код, как обычно, здесь: https://github.com/Djangist/ComposeArchSample
Проблема с минификацией в com.android.tools.build:gradle:4.2.2

Не знаете как провести рабочий день? Скучно пилить новые бизнес-фичи?

Можно, например, попытаться понять в чем заключается проблема
java.lang.IllegalArgumentException: Method return type must not include a type variable or wildcard
с API-методом в Retrofit, который возвращает обычный Single<ResponseDto>.

Оказывается, в версии 4.2.2 плагина com.android.tools.build:gradle намудрили что-то с минификацией и он выкашивает dto-объекты ответа, которые не используются.

Суть проблемы: https://github.com/square/retrofit/issues/3588
Распространите коллегам, чтобы были в курсе, а то минус день-другой обеспечены.
Вышел Compose 1.0!

C одной стороны прекрасная новость и можно активнее учить основы, пробовать внедрять какие-то кусочки UI (без фанатизма), а с другой привязки разных библиотек к Compose все еще в альфа, бета-версиях, что не очень.

Кстати, обновил демо проект на версию 1.0.

Немного радости от Compose Team:
https://www.youtube.com/watch?v=kLA1QwDjioc
Любопытная библиотечка для дебаггинга на самом девайсе.

Показывает логи 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
С праздником, друзья!