Мобильная разработка Мобильная разработка: карта трека и чем мобильный контекст отличается
0%

Мобильная разработка: карта трека и чем мобильный контекст отличается

Мобильная разработка: карта трека и чем мобильный контекст отличается

Что такое мобильное приложение с точки зрения инженера

Определение «программа для телефона» бесполезно: оно описывает форм-фактор, а не работу. Работающее определение звучит так.

Мобильное приложение — это долгоживущий процесс на чужом устройстве, который делит дефицитные ресурсы с другими приложениями, работает в сети без гарантий, исполняется под управлением ОС, имеющей право убить его в любой момент, и обновляется только через посредника, способного отказать в публикации.

Каждая часть определения порождает класс задач, которых нет ни в бэкенде, ни в вебе:

  • Долгоживущий процесс на чужом устройстве. Приложение не перезапускается с каждым запросом: утечка памяти проявится через час использования, а не на следующем реквесте. Состояние копится, и его надо уметь и сохранять, и выбрасывать.
  • Дефицитные общие ресурсы. Батарея не пополняется по вашему запросу, память делится с десятком приложений, CPU троттлится при нагреве. Вы не «оптимизируете производительность» — вы тратите чужой бюджет, и пользователь видит цифру расхода в настройках рядом с названием вашего приложения.
  • Сеть без гарантий. Не «есть интернет или нет», а весь спектр между: 3G в метро, Wi-Fi, отвечающий на ping, но не пропускающий TLS, переключение сети посреди загрузки.
  • ОС как арбитр. Система решает, когда усыпить процесс, когда разбудить, сколько секунд дать на фоновую работу и стоит ли убить приложение ради того, что сейчас на экране.
  • Посредник в дистрибуции. Между git push и пользователем — подпись, ревью, поэтапная раскатка и главное: откатить уже установленную версию невозможно.

Если из веба вы приносите привычку «сервер починит», мобильная разработка её отбирает: код уже на устройствах, и до конца их жизни вы живёте с тем, что выпустили. Трек — про платформу, клиентскую инженерию и эксплуатацию мобильного продукта; принципы клиентской архитектуры пересекаются с треком Frontend, но именно пересекаются, а не совпадают — ниже видно, почему.

Откуда взялись сегодняшние правила

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

Три вывода на весь трек. Ограничения ОС растут, а не ослабевают: каждая версия iOS и Android отбирает возможности у фона, трекинга и доступа к данным — закладывайте архитектуру, которая переживёт очередное ужесточение. Кроссплатформа не победила и не проиграла: она заняла нишу, и спор «натив против Flutter» без контекста продукта бессмыслен. Декларативный UI победил везде: SwiftUI, Compose, Flutter и React Native описывают интерфейс как функцию состояния, и навык переносится между стеками почти целиком.

Стек: где живёт ваш код и кто им управляет

Мобильный стек и внешние среды: сеть и канал доставки

Ценность схемы диагностическая: симптом «приложение тормозит» лечится по-разному на разных слоях.

Слой Типичный симптом Чем смотреть
Код приложения лишние перерисовки списка, синхронный I/O в главном потоке Layout Inspector, SwiftUI _printChanges, Time Profiler
Фреймворк и рантайм долгая инициализация, GC-паузы, забитый JS-поток Perfetto, Instruments, профайлер Hermes
SDK платформы системный диалог блокирует поток, депрекейт API системные логи, os_signpost, systrace
ОС-арбитр приложение убито в фоне, задача не выполнялась сутки Android vitals, dumpsys, MetricKit, Battery Historian
Железо троттлинг после трёх минут, разброс устройств в 10 раз Firebase Test Lab, Xcode Organizer по моделям

Правило то же, что в любой оптимизации: чем ниже слой, тем дороже ошибка и тем меньше рычагов, поэтому начинают сверху — с вопроса «нужна ли эта работа вообще». Как ОС раздаёт CPU и память, разобрано в статье про процессы и планирование; здесь важно следствие: вы конкурируете за ресурсы, а не владеете ими.

Отличие первое: энергия — чужой ресурс, который нельзя пополнить

В вебе энергопотребление не метрика продукта. В мобильном это метрика, которую пользователь видит по имени вашего приложения в настройках батареи и по которой удаляет приложения. Ключевая интуиция: энергия — площадь под графиком мощности во времени, а не «количество запросов». Самый дорогой компонент почти всегда радиомодем, и дорог он не в момент передачи, а после неё.

Хвост радиомодема: почему шесть мелких запросов дороже одного батча

Сотовый модем не выключается сразу после последнего байта: он держит канал ещё несколько секунд, чтобы не платить полсекунды на повторное соединение. Этот tail energy и есть главный источник расхода. Отсюда правила, одинаковые для всех стеков: батчите сеть (десять аналитических событий в минуту — десять пробуждений модема, одна отправка раз в 15 минут по Wi-Fi — одно); отдавайте планирование системе, потому что WorkManager и BGTaskScheduler склеивают задачи разных приложений в общие окна пробуждения, а свой таймер этого не умеет; не опрашивайте, а подписывайтесь — polling каждые 30 секунд убивает батарею, пуш будит устройство только когда есть новость; помните, что непрерывный GPS стоит единицы процентов заряда в час, а значимые изменения местоположения и геозоны — доли процента.

// Android: синхронизацию планирует система, а не вы
val sync = PeriodicWorkRequestBuilder<SyncWorker>(6, TimeUnit.HOURS)
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.UNMETERED) // только не-мобильный трафик
            .setRequiresBatteryNotLow(true)                // не добиваем севшую батарею
            .build()
    )
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
    .build()

// KEEP не плодит дубли задачи при каждом запуске приложения
WorkManager.getInstance(context)
    .enqueueUniquePeriodicWork("catalog-sync", ExistingPeriodicWorkPolicy.KEEP, sync)
// iOS: та же идея — просим систему разбудить нас, когда ей удобно
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.example.shop.refresh", using: nil) { task in
    task.expirationHandler = { SyncCoordinator.shared.cancel() } // окно измеряется секундами
    Task {
        let ok = await SyncCoordinator.shared.refreshCatalog()
        task.setTaskCompleted(success: ok)
        scheduleNextRefresh() // повторную заявку подаём сами, автоповтора нет
    }
}

Общий мотив: вы не запускаете фоновую работу — вы подаёте заявку, а система решает, когда и запустит ли вообще. Квоты и ограничения — в статье про фоновую работу и пуши.

Отличие второе: сеть, которая врёт

Веб-страница при потере сети просто не открывается — и это честно. Мобильное приложение открывается всегда и обязано вести себя разумно в четырёх состояниях сети, а не в двух.

Состояние Что видит код Что должно делать приложение
Быстрая сеть RTT 20–60 мс грузить нормально, префетчить осторожно
Медленная сеть RTT 200–600 мс, потери показать локальные данные сразу, догрузить в фоне
lie-fi соединение есть, ответов нет жёсткий таймаут 10–15 с и переход в оффлайн-режим
Оффлайн системный флаг «нет сети» полноценный экран из локальной БД, запись в очередь

Худшее из этого — lie-fi: капча отельного Wi-Fi, забитая базовая станция, NAT, молча выбросивший ваше соединение. Флаг «подключено» есть, данные не идут, а приложение, верящее флагу, показывает вечный спиннер. Второе, что ломается только на мобильном: сеть меняется под ногами. Пользователь вышел из дома — Wi-Fi сменился на LTE, IP изменился, все открытые TCP-соединения оборваны. Отсюда любовь мобильных команд к QUIC с миграцией соединений и к идемпотентным запросам.

// iOS: конфигурация сессии под мобильную реальность
let config = URLSessionConfiguration.default
config.waitsForConnectivity = true            // ждём появления сети, а не падаем сразу
config.timeoutIntervalForRequest = 15         // висеть дольше на плохой сети бессмысленно
config.timeoutIntervalForResource = 120       // общий бюджет на ресурс вместе с ретраями
config.allowsExpensiveNetworkAccess = false   // фоновую синхронизацию не тянем через сотовую
let session = URLSession(configuration: config)
// React Native: «подключено» и «интернет работает» — разные вещи
NetInfo.addEventListener(state => {
  // isConnected говорит про линк, isInternetReachable — про реальную достижимость
  syncQueue.setOnline(state.isConnected === true && state.isInternetReachable === true);
  // на дорогой сети шлём только действия пользователя, аналитику копим до Wi-Fi
  syncQueue.setMetered(state.details?.isConnectionExpensive === true);
});

Каждый ретрай идёт с экспоненциальной задержкой, джиттером и ключом идемпотентности, иначе двойное списание — вопрос времени. Механика разобрана в статье про идемпотентность и гарантии доставки: мобильный клиент — это просто ещё один ненадёжный узел распределённой системы, с самой плохой связью из всех.

Отличие третье: оффлайн — не режим, а состояние по умолчанию

Правильная модель: локальная база — источник правды для UI, сеть — механизм её обновления. Не наоборот. Экран читает из локального хранилища и рисуется за миллисекунды, синхронизация идёт отдельно и обновляет то же хранилище. Запись работает симметрично, через outbox: изменение сразу применяется локально и одновременно кладётся в очередь на отправку.

Три ошибки первого оффлайн-проекта. Очередь в памяти: процесс убили — операции пропали, поэтому очередь обязана лежать в БД, а не в списке внутри ViewModel. Нет ключа идемпотентности: ретрай после таймаута создаёт второй заказ, а UUID должен генерировать клиент до отправки, а не сервер после. «Побеждает последний»: last-write-wins тихо теряет данные и в деньгах или документах недопустим.

Локальное хранилище на обеих платформах — почти всегда SQLite (Room и SQLDelight на Android, Core Data, SwiftData и GRDB на iOS, Drift и Isar во Flutter). Свойства и подводные камни встраиваемых БД — в статье про SQLite, стратегии синхронизации и разрешения конфликтов — в данных и оффлайне.

Отличие четвёртое: разрешения и приватность

В вебе разрешения — исключение. В мобильном это основной способ взаимодействия приложения с миром, и отозвать их можно в любой момент, не убивая приложение. Три правила, за нарушение которых наказывают и сторы, и пользователи. Спрашивать в момент нужды, а не на старте: экран приветствия с пятью системными диалогами подряд — гарантированный отказ. Показывать «зачем» до системного диалога: системный запрос выдаётся один раз, отказ отправляет пользователя в настройки навсегда, и свой экран-объяснение перед ним поднимает согласие в разы. Деградировать, а не ломаться: нет геолокации — ручной ввод адреса, нет камеры — выбор из галереи; приложение, падающее на отказе, отклонит ревью.

// iOS: ветвление по статусу, а не «просто спросить»
func requestNearby(_ manager: CLLocationManager) {
    switch manager.authorizationStatus {
    case .notDetermined:
        manager.requestWhenInUseAuthorization()   // первый и единственный шанс
    case .denied, .restricted:
        showManualCityPicker()                    // не уговариваем, даём альтернативу
    case .authorizedWhenInUse, .authorizedAlways:
        manager.requestLocation()
    @unknown default:
        showManualCityPicker()                    // новые статусы появляются каждый год
    }
}
// Android: три ветки — уже есть, надо объяснить, надо спросить
private val requestLocation = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { granted -> if (granted) viewModel.onGranted() else viewModel.onDenied() }

private fun onNearbyClicked() = when {
    checkSelfPermission(ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED -> viewModel.onGranted()
    // система сама подсказывает, что пользователь уже отказывал: нужен экран-объяснение
    shouldShowRequestPermissionRationale(ACCESS_COARSE_LOCATION) ->
        showRationale { requestLocation.launch(ACCESS_COARSE_LOCATION) }
    else -> requestLocation.launch(ACCESS_COARSE_LOCATION)
}

Отдельный слой — декларативная приватность. Apple требует privacy manifest с описанием собираемых данных и причин обращения к чувствительным API, а запрос рекламного идентификатора идёт через отдельный диалог App Tracking Transparency. Google Play требует раздел Data Safety, соответствующий фактическому поведению приложения. Расхождение декларации и реальности — не замечание, а снятие с публикации; детали в платформах и релизе и сторах.

Отличие пятое: процесс, который убивают

Самое неочевидное для пришедших из веба и бэкенда: ОС убивает ваш процесс без предупреждения и без возможности возразить. Не «может в теории» — убивает регулярно, как штатную операцию: пользователь свернул приложение, открыл камеру, системе понадобилась память.

Терминология разная, суть одна:

Что происходит iOS Android
Экран виден и активен active RESUMED
Частично перекрыт inactive STARTED, не RESUMED
Ушёл в фон background STOPPED
Исполнение остановлено suspended процесс кэширован
Убит системой jetsam low-memory killer
Что переживает смерть NSUserActivity, state restoration SavedStateHandle, onSaveInstanceState

Вывод один и очень жёсткий: любое состояние, которое нельзя восстановить с сервера или из локальной БД, должно быть записано на диск до ухода в фон. Наполовину заполненная форма, позиция скролла, черновик сообщения теряются, если живут только в объекте в куче.

// Android: SavedStateHandle переживает смерть процесса, обычное поле — нет
class CheckoutViewModel(repo: OrderRepository, private val state: SavedStateHandle) : ViewModel() {
    var promoCode: String                       // сохраняется в Bundle и восстанавливается
        get() = state["promo"] ?: ""
        set(value) { state["promo"] = value }
    // подписка живёт, только пока экран виден: WhileSubscribed отменяет её в фоне
    val cart: StateFlow<Cart> = repo.observeCart()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), Cart.EMPTY)
}
// iOS: единая точка реакции на смену фазы сцены
@main
struct ShopApp: App {
    @Environment(\.scenePhase) private var scenePhase
    var body: some Scene {
        WindowGroup { CatalogView() }
            .onChange(of: scenePhase) { _, phase in
                switch phase {
                case .active:     SyncCoordinator.shared.resume()      // экран виден
                case .inactive:   SyncCoordinator.shared.pauseTimers() // звонок или свитчер
                case .background: SyncCoordinator.shared.flushToDisk() // счёт идёт на секунды
                @unknown default: break
                }
            }
    }
}

Кроссплатформенные стеки пробрасывают тот же контракт: во Flutter это didChangeAppLifecycleState с состояниями resumed, inactive, paused, hidden, detached, в React Native — AppState.addEventListener('change', ...). Заведите в команде проверку с первого спринта: на Android включите «Не сохранять действия» в настройках разработчика, на iOS убивайте приложение из Xcode при свёрнутом состоянии. Если после возврата пользователь оказался на главном экране с пустой формой — это баг, который в проде выглядит как «приложение всё теряет».

Отличие шестое: релиз через посредника

В вебе плохой релиз откатывается за минуты. В мобильном не откатывается вовсе: у пользователя стоит конкретная версия, и удалить её оттуда вы не можете.

Три следствия закладывают в архитектуру до первого релиза, а не после первого инцидента. Фича-флаги и удалённый конфиг обязательны — единственный способ откатить поведение без нового ревью; флаг читается из локального кэша, чтобы работать оффлайн, и имеет безопасное значение по умолчанию. Механизм принудительного обновления: сервер должен уметь сказать старому клиенту «дальше нельзя», иначе древние версии будут стучаться в API годами. Версионирование API с расчётом на долгий хвост: через год заметная доля пользователей сидит на старых сборках, и ломающее изменение контракта ломает их всех.

Аспект Веб Мобильное приложение
Выкатка минуты, свои правила часы или дни, ревью посредника
Откат предыдущий билд за минуту невозможен, только флаг или новая версия
Версии в проде одна все, что вы когда-либо выпускали
Кто может запретить релиз вы сами Apple, Google, регулятор
Диагностика у пользователя DevTools открыты всем только ваша телеметрия и крэш-репорты

Общие практики поэтапных выкаток разобраны в стратегиях доставки — в мобильном они те же по смыслу, но без права на быстрый откат.

Нативная или кроссплатформа: честный разбор

Самый частый и самый плохо задаваемый вопрос индустрии. Плохо — потому что спрашивают «что лучше», а отвечать можно только на «что дешевле для этого продукта, при этой команде и на этом горизонте».

Что реально шарится, а что нет

Иллюзия «пишем один раз» разбивается о факт: общий код — это логика, а не приложение. Всё платформенное придётся делать дважды в любом стеке.

Часть работы Два натива Flutter или React Native KMP с нативным UI
Экраны и навигация дважды один раз дважды
Доменная логика, модели, сеть, кэш, БД дважды один раз один раз
Пуши, deep links, разрешения, биометрия дважды дважды плюс отладка моста дважды
Виджеты, часы, системные интеграции дважды дважды, часто нативно дважды
Сборка, подпись, сторы, ревью дважды дважды дважды
Дизайн, продукт, бэкенд, аналитика один раз один раз один раз

Реалистичная оценка: кроссплатформа экономит 25–40% усилий клиентской команды, а не 50%, и почти ничего не экономит на релизном цикле и работе со сторами. Экономия тем выше, чем больше в продукте типовых экранов со списками и формами, и тем ниже, чем больше системных интеграций.

Стоимость: полная, а не только разработка

Статья Натив Flutter React Native KMP
Старт разработки медленнее, две кодовые базы быстро быстро при React-опыте средне
Поддержка через два года предсказуемая зависит от плагинов зависит от плагинов и мостов предсказуемая
Новая версия ОС осенью доступна сразу ждёте фреймворк ждёте библиотеки UI сразу, логика не затронута
Отладка «странных» багов инструменты платформы плюс слой фреймворка плюс мост и JS-поток инструменты платформы

Ключевая мысль: дешёвый старт и дешёвая поддержка — разные оси. Кроссплатформа выигрывает старт почти всегда; поддержку — только там, где продукт остаётся в рамках типовых экранов.

Качество: механика важнее маркетинга

  • Flutter рисует свои виджеты собственным рендерером (Impeller, ранее Skia). Плюс — пиксель в пиксель одинаково везде и полный контроль анимаций. Минус — это не системные элементы управления: выделение текста, специальные возможности, системные меню и клавиатурные тонкости приходится догонять, а обновление ОС может незаметно разойтись с вашим отрисованным UI.
  • React Native после перехода на новую архитектуру (Fabric, TurboModules, JSI, Hermes) рендерит настоящие нативные вью, и внешне это натив. Цена — отдельный JS-поток: тяжёлая логика в нём даёт задержки ввода, а сложные жесты требуют аккуратной работы с Reanimated и Gesture Handler.
  • Kotlin Multiplatform делит только логику: сеть, БД, модели, валидацию; UI остаётся нативным на обеих платформах. Максимальное качество интерфейса при меньшей доле общего кода; Compose Multiplatform поднимает эту долю, приближая компромисс к Flutter.
  • Натив — потолок по качеству и доступу к платформе по определению, потому что это и есть платформа. Всё новое появляется здесь первым.

Найм: то, о чём забывают на этапе выбора

  • iOS и Android разработчики — самый предсказуемый рынок: понятно, как собеседовать, понятен грейд, легко заменить человека.
  • Flutter — растущий, но перекошенный пул: много джунов, мало тех, кто чинил сложные проблемы производительности и писал платформенные каналы.
  • React Native — пул велик за счёт React-разработчиков, и это ловушка: веб-React-разработчик не равен мобильному инженеру, он не знает про жизненный цикл процесса, разрешения, сторы, фон и энергию — то есть ровно про всё, о чём эта статья.
  • KMP — самый узкий рынок: нужен человек, сильный в Kotlin и понимающий iOS. Зато такие люди обычно сильные. Отдельный фактор — автобусный номер: один энтузиаст Flutter в компании, где остальные пишут натив, это риск, а не оптимизация.

Как выбирать

Начинайте с вопроса «сколько процентов экранов — это списки и формы?». Больше 80% — кроссплатформа почти наверняка окупится. Меньше 50% — считайте честно, экономия может оказаться отрицательной. Если продукт стоит на системных возможностях (камера, фон, NFC, аудио, ML на устройстве), кроссплатформа не сэкономит: эти части всё равно нативные.

Второе правило: решение обратимо только дорого. Переезд с кроссплатформы на натив — это переписывание клиента, обратно — тоже. Поэтому выбор стека — архитектурное решение уровня выбора базы данных, и его документируют как ADR, а не принимают по симпатии к языку. Разбор — в кроссплатформе.

Один экран на четырёх стеках

Насколько похожи современные декларативные подходы — на примере одного списка товаров:

// SwiftUI
struct CatalogView: View {
    @State private var model = CatalogModel()
    var body: some View {
        List(model.items) { ProductRow(product: $0) }
            .overlay { if model.isOffline { OfflineBanner() } }
            .task { await model.load() } // задача отменяется при уходе с экрана
    }
}
// Jetpack Compose
@Composable
fun CatalogScreen(vm: CatalogViewModel = viewModel()) {
    val state by vm.state.collectAsStateWithLifecycle() // отписка при уходе в фон
    Column {
        if (state.isOffline) OfflineBanner()
        LazyColumn { items(state.items, key = { it.id }) { ProductRow(it) } }
    }
}
// Flutter
class CatalogScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final state = context.watch<CatalogModel>();
    return Column(children: [
      if (state.isOffline) const OfflineBanner(),
      Expanded(child: ListView.builder(   // ленивый список, а не Column со всем сразу
        itemCount: state.items.length,
        itemBuilder: (_, i) => ProductRow(product: state.items[i]),
      )),
    ]);
  }
}
// React Native: тот же экран, обязательно на виртуализированном списке
export function CatalogScreen() {
  const { items, isOffline } = useCatalog();
  return (
    <View style={styles.root}>
      {isOffline && <OfflineBanner />}
      <FlatList data={items} keyExtractor={i => i.id} initialNumToRender={10}
        renderItem={({ item }) => <ProductRow product={item} />} />
    </View>
  );
}

Четыре стека, одна модель мышления: состояние сверху, декларативное описание, ленивый список, явная обработка оффлайна. Именно поэтому переход между стеками стоит недорого, а переход из веба в мобильный — дорого: сложность не в синтаксисе, а в контексте.

По каким метрикам судят мобильное приложение

Метрика Разумный ориентир Где смотреть
Холодный старт до интерактивности p90 меньше 1,5–2 с Macrobenchmark и Play Vitals, MetricKit и Instruments
Плавность доля проблемных кадров меньше 1% Perfetto, JankStats, hitch-метрики MetricKit
ANR на Android ниже порога Play: 0,47% пользовательских сессий Android vitals
Сессии без крэшей выше 99,5% Crashlytics, Sentry, Xcode Organizer
Размер загрузки и расход батареи меньше мегабайт, нет лишних пробуждений Play Console, App Store Connect, Battery Historian
Оффлайн-готовность первый экран рисуется без сети проверки в авиарежиме и на lie-fi

Два числа стоит запомнить отдельно. Google Play помечает приложение как проблемное при доле пользовательских ANR от 0,47% и крэшей от 1,09% — превышение бьёт по видимости в сторе, а не только по совести. И исследование Google Play показывало, что каждые дополнительные 6 МБ размера установки стоят примерно 1% конверсии установки: прямая связь между решением «подключим ещё одну библиотеку» и деньгами продукта. Как связывать техническое с продуктовым — в статье про продуктовые метрики.

Типичные ошибки на входе в мобильную разработку

  1. Считать сеть надёжной. Спиннер без таймаута, ретрай без джиттера, запись без ключа идемпотентности.
  2. Держать состояние только в памяти. Работает на симуляторе, разваливается на реальном устройстве через день.
  3. Спрашивать все разрешения на старте. Конверсия падает, ревью придирается, пользователи отказывают навсегда.
  4. Тестировать на флагмане. Ваш телефон в 5–10 раз быстрее медианного устройства аудитории — держите бюджетный аппарат.
  5. Игнорировать размер приложения. Каждая «удобная» библиотека — мегабайты и просадка конверсии установки.
  6. Считать, что фон работает. Doze, App Standby, вендорские «оптимизаторы» и жёсткие окна iOS думают иначе.
  7. Выпускать без фича-флагов. Первый серьёзный баг покажет, что откатывать нечего.
  8. Переносить веб-паттерны буквально. Polling, тяжёлые WebView вместо экранов, скролл, ломающий системные жесты.
  9. Не читать гайдлайны сторов до разработки. Требования к приватности, платежам и удалению аккаунта проще заложить, чем переделать.

Карта трека

Трек состоит из четырёх блоков: платформа (как устроены обе ОС и их правила), клиентские стеки (натив, кроссплатформа, UI и навигация), инженерия приложения (архитектура, данные и оффлайн, фон и пуши) и эксплуатация (производительность, безопасность, тестирование, релиз). Порядок статей — от контекста к деталям и дальше к эксплуатации:

  1. Платформы: iOS и Android изнутри — устройство систем, песочница, форматы приложений, чем правила Apple отличаются от правил Google.
  2. Нативная iOS-разработка — Swift, SwiftUI и UIKit, жизненный цикл, инструменты, экосистема.
  3. Нативная Android-разработка — Kotlin, Compose, компоненты приложения, Jetpack, инструменты.
  4. Кроссплатформа — Flutter, React Native, KMP: механика, границы применимости, цена решения.
  5. Мобильный UI и навигация — паттерны навигации, жесты, адаптация под экраны.
  6. Архитектура мобильного приложения — слои, состояние, MVVM и MVI, внедрение зависимостей, модуляризация.
  7. Данные и оффлайн — локальные БД, стратегии синхронизации, разрешение конфликтов.
  8. Фоновая работа и пуши — ограничения ОС, планировщики, уведомления и их доставка.
  9. Производительность — холодный старт, плавность кадров, батарея, трафик, профилирование.
  10. Безопасность — хранение секретов, биометрия, пиннинг, защита от реверса.
  11. Тестирование — юнит и UI, устройства и фермы, бета-каналы.
  12. Релиз и сторы — подписи, ревью, поэтапная раскатка, аналитика и крэш-репортинг.

Смежные треки: архитектурные паттерны для слоёв и границ, тестирование для мобильных и совместимостных проверок, распределённые системы для синхронизации, TypeScript — если идёте в React Native. Материалы по сетям и безопасности на портале живут в отдельных треках; здесь мы берём из них только мобильную специфику.

Мини-итог

Мобильная разработка — не «фронтенд, только на телефоне». Это инженерия клиента, который живёт на чужом устройстве месяцами, тратит невосполнимую энергию, работает в сети без гарантий, может быть убит ОС в любой момент и обновляется лишь с разрешения посредника. Шесть отличий из этой статьи — энергия, сеть, оффлайн, разрешения, жизненный цикл процесса и релиз через стор — определяют почти все архитектурные решения дальше по треку. Выбор между нативом и кроссплатформой сводится к доле платформенной специфики в продукте и к тому, кого вы способны нанять и удержать. Практический старт: поставьте оба инструмента (Xcode и Android Studio), возьмите бюджетное физическое устройство, а первый учебный проект делайте оффлайн-первым и обязательно проверяйте его в авиарежиме и через сетевой троттлинг — большинство мобильных багов живут именно там.

Источники

Что дальше

Платформы: iOS и Android изнутри, отличия, ограничения, публикация — разберём, как устроены обе системы, что такое песочница на практике, из чего состоят .ipa и .aab, чем модель приложения на iOS отличается от компонентной модели Android и какие правила публикации реально влияют на архитектуру.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов