Mobile VK Hub
رفتن به کانال در Telegram
Комьюнити от VK для мобильных разработчиков. Здесь всё о том, как создаются приложения для миллионов: от нативных подходов до кросс-платформы.
نمایش بیشتر735
مشترکین
-224 ساعت
-27 روز
-330 روز
در حال بارگیری داده...
کانالهای مشابه
هیچ دادهای
مشکلی وجود دارد؟ لطفاً صفحه را تازه کنید یا با مدیر پشتیبانی ما تماس بگیرید.
ابر برچسبها
اشارات ورودی و خروجی
---
---
---
---
---
---
جذب مشترکین
ژوئیه '26
ژوئیه '26
+3
در 0 کانالها
ژوئن '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 کانالها
| تاریخ | رشد مشترکین | اشارات | کانالها | |
| 10 ژوئیه | +1 | |||
| 09 ژوئیه | 0 | |||
| 08 ژوئیه | 0 | |||
| 07 ژوئیه | +2 | |||
| 06 ژوئیه | 0 | |||
| 05 ژوئیه | 0 | |||
| 04 ژوئیه | 0 | |||
| 03 ژوئیه | 0 | |||
| 02 ژوئیه | 0 | |||
| 01 ژوئیه | 0 |
پستهای کانال
+5
Modifier в Jetpack Compose — где порядок имеет значение
Modifier в Compose — то место, где порядок применения реально меняет результат.
.padding().background() и .background().padding() дают разные картинки на экране, и большинство багов на старте — это перепутанный порядок модификаторов.
В карточках — шесть конкретных мест: порядок padding и background, размер клик-зоны, как работает size, объединение трансформаций через graphicsLayer, иммутабельность Modifier и тонкости composed для собственных модификаторов.
#mobilevkhub #compose| 2 | 🤖 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 | 196 |
| 3 | Что и когда выбирать в 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 | 312 |
| 4 | Side effects в Jetpack Compose — шесть инструментов для шести задач
В Compose нет привычных lifecycle-методов, как у Activity или Fragment. Composable вызывается часто и в непредсказуемом порядке. Поэтому любой код, который должен выполниться один раз, при смене параметра или с подпиской и отпиской, оборачиваем в специальный эффект.
Разбираем шесть инструментов под шесть задач в карточках.
#mobilevkhub #compose | 271 |
| 5 | 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 | 402 |
| 6 | 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 #обзор | 462 |
| 7 | 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 |
| 8 | 🤖 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 | 506 |
| 9 | 📱 Официальный релиз финальной стабильной версии 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 | 531 |
| 10 | ✨ Каждый Intent, каждый запрос к системному сервису, каждый callback из Service в Activity проходит через Binder. Подсистема старше публичной версии Android, и большинство разработчиков работает с её обёртками, не заглядывая внутрь.
#mobilevkhub | 387 |
| 11 | 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 | 479 |
| 12 | Array, String, Dictionary — структуры, то есть value types: по правилам копируются при каждом присвоении. На практике копирование происходит редко. Между этими двумя фактами лежит copy-on-write.
#mobilevkhub | 410 |
| 13 | 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 |
| 14 | @Observable в SwiftUI
До iOS 17 реактивный state в SwiftUI строился на ObservableObject с @Published. Это работало, но у подхода был системный изъян: любое изменение любого @Published-свойства перерисовывало все вью, подписанные на объект целиком — даже те, которым это свойство не нужно. В iOS 17 появился макрос @Observable, который решает это на уровне компилятора.
#mobilevk #ios #observable | 430 |
| 15 | 12–13 мая состоится Mobius Spring. В преддверии конференции мы поговорили с нашими постоянными стендистами — руководителями Android-разработки VK Александром Жеребцовым и Богданом Мащенко.
Ребята рассказали, зачем вообще нужны конференции и что нас ждёт на стенде VK в этом году.
Все подробности — в карточках 👆
#mobilevk #mobiusspring | 0 |
| 16 | 👆 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 |
| 17 | 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 |
| 18 | @StateObject vs @ObservedObject в SwiftUI
Оба хранят ObservableObject и перерисовывают вью при изменениях. Разница в одном слове — кто владеет объектом. Но эта разница стоит утечек памяти и ViewModels, которые живут дольше, чем должны.
#mobilevk #swiftui #stateobject #observedobject | 0 |
| 19 | 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 |
| 20 | Swift SDK для Android
В марте 2026 вышел Swift 6.3 с первым официальным Swift SDK для Android. Это не эксперимент сообщества — официальная поддержка прямо в языке. Главная ценность не в том, чтобы писать Android-приложения с нуля на Swift, а в том, чтобы переиспользовать уже написанный Swift-код с iOS.
#mobilevk #swift #android | 0 |
