Academy Minsk News & Announcements


Kanal geosi va tili: Belarus, Ruscha


Самые интересные новости, видео с конференций из Android мира. Анонсы митапов, важных и полезных конференций от сообщества Android Academy Minsk.
Обсудить материал вы можете в чате: https://t.me/androidacademyminsk

Связанные каналы  |  Похожие каналы

Kanal geosi va tili
Belarus, Ruscha
Statistika
Postlar filtri


Kodein DI для Android. Целостность графа и тесты на DI

В данной статье автор рассматривает как можно сделать проверку DI графа с помощью модульных тестов. Такая проверка необходима для тех библиотек, где валидация графа происходит в runtime. И поэтому в статье пример будет основан на Kodein.

В runtime DI нет compile time проверок. Поэтому возникла идея написать юнит-тесты, которые будут проверять целостность графа. Такие проверки позже можно выполнять и на CI.

В итоге тест получился такой:
class ProductFragmentDITest {

...
private lateinit var diSUT: DI
private lateinit var context: DITestContext

@Before
fun setup() {
diSUT = createDI()
context = DITestContext(
integrityRule = integrityRule,
parentDI = Create.unitScope.di,
)
}

@Test
fun `SubDI of ProductFragment has integrity`() = withDIContext(context) {
assertDIHasIntegrity(diSUT)
}
}

Как автор к этому тесту пришел и что такое DITestContext стоит узнать в статье.


Вышло обновление Jetpack библиотек

1️⃣ Activity Version 1.9.0
🔸 ComponentActivity теперь реализует OnUserLeaveHintProvider, чтобы позволить компонентам выполнять обратные вызовы для событий onUserLeaveHint
🔸 OnBackPressedCallback, BackHandler и PredictiveBackHandler теперь предупреждают при вызове onBackPressedDispatcher.onBackPressed()
🔸 Переписали на Kotlin

2️⃣ Core Core-Ktx Core-Testing Version 1.13.0
🔸 minSdkVersion повышен до 19
🔸 Несколько классов были переписаны в Kotlin
🔸 Удален FingerprintManagerCompat, который не используется, начиная с Android V. Стоит переходить на BiometricPrompt
🔸 Добавлен PathParser, который может создавать экземпляр Path из строк пути SVG

3️⃣ Datastore Version 1.1.0
🔸 DataStore теперь поддерживает несколько процессов, обращающихся к одному и тому же файлу, а также возможность наблюдения за разными процессами
🔸 Новый интерфейс хранилища позволяет вам настроить способ хранения или сериализации ваших моделей данных
🔸 Теперь вы можете использовать DataStore в мультиплатформенных проектах Kotlin

В остальном багафиксы.


How to effectively A/B test power consumption for your Android app’s features

Небольшая статья, которая показывает возможности нового Power Profiler в Android Studio. Этот профайлер показывая энергопотребление, происходящее на устройствах во время использования приложения. Также можно запускать A/B-тесты, чтобы сравнивать энергопотребление различных алгоритмов, функций или даже разных версий своего приложения 🚀


в итоге побеждает 12 часов в воскресенье. Буду тогда планировать это время и посмотрим как пойдет. Следите за анонсами))


Как протестировать Android-приложение, которому требуются разрешения

В этой статье автор покажет, как проблемы с разрешениями в тестах решает библиотека Kaspresso.

🔸 Тестировать приложение будем при помощи Kaspresso, поэтому в build.gradle добавляем зависимости:
dependencies {
androidTestImplementation("com.kaspersky.android-components:kaspresso:1.5.3")
androidTestUtil("androidx.test:orchestrator:1.4.2")
}

🔸 При написании тестов будем использовать Page Object — это такой класс, который содержит ссылки на все view-элементы, с которыми нам нужно взаимодействовать:
object MakeCallActivityScreen : KScreen() {

override val layoutId: Int? = null
override val viewClass: Class? = null

val inputNumber = KEditText { withId(R.id.input_number) }
val makeCallButton = KButton { withId(R.id.make_call_btn) }
}

🔸 Чтобы попасть на этот экран, нужно будет в MainActivity кликнуть по соответствующей кнопке, добавляем эту кнопку в MainScreen:
object MainScreen : KScreen() {

override val layoutId: Int? = null
override val viewClass: Class? = null

val simpleActivityButton = KButton { withId(R.id.simple_activity_btn) }
val wifiActivityButton = KButton { withId(R.id.wifi_activity_btn) }
val loginActivityButton = KButton { withId(R.id.login_activity_btn) }
val notificationActivityButton = KButton { withId(R.id.notification_activity_btn) }
val makeCallActivityButton = KButton { withId(R.id.make_call_activity_btn) }
}

🔸 Можем создавать тест. Давайте пока просто откроем экран совершения звонка, введем какой-то номер и кликнем по кнопке:
class MakeCallActivityTest : TestCase() {

@get:Rule
val activityRule = activityScenarioRule()

@Test
fun checkSuccessCall() = run {
step("Open make call activity") {
MainScreen {
makeCallActivityButton {
isVisible()
isClickable()
click()
}
}
}
step("Check UI elements") {
MakeCallActivityScreen {
inputNumber.isVisible()
inputNumber.hasHint(R.string.phone_number_hint)
makeCallButton.isVisible()
makeCallButton.isClickable()
makeCallButton.hasText(R.string.make_call_btn)
}
}
step("Try to call number") {
MakeCallActivityScreen {
inputNumber.replaceText("111")
makeCallButton.click()
}
}
}
}

🔸 В зависимости от того дали вы разрешение или нет, у вас может отобразиться диалог с запросом разрешения на совершение звонков
🔸 На данном этапе проверили работу экрана, убедились, что есть возможность ввести номер и кликнуть на кнопку, но никак не проверили, происходит вызов по введенному номеру или нет
🔸 Для того чтобы проверить, происходит ли в данный момент вызов, можно использовать AudioManager, делается это следующим образом:
val manager = device.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)

🔸 Добавим в тест шаг:
step("Check phone is calling") {
val manager = device.context.getSystemService(AudioManager::class.java)
Assert.assertTrue(manager.mode == AudioManager.MODE_IN_CALL)
}

🔸 Запускаем тест. Тест провален. Это произошло, потому что после клика по кнопке у пользователя было запрошено разрешение. Никто этого разрешения не дал, и следующий экран открыт не был
🔸 Есть несколько вариантов решения возникшей проблемы. Первый вариант — использовать GrantPermissionRule. Создаем список разрешений, которые будут автоматически разрешены на тестируемом устройстве:
@get:Rule
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
android.Manifest.permission.CALL_PHONE
)

И далее автор рассказывает про проблемы и решения, с которыми можно столкнуться.




Лучшее время для встречи
So‘rovnoma
  •   10:00, вскр
  •   11:00, вскр
  •   12:00, вскр
  •   13:00, вскр
  •   14:00, вскр
  •   15:00, вскр
10 ta ovoz


всем привет 👋
В рамках оффлайн встреч Coffee&Code в Минске изучили основы многопоточности в Java/Kotlin. На очереди – Kotlin Coroutines. Давайте вместе выберем время в воскресенье для проведения подобных мероприяти.

Подключайся к голосованию!


Scaling with Deeplinks on Android

В данной статье автор рассказывает как организовать код для поддержки большого количества диплинок в приложении.

🔸 Вначале у автора было всего 12 диплинок и соответственно был некоторый DeeplinkRouter, который помогал переходить на разные экраны:
interface Deeplink {
fun route(uri: Uri): Boolean
}

class NotificationsDeeplink(val navigator: Navigator) : Deeplink {
override fun route(uri: Uri): Boolean {
return if (uri.path == "/notifications") {
navigator.goToNotifications()
true
} else {
false
}
}

class DeeplinkRouter(
notificationsDeeplink: NotificationsDeeplink,
profileDeeplink: ProfileDeeplink,
// ...
) {

private val deeplinks = listOf(
notificationsDeeplink,
profileDeeplink,
// ...
)

fun route(intent: Intent) {
intent.data?.let { uri ->
deeplinks.find { it.route(uri) }
}
}
}

🔸 По мере роста количества диплинков начали возникать две большие проблемы:
1️⃣ Управление огромным количеством диплинков — почти 50
🔸 Нужно было внедрить в DeeplinkRouter. Решили эту проблему, введя аннотацию, которая позволит устранить повторяющийся код:
@RegisterDeeplink
class NotificationsDeeplink(val navigator) : Deeplink {
override fun route(uri: Uri): Boolean {
// ...
}
}

class DeeplinkRouter(val register: DeeplinkRegister) {
fun route(intent: Intent) {
intent.data?.let { uri ->
register.deeplinks.find { it.route(uri) }
}
}
}

🔸 DeeplinkRegister создается во время сборки и отвечает за ведение списка диплинок

2️⃣ Некоторые диплинки чувствительны к порядку
🔸 Например:
class ArticleDeeplink(val navigator: Navigator) : Deeplink {
override fun route(uri: Uri): Boolean {
return if (uri.path?.startsWith("/article") == true && uri.pathSegments.size == 2) {
navigator.goToArticle(id = uri.pathSegments[1])
true
} else {
false
}
}
}

class CommentsDeeplink(val navigator: Navigator) : Deeplink {
override fun route(uri: Uri): Boolean {
return if (uri.path?.startsWith("/article") == true
&& uri.path?.endsWith("/comments") == true
&& uri.pathSegments.size == 3
) {
navigator.goToComments(id = uri.pathSegments[1])
true
} else {
false
}
}
}

🔸 Если пользователь открывает www.doximity.com/article/{id}/comments и сначала вызывается ArticleDeeplink, у CommentsDeeplink никогда не будет возможности обработать
🔸 Необходимо убедиться, что CommentsDeeplink размещается в списке перед ArticleDeeplink. Это хрупкая конструкция и ее становится все труднее решать в больших масштабах
🔸 Вместо этого было бы лучше объединить две диплинки и применить правило:
interface Deeplink {
fun route(uri: Uri) // no longer returns Boolean
}

@RegisterDeeplink("/article")
class ArticleDeeplink(val navigator: Navigator) : Deeplink {
override fun route(uri: Uri) {
when {
uri.pathSegments.size == 2 -> {
navigator.goToArticle(id = uri.pathSegments[1])
}
uri.pathSegments.size == 3 && uri.path?.endsWith("/comments") {
navigator.goToComments(id = uri.pathSegments[1])
}
}
}
}

class DeeplinkRouter(val register: DeeplinkRegister) {
fun route(intent: Intent) {
intent.data?.let { uri ->
val topLevelPath = uri.pathSegments.firstOrNull()
register.findDeeplink("/$topLevelPath")?.let { it.route(uri) }
}
}
}

🔸 DeeplinkRegister будет поддерживать сопоставление путей верхнего уровня с соответствующим определением диплинки. Если совпадение найдено, диплинка возвращается в DeeplinkRouter и получает входящий Uri. Это значительно снижает сложность управления диплинками

Далее автор рассказывает про работу с аннотациями и генерацию кода с помощью KotlinPoet 🚀


@surendar1006/implementing-critical-alerts-on-android-aa49b4d75705' rel='nofollow'>Implementing Critical Alerts on Android

@surendar1006/implementing-critical-alerts-on-android-aa49b4d75705' rel='nofollow'>В данной статье автор рассказывает как можно реализовать Android уведомления под названием critical alerts, которые могут сработать в режиме Do Not Disturb/Не беспокоить.


The First Beta of Android 15

Недавно вышла первая Android 15 Beta. Давайте быстренько пройдемся по основным моментам:
🔸 Режим edge-to-edge в Android 15 будет включен по умолчанию для приложений
🔸 Были сделаны NFC улучшения – добавили observe mode
🔸 Начиная с Android 15, текст можно выравнивать с использованием межбуквенного интервала с помощью JUSTIFICATION_MODE_INTER_CHARACTER
🔸 Android 15 теперь включает поддержку на уровне ОС для архивирования и разархивирования приложений. Приложения с разрешением REQUEST_DELETE_PACKAGES могут вызывать метод PackageInstaller requestArchive, чтобы запросить архивирование
🔸 ProfilingManager – новое API для сбора технической информации о работе приложения
🔸 Представили E2eeContactKeysManager, который упрощает сквозное шифрование, предоставляя API на уровне ОС для хранения криптографических открытых ключей
🔸 Внесены дополнительные изменения, которые не позволяют запускать Activity из фона

Уже сейчас можно попробовать новую версию. Больше деталей можно найти здесь.


Android Studio uses Gemini Pro to make Android development faster and easier

В бот Android Studio теперь называется Gemini, используя модель Gemini 1.0 Pro, чтобы ускорить и упростить разработку Android. Доступен в 180+ странах.

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


всем привет,

Совсем скоро, а именно 15 июня, компания Tinkoff проведет масштабное IT-событие в Минске. Это будет мощное и полезное мероприятие, которое соберет под одной крышей 600-700 человек.
Организаторы готовят 3 параллельных потока:
– Mobile
– QA
– Java/Scala

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

Формирование программы еще продолжается, и у вас есть возможность повлиять на ее наполнение. Если у вас есть идея для доклада или вы хотите поделиться своим опытом, организаторы с радостью помогут вам реализовать это желание.

Для получения более подробной информации и согласования выступления, пожалуйста, свяжитесь с @JChary в Telegram.


Improving dependency sync speeds for your Gradle project

В данной статье автор рассказывает как ускорить Gradle синхронизацию, описав в верном порядке репозитории. К примеру, можно:
pluginManagement {
repositories {
google()
mavenCentral()
}
}

mavenCentral() можно опустить за google, чтобы найти AGP и многие androidx библиотеки. Также могут быть полезными exclusiveContent и content

Данные оптимизации ускорили синк до 3+ минут.


Spotify-Inspired Audio Buffering Slider Animation with Jetpack Compose

В данной статье автор рассказывает как сделать анимацию как на видео с помощью Compose Animation Api. Автора вдохновило приложение Spotify на создание такой анимации.

Были использованы Api: animateFloat, drawLine, infiniteRepeatable и стандартные remember для состояний.

Весь код такого Modifier можно посмотреть здесь.


Exactly what to say in code reviews

В этой статье автор познакомит вас с 7 простыми приемами, позволяющими уважительно оставлять отзывы во время ревью кода. Для каждой техники автор приведет примеры того, как и когда ее использовать.

1️⃣ "Мне интересно…"
🔸 Используйте для легкого предложения, открывая его для обсуждения
🔸 Пример: "Мне интересно, можем ли мы использовать здесь switch/when вместо нескольких if-else"

2️⃣ "Мне любопытно…"
🔸 Используйте, чтобы указать на что-то, что может быть не очень хорошо в текущем подходе
🔸 Пример: "Мне любопытно узнать об accessibility этих UI компонентов. Есть ли у нас что-то, что дало бы нам гарантии в этом отношении?"

3️⃣ "Что ты думаешь о…" — используйте, чтобы сделать прямое предложение, оставляя при этом возможность другому человеку высказать свои мысли
🔸 Пример: "Что думаешь об использовании здесь map вместо изменения массива в целях безопасности?"

4️⃣ "Что произойдет, если…"
🔸 Используйте, когда вы почти уверены, что краевой случай не обработан, но вы все равно хотите позволить другому человеку сказать вам что-то, о чем вы не знаете
🔸 Пример: "Что произойдет, если API вернет здесь ошибку?"

5️⃣ "Я заметил… Что ты думаешь?"
🔸 Используйте, если вы заметили в подходе что-то, что могло бы быть лучше, но хотите получить обратную связь по вашему предложению
🔸 Пример: "Я заметил, что другие файлы в этой папке имеют называются по конвеншену . Стоит ли следовать этому конвеншену и тут?"

6️⃣ "Что > почему"
🔸 Используйте, везде, где это возможно, "что" вместо "почему". Это смягчает язык, чтобы избежать оборонительной реакции
🔸 Пример: "В чем причина добавления собственной логики вместо использования библиотеки?" лучше, чем "Почему вы решили добавить сюда собственную логику вместо использования библиотеки?"

7️⃣ Слова сопереживания > предписывающие слова
🔸 Проверьте, насколько уверены вы в своем утверждении
🔸 Используйте такие слова, как "рассмотреть", "может быть" и "может" вместо таких слов, как "должен", "должен" и "следует"


Testing Proto DataStore

В данной статье автор описывает как можно покрыть тестами работу с Proto DataStore.

Одна из проблем у автора была то, что после выполнения теста в логах было сообщение:
There are multiple DataStores active for the same file: /data/user/0/com.example.app/files/datastore/dataStore_filename.pb. You should either maintain your DataStore as a singleton or confirm that there is no two DataStore’s active on the same file (by confirming that the scope is cancelled).

Эту проблему автор пытался решить через удаление файлов после выполнения каждого теста, т.е. чистил после себя все следы, которы тест мог оставить:
@After
fun cleanup() {
File(testContext.filesDir, "datastore").deleteRecursively()
}

💡 Весь код доступен здесь


Exploring the Android Photo Picker

В данной статье автор рассказывает про Photo Picker: как можно использовать его в собственных приложениях,

По сути все сводится к такому коду:
private val picker = registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia()) { uris ->
// handle uris
}

picker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo))


Mastering Android ViewModels: Essential Dos and Don’ts Part 2 🛠️

В данной статье автор продолжает серию статей по поводу того какие есть практики по работе с ViewModel. В этот раз мы обсудим:
📌 Избегайте использование мутабельных состояний в качестве контрактов
📌 Используйте update{} для обновления MutableStateFlow

🔸 Декларирование MutableStateFlow в качестве контракта может привести к ряду проблем, связанных с целостностью данных. Вот некоторые из основных проблем:
📌 Нарушает инкапсуляцию
📌 Риски целостности данных
📌 Повышенная сложность
📌 Проблемы параллелизма
📌 Проблемы тестирования
📌 Архитектурная ясность
📌 Отсутствие контроля над подписчиками

🔸 Вариант плохой практики:
class RatesViewModel constructor(
private val ratesRepository: RatesRepository,
) : ViewModel() {
val state = MutableStateFlow(RatesUiState(isLoading = true))
}

🔸 Чтобы устранить эти проблемы, обычно рекомендуется предоставлять состояние ViewModels только для чтения с помощью StateFlow или LiveData
🔸 Этот подход поддерживает инкапсуляцию и позволяет ViewModel более эффективно управлять своим состоянием
🔸 Изменения состояния можно вносить с помощью четко определенных методов в ViewModel, которые могут проверять и обрабатывать изменения по мере необходимости:
class RatesViewModel constructor(
private val ratesRepository: RatesRepository,
) : ViewModel() {
private val _state = MutableStateFlow(RatesUiState(isLoading = true))
val state: StateFlow
get() = _state.asStateFlow()
}

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

🔸 Обратимся к способу обновления состояния в StateFlow. Можно к примеру сделать так:
mutableStateFlow.value = mutableStateFlow.value.copy()

🔸 Этот метод включает в себя непосредственную установку значения MutableStateFlow путем создания копии текущего состояния с желаемыми изменениями. Этот подход прост и хорошо работает для простых обновлений состояния
🔸 Однако это не атомарный подход, а это означает, что если несколько потоков одновременно обновляют состояние, вы можете столкнуться с состоянием гонки
🔸 Другой вариант:
mutableStateFlow.emit(newState())

🔸 Использование .emit() позволяет отправлять новое состояние в MutableStateFlow. Хотя .emit() является потоко-безопасным и может использоваться для одновременных обновлений, это suspend функция
🔸 Это означает, что его следует вызывать внутри корутины
🔸 Последний вариант:
mutableStateFlow.update { it.copy(// state modification here) }

🔸 Почему .update{} часто является предпочтительным подходом:
📌 Атомарность
📌 Потокобезопасность
📌 Простота и безопасность

userStateFlow.update { currentUser ->
currentUser.copy(age = currentUser.age + 1)
}


Rewriting Home Feed on Android & iOS

Ребята из reddit рассказали как они переписали Home Feed для Android и iOS.

Android использует Jetpack Compose, MVVM и BDUI подход. iOS использует собственные компоненты SliceKit, MVVM и BDUI подход.

🔸 Эксперимент в продакшене работал с середины 23 года и только сейчас стал доступен всем. Во время экспериментов команда сделала много разных открытий и исправлений, чтобы улучшить пользовательский опыт 🚀
🔸 Базовой метрикой для ребят была – Time To Interact!
🔸 Перед тем как идти и крушить все, ребята сделали документ. Хороший проектный документ должен охватывать нецелевые метрики и следить за тем, чтобы команда не отвлекалась. Команда остановилась на следующих четырех показателях успеха, в произвольном порядке:
1️⃣ Home Time to Interact

🔸 Home TTI = App Initialization Time (Code) + Home Feed Page 1 (Response Latency + UI Render)
🔸 Измеряли это с момента открытия сплэш экрана до момента завершения рендеринга первой вьюхи главного экрана

📌 Цели
🔸 Делать как можно меньше манипуляций на стороне клиента и визуализировать фид в том виде, в каком он задан сервером
🔸 Переместить предварительную загрузку главного фида как можно раньше при запуске приложения

📌 Нецелевые метрики
🔸 Улучшить время инициализации приложения

2️⃣ Размер и задержка ответа на Home запрос

📌 Цели
🔸 Оптимизировать запрос GQL исключительно для первого рендеринга и оптимизировать использование фрагментов на стороне клиента
🔸 Несущественные поля с отложенной загрузкой используются только для аналитики и прочего
🔸 Поэкспериментировать с разными размерами страниц для страницы 1

📌 Нецели
🔸 Изучить подход, отличный от GraphQL. В предыдущих итерациях исследовали схему Protobuf

3️⃣ Производительность разработчиков

🔸 Добавление любой новой фичи в существующую ленту не было быстрым и требовало от команды в среднем 1–2 спринта
🔸 Хотели измерить скорость новой разработки, время выполнения изменений и удовлетворенность разработчиков

📌 Цели
🔸 Делать все быстрее
🔸 Создать новый стек для построения фидов
🔸 Создать инструменты внедрения зависимостей

📌 Нецели
🔸 Оптимизация времени сборки

Далее автор рассказывает как они к этим всем целям шли и какие вещи замеряли. Есть свои полезные моменты работы в больших компаниях!

20 ta oxirgi post ko‘rsatilgan.