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


kotlinx.coroutines 1,10.0

Вышла новая версия корутин. Основные изменения:
🔸 Обновиили Kotlin до версии 2.1.0
🔸 Добавили Flow.any, Flow.all и Flow.none
🔸 Реорганизован код kotlinx-coroutines-debug и kotlinx-coroutines-core, чтобы избежать разделения пакета между двумя артефактами
🔸 Исправлен NullPointerException при использовании десериализованных Java исключений kotlinx-coroutines-core
🔸 Исправлена ​​ошибка, которая задерживала планирование задачи Dispatchers.Default или Dispatchers.IO после yield() в редких сценариях
🔸 Исправлена ​​ошибка, которая не позволяла main() корутине в Wasm/WASI выполняться после вызова delay() в некоторых сценариях
🔸 Исправлено планирование runBlocking задач в Kotlin/Native
🔸 Исправлены некоторые терминальные операторы Flow, которые иногда возобновлялись без учета отмены


The Second Developer Preview of Android 16

Вышла вторая версия превью Android 16. Что о ней известно:
🔸 ProfilingManager был добавлен в Android 15. Чтобы помочь со сложными сценариями трассировки, такие как запуски или ANR, ProfilingManager теперь включает System Triggered Profiling. Приложения могут использовать ProfilingManager#addProfilingTriggers() для регистрации к получению информации об этих потоках:
val anrTrigger = ProfilingTrigger.Builder(
ProfilingTrigger.TRIGGER_TYPE_ANR
)
.setRateLimitingPeriodHours(1)
.build()

val startupTrigger: ProfilingTrigger = //...

mProfilingManager.addProfilingTriggers(listOf(anrTrigger, startupTrigger))

🔸 ApplicationStartInfo был добавлен в Android 15. Android 16 добавляет getStartComponent(), чтобы различать, какой тип компонента вызвал запуск
🔸 В Android 16 добавили haptic APIs
🔸 В Android 16 добавлен JobScheduler#getPendingJobReasons(int jobId), который может возвращать несколько причин, по которым джоб находится в состоянии ожидания, как из-за явных ограничений, установленных разработчиком, так и из-за неявных ограничений, установленных системой
🔸 Добавили JobScheduler#getPendingJobReasonsHistory(int jobId), который возвращает список последних изменений ограничений
🔸 В Android 16 DP2 добавлены hasArrSupport() и getSuggestedFrameRate(int) при восстановлении getSupportedRefreshRates(), чтобы приложениям было проще использовать преимущества ARR
🔸 Начиная с Android 16, корректируют квоту времени выполнения обычного и ускоренного выполнения джобов на основе следующих факторов:
📌 В каком app standby bucket приложения находится приложение
📌 Джоба, запущенные, когда приложение видимо пользователю, и продолжающиеся после того, как приложение становится невидимым, будут придерживаться квоты времени выполнения джобы
📌 Джобы, которые выполняются одновременно с активным сервисом, будут придерживаться квоты времени выполнения джобы

🔸 Метод JobInfo#setImportantWhileForeground полностью задепрекейтили и вызов его – не будет ничего делать

И еще несколько вещей, которые показались не так супер интересными.


Get your apps ready for 16 KB page size devices

Android развивается и одним из ключевых улучшений является изменение размера страницы памяти 16 КБ. Это изменение позволяет системе эффективнее управлять памятью, что приводит к заметному повышению производительности (5–10%) как в приложениях, так и в играх.

Чтобы протестировать приложение на устройствах с памятью 16 КБ, эта фича доступна в качестве опции разработчика на устройствах Google Pixel 8 и 9.

Чтобы приложение работало на устройствах с размером страницы 16 КБ, нужно выполнить следующие действия:
1️⃣ Обновите инструменты: Android Gradle Plugin (AGP) 8.5.1 или выше
2️⃣ Align your native code: если приложение включает нативный код, используйте NDK версии r28 или выше
3️⃣ Обновите SDK и библиотеки


Coffee&Code: Kotlin Coroutines 🤗

Напоминалочка, что сегодня в 19:00 пройдет заключительный воркшоп по теме Kotlin Coroutines 🔥

💡 Все детали встречи вы найдете ниже

Обсудим:
🔸 Recap последней темы
🔸 Пройдемся по Channels
🔸 Сделаем парочку тестов

Детали встречи:
Дата – 19 декабря, четверг в 19 вечера
Место – г. Минск прт. Победителей, д. 110, БЦ Ривьера Плаза (вход со стороны прт. Победителей), 4 этаж – офис Т-Банка
Продолжительность – 1+ час (по желанию)
Тема – Kotlin Coroutines

Жду встречи ❤️

P.S. Если не сможете найти, то пишите мне в телеграмме @PavelSha


What's new in CameraX 1.4.0 and a sneak peek of Jetpack Compose support

Вышла новая версия CameraX 1.4.0. Давайте посмотрим, что внутри:
🔸 В 1.4.0 добавили HDR-предпросмотр и Ultra HDR
🔸 HDR-предпросмотр позволяет включать HDR в Preview без необходимости привязывать VideoCapture
🔸 Чтобы полностью включить HDR, необходимо убедиться, что OpenGL способен обрабатывать определенный формат динамического диапазона:
val openGLPipelineSupportedDynamicRange = setOf(
DynamicRange.SDR,
DynamicRange.HLG_10_BIT
)
val isHlg10Supported =
cameraProvider.getCameraInfo(cameraSelector)
.querySupportedDynamicRanges(openGLPipelineSupportedDynamicRange)
.contains(DynamicRange.HLG_10_BIT)

val preview = Preview.Builder().apply {
if (isHlg10Supported) {
setDynamicRange(DynamicRange.HLG_10_BIT)
}
}

🔸 Ultra HDR добавить в пару строк кода:
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
val cameraInfo = cameraProvider.getCameraInfo(cameraSelector)
val isUltraHdrSupported =
ImageCapture.getImageCaptureCapabilities(cameraInfo)
.supportedOutputFormats
.contains(ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR)

val imageCapture = ImageCapture.Builder().apply {
if (isUltraHdrSupported) {
setOutputFormat(ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR)
}
}.build()

🔸 Добавили поддержку Composable Viewfinder, созданного на основе AndroidExternalSurface и AndroidEmbeddedExternalSurface:
class PreviewViewModel : ViewModel() {
private val _surfaceRequests = MutableStateFlow(null)

val surfaceRequests: StateFlow
get() = _surfaceRequests.asStateFlow()

private fun produceSurfaceRequests(previewUseCase: Preview) {
previewUseCase.setSurfaceProvider { newSurfaceRequest ->
_surfaceRequests.value = newSurfaceRequest
}
}

// ...
}

@Composable
fun MyCameraViewfinder(
viewModel: PreviewViewModel,
modifier: Modifier = Modifier
) {
val currentSurfaceRequest: SurfaceRequest? by
viewModel.surfaceRequests.collectAsState()

currentSurfaceRequest?.let { surfaceRequest ->
CameraXViewfinder(
surfaceRequest = surfaceRequest,
implementationMode = ImplementationMode.EXTERNAL, // Or EMBEDDED
modifier = modifier
)
}
}

🔸 В версии 1.4.0 представили две новые suspend функции для упрощения инициализации камеры и захвата изображения:
val cameraProvider = ProcessCameraProvider.awaitInstance()
val imageProxy = imageCapture.takePicture()
imageProxy.close()

🔸 Добавили mirror mode для превью через  Preview.Builder.setMirrorMode
🔸 Наложение эффектов в реальном времени. Доступен набор стандартных эффектов: Overlay Effect и Media3 Effect:
val media3Effect =
Media3Effect(
requireContext(), PREVIEW or VIDEO_CAPTURE or IMAGE_CAPTURE,
mainThreadExecutor(), {}
)
media3Effect.setEffects(listOf(RgbFilter.createGrayscaleFilter())
cameraController.setEffects(setOf(media3Effect))

🔸 Представлена ​​новая мощная фича – вспышка экрана. Вспышка экрана использует дисплей телефона:
previewView.setScreenFlashWindow(activity.getWindow());
imageCapture.screenFlash = previewView.screenFlash
imageCapture.setFlashMode(ImageCapture.FLASH_MODE_SCREEN)


Coffee&Code: Kotlin Coroutines 🤗

Пришло время последней пачки материалов по Kotlin Coroutines. Встречаемся в четверг в 19:00, чтобы закрепить и добить последние моменты по Kotlin Coroutines 🔥

💡 Все детали встречи вы найдете ниже

Обсудим:
🔸 Recap последней темы
🔸 Пройдемся по Channels
🔸 Сделаем парочку тестов

Детали встречи:
Дата – 19 декабря, четверг в 19 вечера
Место – г. Минск прт. Победителей, д. 110, БЦ Ривьера Плаза (вход со стороны прт. Победителей), 4 этаж – офис Т-Банка
Продолжительность – 1+ час (по желанию)
Тема – Kotlin Coroutines

Жду встречи ❤️

P.S. Если не сможете найти, то пишите мне в телеграмме @PavelSha


Media3 1.5.0 — what’s new?

В данной статье автор рассказывает про новинки, что появились в новом обновлении Media3 1.5.0.

🔸 Transformer теперь позволяет экспортировать неподвижное изображение или видео из фотографий с движением. Изображение фотографии с движением экспортируется, если установлена ​​длительность изображения соответствующего MediaItem. В противном случае экспортируется видео фотографии с движением
🔸 Ускорили кодирование изображения в видео благодаря оптимизации в DefaultVideoFrameProcessor.queueInputBitmap()
🔸 Transformer поддерживает AudioEncoderSettings 
🔸 В версии 1.1.0 была добавлена библиотека muxer library, чтобы создавать MP4 контейнеры. Микшер поддерживает широкий спектр аудио- и видеокодеков:
implementation ("androidx.media3:media3-muxer:1.5.0")

🔸 Чтобы использовать Transformer:
val transformer = Transformer.Builder(context)
.setMuxerFactory(InAppMuxer.Factory.Builder().build())
.build()

🔸 Добавили DefaultPreloadManager.Builder, который значительно упрощает создание компонентов предварительной загрузки и плеера:
val preloadManagerBuilder = DefaultPreloadManager.Builder()
val preloadManager = preloadManagerBuilder.build()
val player = preloadManagerBuilder.buildExoPlayer()

🔸 Добавили возможность предварительной загрузки следующего элемента в плейлист ExoPlayer:
player.preloadConfiguration =
PreloadConfiguration(/* targetPreloadDurationUs= */ 5_000_000L)

🔸 Предварительную загрузку плейлиста можно снова отключить с помощью PreloadConfiguration.DEFAULT:
player.preloadConfiguration = PreloadConfiguration.DEFAULT

🔸 Добавили новый модуль media3-decoder-iamf, который позволяет воспроизводить иммерсивные аудиодорожки IAMF в файлах MP4:
implementation ("androidx.media3:media3-decoder-iamf:1.5.0")

🔸 Также добавили набор extensions:
implementation ("androidx.media3:media3-common-ktx:1.5.0")


You Are Going to Need It

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

Он рассмотрел пример двух вариаций возведения числа в квадрат через использования функции pow(2) или просто переумножение числа на само себя:
@RunWith(AndroidJUnit4::class)
class MathBenchmark {
  @get:Rule
  val benchmarkRule = BenchmarkRule()

  val data = FloatArray(8_192) {
    it.toFloat() / 3f
  }

  @Test
  fun pow2() {
    benchmarkRule.measureRepeated {
      for (f in data) {
        f.pow(2f)
      }
    }
  }

  @Test
  fun square() {
    benchmarkRule.measureRepeated {
      for (f in data) {
        f * f
      }
    }
  }
}

Первые измерения показали одинаковый результат. Многие на этом шаге остановятся, хотя это будет неверно измеренный результат. Роман пошел дальше, чтобы понять почему так именно произошло. Оказалось, что все дело в оптимизациях, которые применяет ART.

Получилась простая статья, понятная и с хорошим посылом!


Kotlin Exception Handling: Why Singleton Exceptions are a bad idea

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

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


Let’s build an Android camera app! CameraX + Compose

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

🔸 CameraX имеет 4 конкретных юз кейса:
1️⃣ Предварительный просмотр
2️⃣ Захват изображения для фотосъемки
3️⃣ Видеозахват для захвата движущихся изображений и звука
4️⃣ Анализ изображения, который позволяет делать умные вещи с выходными данными камеры в реальном времени

🔸 Первым шагом добавляем зависимость библиотеки
🔸 Вторым шагом добавляем PreviewView на UI:
@Composable
fun CameraPreview(
modifier: Modifier = Modifier
) {
AndroidView(modifier = modifier,
factory = { context ->
PreviewView(context)
}
)
}

🔸 Запустив данный код получим белый экран, потому что PreviewView по сути является просто холстом для рисования
🔸 Далее нужно создать вариант использования Preview:
@Composable
fun CameraPreview( ... ) {

val previewUseCase = remember { androidx.camera.core.Preview.Builder().build() }

...
}

🔸 Затем нужно сказать Preview рисовать на холсте, предоставленном PreviewView:
AndroidView(
...
factory = { context ->
PreviewView(context).also {
previewUseCase.surfaceProvider = it.surfaceProvider
}
}
)

🔸 CameraProvider используется для привязки вариантов использования к камере и для того, чтобы делать это с учетом жизненного цикла:
@Composable
fun CameraPreview( … ) {
val localContext = LocalContext.current

LaunchedEffect(Unit) {
cameraProvider = ProcessCameraProvider.awaitInstance(localContext)
}


}

🔸 Теперь сможем связать вариант использования Preview с камерой и жизненным циклом:
@Composable
fun CameraPreview( … ) {



fun rebindCameraProvider() {
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_FRONT)
.build()

cameraProvider?.bindToLifecycle(
localLifecycleOwner,
cameraSelector,
previewUseCase
)
}


}

🔸 Функцию rebindCameraProvider() нужно вызывать только один раз, когда доступны и PreviewView, и ProcessCameraProvider:
AndroidView(modifier = modifier,
factory = { context ->
PreviewView(context).also {
previewUseCase.surfaceProvider = it.surfaceProvider
rebindCameraProvider()
}
}
)

🔸 Добавим параметр в наш Composable, чтобы можно было контролировать какую камеру использовать. Поскольку выбор камеры указан как часть привязки CameraProvider, нужно будет выполнить повторную привязку, когда она изменится:
@Composable
fun CameraPreview(
lensFacing: Int = CameraSelector.LENS_FACING_BACK,

) {


fun rebindCameraProvider() {
cameraProvider.unbindAll()
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(lensFacing)
.build()

}

LaunchedEffect(lensFacing) {
rebindCameraProvider()
}


}

🔸 В итоге результат получается такой
🔸 Еще можно добавить зум:
@Composable
fun CameraPreview(
zoomLevel: Float,
...
) {
...
LaunchedEffect(zoomLevel) {
cameraControl?.setLinearZoom(zoomLevel)
}

}

🔸 Чтобы захватить изображение с камеры в полном разрешении, нужно использовать вариант использования ImageCapture:
@Composable
fun CameraPreview(
...
imageCaptureUseCase: ImageCapture
) {
...
fun rebindCameraProvider() {
cameraProvider?.let { cameraProvider ->
val camera = cameraProvider.bindToLifecycle(
localContext as LifecycleOwner,
cameraSelector,
previewUseCase, imageCaptureUseCase
)

}
}

}


Обновление Jetpack библиотек от 11 и 12 декабря

1️⃣ Lifecycle Version 2.9.0-alpha08
🔸 Добавили ViewModelScenario.recreate для имитации System Process Death, воссоздающего тестируемую ViewModel и все связанные компоненты
🔸 Экземпляры LifecycleOwner и ViewModelStoreOwner, полученные через findViewTree, теперь можно разрешить через родительские элементы представления, такие как ViewOverlay

2️⃣ Core-Viewtree Version 1.0.0-alpha01
🔸 Первый релиз библиотеки core-viewtree
🔸 Представлена ​​концепция View, которая может иметь непересекающегося родителя. Непересекающийся родитель — это отдельный объект View, который действует как родитель представления, но не устанавливается через свойство View.parent. Примерами таких View являются ViewOverlays, всплывающие окна и диалоговые окна

3️⃣ Core-Telecom Version 1.0.0-beta01
🔸 Первая сборка новой библиотеки. Это набор API, который поможет сделать интеграцию звонков в приложение проще
🔸 Извлечение доступных audio endpoints до добавления вызова
🔸 Экспериментальная поддержка API для расширений вызовов приложений VOIP
🔸 Поддержка отображения участников группового вызова или собрания и описания того, какой участник активен и многое другое

4️⃣ ViewPager Version 1.1.0
🔸 Добавлена ​​поддержка эффекта при скролле в Android 12

Было пофикшано много багов и сделано несколько улучшений в остальных артефактах.

Также вышли артефакты для недавно представленного Android XR:
🔸 ARCore for Jetpack
🔸 Jetpack Compose for XR
🔸 Material Design for XR
🔸 Jetpack SceneCore
🔸 XR Runtime Version

Android XR – новая операционная система, созданная для следующего поколения вычислений. Созданная в сотрудничестве с Samsung, Android XR предназначена для AR и VR и очков.


Kotlin trick: writing shared Enum utility code

В данной статье автор делиться своими набросками при работе с enum в коде. К примеру, хотите, чтобы элементы enum были сразу отсортированы, то можно сделать так:
object Enums {
inline fun checkEntriesSorted() {
val names = enumEntries().map { it.name }
check(names == names.sorted()) {
"${T::class.java.simpleName} enum entries should be sorted alphabetically"
}
}
}

enum class Screen {
CartScreen,
HomeScreen,
;

companion object {
init {
Enums.checkEntriesSorted()
}
}
}

И вот такие мелкие моменты автор приводит в статье.


Vulkan 1.4: Faster app loads, less stutter and less Memory Usage

Вышел Vulkan 1.4 с важной фичей внутри – Host Image Copy на основе VK_EXT_host_image_copy. Данная фича позволяет приложению передавать данные изображения с помощью CPU вместо GPU, а также экономить память. Детали можно найти в статье.


Вопперы и табы: как мы сделали меню для Burger King

В прошлом году у Android-команды на проекте Burger King был мощный вызов: сделать редизайн главного меню. Задача была непростая по двум причинам.
Первая — легаси код. Вторая — А/В тестирование. И результат — старое меню и его логику нужно сохранить.

Решили написать меню с нуля. В данной статье автор делиться как делали часть этой фичи — табы и саб-табы.

🔸 Создать новый экран со списком блюд — это не очень сложно. Но нужно было синхронизировать скролл списка и выбор табов
🔸 Решения из коробки не было. Material-библиотека предлагает TabLayoutMediator, но он работает для связки TabLayout и ViewPager2
Что надо было решить при синхронизации табов:
📌 Синхронизация табов и списка
При скролле должны были понять, в какой категории товаров находится пользователь, и выбрать нужный таб. И наоборот — при клике на таб должен происходить подскролл списка к нужной категории

📌 Синхронизация саб-табов и списка
При выборе категории с подкатегориями появляется второй TabLayout, в котором и отображаются эти подкатегории

📌 Обратный скролл
Каждый раз нужно выбирать категорию в TabLayout при скролле в обе стороны

📌 Скролл при клике на таб
TabLayout не различает, кто кликнул по табу: пользователь или программа. Он всё равно вызывает callback OnTabSelectedListener. Это вынуждает использовать boolean-флаги, чтобы скролл пальцем и скролл по клику на таб не мешали друг другу

📌 Кастомный вид табов
У TabLayout.Tab есть поле customView. Это поле помогает легко установить верстку с иконкой и текстом

📌 Ripple-эффект табов
View в TabLayout.Tab не меняет ripple-эффект, если установлен кастомный индикатор

Далее автор рассказывает с кодом как это все у него получилось сделать 🔥


User-Agent Reduction on Android WebView

Начиная с версии Chrome 107, на Android 16 строка User-Agent по умолчанию в Android WebView будет сокращена:
Mozilla/5.0 (Linux; Android 10; K; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/125.000 Mobile Safari/537.36

Это все делается, чтобы минимизировать возможность идентификации пользователя.


Notes on Immutability

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

🔸 Давайте посмотрим на пример с мутабельными данными:
fun decodeToMutablePerson(json: String, city: String, postalCode: String): MutablePerson {
val person = MutablePerson()
person.city = city
person.postalCode = postalCode
decode(json, person)
validatePerson(person)
return person
}

fun decode(json: String, person: MutablePerson) {
// Pretend we decode.
person.name = "name"
person.age = 100
}

fun validatePerson(person: MutablePerson) {
require(!person.name.isNullOrBlank())
requireNotNull(person.age)
require(!person.city.isNullOrBlank())
require(!person.postalCode.isNullOrBlank())
}

class MutablePerson {
var name: String? = null
var age: Int? = null
var city: String? = null
var postalCode: String? = null
}

🔸 В функции decodeToMutablePerson создается объект с мутабельными свойствами и далее в разных функциях меняет их значения. К такому коду много вопросов и самое важное, что теряется безопасность вызова функций
🔸 Гораздо легче будет сразу отрефачить и сделать иммутабельные свойства:
fun decodeToPerson(json: String, city: String, postalCode: String): Person {
val info = decode(json)
val person = Person(info.name, info.age, city, postalCode)
validatePerson(person)
return person
}

fun decode(json: String): PersonInfo {
// Pretend we decode.
return PersonInfo("name", 100)
}

class PersonInfo(val name: String, val age: Int)

fun validatePerson(person: Person) {
require(person.name.isNotBlank())
require(person.city.isNotBlank())
require(person.postalCode.isNotBlank())
}

class Person(val name: String, val age: Int, val city: String, val postalCode: String)

🔸 Давайте посмотрим еще на второй пример:
fun main() {
val numbers = arrayListOf(1, 2, 3, 4)
val filtered = filter(numbers) { it % 2 == 0 }
// What this prints:
// 1) - 1, 2, 3, 4
// 2) - 2, 4
// 3) - something else?
println(filtered)
val mapped = map(numbers) { it + 10 }
// What this prints:
// 1) - 10, 20, 30, 40
// 2) - 12, 14
// 3) - something else?
println(mapped)
}

fun filter(input: MutableList, predicate: (T) -> Boolean): MutableList {
val iterator = input.listIterator()
for (element in iterator) {
if (!predicate(element)) iterator.remove()
}
return input
}

🔸 Как видно, функция не использует принцип неизменяемого кода. Это приводит к 3 ключевым проблемам с функцией фильтра:
📌 Функция выполняет свою работу, напрямую изменяя входные данные. Теперь знаем, что функция println(mapped) не выведет то, что ожидалось
📌 Это будет неожиданно при чтении кода. Иногда непросто заметить такие вещи при чтение пулл реквеста
📌 Еще одна большая проблема в реализации заключается в том, что используется итератор в изменяемом списке. Если есть несколько активных итераторов, итерирующих и мутирующих список одновременно из другого потока, то можно получить ConcurrentModificationException

🔸 Поэтому лучше писать с помощью иммутабельности. К примеру, функция map:
fun map(input: List, transform: (T) -> R): List {
val result = mutableListOf()
for (element in input) {
val r = transform(element)
result.add(r)
}
return result
}

🔸 Использование изменяемых объектов локально внутри функции и только с одной ссылкой вполне допустимо
🔸 Автор не предлагает полностью отказаться от мутабельности. Изменяемость следует использовать только тогда, когда это необходимо


Testing Different Navigation Options with Compose

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

К примеру, тест на навигацию через кнопки:
@Test
fun buttonNavigationWorksCorrectly() {
...

// Navigate forward
rightButton.performClick()

labels.assertIsDisplayed()

labels.onChildren().assertAny(hasText("2015"))

// Navigate forward
rightButton.performClick()
rightButton.performClick()
rightButton.performClick()
rightButton.performClick()

// Navigate back
leftButton.performClick()
leftButton.performClick()

labels.onChildren().assertAny(hasText("2017"))
}


UI State, Callbacks and Equality Pitfalls

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

🔸 Вместо того, чтобы иметь:
data class SuperheroViewEntity(
val id: Long,
val name: String,
val imageUrl: HttpUrl,
val onFavoriteClicked: () -> Unit
)

// ViewModel
fun Superhero.toViewEntity() =
SuperheroViewEntity(
id,
name,
imageUrl,
if (favorite) { onRemoveFromFavorites(id) } else { onAddToFavorites(id) }
)

🔸 Можно сделать:
sealed class FavoritesAction
data class Add(id: Long): FavoritesAction
data class Remove(id: Long): FavoritesAction

data class SuperheroViewEntity(
val id: Long,
val name: String,
val imageUrl: HttpUrl,
val favoritesAction: FavoritesAction,
)

// ViewModel
fun Superhero.toViewEntity() =
SuperheroViewEntity(
id,
name,
imageUrl,
if (favorite) FavoritesAction.Remove(id) else FavoritesAction.Add(id)
)


Function Properties in Data Classes are Code Smells

Для автора использование функций в качестве свойств в основном конструкторе дата классов — это код с душком, потому что:
📌 Классы данных представляют данные. Данные — это значение. Данные никогда не выполняются
📌 Функции — это не данные. Они производят значения при выполнении

В данной статье автор защищает эту идею через пояснение как будет в этом случае работать стандартная проверка на сравнение.

🔸 Используйте дата классы только для данных. Если вам нужно включить функции или поведение, например обратные вызовы:
📌 Используйте обычный класс
📌 Переопределите equals(), hashCode() и toString() вручную


Dagger 2.53

Недавно вышел новый Dagger 2.53. Из нового:
🔸 @Binds теперь требуют явной возможности принимать значение null
@Module
interface MyModule {
@Binds fun bindToNullableImpl(impl: FooImpl?): Foo?
}

🔸 Указание скоупа теперь запрещено для @Binds, которые делегируют создание реализациям
@Module
interface MyModule {
@Binds fun bindToProductionImpl(impl: FooImpl): Foo
}

🔸 @JvmSuppressWildcards теперь требуется для multibound map в KSP
class MyClass
@Inject constructor(
multiboundMap: Map
)

🔸 Удалена поддержка Java 7
🔸 Обновили версию плагина Hilt Gradle AGP до 8.1
🔸 Несколько фиксов и незначительных изменений

20 ta oxirgi post ko‘rsatilgan.