Мобильная разработка
13.1K subscribers
1.03K photos
238 videos
3.16K links
Актуальное по мобильной разработке — Android, iOS, кроссплатформа

Разместить рекламу: @tproger_sales_bot
Правила общения: https://tprg.ru/rules
Другие каналы: @tproger_channels
Сайт: https://tprg.ru/site

Регистрация в перечне РКН: https://tprg.ru/oVBP
Download Telegram
RevenueCat позволяет проверить план изменения продуктов магазина до его применения

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

В обновлении RevenueCat AI Toolkit для этого описан процесс через CLI: rc products store plan строит план из файла, show показывает его, а apply применяет изменения отдельным действием.

Так желаемое состояние каталога можно хранить в CSV и отправлять на ревью вместе с кодом. Проверка плана особенно полезна, когда запрос касается сразу двух магазинов или нескольких цен.

Сам CLI находится в публичной бете. Для автоматизации у него есть JSON-вывод, описание команд и режим --no-input, который завершает команду ошибкой вместо ожидания ответа. Создание плана ещё не означает, что цены в магазине уже изменились.
AndroidX Security State показывает исправления безопасности отдельно для компонентов устройства

Одна дата обновления Android не описывает всю картину: система, модули Google Play и ядро обновляются разными путями.

AndroidX Security State позволяет различать три состояния: что установлено на устройстве, какие исправления опубликованы и какое обновление уже доступно для установки именно здесь. Эти сведения помогают приложению объяснить пользователю, чего не хватает.

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

Доступность сведений об ожидающем обновлении зависит от клиента обновлений: он должен предоставлять их через поддерживаемый механизм. Поэтому отсутствие таких данных нельзя автоматически трактовать как отсутствие исправлений или подтверждение безопасности устройства.
Swift 6.4 разрешает писать some View? и any P? без лишних скобок

Раньше optional-тип приходилось записывать как (some View)? или (any Delegate)?. Без скобок парсер связывал вопросительный знак с протоколом, а затем отклонял результат.

В Swift 6.4 изменили это правило: some View? означает optional от скрытого конкретного типа, а any Delegate? — optional от экзистенциального типа. Старые записи со скобками остаются корректными.

Исключение — композиция протоколов. Для неё всё ещё нужны (any Codable & Sendable)? или any (Codable & Sendable)?. Короткая запись any Codable & Sendable? не принимается.

Изменение упрощает объявления делегатов и возвращаемых значений. Разницу между some и any оно сохраняет, а переходить на новый синтаксис во всём проекте не обязательно.
Rive 6.28 автоматически перерисовывает анимацию при изменении связанных данных

В приложении поменялось значение модели, а анимация осталась прежней. В новом Swift Concurrency runtime Rive обновления связанных данных теперь сами вызывают перерисовку.

В релизе rive-ios 6.28.0 это работает для основной и глобальных моделей, включая вложенные модели и элементы списков. Отдельно следить за dirtyStream() ради перерисовки больше не требуется; API объявлен устаревшим.

При миграции важно сохранить нужные экземпляры моделей у себя и явно связать их с машиной состояний. У новых удобных инициализаторов устаревший getter Rive.viewModelInstance возвращает nil — прежний способ получить модель уже не подходит.

На странице релизов есть пример перехода. После 6.28.0 также вышла 6.28.1 с исправлениями и улучшениями списков, поэтому при обновлении стоит смотреть изменения обеих версий.
Google Play позволяет сообщать о проблемах оплаты подписки прямо в приложении

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

В разборе обновлений подписок Google Play Google отдельно выделяет In-App Messaging API. Он доступен всем разработчикам и позволяет показывать пользователю сообщения о проблемах оплаты и предстоящих изменениях цены внутри приложения.

Вместо собственного экрана с общим текстом «что-то пошло не так» можно подключить предусмотренный магазином сценарий восстановления оплаты. Это стоит проверить рядом с обработкой состояний подписки на сервере.

Само сообщение не подтверждает успешный платёж. Доступ к платным возможностям всё равно должен соответствовать актуальному состоянию покупки. В статье также описаны механизмы удержания и возврата подписчиков — их доступность нужно проверять отдельно.
Typed throws в Swift позволяет проверять обработку конкретного типа ошибок при компиляции

Обычный throws сообщает, что функция может завершиться ошибкой, но не ограничивает её конкретным типом. В catch приходится выяснять, что именно прилетело.

В разборе typed throws функция объявляет throws(CatError). Когда тип сохраняется до обработчика, catch получает CatError, и компилятор проверяет полноту switch по вариантам перечисления.

Однако конкретный тип можно потерять. Например, если внутри do вызываются функции с разными типами ошибок, результат может расшириться до any Error. Типизация замыканий тоже влияет на то, что дойдёт до вызывающего кода.

У публичного API есть цена: расширение набора ошибок затрагивает клиентов с исчерпывающей обработкой. Поэтому точный тип полезен там, где этот контракт действительно нужен. Автоматически заменять им все обычные throws не стоит.
Google открыл публикацию Android-игр для автомобильных экранов с запуском только на стоянке

Игры для Android Auto и Android Automotive OS вышли из беты: теперь разработчики могут отправлять их в открытое тестирование и в рабочую публикацию Google Play.

В руководстве по адаптации разобраны настройки манифеста и требования к поведению. Для Android Auto поддержка игр начинается с Android 15; для Automotive OS отдельно объявляется автомобильный форм-фактор.

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

Ещё понадобятся адаптация под широкие и вертикальные экраны и проверка качества. Android Auto тестируют через Desktop Head Unit, Automotive OS — через соответствующий эмулятор. Одних изменений манифеста для публикации недостаточно.
🔥1😁1
derivedStateOf полезен, когда экрану нужны не все изменения состояния

Кнопку «Наверх» интересует, ушёл ли пользователь с начала списка. Индекс первой видимой строки меняется с 1 на 2, затем на 3, но кнопка всё это время должна оставаться видимой.

Для такого случая в документации Compose используют remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }. Интерфейс наблюдает булев результат и не получает новое значение при каждом изменении индекса. Рекомпозиции из-за других входов composable этот приём не отменяет.

А вот собирать имя и фамилию через derivedStateOf обычно незачем: итоговая строка должна обновляться вместе с ними. У механизма есть собственная цена. Прежде чем добавлять его, сравните частоту изменения входа и нужного результата. Минимальную версию Compose для своего проекта сверяйте отдельно: этот раздел её не задаёт.
Смена id отменяет SwiftUI task, но не останавливает любой код внутри

В поиске пользователь уже дописал запрос, а обработка предыдущего ещё идёт. Модификатор .task(id: query) связывает задачу со значением запроса: при изменении id SwiftUI отменяет старую задачу и создаёт новую. Само значение должно быть Equatable. Базовый механизм доступен с iOS 15.

Дальше действует правило Swift Concurrency: отмена кооперативная. Произвольная функция может продолжить работу, если сама не проверяет отмену и не вызывает код, который на неё реагирует. Для собственной долгой обработки есть Task.checkCancellation(), бросающий ошибку при отмене.

Поэтому при разборе такого поиска полезно проследить весь путь запроса до обновления экрана. Если тяжёлая обработка игнорирует отмену, один .task(id:) этого не исправит. Он также не гарантирует защиту от всех устаревших результатов.
Форма во Flutter может пережить завершение фонового процесса

Пользователь заполнял форму, переключился в другое приложение, а Android освободил память и завершил фоновый процесс. При возвращении экран создаётся заново. Для незавершённого ввода и выбранной вкладки у Flutter есть state restoration.

Восстановление подключается по уровням: restorationScopeId у приложения, restorationId у поддерживающих его виджетов. Собственное состояние хранят в RestorableProperty и регистрируют в restoreState через RestorationMixin. Данные передаются движку по мере изменений, а не только в последний момент перед завершением процесса.

Навигацию нужно настроить отдельно. При прямой работе с Navigator стек восстанавливают через методы вроде restorablePush. А важный черновик всё равно стоит сохранять в постоянном хранилище: объём instance state на Android ограничен, и restoration не даёт гарантии для любого принудительного закрытия приложения. Проверка через hot reload также не заменяет тест восстановления после завершения процесса.
👎2
Счётчик в DataStore увеличивайте внутри updateData

Прочитать значение, прибавить единицу в интерфейсе, записать результат — между этими действиями другое обновление может изменить тот же счётчик. Если записать число из старого состояния экрана, прибавление потеряется.

У Jetpack DataStore преобразование выполняют внутри updateData. В официальном примере с JSON новый объект строится из переданного текущего состояния: settings.copy(exampleCounter = settings.exampleCounter + 1). Чтение, изменение и запись образуют одну атомарную транзакцию. Для Preferences есть и suspend-функция edit.

Эта гарантия требует правильного использования: один активный экземпляр DataStore на файл в одном процессе и неизменяемый тип данных. Для нескольких процессов нужен MultiProcessDataStore, доступный с версии 1.1.0. Гарантия не распространяется на общую транзакцию с сервером или другим файлом. Для сложных связанных данных уместнее Room.