Mobile VK Hub
رفتن به کانال در Telegram
Комьюнити от VK для мобильных разработчиков. Здесь всё о том, как создаются приложения для миллионов: от нативных подходов до кросс-платформы.
نمایش بیشتر737
مشترکین
اطلاعاتی وجود ندارد24 ساعت
+17 روز
-430 روز
در حال بارگیری داده...
کانالهای مشابه
هیچ دادهای
مشکلی وجود دارد؟ لطفاً صفحه را تازه کنید یا با مدیر پشتیبانی ما تماس بگیرید.
ابر برچسبها
اشارات ورودی و خروجی
---
---
---
---
---
---
جذب مشترکین
ژوئن '26
ژوئن '26
+2
در 1 کانالها
مه '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 کانالها
| تاریخ | رشد مشترکین | اشارات | کانالها | |
| 19 ژوئن | 0 | |||
| 18 ژوئن | 0 | |||
| 17 ژوئن | 0 | |||
| 16 ژوئن | 0 | |||
| 15 ژوئن | 0 | |||
| 14 ژوئن | 0 | |||
| 13 ژوئن | 0 | |||
| 12 ژوئن | 0 | |||
| 11 ژوئن | 0 | |||
| 10 ژوئن | +1 | |||
| 09 ژوئن | 0 | |||
| 08 ژوئن | 0 | |||
| 07 ژوئن | 0 | |||
| 06 ژوئن | 0 | |||
| 05 ژوئن | 0 | |||
| 04 ژوئن | 0 | |||
| 03 ژوئن | +1 | |||
| 02 ژوئن | 0 | |||
| 01 ژوئن | 0 |
پستهای کانال
+5
XCTest жил в Swift с самого начала и был тонкой надстройкой над тестовой моделью Objective-C: классы-наследники XCTestCase, методы с префиксом test, около 40 ассертов, отдельный test plan для управления параллелизмом. На WWDC 2024 Apple представила Swift Testing — нативный фреймворк на макросах, который поставляется с Xcode 16, открыт под Apache 2.0, работает на Linux и Windows. Server-side проекты вроде swift-aws-lambda-runtime убрали зависимость от XCTest ещё в июне 2025 года.
Чинить старый фреймворк выходило дороже, чем сделать новый. Развивать XCTest означало тащить за собой Obj-C runtime: классовое наследование XCTestCase, discovery тестов по имени функции, stringly-typed-ассерты, которые при провале знают только runtime-значения, но не само выражение.
Параллелизм работал через запуск нескольких процессов, по тесту на процесс, async/await выражался через XCTestExpectation. С приходом Swift Macros в Swift 5.9 у Apple появился чистый инструмент: discovery, ассерты и конфигурация — всё на этапе компиляции, без runtime reflection. Рассказываем об основных фичах Swift Testing в карточках.
#mobilevk #обзор
| 2 | Compose сам решает, какие composable можно пропустить при рекомпозиции: если функция skippable и все её параметры не изменились, повторного вызова не происходит. Правило ломалось на параметрах, которые компилятор не мог считать stable, — LocalDateTime, List<T> вместо ImmutableList<T>, любой класс из чужого модуля без @Stable. Один такой параметр блокировал skip и каскадно дёргал рекомпозицию вглубь.
Команды реагировали аннотациями @Stable и @Immutable на каждом доменном классе, обёртками над списками и remember на каждой лямбде. Получался шум, который сложно сопровождать и легко поставить неправильно — @Stable на классе с фактически мутабельным состоянием создаёт скрытые баги.
Strong skipping (Compose Compiler 1.5.4+, по умолчанию в Compose 1.7) изменил два правила.
Первое: unstable-параметры сравниваются по instance equality (===) вместо немедленного отказа от skip. Если ViewModel вернул тот же экземпляр Order, composable пропускается, даже если класс формально нестабилен.
@Composable
fun OrderRow(order: Order, onTap: () -> Unit) { ... }
// раньше: unstable Order → всегда рекомпозиция
// теперь: skip, если order === предыдущему order
Второе важнее: лямбды с unstable-захватами компилятор оборачивает в remember автоматически.
// раньше приходилось писать руками
val onTap = remember(order.id) { { viewModel.markPaid(order.id) } }
// со strong skipping эквивалент пишется компилятором:
val onTap = { viewModel.markPaid(order.id) }
// → разворачивается в remember(viewModel, order.id) { { ... } }
Из практического: большую часть @Stable и @Immutable на доменных классах можно убирать. Они не вредят, но добавляют шум в коде и в дифах.
Три места, где нужно остановиться.
// 1. LazyListScope-лямбды НЕ мемоизируются автоматически --
// они живут вне @Composable. Если контент тяжёлый, оборачивайте руками.
LazyColumn {
items(orders, key = { it.id }) { order ->
OrderRow(order, onTap = remember(order.id) { { ... } })
}
}
// 2. @Stable нужен, если ViewModel выдаёт НОВЫЕ инстансы
// с теми же данными -- типичный случай DTO → UI-model маппинга
@Stable
data class OrderUi(val id: String, val total: Money)
// 3. enum и sealed class инферятся stable без аннотации -- она не нужна
Включение, если ещё не на 1.7+
// build.gradle.kts
composeCompiler {
enableStrongSkippingMode.set(true)
}
Эффект меряйте через Layout Inspector с включёнными recomposition counts или composition tracing. Без замеров легко поверить, что стало быстрее, тогда как часть scope всё ещё перерисовывается на каждой эмиссии состояния. И отдельно — key в LazyColumn обязателен, без него счётчики рекомпозиции показывают завышенные значения независимо от skipping-режима.
#mobilevkhub #strongskipping | 217 |
| 3 | 🤖 Android
🟣 Metro DI вышел в стабильной версии. Это новый DI-фреймворк для Kotlin и KMP — без KAPT и KSP, генерация через Kotlin Compiler Plugin, граф зависимостей проверяется на этапе компиляции.
🟣 Context Parameters получили статус Stable в Kotlin 2.4.0-Beta2. Зависимости передаются через контекст вместо протаскивания через сигнатуры функций.
🟣 JetBrains обновили дефолтную структуру KMP-проектов: общий код теперь живёт в shared, под каждую платформу — отдельный application-модуль. Изменение связано с AGP 9.
🟣 Jetpack Paging 3.5.0 добавил работу с данными как со StateFlow и явные методы append(), prepend(), refresh(), retry(). Пагинация стала управляемее в Compose-сценариях.
🟣 AndroidX WebKit 1.16.0 — стабильный async-старт WebView. WebView можно прогревать заранее, а Navigation API даёт доступ к этапам навигации и метрикам FCP/LCP без JS.
🟣 Jetpack Telecom 1.1.0: VoIP-звонки отображаются в системной истории вызовов, callback работает прямо из нативного дайлера. Фича доступна на Android 16.1+.
🟣 Android Bench — бенчмарк LLM для Android-разработки. В свежем исследовании GPT 5.5 и 5.4 показали себя сильнее Claude.
▶️ iOS
🟣 Опубликовали записи докладов с try! Swift Tokyo 2026. Из интересного — выступления про Swift Concurrency Type System, скрытую силу Async Sequences и то, почему SwiftUI устроен именно так.
🟣 Apple показала финалистов Apple Design Awards 2026. Это хороший ориентир не только по визуальному качеству, но и по тому, какие паттерны Apple сейчас считает сильными: нативность, аккуратная работа с платформой, доступность и внимание к деталям.
🟣 Swift Concurrency: два материала про подводные камни. Первый — про неочевидные suspension points, из-за которых операции ведут себя непредсказуемо. Второй — про concurrency crashes в Swift 6: часть проблем ловится в runtime на границах акторов, GCD, Core Data и delegate callbacks.
🟣 Task.immediate в Swift 6.2: async-работа начинается сразу в текущем execution context до первого настоящего suspension point. Нюанс небольшой по формулировке, но важный для производительности.
🟣 Сравнение способов защиты shared state: actors, DispatchQueue и locks — с разбором, когда что применять.
🟣 Floating Safe Area Bar в SwiftUI: практический пример всплывающей карточки с CTA-кнопкой через safeAreaBar, вариант для iOS 26 и fallback для iOS 18.
🟣 Гайд по FormatStyle — шпаргалка для форматирования дат, чисел и других значений без кастомных решений.
#дайджест #mobilevkhub | 278 |
| 4 | 📱 Официальный релиз финальной стабильной версии Android 17 запланирован на июнь 2026 года, первым его получат Pixel, затем Samsung, а начиная с осени все остальные устройства. Кодовое имя — Cinnamon Bun. Для приложений с targetSdkVersion = 37 Google убирает лазейки, которые позволяли игнорировать адаптивное поведение на больших экранах. И это не единственное ломающее изменение.
Ориентация и изменение размера
Начиная с Android 16 Google двигался в сторону адаптивной разработки. Android 17 делает это обязательным, приложения с таргетом SDK 37 больше не могут блокировать поворот или ресайз на планшетах и складных устройствах. Ограничения на ориентацию и размер окна игнорируются на больших экранах. Если приложение до сих пор живёт в портрете и использует setRequestedOrientation() как костыль, то на Android 17 оно перестанет работать.
Среда выполнения и рефлексия
ART получает реализацию android.os.MessageQueue без блокировок, чтобы снизить конкуренцию потоков и уменьшить число пропущенных кадров. Это сломает код, который через рефлексию лезет в приватные поля MessageQueue.
Второе изменение жёстче. Мутация static final полей через рефлексию теперь бросает IllegalAccessException, а запись через JNI может уронить приложение. Библиотеки, которые полагались на патчинг во время выполнения, перестанут работать.
Сертификаты и сеть
Прозрачность сертификатов теперь включена по умолчанию для Android 17. Открытый HTTP-трафик заблокирован, а флаг android:usesCleartextTraffic="true" без явной конфигурации сетевой безопасности не поможет. Если приложение ещё ходит по HTTP, то пора переходить на HTTPS с корректной конфигурацией.
Локальная сеть под разрешением
Новое runtime-разрешение ACCESS_LOCAL_NETWORK: по умолчанию приложение не имеет доступа к локальной сети. Это закрывает возможность фингерпринтинга, но ломает код, который раньше ходил в локальную сеть без разрешений.
Безопасная загрузка нативных библиотек
System.load() теперь требует, чтобы загружаемая нативная библиотека была доступна только для чтения. Иначе упадёт UnsatisfiedLinkError. Это закрывает класс атак через загрузку вредоносного кода, но ломает библиотеки, которые писали в файл перед загрузкой.
Фоновый звук и SMS
Фоновое воспроизведение, запросы аудиофокуса и API изменения громкости ограничены для фоновых контекстов. Обработка SMS тоже стала консервативнее: стандартные одноразовые коды задерживаются для большинства таргетов, чтобы снизить риск перехвата.
Что делать сейчас
Четыре шага до стабильного релиза:
🟣проверьте рефлексию и JNI-вызовы вокруг MessageQueue и static final
🟣проведите аудит конфигурации сетевой безопасности
🟣прогоните QA на больших экранах с targetSdkVersion = 37
🟣протестируйте фоновый звук и SMS
Target SDK 37 — это не формальность. На этот раз Google режет не только API, но и привычные обходные пути.
#android17 #mobilevkhub | 430 |
| 5 | ✨ Каждый Intent, каждый запрос к системному сервису, каждый callback из Service в Activity проходит через Binder. Подсистема старше публичной версии Android, и большинство разработчиков работает с её обёртками, не заглядывая внутрь.
#mobilevkhub | 348 |
| 6 | Compose сам решает, какие composable можно пропустить при рекомпозиции: если функция skippable и все её параметры не изменились, повторного вызова не происходит. Раньше правило ломалось на параметрах, которые компилятор не мог считать stable, — LocalDateTime, List<T> вместо ImmutableList<T>, любой класс из чужого модуля без @Stable. Один такой параметр блокировал skip и каскадно дёргал рекомпозицию вглубь.
Команды реагировали аннотациями @Stable и @Immutable на каждом доменном классе, обёртками над списками и remember на каждой лямбде. Получался шум, который сложно сопровождать и легко поставить неправильно — @Stable на классе с фактически мутабельным состоянием создаёт скрытые баги.
Strong skipping (Compose Compiler 1.5.4+, по умолчанию в Compose 1.7) изменил два правила.
Первое: unstable-параметры сравниваются по instance equality (===) вместо немедленного отказа от skip. Если ViewModel вернул тот же экземпляр Order, composable пропускается, даже если класс формально нестабилен.
fun OrderRow(order: Order, onTap: () -> Unit) { ... }
// раньше: unstable Order → всегда рекомпозиция
// теперь: skip, если order === предыдущему order
Второе важнее: лямбды с unstable-захватами компилятор оборачивает в remember автоматически.
val onTap = remember(order.id) { { viewModel.markPaid(order.id) } }
// со strong skipping эквивалент пишется компилятором:
val onTap = { viewModel.markPaid(order.id) }
// → разворачивается в remember(viewModel, order.id) { { ... } }
Из практического: большую часть @Stable и @Immutable на доменных классах можно убирать. Они не вредят, но добавляют шум в коде и в дифах.
Три места, где нужно остановиться:
// они живут вне @Composable. Если контент тяжёлый, оборачивайте руками.
LazyColumn {
items(orders, key = { it.id }) { order ->
OrderRow(order, onTap = remember(order.id) { { ... } })
}
}
// 2. @Stable нужен, если ViewModel выдаёт НОВЫЕ инстансы
// с теми же данными -- типичный случай DTO → UI-model маппинга
@Stable
data class OrderUi(val id: String, val total: Money)
// 3. enum и sealed class инферятся stable без аннотации -- она не нужна
Включение, если ещё не на 1.7+ :
composeCompiler {
enableStrongSkippingMode.set(true)
}
Эффект меряйте через Layout Inspector с включёнными recomposition counts или composition tracing. Без замеров легко поверить, что стало быстрее, тогда как часть scope всё ещё перерисовывается на каждой эмиссии состояния. И отдельно — key в LazyColumn обязателен, без него счётчики рекомпозиции показывают завышенные значения независимо от skipping-режима.
#mobilevkhub #compose #strongskipping | 473 |
| 7 | Array, String, Dictionary — структуры, то есть value types: по правилам копируются при каждом присвоении. На практике копирование происходит редко. Между этими двумя фактами лежит copy-on-write.
#mobilevkhub | 410 |
| 8 | derivedStateOf — когда скролл перерисовывает экран каждый пиксель
Compose перерисовывает composable, когда читаемое им состояние меняется.
Со скроллом это создаёт проблему: lazyListState.firstVisibleItemIndex меняется при каждом пикселе прокрутки. Если кнопка «Наверх» читает это значение напрямую — она перерисовывается сотни раз в секунду, хотя визуально ничего не меняется.
// Перерисовывается при каждом пикселе скролла
@Composable
fun ScrollScreen() {
val listState = rememberLazyListState()
val showButton = listState.firstVisibleItemIndex > 0
LazyColumn(state = listState) { ... }
if (showButton) ScrollToTopButton()
}
showButton пересчитывается на каждый frame. Если список рендерит сложные элементы — это заметно на слабых устройствах.
derivedStateOf говорит Compose: «пересчитывай значение при изменении источника, но рекомпозицию запускай только если результат изменился».
@Composable
fun ScrollScreen() {
val listState = rememberLazyListState()
val showButton by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}
LazyColumn(state = listState) { ... }
if (showButton) ScrollToTopButton()
}
Теперь showButton пересчитывается на каждый пиксель — это внутренняя работа Compose. Но рекомпозиция ScrollScreen происходит только в двух случаях: когда firstVisibleItemIndex переходит с 0 на 1 и обратно.
remember { derivedStateOf { ... } } — стандартная связка. Без remember derivedStateOf пересоздаётся при каждой рекомпозиции. Без derivedStateOf — рекомпозиция на каждое изменение источника. Нужны оба.
Паттерн полезен везде, где состояние меняется часто, а UI реагирует редко:
// Показать заголовок только когда проскроллили дальше порога
val showHeader by remember {
derivedStateOf { listState.firstVisibleItemScrollOffset > 100 }
}
// Кнопка отправки активна только когда оба поля заполнены
val isFormValid by remember {
derivedStateOf { name.isNotBlank() && email.contains("@") }
}
Важно понять, что derivedStateOf создан не для бизнес-логики и не для тяжёлых вычислений. Это инструмент для трансформации часто меняющегося UI-состояния в редко меняющееся прямо в composable. Если логика требует данных из репозитория или занимает время — это задача для ViewModel с map на Flow.
#mobilevk #android #kotlin #jetpackcompose | 469 |
| 9 | @Observable в SwiftUI
До iOS 17 реактивный state в SwiftUI строился на ObservableObject с @Published. Это работало, но у подхода был системный изъян: любое изменение любого @Published-свойства перерисовывало все вью, подписанные на объект целиком — даже те, которым это свойство не нужно. В iOS 17 появился макрос @Observable, который решает это на уровне компилятора.
#mobilevk #ios #observable | 430 |
| 10 | 12–13 мая состоится Mobius Spring. В преддверии конференции мы поговорили с нашими постоянными стендистами — руководителями Android-разработки VK Александром Жеребцовым и Богданом Мащенко.
Ребята рассказали, зачем вообще нужны конференции и что нас ждёт на стенде VK в этом году.
Все подробности — в карточках 👆
#mobilevk #mobiusspring | 0 |
| 11 | 👆 Android
🟣Android CLI — новый слой поверх SDK
Google выпустила CLI для Android-разработки: android create для старта проекта, android sdk install для установки компонентов, android emulator/run для управления девайсами, android docs для доступа к документации, android skills для подключения инструкций агентам. В агентных сценариях — до 70% меньше токенов на установку, задачи в 3 раза быстрее.
🟣Android 17 Beta 4
Новые ограничения на использование памяти, автоматическое обнаружение аномалий в потреблении ресурсов, постквантовое шифрование в Android Keystore.
🟣Multi-device в эмуляторе
Нативная коммуникация между эмуляторами: до 4 устройств в одной сети, Wi-Fi Direct и NSD из коробки.
🟣Jetpack Media3 1.10.0
Готовый Player composable, ProgressSlider с жестами, PlaybackSpeedControl. Модуль media3-ui-compose-material3 приближается к out-of-the-box плееру на Compose.
🟣Holo — отладка в терминале
Набор инструментов для работы с приложением через терминальный интерфейс: логи, анализ поведения, отладка без GUI.
👆 iOS
🟣Swift 6.3
Улучшенный интероп с C через аннотацию @c, module selectors для управления вызовом API из нужного модуля, превью Swift Build в SwiftPM, улучшения в DocC и Swift Testing.
🟣Деманглинг в рантайме (Swift 6.4)
Встроенная функция деманглинга символов — читаемые имена функций без сторонних тулов.
🟣Composable Architecture 2.0
Макрос @Feature заменяет редьюсеры, API ближе к SwiftUI, меньше boilerplate при работе со state, новые механики взаимодействия между фичами.
🟣Codex для iOS
Поддержка через CLI и систему скиллов: работа без Xcode, AI-агенты для типовых задач, open-source скиллы для кастомизации.
🟣anyAppleOS
Один anyAppleOS вместо перечисления iOS/macOS/watchOS при проверке доступности API.
✨ Статья от инженеров VK на Хабр:
Ферма коммуникаций: система принятия решений для UI-промо в мобильном приложении
#дайджест #mobilevk | 0 |
| 12 | LaunchedEffect(Unit) — баг, которого не видно в превью
LaunchedEffect(Unit) выглядит как «запустить один раз при открытии экрана». Так его и используют. Проблема в том, что «один раз» означает «пока composable в композиции» — и это работает правильно ровно до того момента, как параметры экрана меняются.
Пример сценария: экран профиля получает userId. Хочется загрузить данные при открытии:
fun ProfileScreen(userId: String) {
LaunchedEffect(Unit) {
viewModel.loadUser(userId)
}
}
Работает в превью, тестах и на первом открытии. Ломается, когда приложение навигирует с userId = "ivan" на userId = "masha" — если composable остаётся в стеке и переиспользуется. LaunchedEffect(Unit) не перезапустится — ключ Unit не изменился. На экране останется профиль Ивана с идентификатором Маши.
Правильный ключ — та переменная, при изменении которой эффект должен перезапуститься:
@Composable
fun ProfileScreen(userId: String) {
LaunchedEffect(userId) { // перезапустится при смене userId
viewModel.loadUser(userId)
}
}
Есть и обратная ошибка — передать лямбду как ключ:
LaunchedEffect(viewModel::loadUser) { // ❌ новый экземпляр лямбды = новый ключ при каждой рекомпозиции
viewModel.loadUser(userId)
}
Лямбда создаёт новый объект при каждой рекомпозиции, ключ меняется, эффект перезапускается постоянно. Нужно использовать стабильные ключи: примитивы, id, стабильные объекты.
Ещё один случай, где Unit правильный — действительно однократная инициализация, не зависящая от параметров. Аналитика при открытии экрана, регистрация listener, запуск таймера на splash screen:
LaunchedEffect(Unit) {
analyticsTracker.trackScreenOpen("profile") // не зависит от userId
}
Перед тем, как написать LaunchedEffect(Unit) — один вопрос: «что должно произойти, если параметры этого composable изменятся?». Если ответ «ничего» — Unit правильный. Если ответ «эффект должен повториться» — нужен нормальный ключ.
#mobilevk #kotlin #compose | 0 |
| 13 | @StateObject vs @ObservedObject в SwiftUI
Оба хранят ObservableObject и перерисовывают вью при изменениях. Разница в одном слове — кто владеет объектом. Но эта разница стоит утечек памяти и ViewModels, которые живут дольше, чем должны.
#mobilevk #swiftui #stateobject #observedobject | 0 |
| 14 | runCatching глотает CancellationException — и это ломает отмену корутин
runCatching выглядит как хорошая замена try/catch: оборачивает блок кода, возвращает Result, не бросает исключений. Но у него есть одно свойство, которое проявляется как корутины-зомби, утечки и запросы, которые продолжают выполняться после того, как экран закрыт.
Проблема в том, что runCatching ловит буквально всё, включая CancellationException. А CancellationException — это механизм, через который structured concurrency сообщает корутине: «тебя отменили, остановись». Если поймать его и не перебросить — корутина продолжит работу как ни в чём не бывало.
// ViewModel уничтожен, scope отменён
// но корутина продолжает работать
viewModelScope.launch {
val result = runCatching {
api.fetchUser(id) // этот запрос всё ещё летит
}
// мы здесь, хотя должны были остановиться
result.onSuccess { updateUi(it) }
}
api.fetchUser() выбрасывает CancellationException при отмене scope. runCatching ловит его, упаковывает в Result.failure — и код после него продолжает выполняться. Из этого получаем обращение к UI из уничтоженного scope, лишние запросы к серверу, утечки.
Поправить просто: нужно явно перебрасывать CancellationException.
suspend fun <T> safeCall(block: suspend () -> T): Result<T> {
return try {
Result.success(block())
} catch (e: CancellationException) {
throw e // не глотаем -- cancellation должна пройти наверх
} catch (e: Exception) {
Result.failure(e)
}
}
Или через extension на Result:
inline fun <T> runCatchingSafe(block: () -> T): Result<T> =
runCatching(block).also {
it.exceptionOrNull()?.let { e ->
if (e is CancellationException) throw e
}
}
То же самое с try/catch (e: Exception) — CancellationException наследует от Exception в Kotlin, поэтому стандартный catch тоже его поймает. Правило одно: если в корутине ловишь Exception, всегда проверяй на CancellationException и перебрасывай.
viewModelScope.launch {
try {
val user = api.fetchUser(id)
updateUi(user)
} catch (e: CancellationException) {
throw e // обязательно
} catch (e: Exception) {
showError(e)
}
}
Есть еще одно место, где это всплывает — withContext. Если внутри withContext использовать runCatching и поймать CancellationException, переключение контекста не отменится корректно. Корутина застрянет в неопределённом состоянии.
Проверить, что в проекте есть проблема — легко. Ищем runCatching или catch (e: Exception) внутри корутин и suspend-функций. Если нет явного rethrow для CancellationException — это потенциальный зомби.
#mobilevk #kotlin #runcatching #cancellationexception | 0 |
| 15 | Swift SDK для Android
В марте 2026 вышел Swift 6.3 с первым официальным Swift SDK для Android. Это не эксперимент сообщества — официальная поддержка прямо в языке. Главная ценность не в том, чтобы писать Android-приложения с нуля на Swift, а в том, чтобы переиспользовать уже написанный Swift-код с iOS.
#mobilevk #swift #android | 0 |
| 16 | В iOS 26.4 сломалась доставка silent-пушей через CloudKit. Приложения перестали получать облачные обновления: didReceiveRemoteNotification не вызывался. Подписки, APNs-токены, entitlements — всё на месте. Ошибка проявляется только в production-контейнерах, development работает нормально. Пользователь видит устаревшие данные, пока не закроет и не откроет приложение заново. Код приложения не менялся — баг появился после обновления iOS.
Что именно сломалось
CloudKit использует подписки на изменения — CKQuerySubscription и zone-change flows — чтобы уведомить приложение о новых записях в iCloud. Подписка существует, запись подходит под предикат, APNs генерирует silent push. На iOS 26.4 уведомление генерируется, но не доставляется приложению. Регрессия на уровне ОС в цепочке CloudKit → APNs. На iOS 26.3.1 тот же код, те же подписки, те же контейнеры работали корректно.
Почему баг трудно поймать
Конфигурация приложения не содержит ошибок. Подписки созданы, токены получены, entitlements подключены. В логах пусто: нет ни краш-логов cloudd, ни error-колбэков, ни диагностических событий. Обычный путь отладки — проверить свой код, пересоздать подписки, откатиться на рабочую версию — не даёт результата, потому что проблема на стороне ОС. Ситуация усложняется троттлингом CloudKit-подписок: после серии тестовых изменений подписки могут перестать приходить на 24 часа. Это маскирует реальный баг под ожидаемое ограничение платформы.
SwiftData против Core Data
Регрессия затрагивает оба стека, но SwiftData приложения страдают сильнее. SwiftData скрывает sync-механику за автоматическими абстракциями. При поломке push-триггера синхронизация выглядит как полный отказ: нет точек входа для диагностики или recovery. Core Data + CloudKit обычно устойчивее — команды чаще держат явные fetch-, merge- и conflict-resolution пути, которые можно вызвать вручную. При том же баге Core Data приложение теряет актуальность данных, но сохраняет контроль над ситуацией.
Что делать до фикса
🟣Не полагаться на silent push как единственный триггер синхронизации.
🟣Polling CKFetchRecordZoneChangesOperation по таймеру или событию lifecycle.
🟣Явный fetch при переходе приложения в foreground.
🟣Идемпотентная reconciliation локального состояния при каждом sync.
Баг блокирует push-триггер, но не fetch-API: CloudKit возвращает актуальные данные, если запросить их напрямую.
Silent push — не гарантия доставки. Архитектура синхронизации должна работать без него.
#mobilevkhub #cloudkit #ios | 0 |
| 17 | remember vs rememberSaveable vs retain — когда использовать каждый
Каждый хранит состояние по-своему и живёт разное время. Выбор не того варианта — либо потеря данных при повороте экрана, либо утечка памяти, либо лишняя нагрузка на Bundle.
#mobilevk #android #kotlin #jetpackcompose | 0 |
| 18 | Predictive Back — анимация, которая показывает, куда ведёт свайп назад ещё до того, как пользователь его завершил. В Android 15 она включена по умолчанию для всех приложений. В Android 16 onBackPressed() удалён полностью, KeyEvent.KEYCODE_BACK не диспатчится в приложение.
Если приложение переопределяет onBackPressed() — оно ломает анимацию. Система регистрирует, что приложение перехватывает back-событие, и не показывает preview. Пользователь видит обычный резкий переход вместо плавного предиктивного.
Правильный API — OnBackPressedCallback через onBackPressedDispatcher. Он работает с Android 13+ и не ломает Predictive Back:
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onBackPressedDispatcher.addCallback(this) {
// кастомная логика: спросить пользователя, сохранить черновик
if (canGoBack()) {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
} else {
showDiscardDialog()
}
}
}
}
Когда кастомная логика не нужна, нужно выключить callback через isEnabled = false и передать управление системе. Тогда запустится предиктивная анимация.
В Jetpack Compose — BackHandler:
@Composable
fun EditScreen(hasUnsavedChanges: Boolean) {
BackHandler(enabled = hasUnsavedChanges) {
showDiscardDialog()
}
// когда hasUnsavedChanges = false, BackHandler не активен
// система показывает предиктивную анимацию
}
BackHandler с enabled = false — это не то же самое, что отсутствие BackHandler. Первый всё равно регистрирует callback (пусть и неактивный), второй не регистрирует ничего. Разница: при enabled = false система знает, что перехвата нет, и показывает анимацию.
Главная ошибка при миграции — добавить OnBackPressedCallback для логирования или аналитики с PRIORITY_DEFAULT. Любой активный callback с дефолтным приоритетом потребляет событие и блокирует анимацию. Для наблюдения без потребления — в Android 16 появился PRIORITY_SYSTEM_NAVIGATION_OBSERVER.
Манифест нужно обновить один раз:
<application
android:enableOnBackInvokedCallback="true">
Без этого флага Predictive Back не работает, даже если код правильный. На Android 15 без флага система показывает обычные анимации. На Android 16 с таргетом 36 флаг игнорируется — predictive back включён принудительно.
#mobilevk #android #predictiveback | 0 |
| 19 | В большом мобильном продукте коммуникации запускаются разными командами: подписки, апселлы, промоакции, A/B-эксперименты — и все претендуют на один экран. У каждой инициативы свои условия показа, сегменты и метрики. Без единого механизма принятия решений это превращается в набор локальных проверок без общих правил.
▶️ В карточках разбираем нашу статью на Хабре про Ферму коммуникаций — систему, которую мы построили для мобильных продуктов Почты и Облака Mail.
#mobilevk #ios #android #bdui #kotlett | 0 |
| 20 | 🙂 Кейс команды RuStore: как внедрение темной темы в B2B-продукты привело к полному рефакторингу дизайн-системы.
Попытка добавить тёмную тему в B2B RuStore Консоль обернулась неожиданной проблемой: интерфейс стал плоским и нечитаемым.
Оказалось, что простая инверсия цветов не работает в сложных десктопных интерфейсах, а старая система токенов, общая с мобильным приложением, тянет развитие продукта назад. Чтобы это исправить, команде пришлось перестроить работу с цветом с нуля: разделить семантику, перейти на модель OKLCH и автоматизировать синхронизацию Figma с кодом.
В карточках рассказываем, как провели рефакторинг дизайн-системы, не останавливая продуктовую разработку. Подробности и примеры реализации — в статье: https://habr.com/ru/companies/vk/articles/1017912/
#mobilevk #designsystem #RuStore | 0 |
اکنون در دسترس! پژوهش تلگرام ۲۰۲۵ — مهمترین بینشهای سال 
