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

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

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

Обсудить что-либо: @activitynotfound
Download Telegram
Новое с Google IO 2021. Инструменты разработки.

Ну что, друзья? Начнем погружаться в свежие новости с прошедшего Google IO?
Интересного, конечно, не так много.

Первым делом посмотрим, что нового принесет нам свежая версия Android Studio (на момент поста еще пока Beta 3), которая теперь получила свое отдельное имя - Arctic Fox.

Коротко и тезисно, а к посту прикреплена картинка на заметку со всеми изменениями.
* Обновили платформу IntelliJ на версию 2020.3.1
* Добавили Compose Design Preview с настройками (ориентация экрана, шрифт, ширина и высота), но нужно добавлять аннотацию @Preview. Можно будет просматривать изменения в дизайне "на лету" (поменяли условно текст в коде и изменения сразу будут видны в Preview). Наверное, самое интересное из всего списка.
* Добавили возможность просматривать векторную анимацию (ресурс animated-vector)
* Layout Validation - просмотр корректности макетов на разных разрешениях (одобряю)
* В эмуляторе добавили более удобную отладку датчиков
* Snapshots for test failures: добавив в builde.gradle опцию failureRetentation после запуска тестов можно видеть снепшоты (состояния) эмулятора на проваленных тестах
* Work Manager Inspector: поможет отлаживать ваши асинхронные задачи, запущенные через WorkManager. Посмотреть какая запущена, какие данные есть и прочее (показывает даже граф задач)
* Новые плюшки рефакторинга: migrate to non transitive R classes, refactoring preview для просмотра сделанных изменений (вот это точно одобряю)
* Android 12 Lint checks - новые правила и обновленный Lint
* Станет доступен Coroutines debugger - отладка корутин станет немного проще (давно пора бы)

Вообще, судя по разным видео с конфы, которые успел посмотреть - нас довольно активно готовят к переходу на Compose. Многие библиотеки получили привязки к Compose и пообещали совсем скоро версию Compose 1.0. Пожалуй, это основной посыл всего Google IO для Android.
Новое с Google IO 2021. JetPack.

Продолжаем смотреть, что нового нам рассказали на Google IO. Теперь пройдемся немного по JetPack. Напомню, JetPack - набор библиотек от Google, который помогает ускорить разработку. Заточено все в основном под гугловский MVVM с ViewModel и LiveData (теперь уже StateFlow). По большому счету они растянули многие core классы в отдельные пакеты, что упрощает обновление и поддержку библиотек и бонусом делает меньше размер приложения. Раньше был огромный Support AppCompat в котором подключалось и все что нужно и все что нет.

А нового в JetPack, кстати, не так много:
* Обновили Experimental annotations
* Добавили LifecycleOwner для CameraX
* Добавили либу AppSearch для создания поисковых запросов из локальных данных приложения (поддерживает full-text search, индексацию).
Подробнее: https://developer.android.com/guide/topics/search/appsearch?hl=ru
* Добавили Security Crypto - либа для шифрования файлов и shared preferences
* Выкатили стабильные версии библиотек: Hilt, DataStore, WorkManager. Теперь наконец-то можно внедрять Hilt и не бояться, что все развалится с новой минорной версией, как однажды у меня было в пет-проекте.
* Довольно много изменений в Room:
- Experimental support for Kotlin Symbol Processing
- Появилась встроенная поддержка энамов
- Поддержка RxJava3
- Добавили QueryCallback для разных задач после/во время выполнения запроса к БД (например, можно добавить логгирование)
- @ProvidedTypeConverter для конвертеров типов
* Activity Result API (с версии androidx.fragment 1.3.0). Вообще, не новинка, но почему-то упоминается Гуглом как что-то новое.
Подробнее: https://developer.android.com/training/basics/intents/result
* Google Shortcuts library.
Подробнее: https://developer.android.com/guide/topics/ui/shortcuts/creating-shortcuts#gsi-library
* Обновление в Paging (библиотека для пагинации): добавили поддержку Coroutines and Flow, поддержку для headers, footers, dividers
* Добавили привязки к Compose в: Hilt, Paging, Navigation, Activity, ViewModel, ConstraintLayout (грядет эра еще большего говнокода 😞)

Кстати, изменения по либам JetPack можно смотреть по этой доке:
https://developer.android.com/jetpack/androidx/versions
Новое с Google IO 2021. Android 12.

И напоследок изменения в новой Android 12:
* Deprecated @Deprecated
* Новые цвета в теме DayNight
* Rounded corners API
* Много изменений по виджетам рабочего стола, в том числе refreshing widgets.
Подробнее: https://developer.android.com/about/versions/12/features/widgets
* Добавили анимации сплеш-скрина. Любое приложение теперь с анимацией сплеш-скрина, которой можно управлять через метод Activity.getSplashScreen()
* Небольшая пачка изменений по допиливанию нотификаций
* Picture in Picture: теперь возврат на Home через Swipe Up жест
* Добавили блюрэффект из коробки: imageView.setRenderEffect(RenferEffect.createBlurEffect()) и атрибуты темы windowBlurBehindEnabledRadius, windowBlurBehindRadius, windowBackgroundBlurRadius можно делать даже заблюренные виджеты рабочего стола.
* Добавили новый ripple effect (довольно странный)
* Новый Edge effect (элемент меняет размер, так называемый stretch effect)
* Поддержка нового формата графики AVIIF
* Audio-coupled Haptic Playback для треков (не на всех девайсах доступно)
* Privacy: добавили новые permissions на сканирование и подключение к Bluetooth, новые опции для Location permission, Toast для копирования данных из клипборда, foreground restrictions (Expedited Jobs: WorkRequest.Builder.setExpedited ->
Pre android 12 -> Foreground services
Android 12 -> JobInfo.Builder.setExpedited())
* Объединили несколько обработчиков контента в один - setOnRecieveContentListener (один лисенер для drag & drop, keyboard stickers, copy & paste menu)
* Добавили библиотеку для замера производительности (теперь можно замерять скорость старта приложения).
Подробнее: https://developer.android.com/studio/profile/benchmark

Полный список изменений здесь: https://developer.android.com/about/versions/12/features
Эксперименты с Compose. Часть 1.

Начинаем новый цикл постов, который посвящен Compose от которого нам, видимо, никуда не деться.
План примерно таков:
1. Накидать быстро базовый UI
2. Отрефакторить то, что получилось в пункте 1.
3. Добавить несколько архитектур и разбить все грамотно, а не как в классических постах, где пример делается ради примера. Добавим сперва гугловый MVVM, потом попробуем MVI и в конце возможно классический и всеми любимый MVP. Пока сложно сказать понадобится ли нам разбиение на модули или слои по Clean, но добавим в дальнейшем и его.
4. Посмотреть как все это тестируется. Добавим тестирование.

За основу UI взят главный экран моего приложения. Его вторая версия как раз в разработке и отдельно любопытно насколько быстро этот экран можно переписать на Compose.
Также, в качестве вводной стоит отметить, что Compose до этого не изучал.

По итогу первого захода потрачено примерно 2,5 часа. Результат справа и результат довольно скромный.
По впечатлениям все довольно не обычно. Приходится перестраивать мозг, ибо теперь весь UI в коде.

Что понравилось:
* Понравилось как работает Preview в Android Studio. Build -> refresh и уже виден результат, при этом не нужен ни эмулятор ни устройство.
* Можно быстро накидать UI (быстро при условии, что вы хорошо въехали в основы).
* Composable-функции можно довольно гибко раскидывать по коду и переиспользовать (не только в текущем проекте).
* В теории, появятся тулзы, которые будут генерировать UI-код. Скорее всего к этому все и делалось.
* Можно быстро изучить основы (особенно по примерам кода)

Что не понравилось:
* Гуглобаги. Чтобы подключить и использовать ConstraintLayout пришлось обновить версию Kotlin и самого Compose (на моей текущей был краш), добавить отдельную либу.
* Видеть весь код UI в обычных .kt файлах проекта очень странно. Скорее всего дело привычки, но без четкой структуры явно будет каша от проекта к проекту.
* Пока не понятно как использовать constraints в дочерних элементах. Надо разбираться, сходу не вышло.
* Прописал в теме цвет фона, а при использовании Scaffold он перетерся. Нигде в доке не увидел почему так.
* Не хватает подробной доки. Благо есть куча примеров кода, которые и приходится смотреть.

Мой репозиторий:
https://github.com/Djangist/ComposeArchSample
Гугло-примеры:
https://github.com/android/compose-samples/
Интересный PlayGround:
https://foso.github.io/Jetpack-Compose-Playground/
Пост 14. Parcelable vs Serializable.

В Android часто приходят разработчики из мира Java SE / Java EE и как только возникает задача сделать сериализацию для модельки, первое, что приходит на ум - использовать уже знакомый интерфейс Serializable. Однако в Android лучше использовать Parcelable для большинства задач, тем более что сейчас это стало значительно проще.

Самый частый пример использования сериализации - это передача сложной модели из одного экрана в другой. Например, есть список транзакций и при тапе на транзакцию нужно передать модель для экрана просмотра деталей. Для передачи модели между экранами (через Bundle) нам и нужна сериализация.

В чем проблема Serializable?
1. Рефлексия. Соответственно не очень быстро. Поля объекта конвертируются в байтовый поток при сериализации.
2. Поле serialVersionUID. Если поле не задано, то механизм сериализации сам будет вычислять его дефолтное значение, поэтому рекомендуется всегда добавлять это поле в свой класс. Но во-первых, очень просто забыть это сделать, а во-вторых, некоторые классы содержат только дефолтное значение и с расчетом значения для вашего класса могут быть проблемы.
3. Придуман в основном для сериализации данных на диске или отправки через сеть, но и для этих кейсов можно рассмотреть другие аналоги (Externalizable, ObjectInputStream и даже GSON)

В отличии от Serializable Parcel как раз реализован для межпроцессного взаимодействия (IPC). Поэтому, самое лучшее решение в случае передачи данных между экранами использовать Parcelable.

Раньше нужно было вручную реализовывать этот интерфейс, потом уже Android Studio через подсказку научилась подставлять дефолтную его реализацию, появились библиотеки (например Parceler), но сейчас лучше всего (и проще) использовать плагин kotlin-parcelize.
Добавили плагин в app/build.gradle:

plugins {
Id: 'kotlin-parcelize'
}

Потом добавили к модельке аннотацию @Parcelize и само собой интерфейс Parcelable:

@Parcelize
data class Transaction(val transactionId: Int, val userId: String, val sum: Double): Parcelable

И все у вас хорошо.
Подробнее о плагине: https://developer.android.com/kotlin/parcelize
Пост 15. Kotlin Nothing.

В Kotlin есть два типа, которые часто можно спутать между собой: Unit и Nothing. Можно подумать, что оба не возвращают ничего. Но между ними есть существенная разница (оба типа наследники от Any). В случае Nothing понятие возвращаемого значения просто не имеет смысла. Не смотря на то, что практических применений у Nothing около двух (или трех), об этом типе часто любят спрашивать на собеседованиях.

Функции/методы, которые возвращают Nothing не возвращают управление. Самый простой пример для понимания - это функции с бесконечным циклом. Пока какая-то логика внутри цикла не сработает и не остановит цикл - код после цикла не выполнится.

Какой практический смысл у Nothing?
Например, валидация полей. У вас есть функция, кидающая пользовательское исключение в случае какой-либо ошибки:

fun showError(message: String): Nothing {
// перед этим можно еще аналитику добавить или логгирование
throw ValidationException(message)
}

Такая функция позволит нам писать выражения вида:

val name = userResponse.name ?: showError("No name field")

Компилятор знает, что функция с таким типом не вернет управления, и использует эту информацию при анализе кода вызова функции. Если бы функция showError возвращала Unit, подобное выражение написать не получилось бы.

Кстати, стандартная функция TODO(), которая часто подставляется средой при генерации кода, реализована также с помощью Nothing:

@kotlin.internal.InlineOnly
public inline fun TODO(reason: String): Nothing = throw NotImplementedError("An operation is not implemented: $reason")
Наконец-то дожили до момента, когда в Navigation Component добавили поддержку multiple back stacks и можно даже задуматься о выпиливании сторонних библиотек для роутинга.

Пока еще в альфе.

Менять в коде ничего не нужно. Достаточно обновить либу фрагментов до 1.4.0-alpha01 и собственно сам navigation до 2.4.0-alpha01.
Детали:
https://www.youtube.com/watch?v=Covu3fPA1nQ
https://medium.com/androiddevelopers/multiple-back-stacks-b714d974f134
Просто оставлю эту ссылку здесь: https://refactoring.guru/ru/design-patterns

Паттерны проектирования доступным языком с примерами кода, а также описание техник рефакторинга.
И все это с удобной навигацией.
Сохраняйте в закладки, если вдруг еще не слышали о нем.
Что новенького в рассылках #4

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

Что интересного из статей?

Аналогичный моему опыту пример переноса UI из xml на Compose:
https://proandroiddev.com/migration-to-compose-c6eb63f187f9
Самое пока странное - это передача ViewModel в параметры composable-функций, чтобы можно было корректно оперировать стейтом (состоянием UI-элементов). Как раз на этом этапе я немного застрял, потому что передавая вьюмодель в параметры мы теряем слабую связность между частями приложения и хочется попробовать разные варианты.
Пример, кстати, живой: с Hilt и намёком на архитектуру. Отдельно порадовало упоминание в статьей сторонней лимбы Гугла для Compose - Appcompanist в которой есть SwipteToRefresh, Pager и другие часто используемые компоненты. Почему только они не в стандартной либе Compose? Загадка.

Хорошая статейка с примерами использования не самых частых, но весьма полезных функций для работы с коллекциями в Kotlin:
https://dev.to/kotlin/advanced-kotlin-collection-functionality-5e90

Статейка к видео о Multiple Back Stacks в Navigation Component:
https://medium.com/androiddevelopers/navigation-multiple-back-stacks-6c67ba41952f

Статья по Kotlin Symbol Processing:
https://proandroiddev.com/ksp-fact-or-kapt-7c7e9218c575
KSP быстрее Kapt и якобы со временем (когда многие популярные либы перейдут на него) поможет ускорить сборку проекта. Плюс есть свой Kotlin API.
Статья больше для любознательных и тех, кто поддерживает собственные библиотеки для Android.

Видео про новые фичи в Kotlin Mobile Multiplatform:
https://www.youtube.com/watch?v=QJqLpTw3vwI

Что по коду/либам?
Интересная штука - https://github.com/organicmaps/organicmaps - форк приложения MAPS.ME для Android/iOS. Можно подглядеть как делать оффлайн карты на базе данных OpenStreetMap.

Либа-коллекция расширений для Android классов:
https://github.com/zsmb13/requireKTX
Можно что-то и себе затянуть интересное.

В качестве p.s.
По поводу Hilt. Столкнулся с такой вот ошибкой, которая все еще актуальна: https://github.com/google/dagger/issues/2337
А вот если обновить Gradle до classpath "com.android.tools.build:gradle:7.1.0-alpha02" - о чудо, работает даже на последней версии 2.37.
Гугл как всегда.
Если у вас бесконечно крашится приложение Google - вы не одиноки!
Нужно зайти в Приложения -> Google -> тапнуть на троеточие - > Удалить обновления.
Проверил на себе - работает! А то уже хотелось свой Pixel 3a в окно выкинуть.

И да, мы тут над нашим банком иногда угараем, что фичи никто не тестирует, а тут такой фейл от Google.
Вообщем, тестирование, тестирование и еще раз тестирование.

Детали: https://www.androidauthority.com/google-app-crashing-1237738/
Эксперименты с Compose. Часть 2.

Продолжаем разбираться с Compose.
Впечатления стали более приятными.
Интерфейс приложения стал более интересным на вид и заметил, что во время "верстки" стал чаще пользоваться Preview.

Добавил горизонтальный (LazyRow) и вертикальный список (LazyColumn).
Прелесть в том, что не нужны больше адаптеры, но интересно будет рассмотреть пример с обновлением данных (кейс в котором часто используется DiffUtils, чтобы не обновлять список целиком).
Небольшой пример добавления списка элементов:

LazyRow {
items(hourItems) {
HoursItem(it)
Spacer(modifier = Modifier.size(8.dp))
}
}

items по сути заменяет нам цикл по элементам - можно для каждой модели быстро добавить макет элемента списка.

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

В корневом элементе теперь SwipeToRefresh, который вынесен почему-то в отдельную библиотеку Accompanist и вам нужно дополнительно ее подключать.
С этим элементом есть проблема. Сам жест свайп ту рефреш сейчас работает только где пустое пространство, а не поверх всего макета как обычно и должно быть. Придется скорее всего немного подправить макет. Есть вариант использовать модификатор nestedscroll, но пока примеров с ним особо не нашлось. А если добавить в Column модификатор verticallscroll все крашится. Вообщем, пока все со свайп ту рефреш сыровато.

Кстати, в Appcompanist еще есть и Pager (аналог ViewPager), Insets, расширения работы с библиотеками загрузки изображений (Coil и Glide) и Permissions. Скорее всего список утилит в этой либе будет расширяться.

Разобрался с ConstraintLayout. Его гибкость по-прежнему с нами, но задавать все констрейнты в коде, конечно, не привычно:

ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
val (column, image, text1, text2) = createRefs()
Column(horizontalAlignment = Alignment.Start,
modifier = Modifier.constrainAs(column) {
start.linkTo(parent.start)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
}) {
Text(text = weatherData.dayOfMonth)
Text(text = weatherData.dayOfWeek)
}

Сперва нужно создать ссылки через createRefs(), которые мы потом присвоим нужным элементам, чтобы связывать их друг с другом.

Text(
text = weatherData.minTemp.toString(),
style = Typography.caption,
modifier = Modifier.constrainAs(text2) {
end.linkTo(parent.end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
}
)

Добавил базовую архитектуру: репозитории кидают мокированные данные, интерактор ими "управляет" и вьюмодель вытаскивает их из интерактора и на эти данные подписываются composable-функции.
Управляет зависимостями Hilt. С ним чуть пришлось повозиться (баг с Gradle из поста выше). В итоге обошлось без передачи экземпляра вьюмодели в функцию, хотя сами вьюмодели можно получать и в скоупе composable-функции.

А в следующий раз поговорим про states (состояния), анимации и сделаем наш экран еще живее.

Репозиторий: https://github.com/Djangist/ComposeArchSample
Пост 16. Передача данных между фрагментом и диалогом через Navigation Component.

Недавно вспомнил о вариантах передачи данных между фрагментами, например, когда вызываемый фрагмент / диалог (DialogFragment) возвращает данные. Немного взгрустнув от того, что у нас в текущем проекте все еще старая версия библиотеки фрагментов и мы не можем использовать новый способ, вспомнил про еще одно решение с Navigation Component. Этот кейс тоже не для нашего старого проекта, в котором пришлось использовать setTargetFragment, но возможно, кому-то будет полезен.

1. Подписываемся на получение данных из вызываемого диалога / фрагмента:

private fun observeLocationDialogResult() {
findNavController().currentBackStackEntry
?.savedStateHandle
?.getLiveData<SetupLocationDialogFragment.Button>(KEY_BUTTON)
?.observe(viewLifecycleOwner, {
when (it) {
SetupLocationDialogFragment.Button.AUTO -> {
requestPermissions()
}
...
}
})
}

Здесь мы обращаемся к новому модулю SavedStateHandle (его, кстати, можно использовать и во ViewModel) и подписываемся на лайвдату, которую он любезно нам предоставил. Когда из диалога прилетит нужное нам значение (в нашем случае, когда пользователь нажмет на кнопку) - мы обработаем его нужным образом.

2. Когда придет время - вызываем наш диалог или фрагмент обычным способом:

findNavController().navigate(MainFragmentDirections.actionMainFragmentToSetupLocationDialogFragment())

3. Обрабатываем нажатие на кнопку диалога или какое-либо другое событие в самом диалоге:

binding.useAutoLocation.setOnClickListener {
findNavController().previousBackStackEntry?.savedStateHandle?.set(
KEY_BUTTON,
Button.AUTO
)
dismiss()
}

Здесь мы устанавливаем значение в поле, которое будем ловить в пункте 1.
На этом все. Не нужны ни onActivityResult, ни разные костыли.

А про другие варианты передачи данных между фрагментами поговорим в следующих постах.
Столкнулся в пятницу с любопытным багом.
Если у вас в проекте используется библиотека Gson, то баг может быть актуальным и для вас.

Немного предистории. У нас есть Jenkins, который собирает билды тестеровщикам.
И вот в чем прикол: на моей локальной dev-сборке нужный запрос прекрасно работал, а у тестера нет.
Начали разбираться.
Сборка в дженкинсе обфусцируется и это была единственная разница между билдами.

И вот тут самое интересное.
Body для POST-запроса выглядит так:

{ query: "address string" }

А вот так выглядит наш класс для этого же Body:

data class SuggestionBody(
val query: String
)

Вроде бы все логично. Аннотации @SerializedName нет, ибо поле класса совпадает с названием поля тела POST-запроса.
Но штука вот в чем.
Если аннотации нет, поле обфусцируется в произвольное имя и запрос становится не корректным.
Поэтому лучше всегда дописывать к полям аннотацию @SerializedName:

data class SuggestionBody(
@SerializedName("query")
val query: String
)

Такие дела.
Пост 17. Передача данных между фрагментом и диалогом через set/getTargetFragment.

Продолжаем вспоминать какие существуют способы передачи данных между фрагментом и диалогом.
В этот раз поговорим о довольно часто используемом способе, но думаю не все о нем знают даже сейчас, в 2021 году. Особенно, если вы только начинаете изучать Android.

У фрагмента есть методы setTargetFragment и getTargetFragment (в случае использования Kotlin просто targetFragment). И если проект не самый новый - это довольно часто используемый вариант. Однако, этот способ стоит заменить на более новые варианты.

Пример использования.
Первым делом перед показом диалога мы добавляем вызов:

dialog.setTargetFragment(this, REQUEST_CODE_TRY_AGAIN)

Константа REQUEST_CODE_TRY_AGAIN понадобится нам, чтобы отловить результат работы именно нашего диалога, который прилетит в onActivityResult.
Далее, во фрагменте, где запускается показ диалога и где нужно получить результат его работы мы переопределяем onActivityResult:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_TRY_AGAIN && resultCode == Activity.RESULT_OK) {
presenter.tryAgain()
}
}

Далее, в самом диаложке (например, при клике на кнопку) мы дергаем targetFragment?.onActivityResult():

binding.tryAgain.setOnClickListener {
targetFragment?.onActivityResult(REQUEST_CODE_TRY_AGAIN, Activity.RESULT_OK, null)
dismiss()
}

В последнем параметре обычно передается Intent с данными, но в нашем случае это не нужно.

Немного выглядит это все кривовато, не находите?
Тем не менее прекрасно работает. Но null-safety вызов onActivityResult здесь не просто так. Есть кейсы, когда targetFragment действительно null и тогда результата работы диалога мы не получим. У нас на проде такое встречалось, но очень редко и кейсы довольно специфичные.

Но стоит помнить, что данный способ передачи данных уже не рекомендован к использованию и метод getTargetFragment Deprecated in API level 28 (Android 9).
А какой подход нам дали взамен поговорим в следующий раз.
Пост 18. Диалог с закруглением и произвольными отступами по краям.

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

Почему не самая лучшая идея?
Во-первых, слишком «айосно», во-вторых, и это наиболее важный для меня пункт - для диалогов такого плана есть ботомшиты. Все таки у каждой платформы есть свои гайдлайны, которых стоит придерживаться. Да и пользовательский опыт платформы очень важен. А пользователи Android привыкли чаще всего видеть диалог по центру экрана.

Ну да ладно, зато задачка любопытная.

Прежде всего нам нужно создать нужный shape с закругленными краями:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="https://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/purple_700" />
<corners android:radius="20dp" />
<padding android:bottom="16dp" />
</shape>

Здесь стоит только обратить внимание на отступы снизу. Важный нюанс, иначе кнопка будет прилеплена к краю макета.

А теперь мы создадим ресурс, который и будет фоном диалога с отступами по краям:

<?xml version="1.0" encoding="utf-8"?>
<inset
xmlns:android="https://schemas.android.com/apk/res/android"
android:drawable="@drawable/rounded_corners_shape"
android:insetRight="16dp"
android:insetLeft="16dp"
android:insetBottom="54dp">
</inset>

Наш созданный shape здесь в качестве drawable и нужные отступы.

И теперь, чтобы все это заработало, нам нужно добавить несколько строк в коде самого диалога:

with(requireDialog()) {
window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
window?.setGravity(Gravity.BOTTOM)
}

Понятное дело, без объекта WIndow не обошлось.
1. Ставим прозрачный фон, иначе будет видна не нужная белая часть диалога.
2. Указываем выравнивание диалога к нижнему краю (Gravity.Bottom).
3. И бонусом растягиваем окно диалога по ширине, иначе он будет выглядеть по ширине контента.
Получаем «красоту» как на скриншоте.

На всякий случай запушил пример в репу если кому-то понадобиться форкнуть себе: https://github.com/Djangist/RoundedDialogDemo
Когда проект растет есть два пути (два стула?) по которому обычно идут команды.
1. Сделать рефакторинг пакетов и вынести функционал по фичам, чтобы проще было искать и поддерживать нужный код. Разбиение по Clean Architecture тогда делается обычно внутри каждого пакета с фичей.
2. Вынести фичи в отдельные модули.
По первому варианту все понятно. Основная проблема - это сложность поиска нужного кода и более медленная сборка проекта.

А вот у второго варианта много и преимуществ и недостатков.
Хорошая статейка, которая освещает эти плюсы и минусы вот: https://proandroiddev.com/the-abc-of-modularization-for-android-in-2021-e7b3fbe29fca

Но на практике все не так радужно и лучше выбрать один из подходов до того как проект станет очень большим.

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

Спросите почему до сих пор не вынесли?
Не знаю, но справедливости ради кусок там очень большой и сейчас проект с нуля собирается 6-7 минут на iMac с 64 ГБ ОЗУ.
Если закладываться на многомодульность сразу, то такой ситуации, понятное дело, не возникнет, но на старте придется потратить времени побольше.
Пост 19. Fragment Result API.

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

Fragment Result API появился с версии Fragment 1.3.0-alpha04.

Все предельно просто.
1. Во фрагменте, где нужно получить данные ставим обработчик:

setFragmentResultListener(KEY) { key, bundle ->
if (bundle.containsKey(key)) {
Toast.makeText(requireContext(), bundle[key].toString(), Toast.LENGTH_LONG).show()
}
}

setFragmentResultListener - это extension для parentFragmentManager.setFragmentResultListener(). Напомню, что extensions для фрагмента доступны в зависимосте: androidx.fragment:fragment-ktx.
KEY - ключ по которому будем ловить результат. Он должен совпадать с тем, что вернет диалог.
2. В диалоге по нажатию на кнопку возвращаем нужный результат:

setFragmentResult(KEY, bundleOf(KEY to "Pff"))

Если в bundleOf передать другое значение, отличное от KEY, Toast не отобразится.
На этом все.

Запушил изменения, если кто-то захочет посмотреть на живом примере: https://github.com/Djangist/RoundedDialogDemo
Пятница.
Давайте устроим новую рубрику #вопросыответы дабы немного оживить наш канал.
Пишите вопросы под этим постом - потрындим о всяком разном айтишном, андроидном и разработческом.
Что новенького в рассылках #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