Mobile VK Hub
رفتن به کانال در Telegram
Комьюнити от VK для мобильных разработчиков. Здесь всё о том, как создаются приложения для миллионов: от нативных подходов до кросс-платформы.
نمایش بیشتر730
مشترکین
-224 ساعت
-37 روز
+130 روز
در حال بارگیری داده...
کانالهای مشابه
هیچ دادهای
مشکلی وجود دارد؟ لطفاً صفحه را تازه کنید یا با مدیر پشتیبانی ما تماس بگیرید.
ابر برچسبها
اشارات ورودی و خروجی
---
---
---
---
---
---
جذب مشترکین
سپتامبر '26
سپتامبر '260
در 0 کانالها
اوت '26
+10
در 1 کانالها
Get PRO
ژوئیه '26
+5
در 0 کانالها
Get PRO
ژوئن '26
+4
در 1 کانالها
Get PRO
مه '26
+6
در 1 کانالها
Get PRO
آوریل '26
+9
در 1 کانالها
Get PRO
مارس '26
+9
در 0 کانالها
Get PRO
فوریه '26
+18
در 1 کانالها
Get PRO
ژانویه '26
+18
در 1 کانالها
Get PRO
دسامبر '25
+868
در 33 کانالها
Get PRO
نوامبر '25
+67
در 7 کانالها
| تاریخ | رشد مشترکین | اشارات | کانالها | |
| 03 سپتامبر | 0 | |||
| 02 سپتامبر | 0 | |||
| 01 سپتامبر | 0 |
پستهای کانال
+9
OOM в Android: почему чистый heap dump больше ничего не гарантирует
Дамп кучи чист, свободные мегабайты на месте, а процесс убит системой. Сегодня это нормальная ситуация: Android 17 ограничивает память всего процесса. Google объявил, что с февраля 2027 года станут обязательными пороги качества по anonymous RSS + swap и bitmap usage.
В карточках разбираем, почему пустой дамп кучи больше не спасает от вылетов.
#mobilevkhub #android
| 2 | Compose 1.12: mesh-градиенты, широкий цвет и обязательный AGP 9.1.1
Августовский Compose привёз набор визуальных API, но начать придётся не с них. Версия 1.12 собирается только под compileSdk 37 и требует AGP не ниже 9.1.1 — то есть трогает не библиотеку, а всю сборку проекта.
Команды со свежим тулчейном отделаются формальностью. Остальным придётся заводить отдельную задачу: AGP 9 выкинул поддержку proguard-android.txt, поменял упаковку нативных библиотек и тянет более новый Gradle. Узнать об этом лучше до того, как кто-то поднимет BOM в ветке.
implementation(platform("androidx.compose:compose-bom:2026.08.00"))
Ради чего всё это — видно с первого нововведения. Обычный градиент растягивает цвет вдоль линии или от центра, и дальше двух-трёх оттенков выглядит грязно. Mesh-градиент устроен иначе: задаёте сетку вершин со своим цветом у каждой, и заливка растекается между ними во все стороны. Так рисуют мягкие переливы, ради которых раньше подкладывали картинку или тащили библиотеку с шейдерами.
val painter = remember {
MeshGradientPainter(rows = 1, columns = 1) {
setVertex(0, 0, Offset(0f, 0f), Color.Red)
setVertex(0, 1, Offset(1f, 0f), Color.Blue)
setVertex(1, 0, Offset(0f, 1f), Color.Green)
setVertex(1, 1, Offset(1f, 1f), Color.Yellow)
}
}
Box(Modifier.fillMaxWidth().aspectRatio(16 / 9f).paint(painter))
Читается это проще, чем выглядит. Координаты нормализованные: (0f, 0f) — левый верхний угол, (1f, 1f) — правый нижний, пиксели знать не нужно. Сетка 1 × 1 даёт один участок и четыре вершины по углам: их всегда на одну больше, чем участков. Кривизну границ задают контрольные точки Безье, но их можно не трогать — Compose подберёт сам.
Второе изменение из той же области — широкий цветовой охват. Экраны флагманов давно показывают цвета за пределами sRGB, но Compose работал в старом пространстве и обрезал лишнее: насыщенный брендовый оттенок доезжал до пользователя блёклым. Теперь цвета и шейдеры уходят на отрисовку как есть, а если платформа пространство не поддерживает — например, OkLab на старом API, — Compose откатится к sRGB.
Есть в релизе и вещи попроще. Вариативные шрифты научились загружаться на лету, Grid получил именованные области — раскладку описываешь словами, а не индексами строк и колонок, как в CSS Grid. А связка с Credential Manager, через который проходят passkeys, обзавелась готовыми точками входа вместо сборки через Activity.
А ещё Modifier.onFirstVisible() пометили устаревшим. Вместо него теперь Modifier.onVisibilityChanged() — он отслеживает порог видимости точнее.
Если проект уже на AGP 9, обновляйтесь смело: mesh-градиенты и широкий цвет закрывают то, ради чего дизайнеры просят «как в макете, но у нас так не умеет». Если сидите на AGP 8, спешить некуда — Compose 1.11 продолжает получать патчи, а тулчейн лучше поднимать отдельно от продуктовых задач.
#mobilevkhub #compose | 220 |
| 3 | Swift export в Kotlin 2.4: общий код без прослойки из Objective-C
В KMP давно живёт неприятная асимметрия. Android-разработчик подключает общий модуль и пишет обычный Kotlin. iOS-разработчик получает фреймворк, прошедший через Objective-C, и видит в Xcode совсем не то, что написано в общем коде.
Виноват путь, по которому Kotlin попадал в Swift. Компилятор собирал заголовки Objective-C, Swift читал их и достраивал свою модель — и на каждом шаге что-то терялось.
Int? превращался в обёртку KotlinInt, потому что в Objective-C примитив не бывает nil. Перегруженные функции получали суффиксы к именам, чтобы не столкнуться друг с другом.
Пакеты схлопывались в плоский список с подчёркиваниями. Suspend-функции приезжали колбэками, а Flow в Swift вообще не существовал, под него писали обёртки руками или тащили отдельные библиотеки интеропа.
В Kotlin 2.4 Swift export дошёл до альфы и убирает Objective-C из этой цепочки. Компилятор генерирует Swift-код напрямую.
kotlin {
swiftExport {
moduleName = "Shared"
flattenPackage = "com.example.shared"
}
}
Int? остаётся Int?. Перегрузки вызываются по-человечески. Пакеты Kotlin превращаются во вложенные Swift-энумы, а flattenPackage срезает общий префикс, если он только мешает читать.
Больше всего повседневную работу меняют корутины. Suspend-функция вызывается из Swift как обычная асинхронная, а Flow<String> приезжает как AsyncSequence с элементом String — по нему сразу работает for await. Настраивать ничего не нужно, так работает по умолчанию.
Заодно в 2.4 появилась обратная дорога: Swift-пакеты объявляются зависимостями KMP-модуля прямо в Gradle. Раньше связку с SwiftPM собирали вручную на стороне Xcode, теперь она описывается там же, где остальные зависимости — и уходить с CocoaPods стало проще.
При этом у альфы есть ограничение. Swift export работает только при прямой интеграции фреймворка. Если проект интегрирует KMP через CocoaPods или подключает KMP‑фреймворк как binaryTarget в SwiftPM, включить Swift export не получится. На этой схеме сидит большинство существующих проектов, так что пробовать удобнее на новом модуле, чем на боевом.
API ещё будет ломаться между версиями, а часть возможностей доезжает по ходу: sealed-иерархии и наследование Kotlin-интерфейсов на стороне Swift появились уже в 2.4.20.
#mobilevkhub #kotlin | 219 |
| 4 | Navigation 3: back stack, которым наконец владеете вы
Старая навигация в Compose всегда была компромиссом. Библиотеку спроектировали семь лет назад под фрагменты, и Compose она увидела уже взрослой — отсюда NavController, который прячет стек экранов внутри себя, и строковые маршруты вместо типов. Чтобы узнать, что лежит в истории, приходилось спрашивать разрешения у библиотеки.
В ноябре 2025 вышла Navigation 3 и перевернула эту модель. Стек экранов стал обычным списком, который живёт в вашем состоянии. Добавляете экран — добавляете элемент, идёте назад — убираете последний. Библиотека на список только смотрит и рисует то, что в нём лежит.
@Serializable data object Home : NavKey
@Serializable data class Detail(val id: String) : NavKey
val backStack = rememberNavBackStack<NavKey>(Home)
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
entry<Home> { HomeScreen(onOpen = { backStack.add(Detail(it)) }) }
entry<Detail> { key -> DetailScreen(key.id) }
}
)
Экраны здесь — сериализуемые объекты с интерфейсом NavKey, а не строки с параметрами. Компилятор проверит, что Detail получил объявленные поля, а rememberNavBackStack восстановит стек после смерти процесса.
Теперь про декораторы, потому что с ними легко ошибиться. rememberViewModelStoreNavEntryDecorator привязывает жизненный цикл ViewModel к записи в стеке: убрали экран — вью-модель очистилась. Забудете его добавить, и вью-модели доживут до конца Activity, утащив с собой состояние экранов, с которых пользователь ушёл десять минут назад. В Nav2 это делалось само.
Широкие экраны обслуживает механизм Scenes: он решает, сколько элементов стека показать одновременно. Готовую стратегию под список и детали подключает rememberListDetailSceneStrategy, а экраны помечаются метаданными вроде ListDetailSceneStrategy.listPane(). Телефон покажет одну панель, планшет развернёт две — из того же стека, без второй ветки навигации.
Есть пара мест, где придётся повозиться. Глубокие ссылки собираются руками, аналога deepLink из Nav2 пока нет — рецепты Google выложил в отдельный репозиторий. И нижняя навигация с сохранением состояния вкладок требует своего стека на каждую вкладку.
Совместимости с Nav2 нет, так что модуль придётся переписать целиком. К релизу Google выпустил гайд по миграции и предложил скормить его агенту в Android Studio. Nav2 при этом продолжает получать патчи, гнать никто не заставляет.
А вы уже пробовали Nav3?
#mobilevkhub #kotlin | 295 |
| 5 | Swift Testing: как Apple переписала тесты на макросах
XCTest появился в 2013 году, ещё до Swift, и родословная видна в любом тестовом файле: класс-наследник XCTestCase, метод с обязательным префиксом test, вызов одной из четырёх десятков функций XCTAssert.
Swift Testing приехал с Xcode 16 и Swift 6 и пересобрал модель целиком. Тесты стали функциями, наборы — типами, настройка — трейтами, сорок ассертов схлопнулись в два макроса, а параллельный запуск включён по умолчанию.
➡️ В карточках: синтаксис, диагностика падений, параметризованные тесты, что ломается при параллельном прогоне и до какой границы вообще доходит миграция.
#mobilevkhub #swift | 268 |
| 6 | Swift 6.4 — релиз, который убирает то, к чему все успели привыкнуть
Swift в этом году обновился дважды: 6.3 в марте с официальным Android SDK, и 6.4 на июньском WWDC внутри Xcode 27. Второй релиз скромнее по содержанию — ни крупных нововведений, ни ломающих изменений. Он состоит из мелких правок там, где годами приходилось писать обходные пути, причём настолько давно, что многие перестали считать это проблемой.
Самая заметная из них живёт в проверках доступности. Если приложение крутится на пяти платформах Apple, каждая аннотация превращается в простыню:
@available(macOS 27, iOS 27, watchOS 27, tvOS 27, visionOS 27, *)
func showStatus() { }
В 6.4 это сворачивается в @available(anyAppleOS 27, *) — одно имя вместо пяти, работает и в #if. Оговорка существенная: сокращение годится для случая, когда список платформ был шумом. Если API реально приезжает в разных версиях на разных системах, платформы по-прежнему выписываются поимённо — эта разница несёт смысл.
Следующее изменение ждали дольше. SE-0493 разрешил вызывать асинхронный код внутри defer:
func fetchData() async throws -> Data {
let connection = try await open()
defer { await connection.close() }
return try await connection.read()
}
Раньше закрытие асинхронного ресурса приходилось либо дублировать на каждом выходе из функции, либо отправлять в отдельную задачу — теряя гарантию, что очистка вообще успеет отработать. Теперь defer работает одинаково для синхронных и асинхронных ресурсов. Держать в голове стоит две вещи: await добавляет точки приостановки в самый конец функции, а отмена задачи внутри блока работает как обычно, так что очистку можно случайно оборвать на полпути.
Третье изменение адресное. Noncopyable-типы вроде Span и InlineArray упирались в обычный цикл: Sequence копирует элементы по ходу обхода, а копировать тут нечего — код не собирался. Обходили индексной арифметикой, ручным while или объявляли тип копируемым, возвращая ровно то дублирование, от которого уходили. Новый протокол Iterable заимствует элементы вместо копирования, и for-in начинает работать. Побочный выигрыш достаётся остальным: на объектах и copy-on-write типах обход больше не дёргает подсчёт ссылок.
Из мелочей, заметных в повседневной работе: withTaskCancellationShield защищает участок, который обязан доработать даже после отмены — сброс файла на диск, например. Тип теперь может явно отказаться от Sendable через ~Sendable. Класс, которому @unchecked Sendable требовался только из-за weak var, переводится на weak let и проходит нормальную проверку. any P? пишется без скобок. А компилятор начал предупреждать, когда задача молча теряет брошенную ошибку.
Отдельно стоит выигрыш, который не требует правок в коде. Перенос Foundation с Objective-C на Swift дошёл до URL: NSURL и CFURL объединены в одну реализацию, разбор адресов ускорился до четырёх раз. Обновления тулчейна достаточно.
#mobilevkhub #swift | 357 |
| 7 | Delegated properties в Kotlin — что стоит за by
by lazy, by viewModels(), by remember, by Delegates.observable встречаются в любом Kotlin-проекте. За синтаксическим сахаром — один механизм: delegated properties. Понимание работы позволяет читать библиотечный код и писать свои делегаты для повторяющихся паттернов в кодовой базе.
Идея
Property делегируется другому объекту через by. Компилятор превращает чтение и запись в вызовы getValue и setValue у делегата:
class User {
var name: String by NameDelegate()
}
class NameDelegate {
private var value: String = "default"
operator fun getValue(thisRef: Any?, property: KProperty<*>): String = value
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
value = newValue
}
}
Для val нужен только getValue, для var — оба. Сигнатуры фиксированы: thisRef — владелец свойства, property — метаинформация.
Стандартная библиотека даёт готовые делегаты. lazy вычисляет значение при первом чтении и кеширует:
val heavyResource: Resource by lazy {
computeExpensiveResource()
}
По умолчанию thread-safe (SYNCHRONIZED). Для однопоточного контекста — lazy(LazyThreadSafetyMode.NONE), быстрее.
Delegates.observable — var с колбэком на смену значения:
var status: Status by Delegates.observable(Status.Idle) { _, old, new ->
log("status: $old -> $new")
}
Ещё из stdlib — Delegates.vetoable (отклоняет присваивание при false-предикате) и Delegates.notNull (аналог lateinit для примитивов).
Отдельный случай — property из Map. val name: String by map компилируется в map["name"] с приведением типа. Удобно для JSON-подобных структур.
Кастомные делегаты — для повторяющихся паттернов. Частый случай — SharedPreferences с типизацией:
class StringPref(
private val prefs: SharedPreferences,
private val key: String,
private val default: String = ""
) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String =
prefs.getString(key, default) ?: default
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
prefs.edit().putString(key, value).apply()
}
}
class Settings(prefs: SharedPreferences) {
var userName: String by StringPref(prefs, "user_name")
var theme: String by StringPref(prefs, "theme", "light")
}
Присваивание пишет в prefs, чтение читает — без строки prefs.getString в каждом месте. Аналогично для DataStore, in-memory кеша с TTL, property из flag-системы.
Ещё случай — property, доступная только по роли или в состоянии. Делегат проверяет условие в getValue, кидает исключение или возвращает дефолт. Переиспользуется без копипаста.
Delegated properties — из тех фич, что сначала выглядят экзотикой, а через месяц обнаруживаются в своём же коде. Писать делегат стоит, когда одна логика getter/setter появляется в третьем месте. | 269 |
| 8 | Compose Layout API
Row, Column и Box закрывают почти всё, пока не появляется макет, который в них не ложится: чипы со своими правилами переноса, футер, которому нужна высота хедера, колонки одной ширины по самому длинному тексту.
Под тремя контейнерами лежит обычный публичный API: measure-политика, Constraints в целых пикселях и вторая фаза измерения для анимаций. В карточках рассмотрим шесть его кусков в порядке от самого дешёвого к самому тяжёлому.
#mobilevkhub #compose #api | 419 |
| 9 | Июль в мобилке был про то, как обе платформы готовятся к финальным осенним релизам. Compose дошёл до rc 1.12, а самому фреймворку исполнилось 5 лет — Google в юбилейном посте пошёл дальше поздравлений и формально закрепил Compose-first, отправив View toolkit в maintenance mode. На iOS параллельно обкатывается версия 27 через беты со свежим Siri.
🟣Compose 1.12.0-rc01
29 июля вышел Compose 1.12.0-rc01. Release Candidate значит, что от финалки его отделяет полировка. Из содержательного — v2-тестирование как дефолт: StandardTestDispatcher вместо UnconfinedTestDispatcher. В тестах теперь явно нужен advance clock через virtual time, и рассчитывать, что корутины выполнятся сразу, не стоит. Старые тесты придётся адаптировать, зато тайминги становятся предсказуемыми.
Из более узкого — HostDefaultProvider и compositionLocalWithHostDefaultOf дают DI без зависимости от compose-ui, что критично для KMP-модулей, где раньше приходилось изобретать обёртки. Плюс кастомные Preview через @PreviewWrapper — можно инжектить тему, локаль или стейт без копипаста в каждый @Preview.
🟣Compose-first
В июле Compose исполнилось 5 лет. Google не ограничился поздравительным постом и закрепил Compose-first: все новые UI-разработки теперь только на Compose, View toolkit — в maintenance. По их же статистике, 68% из топ-1 000 приложений в Google Play уже на Compose. Если проект ещё на XML, новые фичи Material Design будут только через Compose — стратегическое окно миграции сужается.
🟣iOS 27 и новый Siri
На iOS тем временем Apple обкатывает версию 27 через беты — beta 4 вышла 20 июля, public beta 2 — 22 июля. Главное там — новый Siri, тот самый, которого Apple год назад анонсировала и потом откладывала: мультистеп-команды, синхронизируемая через iCloud история разговоров, прикрепление файлов и картинок к запросам.
Для разработчиков ключевое — App Intents миграция: чтобы Siri работала с приложением, у него должны быть определены явные intents на критичные действия. Кто ещё не начал — время пришло, потому что осенью GA, и пользователи будут ждать Siri-интеграции.
🟣Kotlin 2.2, Swift 6.3, Xcode 27
Параллельно тихо рассасывается Kotlin 2.2. Для Compose там главное — улучшенный stability inference: компилятор чаще правильно определяет стабильность типов без ручных @Stable-аннотаций. Меньше лишних рекомпозиций из коробки, стоит зайти в Layout Inspector и удивиться, где раньше боролись руками.
Из околосвифтового — 6.3 привёз first-class Android SDK, шарить логику между iOS и Android на Swift теперь можно официально.
Прототипирование ощутимо ускорилось — Xcode 27 в бете позволяет быстро создавать untitled-проекты и запускать отдельные Swift-файлы с превью в одной вкладке.
#mobilevkhub #дайджест | 321 |
| 10 | OkHttp interceptors — где какой ставить и когда нужен EventListener
Interceptor в OkHttp — способ вмешаться в запрос между вызовом и ответом. Через них добавляют заголовки, логируют, ретраят, кешируют. Interceptor можно поставить в двух местах — Application и Network, и они ведут себя по-разному. Плюс есть EventListener для метрик, который путают с interceptor. Разберём, где что.
OkHttpClient имеет два списка interceptor: addInterceptor() и addNetworkInterceptor(). По названию разница неочевидна, последствия — большие.
Application interceptor видит запрос до внутренней обработки OkHttp: кеша, retry, follow-redirect. Вызывается один раз на логический запрос, даже если внутри OkHttp сделает три HTTP-вызова (redirect 301 → 200). Промежуточных ответов не видит.
class AuthInterceptor(private val tokens: TokenStore) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer ${tokens.access}")
.build()
return chain.proceed(request)
}
}
Кейсы application interceptor: авторизация, User-Agent, device-id, ретраи на 401 с обновлением токена, локальный кеш поверх ответов.
Network interceptor видит каждый физический HTTP-вызов, включая redirect и retry. Видит реальные заголовки соединения (Content-Length, Content-Encoding, Connection). Может изменить ответ после сети, но до внутренней обработки.
Кейсы network interceptor: логирование каждой попытки (включая redirect-цепочки), измерение размера уже сжатых ответов, работа с транспортными заголовками. OkHttp Logging Interceptor идёт сюда, если нужно видеть все физические вызовы.
Правило. Модификация на уровне бизнес-логики — application. Работа на уровне сети, метрики соединения, инспекция redirect-цепочек — network. Сомневаетесь — берите application.
EventListener — отдельный API для метрик. В отличие от interceptor, не может изменить запрос или ответ, но точно измеряет время каждой фазы: DNS lookup, TCP connect, TLS handshake, отправка запроса, получение заголовков, чтение тела.
Список коллбэков: dnsStart/dnsEnd, connectStart/connectEnd, secureConnectStart/secureConnectEnd, responseHeadersStart/End, responseBodyStart/End, всего около десятка.
Регистрируется через .eventListenerFactory { MetricsEventListener() } — свой экземпляр на каждый Call для хранения состояния между коллбэками. Правильный инструмент для собственной телеметрии: сколько ушло на DNS, где висим на TLS, какой процент времени идёт на чтение тела. Через interceptor этих цифр не получить.
Ко всему этому Interceptor вызывается на потоке OkHttp dispatcher, не на main. Тяжёлые операции (доступ к базе, парсинг, синхронный вызов API) блокируют HTTP-пул и снижают throughput сети. Всё, что тяжелее правки заголовков, лучше вынести из interceptor.
А как у вас разделена логика между application и network interceptor?
#mobilevkhub #okhttp #kotlin | 368 |
| 11 | Kotlin inline и reified — зачем это вообще нужно
Про inline-функции в Kotlin многие знают только то, что «они быстрее» и стоят в стандартной библиотеке рядом с map и filter. На самом деле inline — это не про производительность в чистом виде, а про то, что можно сделать невозможное в обычных функциях: reified generics, non-local return из лямбд и полноценные DSL без runtime-оверхеда. Разберём по порядку.
Базовое поведение: inline вставляет тело функции прямо в место вызова:
inline fun measureTime(block: () -> Unit): Long {
val start = System.currentTimeMillis()
block()
return System.currentTimeMillis() - start
}
measureTime { doWork() }
// компилятор превращает в:
// val start = System.currentTimeMillis()
// doWork()
// val time = System.currentTimeMillis() - start
Никакого объекта-лямбды, никакого вызова invoke — код разворачивается на месте. Для маленьких хелперов с лямбдами это спасает от аллокаций в горячих циклах.
Главная фича — reified type parameters. В обычной функции T стирается в рантайме, и T::class или is T не работают. Внутри inline-функции с reified тип сохраняется, потому что тело подставляется по месту:
inline fun <reified T> Any?.safeCast(): T? {
return if (this is T) this else null
}
val x: Any = "hello"
val s: String? = x.safeCast() // работает
На этом стоит весь Gson-style API: gson.fromJson<UserDto>(json) вместо gson.fromJson(json, UserDto::class.java). Или в Compose: hiltViewModel<HomeViewModel>() вместо явной передачи класса.
Второй кейс — non-local return из лямбды. Обычная лямбда не может сделать return из внешней функции, только return@label. Внутри inline-лямбды return работает как в обычном коде:
fun findUser(id: Long): User? {
users.forEach { user ->
if (user.id == id) return user // выход из findUser
}
return null
}
Работает потому, что forEach в stdlib — inline. Без этого пришлось бы возвращать из forEach через return@forEach и потом отдельно проверять флаг.
crossinline и noinline — модификаторы для параметров-лямбд. noinline говорит компилятору «эту лямбду не разворачивай, оставь как объект» — нужно, если её передают дальше в обычную функцию. crossinline запрещает non-local return, что критично, когда лямбда вызывается не сразу, а в колбэке или корутине.
Подводные камни тоже есть. Inline-функции разворачиваются в каждом месте вызова, поэтому для больших функций (десятки строк) это раздувает байткод — компилятор ругается предупреждением. Инлайнить имеет смысл функции с параметрами-лямбдами, чтобы убрать аллокации, или там, где reified/non-local return реально нужны. Для обычной логики без лямбд inline ничего не даёт, а иногда даже замедляет за счёт большего DEX.
Если видите fun <T> ... с передачей T::class.java, а рядом есть inline reified вариант — второй почти всегда чище.
А вы где используете inline и reified в своих API?
#mobilevkhub #kotlin | 306 |
| 12 | Анимации в Jetpack Compose — шесть инструментов на разные случаи
В Compose анимаций много, и легко запутаться, что где применять. В карточках разберёмся с шестью основными, которые закрывают почти всё:
🟣 animate*AsState для одного значения
🟣 updateTransition для связанных
🟣 AnimatedVisibility для появления и исчезновения
🟣 AnimatedContent для смены контента
🟣 Crossfade для плавной замены
🟣 InfiniteTransition для повторяющихся эффектов
#mobilevkhub #compose #анимация | 425 |
| 13 | Как ускорить старт приложения на 25–40% без переписывания кода
Baseline Profiles — это метаданные, которые сообщают ART, какие классы и методы нужно AOT-скомпилировать заранее, ещё до первого запуска. Обычно ART компилирует «горячий» код JIT-ом уже во время работы приложения, и на холодном старте это добавляет секунды. Baseline Profiles сдвигают компиляцию с устройства пользователя на этап сборки.
Как работает
При сборке в APK попадает baseline-prof.txt со списком классов и методов для критичных user journey. При установке ProfileInstaller передаёт список в ART, и тот компилирует AOT сразу, не откладывая до JIT в рантайме. Результат — плавнее старт и скролл, меньше jank на первом запуске новой версии.
Настройка
Нужен модуль под MacroBenchmark, который прогонит критичный user journey и соберёт профиль:
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule val rule = BaselineProfileRule()
@Test
fun generate() = rule.collect(
packageName = "com.example.app"
) {
pressHome()
startActivityAndWait()
// прогоняем главный экран, скролл, открытие типового кейса
device.wait(Until.hasObject(By.text("Feed")), 5_000)
}
}
После прогона в src/release/generated/baselineProfiles/baseline-prof.txt появится список правил. Baseline Profile Gradle Plugin автоматически подкладывает его в APK при релизной сборке.
Startup Profiles — надстройка над Baseline Profiles. Тот же файл, но помечает подмножество классов, критичных именно для запуска. R8 использует его для DEX layout optimization — размещает эти классы в первом classes.dex, чтобы ART не пришлось загружать несколько DEX-файлов при старте.
Ловушки
Профиль собирают на варианте с isMinifyEnabled = false, применяют к билду с включённым R8 — плагин делает это автоматически. На эмуляторе прироста не будет, замер только на физическом устройстве, лучше entry-level. Переснимать профиль нужно при заметных изменениях архитектуры: старый на новых экранах ничего не даст.
Cloud Profiles — параллельный механизм Google Play. Google собирает агрегированные профили по реальным пользователям и раздаёт с обновлением, но новая версия долетает с задержкой в день-два. Baseline Profiles работают с момента установки, поэтому одно другое не заменяет — оба вместе дают лучший результат.
Если в проекте до сих пор нет Baseline Profiles и Startup Profiles — это самое простое, что можно сделать для перформанса без единой строки продуктового кода. Плюс включение R8 c isMinifyEnabled = true — все три штуки вместе и дают 25–40% прироста к производительности.
#mobilevkhub #kotlin #baseline | 353 |
| 14 | Как VK пере езжали с XML на Jetpack Compose: базовые классы и паттерны
В большом Android-проекте VK с сотнями экранов долго жила классика — XML-вёрстка, Dagger 2, Cicerone для навигации, MVVM. С ростом кодовой базы связка начала подводить: разрозненные MutableStateFlow для каждого поля состояния давали моргания интерфейса (isLoading уже false, а items ещё пустой), фрагменты обрастали обвязкой на 200 строк, ревью экранов превращалось в путешествие по вложенным <include> и <merge>.
Полтора года команда переезжала на Compose с параллельной сменой MVVM на MVI, не останавливая продуктовую разработку и сохраняя crash-free на 99,95%+.
В карточках рассказываем, как:
🔵отказались от разрозненных Flow ради единого State в MVI
🔵собрали базовый MviViewModel с атомарным updateUiState
🔵превратили жирный BaseFragment в тонкий MviFragment на 20 строк
🔵обошлись без передачи ViewModel в Composable через паттерн ScreenCallbacks
🔵сделали сложные списки в LazyColumn через интерфейс ComposeLazyItem
➡️ Полная версия статьи на Хабре — про Gradle-конфигурацию с convention plugin, ComposableLifecycleObserver, паттерн provide для делегирования отрисовки элементов списка и планы на второй и третий этапы миграции: переход с Dagger 2 на Koin и с Cicerone на Compose Navigation. | 462 |
| 15 | Compose Stability — почему компоненты рекомпозятся и как это починить
В Compose есть механика skippable composables. Если параметры функции стабильны и значения не изменились, Compose пропускает её при рекомпозиции и переиспользует прошлый результат. На этом держится производительность фреймворка. Беда в том, что большинство параметров по умолчанию нестабильны, и LazyColumn неожиданно рекомпозится целиком при изменении одного элемента.
Классический пример — обычный List:
@Composable
fun ProductList(items: List<Product>) {
LazyColumn { items(items) { ProductCard(it) } }
}
List<T> — интерфейс, который может скрывать MutableList под капотом, поэтому компилятор Compose помечает его как unstable. Любая рекомпозиция родителя — и весь список перерендеривается, даже если данные те же.
Три способа починить
Первый — @Immutable на data-классе содержимого:
@Immutable
data class Product(val id: Long, val name: String, val price: Int)
Compose доверяет и помечает класс как stable.
Второй — ImmutableList из kotlinx-collections-immutable:
fun ProductList(items: ImmutableList<Product>) { ... }
ImmutableList гарантированно неизменяемый — у компилятора нет сомнений.
Третий — Stability Configuration File. С Compose Compiler 1.5.5 можно помечать stable-классы из чужих модулей (LocalDateTime, BigDecimal, legacy). Пишем в compose_compiler_config.conf пакеты или классы, подключаем через stabilityConfigurationFile.
Главное изменение Compose 1.7 (август 2024) — Strong Skipping Mode стал default. Теперь все restartable composables становятся skippable, даже с unstable параметрами. Unstable параметры сравниваются по === (instance equality), stable — по equals().
Это спасает от большинства проблем «параметр нестабилен → composable рекомпозится». Но не от всех. Если на каждой рекомпозиции создаётся новый объект — instance equality не помогает:
// плохо -- новый List на каждой рекомпозиции
val items = state.value.map { it.toUi() }
ProductList(items)
// хорошо -- мемоизация
val items = remember(state.value) {
state.value.map { it.toUi() }.toImmutableList()
}
Бонус Strong Skipping — лямбды с unstable captures теперь запоминаются автоматически. Раньше любая лямбда, захватывающая var из родителя, делала composable нестабильным.
Как найти проблемы в проде
Включаем compose-metrics через freeCompilerArgs в Gradle — после сборки в build/compose_compiler/ лежат отчёты *-classes.txt и *-composables.txt. Там видно, какие классы unstable и какие composable не skippable. Точечно лечим самое горячее — обычно это пара классов в shared/domain слое, после фикса которых рекомпозиции падают в разы.
Производительность Compose — это в первую очередь про стабильность параметров. Если LazyColumn тормозит без очевидной причины, первый шаг — отчёт.
#mobilevkhub #compose | 308 |
| 16 | Modifier в Jetpack Compose — где порядок имеет значение
Modifier в Compose — то место, где порядок применения реально меняет результат. .padding().background() и .background().padding() дают разные картинки на экране, и большинство багов на старте — это перепутанный порядок модификаторов.
В карточках — шесть конкретных мест: порядок padding и background, размер клик-зоны, как работает size, объединение трансформаций через graphicsLayer, иммутабельность Modifier и тонкости composed для собственных модификаторов.
#mobilevkhub #compose | 379 |
| 17 | 🤖 Android
🟣 Android 17: Compose-first официально
С выходом Android 17 Google окончательно закрепила курс на Compose: новые API, Jetpack-библиотеки и инструменты будут развиваться прежде всего вокруг декларативного UI.
🟣Android XR SDK Preview 4
XR-эмулятор теперь встроен в Android Studio, а поддержка Compose, Unreal и Godot делает Android XR заметно ближе к реальным продуктовым сценариям.
🟣R8 Configuration Analyzer
В AGP 9.3 появился инструмент, который показывает, какие keep rules мешают оптимизации и обфускации. Для крупных приложений это может дать быстрый прирост в размере и производительности сборок.
🟣ADK для Android
Google представила open-source фреймворк для ИИ-агентов с поддержкой on-device моделей через ML Kit GenAI и AICore. Похоже на начало полноценной ИИ-инфраструктуры внутри Android-стека.
▶️ iOS
🟣WWDC26: Foundation Models и Agentic Apps
Главный тренд года — встроенный ИИ. Apple активно развивает Foundation Models, агентные сценарии и инструменты для интеграции ИИ прямо в приложения.
🟣Системные промпты и SwiftUI-скиллы для Xcode 27
В первой бете появились новые ИИ-возможности и набор встроенных промптов и скиллов. Xcode всё глубже интегрирует ИИ в ежедневную разработку.
🟣UIKit адаптируется под новые формфакторы
Apple продолжает готовить платформу к многооконности, большим экранам и внешним дисплеям. Новые API управления навигацией, сценами и таббаром показывают, куда движется экосистема.
🟣Goodnotes о переходе в браузер через SwiftWasm
Один из крупнейших Swift-продуктов показал реальный кейс переиспользования кодовой базы в вебе без переписывания на JavaScript. Важный сигнал для будущего Swift за пределами экосистемы Apple.
❤️ Новые статьи от инженеров VK на Хабре
Применение Kotlin DSL в TeamCity для автоматизации пайплайнов: кейс команды ВКонтакте
#дайджест #mobilevkhub | 339 |
| 18 | Что и когда выбирать в Kotlin
После StateFlow обычно встаёт следующий вопрос: а как делать события, которые должны прийти каждому подписчику? Или одну задачу на нескольких воркеров? Здесь и начинается выбор между SharedFlow и Channel — двумя инструментами, которые часто путают. Разберём, чем они отличаются и где применять каждый.
SharedFlow — горячий поток событий, который получают все активные подписчики:
private val _events = MutableSharedFlow<Event>(
replay = 0,
extraBufferCapacity = 16,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<Event> = _events.asSharedFlow()
// в одном месте
_events.emit(Event.NavigateTo("settings"))
// в любом collect
events.collect { event -> handle(event) }
replay — сколько последних событий повторно отдать новому подписчику, по умолчанию 0 (старые не доходят). extraBufferCapacity — буфер на случай медленных подписчиков. onBufferOverflow — стратегия при переполнении: SUSPEND, DROP_OLDEST, DROP_LATEST.
Главное свойство — все активные подписчики получают все события. Идеально для broadcast: navigation, snackbar messages, toast-уведомления, аналитические события.
Channel — очередь с send/receive, где каждое сообщение получает ровно один потребитель:
val tasks = Channel<Task>(
capacity = Channel.BUFFERED,
onBufferOverflow = BufferOverflow.SUSPEND
)
// producer
launch {
while (true) tasks.send(fetchTask())
}
// несколько worker-ов, каждое сообщение получает один из них
repeat(4) {
launch {
for (task in tasks) process(task)
}
}
Если четыре воркера читают один канал, каждый task достанется только одному из них. Это work distribution — то, что в SharedFlow сделать нельзя в принципе.
Channel умеет закрываться (close()) и сигнализировать завершение через for (x in channel). SharedFlow — нет, он живёт, пока живёт scope.
Когда что выбирать
SharedFlow — для broadcast (один producer → много consumer): UI events, navigation, snackbar, обновления, которые должен увидеть каждый подписчик.
Channel — для work distribution (один producer → один из многих consumer): producer-consumer между корутинами, когда сообщение нельзя потерять, очереди задач для пула воркеров.
Частая ошибка — использовать SharedFlow там, где нужно гарантировать обработку одним потребителем. С replay = 0 и медленным подписчиком события просто теряются: emit прошёл, никто не успел, всё. Для гарантированной доставки — Channel с capacity = UNLIMITED или SUSPEND.
Обратная ошибка — Channel там, где нужен broadcast. Каждое сообщение уйдёт только одному коллектору, остальные не увидят. Получаем сломанную навигацию из второго экрана.
#mobilevkhub #kotlin | 385 |
| 19 | Side effects в Jetpack Compose — шесть инструментов для шести задач
В Compose нет привычных lifecycle-методов, как у Activity или Fragment. Composable вызывается часто и в непредсказуемом порядке. Поэтому любой код, который должен выполниться один раз, при смене параметра или с подпиской и отпиской, оборачиваем в специальный эффект.
Разбираем шесть инструментов под шесть задач в карточках.
#mobilevkhub #compose | 368 |
| 20 | Flow в Kotlin закрывает то, ради чего раньше тащили RxJava — асинхронные потоки значений. С 2020 это стандарт в Android и KMP, базу учим за вечер. А вот в StateFlow, collectLatest и WhileSubscribed закопано несколько грабель, на которые наступают даже опытные.
Поток создаётся через flow {}:
fun timer(): Flow<Int> = flow {
var count = 0
while (true) {
emit(count++)
delay(1000)
}
}
Корутина внутри стартует, только когда появился первый подписчик, — это и есть cold flow. Каждый новый collect получает поток с нуля, подписался дважды — запросы пошли дважды. Частый баг: один flow собирают и во ViewModel, и в Compose, а потом удивляются, почему запросов к API в два раза больше ожидаемого.
collect блокирует корутину до конца потока: следующее emit ждёт, пока обработается текущее. Бесплатный backpressure из коробки.
Когда нужна обратная стратегия — взять только самое свежее и забить на всё начатое — есть collectLatest:
searchQuery.collectLatest { query ->
val results = repository.search(query)
showResults(results)
}
collectLatest отменяет блок только в suspend-точках. Чистый CPU-bound код без delay, без сети, без yield добежит до конца, и только потом запустится новый. Для тяжёлых вычислений ставим yield() или уносим на Dispatchers.Default.
StateFlow — горячий поток, который всегда держит одно актуальное значение. Новый подписчик мгновенно получает последнее, не дожидаясь следующего emit:
class FeedViewModel : ViewModel() {
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow()
fun load() = viewModelScope.launch {
_state.value = UiState.Content(repository.fetch())
}
}
Это корутины без привязки к Android, с поддержкой KMP. В Compose это основной канал из ViewModel в UI.
Когда есть холодный источник (Flow от Room или Retrofit) и его нужно расшарить между подписчиками — есть stateIn:
val users: StateFlow<List<User>> = repository
.observeUsers()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList()
)
Тут две вещи, которые часто упускают.
Первая: 5_000 — это stopTimeoutMillis, подписка на источник держится ещё 5 секунд после ухода последнего подписчика, ровно чтобы пережить поворот экрана без перезапуска запросов. Второй параметр WhileSubscribed — replayExpirationMillis, через сколько забыть последнее значение, по дефолту Long.MAX_VALUE.
Вторая: scope обязательно viewModelScope, не lifecycleScope. Lifecycle scope умирает при ротации — и stateIn вместе с ним. После пересоздания подписка стартует заново, UI на секунду моргает initialValue. ViewModel переживает ротацию и держит StateFlow живым.
На практике flow {}, StateFlow, collectLatest и stateIn закрывают 90% задач. Дальше debounce, throttle, мультиплексирование — уже под конкретный кейс.
#mobilevkhub #kotlin | 415 |
