Koin Annotations 1.4 в Compose Multiplatform
Зачем использвать:
👉 Код чище: аннотации обеспечивают декларативный и краткий способ определения зависимостей
👉 Поддержка IDE: проще навигация по коду и рефакторинг
👉 Проверка во время компиляции: более раннее обнаружение проблем с внедрением зависимостей (Немного жертвуем скоростью сборки)
👉 Убираем обслуживающий код: KSP автоматически генерирует необходимые extension функции
👉 Multiplatform поддерживается!
Документация: https://insert-koin.io/docs/reference/koin-annotations/start/
Выглядит хорошей возможностью для масштабирования koin как DI в проекте
Зачем использвать:
👉 Код чище: аннотации обеспечивают декларативный и краткий способ определения зависимостей
👉 Поддержка IDE: проще навигация по коду и рефакторинг
👉 Проверка во время компиляции: более раннее обнаружение проблем с внедрением зависимостей (Немного жертвуем скоростью сборки)
👉 Убираем обслуживающий код: KSP автоматически генерирует необходимые extension функции
👉 Multiplatform поддерживается!
Документация: https://insert-koin.io/docs/reference/koin-annotations/start/
Выглядит хорошей возможностью для масштабирования koin как DI в проекте
blog.kotzilla.io
Getting Started with Koin Annotations 1.4 in Compose Multiplatform
Streamline your Compose Multiplatform project by migrating from Koin DSL to Koin Annotations 1.4. Learn dependency setup, component migration, and platform implementations.
Параметризированные Android тесты с Burst 2.0
👉 Писать параметризированные тесты проще и аккуратнее
👉 Android и мультиплатформа поддерживаются из коробки
👉 Проще запускать на большом количестве устройств, решены проблемы с TestParameterInjector
😡 К сожалению, фреймворк не очень хорошо работает с Android Studio, что может осложнить взаимодействие с ним
🤨 Минимальная версия Kotlin 2.0.21, которая прокидывается транзитивно
Пример:
⭐️ 113+ https://github.com/cashapp/burst
👉 Писать параметризированные тесты проще и аккуратнее
👉 Android и мультиплатформа поддерживаются из коробки
👉 Проще запускать на большом количестве устройств, решены проблемы с TestParameterInjector
😡 К сожалению, фреймворк не очень хорошо работает с Android Studio, что может осложнить взаимодействие с ним
🤨 Минимальная версия Kotlin 2.0.21, которая прокидывается транзитивно
Пример:
@Test
fun drinkSoda(
soda: String = burstValues("Pepsi", "Coke"),
ice: Boolean,
distribution: Distribution,
) {
...
}
Please open Telegram to view this post
VIEW IN TELEGRAM
Kotlin Multiplatform Roadmap 2025
Дорожная карта на 2025 год. Как будет развиваться мультиплатформа:
👉 В Compose Multiplatform будет упор на интеграцию Jetpack Compose, улучшение перфоманса iOS. Стабилизация и доведение до релизного состояния таких фичей как навигация, управление ресурсами и переводы, а так же accessibility!
👉 Kotlin-to-Swift экспорт, вместо существующего проблемного Kotlin-to-Objective-C . Первую публичную версию ожидаем в 2025 году.
👉 Больше интеграций с Android Studio для удобной работы. Обещают отдельную KMP IDE базирующуюся на существующей IDE Fleet. Очень хочется попробовать, мы в команде иногда используем Fleet, но не как полноценную IDE, а как вспомогательный инструмент
👉 Развитие экосистемы библиотек для KMP. Очень схоже с тем, чем планирует заниматься и Kotlin команда
👉 Amper и его улучшения, как инструмента для сборки. Пока что выглядит сыро, но обещают ряд доработок в особенности для Compose Multiplatform. Gradle, конечно, плох, а bazel сложен для малых команд, но это вдоль и поперек исследованные инструменты. Amper будет сложно конкурировать, но некоторые сценарии просто невозможно реализовать без глубокой интеграции в KMP (ex. Фуллстак приложение на KMP с бекендом)
👉 Продолжат работать над Gradle и другими инструментами для сборки приложений. Обещают упрощение подключения зависимостей для KMP проектов
Дорожная карта на 2025 год. Как будет развиваться мультиплатформа:
👉 В Compose Multiplatform будет упор на интеграцию Jetpack Compose, улучшение перфоманса iOS. Стабилизация и доведение до релизного состояния таких фичей как навигация, управление ресурсами и переводы, а так же accessibility!
👉 Kotlin-to-Swift экспорт, вместо существующего проблемного Kotlin-to-Objective-C . Первую публичную версию ожидаем в 2025 году.
👉 Больше интеграций с Android Studio для удобной работы. Обещают отдельную KMP IDE базирующуюся на существующей IDE Fleet. Очень хочется попробовать, мы в команде иногда используем Fleet, но не как полноценную IDE, а как вспомогательный инструмент
👉 Развитие экосистемы библиотек для KMP. Очень схоже с тем, чем планирует заниматься и Kotlin команда
👉 Amper и его улучшения, как инструмента для сборки. Пока что выглядит сыро, но обещают ряд доработок в особенности для Compose Multiplatform. Gradle, конечно, плох, а bazel сложен для малых команд, но это вдоль и поперек исследованные инструменты. Amper будет сложно конкурировать, но некоторые сценарии просто невозможно реализовать без глубокой интеграции в KMP (ex. Фуллстак приложение на KMP с бекендом)
👉 Продолжат работать над Gradle и другими инструментами для сборки приложений. Обещают упрощение подключения зависимостей для KMP проектов
The JetBrains Blog
Kotlin Multiplatform Development Roadmap for 2025 | The Kotlin Blog
Kotlin Multiplatform roadmap outlining our key priorities and goals for 2025.
Основные фичи все еще не заявлены, но уже есть ряд исправлений по сравнению с предыдущими версиями
Please open Telegram to view this post
VIEW IN TELEGRAM
Android Studio Release Updates
Android Studio Meerkat | 2024.3.1 Canary 1 now available
Android Studio Meerkat | 2024.3.1 Canary 1 is now available in the Canary channel. If you already have an Android Studio build on the Ca...
PDD - Preview Driven Development
👉 Snapshot Testing лежит в основе. Есть много инструментов помогающих в реализации, начиная от встроенных, заканчивая paparazzi и облачными решениями
👉 Запускаем каждый отдельный экран или часть, как
👉 В каждом пулреквесте вы видите какие части приложения были затронуты вашими изменениями
👉 Для автоматизации сразу же предлагается Github Action и Gradle плагин с глубокой интеграцией в один из облачных инструментов
Еще чуть чуть про PDD было вот тут и на Droidcon тут
👉 Snapshot Testing лежит в основе. Есть много инструментов помогающих в реализации, начиная от встроенных, заканчивая paparazzi и облачными решениями
👉 Запускаем каждый отдельный экран или часть, как
Сomposable
функци в превью внутри нашего окружения. Автор мокает весь уровень данных, что позволяет включить в тестирование viewModel
и увидеть реально возможное поведение приложения👉 В каждом пулреквесте вы видите какие части приложения были затронуты вашими изменениями
👉 Для автоматизации сразу же предлагается Github Action и Gradle плагин с глубокой интеграцией в один из облачных инструментов
Еще чуть чуть про PDD было вот тут и на Droidcon тут
Как лямды ломают хэширование в data class
Надеюсь, вы не кладете лямды напрямую в
Сравнивая два одинаковых
А исправить можно вот так:
👉 Переопределяем
👉 Используем
👉 Используем интерфейс и передаем полноценный объект внутрь
Лучше всего, конечно, вынести лямду куда подальше от
Надеюсь, вы не кладете лямды напрямую в
data class
, а если кладете, то делаете это правильно!
val detective1 = DetectiveDataClass(
name = "Sherlock",
age = 40,
alias = "Holmes",
onDetectiveAlert = { println("Elementary!") }
)
val detective2 = DetectiveDataClass(
name = "Sherlock",
age = 40,
alias = "Holmes",
onDetectiveAlert = { println("Elementary!") }
)
println(detective1 == detective2)
// false, хотя ожидается true
println(detective1.hashCode() == detective2.hashCode())
// false, хотя ожидается true
Сравнивая два одинаковых
data class
’а, получаем неожиданный результатА исправить можно вот так:
👉 Переопределяем
equals
и hashCode
для этого класса. Не оптимально 👉 Используем
method reference
val detective1 = DetectiveDataClass(
name = "Sherlock",
age = 40,
alias = "Holmes",
onDetectiveAlert = ::alertFunction
)
👉 Используем интерфейс и передаем полноценный объект внутрь
Лучше всего, конечно, вынести лямду куда подальше от
data class
Android 16 Preview BAKLAVA
Вроде, только-только Android 15 проводили в релиз, а уже выходит следующая версия. План такой: Превью сегодня ➡️ Бета в Январе ➡️Стабильная бета в Марте ➡️ релиз по готовности (Июнь)
👉 Изменения в API можно глянуть тут
👉 Внутри релиза теперь есть разделение на минорный и мажорный:
👉 Новый встроенный photo picker.
👉 Немного интеграции с Health Connect
👉 Privacy Sandbox
По ощущениям, работы, с релизом Privacy Sandbox, прибавится у всех Android разработчиков. А вы что думаете?
Вроде, только-только Android 15 проводили в релиз, а уже выходит следующая версия. План такой: Превью сегодня ➡️ Бета в Январе ➡️Стабильная бета в Марте ➡️ релиз по готовности (Июнь)
👉 Изменения в API можно глянуть тут
👉 Внутри релиза теперь есть разделение на минорный и мажорный:
val minorSdkVersion = Build.getMinorSdkVersion(VERSION_CODES_FULL.BAKLAVA)
👉 Новый встроенный photo picker.
👉 Немного интеграции с Health Connect
👉 Privacy Sandbox
По ощущениям, работы, с релизом Privacy Sandbox, прибавится у всех Android разработчиков. А вы что думаете?
Android Developers
Android 16 Preview | Android Developers
How the Android 16 Preview works, the timeline, and what is included.
Kotlin 2.1.0-RC2
Внутри много доработок, но самая важная для меня - стабильная работа в xCode 16+. Я, по-глупости, обновился на macOS Sequoia месяц назад, что потянуло за собой новый xCode, и iOS сборка нашего KMM проекта сломалась на моей ноутбуке.
Да, всегда можно указать xCode через
Вывод: Не забывайте отключать автообновления 😅
Внутри много доработок, но самая важная для меня - стабильная работа в xCode 16+. Я, по-глупости, обновился на macOS Sequoia месяц назад, что потянуло за собой новый xCode, и iOS сборка нашего KMM проекта сломалась на моей ноутбуке.
Да, всегда можно указать xCode через
xcodes
в командной строке, но потеря возможности манипулировать нативной частью приложения сильно ограничивала меня. Beta версии 2.1.0 продолжили сыпать ошибками в наш проект, а вот RC2 наконец-таки решил все проблемы и все стало как раньше!Вывод: Не забывайте отключать автообновления 😅
GitHub
GitHub - JetBrains/kotlin: The Kotlin Programming Language.
The Kotlin Programming Language. . Contribute to JetBrains/kotlin development by creating an account on GitHub.
Тестируйте умнее, а не усерднее
Google обновили документацию по тестированию приложений
👉 Акцент на продуктивности разработки. Пишем тесты пропорционально пирамиде тестирования, не переусердствуем с частотой запуска тестов. Все ради минимизации стоимости запуска и написания тестов. Если вы все еще плаваете в терминологии и не отличаете где интеграционные, а где Unit тесты, то внутри есть примеры
👉 Дополнили рекомендации по популярным ныне видам тестирования: Скриншот тестирование (недавно разбирали тут), тестирование производительности
👉 Новый термин для UI тестов, не делающих скриншоты, - поведенческое тестирование
👉 Несколько рекомендаций для больших тестов и robolectric
👉 В связи с распространение Foldable девайсов с нестандартным разрешением. Рекомендации по их тестированию, а так же инструменты которые вам в этом помогут
Google обновили документацию по тестированию приложений
👉 Акцент на продуктивности разработки. Пишем тесты пропорционально пирамиде тестирования, не переусердствуем с частотой запуска тестов. Все ради минимизации стоимости запуска и написания тестов. Если вы все еще плаваете в терминологии и не отличаете где интеграционные, а где Unit тесты, то внутри есть примеры
👉 Дополнили рекомендации по популярным ныне видам тестирования: Скриншот тестирование (недавно разбирали тут), тестирование производительности
👉 Новый термин для UI тестов, не делающих скриншоты, - поведенческое тестирование
👉 Несколько рекомендаций для больших тестов и robolectric
👉 В связи с распространение Foldable девайсов с нестандартным разрешением. Рекомендации по их тестированию, а так же инструменты которые вам в этом помогут
Android Good Reads
Android 16 Preview BAKLAVA Вроде, только-только Android 15 проводили в релиз, а уже выходит следующая версия. План такой: Превью сегодня ➡️ Бета в Январе ➡️Стабильная бета в Марте ➡️ релиз по готовности (Июнь) 👉 Изменения в API можно глянуть тут 👉 Внутри…
Касательно Android 16 DP1, это ведь первый релиз по новой системе ускоренных релизов, верно?
Согласно плану, нас ждёт много нового в средине и в конце года. Новое API и новые
Согласно плану, нас ждёт много нового в средине и в конце года. Новое API и новые
Deprecated
😅. Заранее предупреждают, что лучше настроить CI так, чтобы она гоняла тесты на разных API levels. Кстати, а какой targetSdk у вас сейчас в проекте, следуете рекомендациям от гугла?Полезная библиотека этой недели - https://strikt.io/ (563 ⭐️ )
Устали от
Примеры:
Сильно гибче стандартного функционала. Работает с JUnit5, Minutest и Spek
Устали от
assertTrue(true == true)
? Тогда strikt может упростить вам жизнь!Примеры:
// Flexible assertions about collections
val subject = listOf("Eris", "Thor", "Anubis", "Ra")
expectThat(subject)
.contains("Eris", "Thor", "Anubis")
// “Narrow” the assertion to elements or ranges
expectThat(subject)[0].isEqualTo("Eris")
// Make grouping assertions
val subject = Deity.values().map { it.toString() }
expectThat(subject)
.isNotEmpty()
.any { startsWith("E") }
// Custom assertions are extension functions
fun Assertion.Builder<LocalDate>.isStTibsDay() =
assert("is St. Tib's Day") {
when (MonthDay.from(it)) {
MonthDay.of(2, 29) -> pass()
else -> fail()
}
}
expectThat(LocalDate.of(2020, 2, 29)).isStTibsDay()
Сильно гибче стандартного функционала. Работает с JUnit5, Minutest и Spek
Please open Telegram to view this post
VIEW IN TELEGRAM
Android Good Reads
Kotlin 2.1.0-RC2 Внутри много доработок, но самая важная для меня - стабильная работа в xCode 16+. Я, по-глупости, обновился на macOS Sequoia месяц назад, что потянуло за собой новый xCode, и iOS сборка нашего KMM проекта сломалась на моей ноутбуке. Да…
А вот и релиз!
Кратко:
👉 Новые экспериментальные фичи. Guard условие в
👉 Обновили K2 компилятор и сделали ряд улучшенией в
👉 Для KMM стабилизировали Gradle DSL, много улучшений для Kotlin/Native, Kotlin/Wasm
👉 Поддержка последних версий Android Gradle Plugin
👉 Обновили документацию
Полный список изменений тут
А еще сегодня (28.11) в 7 вечера будет прямая трансляция с разработчиками на тему этого обновления тут
Кратко:
👉 Новые экспериментальные фичи. Guard условие в
when
, break
и continue
не только для своего скоупа. Разбирали частично ---> тут👉 Обновили K2 компилятор и сделали ряд улучшенией в
kapt
👉 Для KMM стабилизировали Gradle DSL, много улучшений для Kotlin/Native, Kotlin/Wasm
👉 Поддержка последних версий Android Gradle Plugin
👉 Обновили документацию
Полный список изменений тут
А еще сегодня (28.11) в 7 вечера будет прямая трансляция с разработчиками на тему этого обновления тут
Хорошая тренировка вместо одинаковых задач на литкоде. А если ваши знакомые хотят попробовать Kotlin, то и отличный интенсив для них
Решаете свой advent of code и хотите посоревноваться? Кидайте ссылки в комменты!
Please open Telegram to view this post
VIEW IN TELEGRAM
Ультимативный гайд по написанию чисто кода на Jetpack Compose
Тут приведу основные идеи для
👉 Именование функций должно быть в PascalCase
👉 Последовательность параметров
Официальная рекомендация Android: Обязательные параметры ->
Более удобная версия:
👉 Либо возвращаем значение из
👉 У каждой функции не более 1 лайаута и при этом она должна быть независимой от места вызова
👉 Каждая функция должна содержать единственный
👉 Старайтесь не держать собственное состояние внутри каждой
👉 Используйте отступы предоставляемые через
Примеры как надо ✅ и как не надо ❌ читаем тут
Тут приведу основные идеи для
@Composable
функции, а то как надо и как не надо можно увидеть внутри статьи:👉 Именование функций должно быть в PascalCase
👉 Последовательность параметров
Официальная рекомендация Android: Обязательные параметры ->
Modifier
-> необязательные параметры -> Вложенная @Composable
Более удобная версия:
Modifier
-> входные данные -> UI параметры -> калбеки -> Вложенная @Composable
👉 Либо возвращаем значение из
@Composable
, либо что-то рисуем. Но не одновременно👉 У каждой функции не более 1 лайаута и при этом она должна быть независимой от места вызова
👉 Каждая функция должна содержать единственный
Modifier
со стандартным значением👉 Старайтесь не держать собственное состояние внутри каждой
@Composable
функции👉 Используйте отступы предоставляемые через
Scaffold
Примеры как надо ✅ и как не надо ❌ читаем тут
Android Good Reads
А вот и релиз! Кратко: 👉 Новые экспериментальные фичи. Guard условие в when, break и continue не только для своего скоупа. Разбирали частично ---> тут 👉 Обновили K2 компилятор и сделали ряд улучшенией в kapt 👉 Для KMM стабилизировали Gradle DSL, много улучшений…
И еще раз про Guard Conditions
👉 Как включить:
1) Проверяем что стоит галочка напротив
2) Добавляем
👉 Без guard conditions:
👉 С guard conditions:
👉 Как включить:
1) Проверяем что стоит галочка напротив
Enable K2 mode
в IDE2) Добавляем
compileOptions
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xwhen-guards")
}
}
👉 Без guard conditions:
when (response) {
is HttpResult.Success -> println("Success")
is HttpResult.Failed -> {
if (response.statusCode == 503) {
println("Maintenance")
} else {
println("Failed with code ${response.statusCode}")
}
}
}
👉 С guard conditions:
when (response) {
is HttpResult.Success -> println("Success")
is HttpResult.Failed if response.statusCode == 503 -> println("Maintenance")
is HttpResult.Failed -> println("Failed with code ${response.statusCode}")
}
JetBrains исследует возможность добавить Hot Reload в Compose
https://github.com/JetBrains/compose-hot-reload (404⭐️ )
Мультиплатформа тоже поддерживается, судя по всему. Для сборки используеть отдельная версия официальных плагинов (
5 минутное видео с демонстрацией от одного из разработчиков
https://github.com/JetBrains/compose-hot-reload (404
Мультиплатформа тоже поддерживается, судя по всему. Для сборки используеть отдельная версия официальных плагинов (
2.1.0-firework.31
) и сам плагин для hot-reload
5 минутное видео с демонстрацией от одного из разработчиков
Please open Telegram to view this post
VIEW IN TELEGRAM
MVI + Jetpack Compose что и как!
Внутри статьи немного теории о том как устроен
👉
👉 Compose навигация
Справедливо заметили, что при таком подходе,
Финальный вид приложения можно посмотреть на GitHub
Внутри статьи немного теории о том как устроен
MVI
(Model
- View
- Intent
) и как он работает.👉
Hilt
, KSP
, Room
и ViewModel
как основа для проекта👉 Compose навигация
Справедливо заметили, что при таком подходе,
ViewModel
должна отвечать только за управление состоянием, а разруливание корутин должно уйти на слой ниже, в репозиторий. Финальный вид приложения можно посмотреть на GitHub
Ищем блокирующие вызовы из неблокирующих потоков
Проверяем приложение на наличие блокирующих вызовов. Они могут вызывать микрофриз, а не краш приложения. С библиотекой BlackHound (1400⭐️ +) вы получите полноценной краш-репорт. Используется вместе с отладочной тулзой для корутин:
Пример:
Краш:
Фикс:
Проверяем приложение на наличие блокирующих вызовов. Они могут вызывать микрофриз, а не краш приложения. С библиотекой BlackHound (1400
org.jetbrains.kotlinx:kotlinx-coroutines-debug
Пример:
import reactor.blockhound.BlockHound
import kotlinx.coroutines.debug.CoroutinesBlockHoundIntegration
import kotlinx.coroutines.*
fun main() {
BlockHound.install(CoroutinesBlockHoundIntegration())
runBlocking {
launch(Dispatchers.Default) {
Thread.sleep(1000) // Краш
}
}
}
Краш:
Exception in thread "main" reactor.blockhound.BlockingOperationError: Blocking call! java.lang.Thread.sleep
at java.base/java.lang.Thread.sleep(Thread.java)
at MainKt$main$1$1.invokeSuspend(Main.kt:12)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
Фикс:
import reactor.blockhound.BlockHound
import kotlinx.coroutines.debug.CoroutinesBlockHoundIntegration
import kotlinx.coroutines.*
fun main() {
BlockHound.install(CoroutinesBlockHoundIntegration())
val droidgr = Dispatchers.IO.limitedParallelism(10) // или просто Dispatchers.IO
runBlocking {
launch(droidgr) {
Thread.sleep(1000) // OK
}
}
}
Please open Telegram to view this post
VIEW IN TELEGRAM
Записи докладов с droidcon London 2024 про Compose:
- A Snapshot Preview of Paparazzi 2.0
- Building State Holders in Compose with Molecule: A New Approach to Reusable UI Components
- Crafting Narratives: Shaping TalkBack with Compose Semantics
- Creating a Custom Compose Layout, Step-by-Step
- Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets
- Deep Dive into the Compose Compiler
- Designing scalable Compose APIs
- Hardware development with KMP and Compose Desktop
- Jetpack Compose: Drawing without pain and recomposition
- Navigation in a Multiplatform World: Choosing the Right Framework for your App
- Personalizing Accessibility
- Project Sparkles: how Compose is changing Android Studio
- Streamlining Permission Request in Jetpack Compose
- Tap it! Shake it! Fling it! Sheep it! - The Gesture Animations Dance!
- Text in Compose: Beyond the Basics
- What’s New in Compose Multiplatform - A Live Tour
- Why is adaptive layout a nightmare?
- A Snapshot Preview of Paparazzi 2.0
- Building State Holders in Compose with Molecule: A New Approach to Reusable UI Components
- Crafting Narratives: Shaping TalkBack with Compose Semantics
- Creating a Custom Compose Layout, Step-by-Step
- Cutting-Edge-to-Edge in Android 15: Using Previews and Testing in Jetpack Compose to Manage Insets
- Deep Dive into the Compose Compiler
- Designing scalable Compose APIs
- Hardware development with KMP and Compose Desktop
- Jetpack Compose: Drawing without pain and recomposition
- Navigation in a Multiplatform World: Choosing the Right Framework for your App
- Personalizing Accessibility
- Project Sparkles: how Compose is changing Android Studio
- Streamlining Permission Request in Jetpack Compose
- Tap it! Shake it! Fling it! Sheep it! - The Gesture Animations Dance!
- Text in Compose: Beyond the Basics
- What’s New in Compose Multiplatform - A Live Tour
- Why is adaptive layout a nightmare?
Начиная с версии Android 16 гугл планирует сокращать User-Agent
Итоговое значение будет выглядеть вот так:
Итоговое значение будет выглядеть вот так:
Mozilla/5.0 (Linux; Android 10; K; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/125.000 Mobile Safari/537.36