20 често задавани въпроса за интервю за Mobile Developer. Мобилната разработка е търсено направление за надеждни iOS, Android и кросплатформени приложения, продуктови задачи, производителност, релийзи, offline режим и интеграция с устройства. Въпросите покриват различни нива и можете да упражнявате устни отговори в нашия тренажор за интервюта.
1Каква е разликата между съхраняването на токен за автентикация в стандартно хранилище тип ключ-стойност като SharedPreferences или UserDefaults и използването на сигурно платформено хранилище като Android Keystore или iOS Keychain?
Стандартните механизми за съхранение като SharedPreferences в Android и UserDefaults в iOS са проектирани за леки, нечувствителни потребителски предпочитания. Те записват данните в некриптирани файлове в обикновен текст (XML или plist) в рамките на изолираната директория (sandbox) на приложението. Всеки с физически достъп до руутнато (rooted) или отключено (jailbroken) устройство, некриптирано резервно копие на устройството или достъп до файловата система може директно да прочете тези токени. За разлика от тях платформените сигурни хранилища — по-конкретно iOS Keychain и Android Keystore (често използван чрез EncryptedSharedPreferences) — предоставят криптиране на съхраняваните данни (data-at-rest encryption). iOS Keychain криптира съхранените елементи с ключове, обвързани с хардуера на устройството (като Secure Enclave), и позволява детайлни политики за достъп. Android Keystore генерира и съхранява криптографски ключове в хардуерно изолирани модули за сигурност (Trusted Execution Environment или StrongBox), гарантирайки, че криптографските ключове никога не се излагат в паметта на приложението и не могат да бъдат извлечени от файловата система.
// --- ANDROID ---
// INSECURE (Plain SharedPreferences):
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
prefs.edit().putString("auth_token", token).apply() // Plaintext XML on disk
// SECURE (EncryptedSharedPreferences backed by Android Keystore):
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val securePrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
securePrefs.edit().putString("auth_token", token).apply()
// --- IOS (Swift) ---
// INSECURE:
UserDefaults.standard.set(token, forKey: "auth_token") // Plain plist on disk
// SECURE (Keychain Services API):
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "user_auth_token",
kSecValueData as String: token.data(using: .utf8)!,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
]
SecItemAdd(query as CFDictionary, nil)
2Каква е основната цел на мобилен CI/CD (Continuous Integration/Continuous Delivery) конвейер и кои уникални етапи и ограничения го различават от конвейерите за внедряване в уеб или бекенд среди?
Основната цел на мобилен CI/CD конвейер е да автоматизира компилирането, тестването, подписването и разпространението на мобилни клиентски приложения с цел поддържане на постоянно качество и повторяеми издания. Непрекъснатата интеграция (CI) валидира промените в кода чрез автоматизирани компилации, статичен анализ и модулно тестване. Непрекъснатата доставка/внедряване (CD) пакетира, подписва и разпространява двоични артефакти към канали за тестване (като TestFlight или Firebase App Distribution) или магазини за приложения. Мобилният CI/CD се различава от уеб и бекенд конвейерите по няколко основни начина: 1. Тип артефакт: Компилациите създават компилирани клиентски двоични файлове (.ipa, .apk, .aab), вместо да се изпълняват директно на сървъри или в контейнерни изображения. 2. Хардуерни ограничения на средата за изпълнение: Компилирането за iOS изисква хардуер с macOS и инструменти от командния ред на Xcode, докато бекенд конвейерите обикновено работят върху леки Linux контейнери. 3. Време за издаване и одобрения: Публикуването за крайни потребители изисква прегледи от магазини за приложения на трети страни (Apple App Store / Google Play Store), което означава, че внедряванията не могат да бъдат върнати моментално чрез повторно внедряване на сървъра. Корекциите изискват изпращане на нов подписан двоичен файл или флагове за функционалности по време на изпълнение.
name: Mobile CI/CD Workflow
on:
pull_request:
branches: [main]
push:
tags:
- 'v*.*.*'
jobs:
ci_validation:
name: Lint & Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Linter
run: ./gradlew lint
- name: Run Unit Tests
run: ./gradlew testDebugUnitTest
cd_release_ios:
name: Build & Distribute iOS
needs: ci_validation
if: startsWith(github.ref, 'refs/tags/v')
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Install Signing Certificates
run: fastlane match appstore --readonly
- name: Build & Upload to TestFlight
run: fastlane ios beta
3Каква е целта на разделянето на презентационния, домейн и слоя за данни в кросплатформено мобилно приложение и как тази структура улеснява споделянето на код между iOS и Android?
Разделянето на приложението на презентационен слой, домейн слой и слой за данни създава ясни граници на отговорност: 1. **Презентационен слой:** Отговаря за рендерирането на потребителския интерфейс (UI), потребителския вход и състоянието на ниво екран (напр. Views, Widgets, ViewModels или Presenters). 2. **Домейн слой:** Капсулира чистата бизнес логика, домейн моделите и сценариите на използване. Той остава независим от UI и платформените рамки. 3. **Слой за данни:** Управлява извличането и съхранението на данни от отдалечени API, локални бази данни или кеш на устройството чрез хранилища (repositories) и източници на данни. В кросплатформената мобилна разработка тази структура улеснява споделянето на код, тъй като домейн слоят и слоят за данни са независими от платформата. Основните бизнес правила, трансформациите на данни и работата с мрежата могат да се споделят на 100% между iOS и Android, което позволява на екипите или да споделят целия стек (както при Flutter или React Native), или да споделят бизнес/данни логиката, като същевременно запазят специфичните за платформата нативни презентационни слоеве (както при Kotlin Multiplatform).
[Presentation Layer] (UI, Screens, ViewModels)
│
▼ calls
[Domain Layer] (Use Cases, Business Rules, Entities)
▲
│ implements interfaces
[Data Layer] (Repositories, API Clients, Local Storage)
4Какви са основните разлики между императивната и декларативната парадигма за разработка на потребителски интерфейс (UI) при мобилните приложения и как уиджетите във Flutter и компонентите в React Native въплъщават декларативния модел?
При императивната разработка на потребителски интерфейс разработчиците пишат явни, последователни инструкции за създаване, модифициране и унищожаване на UI елементи (например намиране на изглед по идентификатор и директно извикване на методи като setText или setVisibility). За разлика от това, декларативните UI модели описват как трябва да изглежда интерфейсът за дадено състояние (често изразено като UI = f(state)). Когато състоянието се промени, рамката определя как ефективно да актуализира визуалното представяне. Flutter и React Native въплъщават тази декларативна парадигма. Във Flutter уиджетите са неизменяеми описания на конфигурацията; когато динамичните данни се променят, извикването на setState() в StatefulWidget насрочва повторно изграждане, при което методът build() връща ново описание на дървото от уиджети, което Flutter съгласува със своите дървета от Element и RenderObject. В React Native компонентите са функции или класове, които връщат JSX; актуализирането на състоянието (state) или свойствата (props) задейства повторно рендиране, при което React съгласува виртуалното дърво от елементи и прилага минимално необходимите нативни промени през моста (bridge) или нативната среда за изпълнение.
5Каква е оперативната разлика между студен старт (cold start), топъл старт (warm start) и горещ старт (hot start) в мобилните приложения и защо студеният старт е най-критичната метрика за бюджетите за производителност?
В жизнения цикъл на мобилните приложения състоянията на стартиране се различават в зависимост от това дали процесът на приложението и състоянието на паметта вече съществуват в операционната система (ОС):
- Студен старт (Cold Start): Приложението стартира от нулата, защото ОС не е създала негов процес или процесът е бил предварително прекратен. ОС трябва да задели нов процес, да зареди двоичните файлове и средата за изпълнение, да инициализира Application/runtime контекста и да визуализира първия изглед. Това състояние има най-голямо забавяне.
- Топъл старт (Warm Start): Процесът на приложението обикновено вече се намира в паметта, но йерархията от изгледи или активността е била унищожена или освободена (например поради пресъздаване във фонов режим или навигация назад). ОС пресъздава потребителския интерфейс/активността, без да се налага да създава нов процес от нулата.
- Горещ старт (Hot Start): Приложението и състоянието на неговия потребителски интерфейс все още се намират изцяло в паметта (например потребителят е превключил към начален екран и веднага се е върнал). ОС просто извежда съществуващата йерархия от изгледи на преден план с почти нулев разход за инициализация.
Студеният старт е най-критичната метрика за бюджетите за производителност, тъй като представлява най-бавното първо впечатление за потребителите. Продължителните студени стартове са пряко свързани с незабавно напускане от страна на потребителите, по-ниско задържане и по-лошо класиране в магазините за приложения (като например преминаване на праговете в Android Vitals).
6Какво представлява нативният мост (native bridge) в кросплатформено мобилно приложение и кога бихте го използвали, вместо да напишете функционалността изцяло на Flutter или React Native?
Нативният мост е комуникационният слой, който позволява на кросплатформения код да извиква специфичен за платформата код на iOS или Android, а на нативния код – да връща резултати или да изпраща събития обратно. Във Flutter това обикновено се прави с platform channels или плъгини. В React Native обикновено се реализира чрез нативни модули, TurboModules или нативни UI компоненти. Използвате мост, когато дадена функционалност изисква нещо, до което чистият Flutter или React Native код няма достатъчно добър достъп – като платформено API, хардуерна възможност на устройството, нативен SDK (Software Development Kit), критична за производителността нативна имплементация или нативен UI компонент. Често срещани примери включват работа с камерата, Bluetooth, push известия, плащания, защитено хранилище, фонови услуги, здравни API или SDK пакети, които предоставят интеграция само за Swift/Objective-C или Kotlin/Java. Добрият мост обикновено предоставя малък и ясен интерфейс към кросплатформения слой, като getBatteryLevel, startBluetoothScan или openNativePaymentSheet, докато специфичната за платформата имплементация обработва реалните детайли за iOS и Android.
7Какво означава архитектура с приоритет на локалната работа (offline-first) в мобилната разработка и по какво се различава фундаментално от стандартното кеширане на HTTP отговори?
В мобилната разработка архитектурата с приоритет на локалната работа (offline-first) третира локалното хранилище като основен източник на истина както за операциите по четене, така и за тези по запис. Вместо да се изчаква завършването на мрежовите заявки преди визуализиране или допускане на потребителски взаимодействия, приложението взаимодейства директно с локалната база данни или слоя за съхранение, докато фонови процеси по синхронизация се грижат за съгласуването на локалните промени с отдалечения сървър, когато има връзка. Това се различава фундаментално от стандартното кеширане на HTTP отговори по няколко начина: 1. **Модел на данните**: HTTP кеширането съхранява необработени мрежови отговори (например JSON съдържание), индексирани по URL на заявката и хедъри. Offline-first архитектурата съхранява структурирани домейн същности в локална база данни (като Room, SQLite или SwiftData/Core Data). 2. **Записи срещу четения**: HTTP кеширането е основно механизъм за оптимизация на четенето и не поддържа нативно локални транзакции за запис или модификации. Offline-first позволява пълноценни локални записи, като нарежда модификациите на опашка за по-късна синхронизация със сървъра. 3. **Възможност за заявки и жизнен цикъл**: Кешираните HTTP отговори подлежат на автоматични политики за изчистване на кеша и не могат да бъдат свободно филтрирани, обединявани или заявявани. Offline-first съхранението предоставя пълни възможности за заявки, индексиране и детерминирано управление на жизнения цикъл независимо от състоянието на мрежата.
/* Network-First with HTTP Caching */
UI -> Network Request -> [Cache Hit ? Return Cached JSON : Fetch from API -> Save to HTTP Cache] -> UI
// Limitation: Read-only optimization; offline writes fail immediately.
/* Offline-First with Local Source of Truth */
UI <-> Observes Local DB (e.g., Room / Core Data / SQLite)
User Write -> Write to Local DB -> Queue Sync Task
Sync Worker (Background) -> Send queued mutations to Server -> Update Local DB with Server Response
8Каква е разликата между отдалечено push известие и локално известие в мобилно приложение?
Основната разлика между локалните и отдалечените push известия се състои в техния произход и начина, по който се задействат:
1. Локалните известия се създават, планират и задействат изцяло на устройството от самото приложение чрез програмни интерфейси на операционната система (като UserNotifications в iOS или AlarmManager/WorkManager/NotificationManager в Android). Те се задействат въз основа на условия от страна на устройството, като конкретна дата/час, таймер за обратно отброяване или географска граница (geofencing). Тъй като се изпълняват локално от фоновия процес (daemon) на операционната система, те не изискват бекенд сървър или активна интернет връзка в момента на доставяне.
2. Отдалечените push известия произхождат от външен сървър на приложението и се предават през интернет чрез шлюзовете за push известия на съответната платформа (APNs за устройства на Apple, FCM за Android). Шлюзът доставя полезния товар (payload) до фоновия процес на ниво операционна система на устройството, като събужда приложението или показва банер. Отдалечените известия са необходими за външни събития в реално време, като входящи чат съобщения, заявки за приятелство или извънредни новини.
let content = UNMutableNotificationContent()
content.title = "Workout Reminder"
content.body = "Time for your daily exercise routine."
content.sound = .default
// Triggers locally after 1 hour (3600 seconds) without any server interaction
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3600, repeats: false)
let request = UNNotificationRequest(identifier: "dailyWorkout", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Failed to schedule notification: \(error)")
}
}
9Каква е структурната и оперативната разлика между Android App Bundle (.aab), APK (Android Package) и архивен файл за iOS IPA (.ipa) при разпространение на приложения през магазини за приложения?
APK (Android Package) е формат за изпълним пакет, съдържащ компилиран DEX байткод, ресурси, асети и нативни библиотеки, който може директно да бъде инсталиран и стартиран на Android устройство. Един универсален APK файл съдържа ресурси за всички плътности на екрана, процесорни архитектури и езици, което води до по-голям размер за изтегляне. Android App Bundle (.aab) е формат за качване и публикуване, изискван от Google Play за разпространение през магазина. AAB не може да бъде инсталиран директно на устройство; вместо това Google Play използва пакета и Play App Signing, за да генерира оптимизирани разделени APK файлове (split APKs), съобразени динамично с архитектурата, плътността на екрана и езика на конкретното устройство (Dynamic Delivery). Един iOS IPA (.ipa) е архивен файл на приложение (zip контейнер, съдържащ директорията Payload, пакета `.app`, ресурси за подписване и метаданни). При качване в App Store Connect Apple прилага App Thinning (например App Slicing), за да предостави само ресурсите и двоичните файлове, необходими за устройството, което изтегля приложението. От оперативна гледна точка APK и IPA файловете могат да бъдат инсталирани директно на физически тестови устройства (при наличие на съответното подписване и провизиониране), докато AAB файлът трябва първо да бъде преобразуван в разделени APK файлове (например с помощта на `bundletool`), преди локална инсталация на устройството.
# 1. Standalone APK installs directly via ADB
adb install myapp-universal.apk
# 2. AAB requires bundletool to generate device-tailored split APKs
bundletool build-apks --bundle=myapp.aab --output=myapp.apks --connected-device
# 3. Install the generated split APK set onto the connected device
bundletool install-apks --apks=myapp.apks
10Каква защита предоставя TLS (Transport Layer Security) за мрежовите заявки на мобилно приложение и как съвременните мобилни операционни системи налагат сигурни настройки за транспорт по подразбиране?
TLS (Transport Layer Security) защитава мрежовите извиквания на мобилното приложение, като предоставя три основни гаранции: конфиденциалност (криптиране на данните в транзит, така че трети страни да не могат да четат полезния товар или хедърите), интегритет на данните (засичане на всяка манипулация или модификация на заявките и отговорите в транзит) и автентикация на сървъра (валидиране на цифровия сертификат на сървъра спрямо доверени сертифициращи органи за предотвратяване на атаки от типа Man-in-the-Middle). Съвременните мобилни операционни системи налагат сигурен транспорт по подразбиране, като блокират некриптирания HTTP трафик в прав текст: 1. iOS налага App Transport Security (ATS), изисквайки мрежовите връзки (например тези през URLSession) да използват HTTPS с TLS 1.2+ освен ако в Info.plist не са дефинирани изрични изключения за домейни. 2. Android (API 28+) забранява HTTP трафика в прав текст по подразбиране (`cleartextTrafficPermitted=false`). Разрешаването на некриптиран HTTP изисква изрични изключения чрез Network Security Configuration (`network_security_config.xml`) или атрибута в манифеста `android:usesCleartextTraffic`.
11Как имплементирате биометрична автентикация (Face ID / пръстов отпечатък) със защитено хранилище на платформата, за да гарантирате, че криптографските ключове се отключват само след успешна биометрична верификация?
За сигурно ограничаване на достъпа до криптографски ключове чрез биометрия, приложението не трябва да разчита просто на булев UI callback от биометричния прозорец. Вместо това криптографските ключове трябва да се генерират и съхраняват в хардуерно защитено хранилище (Secure Enclave в iOS, Android Keystore / StrongBox в Android), конфигурирано с политики за контрол на достъпа, които изискват биометрична автентикация преди използването на ключа. В iOS ключовете или елементите в Keychain се конфигурират чрез `SecAccessControlCreateWithFlags` с флагове като `.biometryCurrentSet` (или `.userPresence`). Когато частният ключ бъде поискан за подписване или декриптиране, операционната система автоматично подканва потребителя за Face ID / Touch ID. В Android ключовете се генерират с помощта на `KeyGenParameterSpec.Builder` с `.setUserAuthenticationRequired(true)`. Криптографска операция (като `Cipher` или `Signature`) се инициализира и подава като `BiometricPrompt.CryptoObject` към `BiometricPrompt.authenticate()`. Ключът се отключва и може да се използва единствено в `onAuthenticationSucceeded` чрез този автентикиран `CryptoObject`. Използването на `.biometryCurrentSet` (iOS) или `setInvalidatedByBiometricEnrollment(true)` (Android) гарантира, че ако на устройството бъде регистриран нов пръстов отпечатък или биометрични данни, съществуващите ключове стават окончателно невалидни, предотвратявайки неоторизиран достъп при компрометиран парокод на устройството.
val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
"biometric_auth_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.build()
keyGen.init(spec)
keyGen.generateKey()
12Какви стратегии за кеширане и оптимизации на пайплайна можете да приложите, за да намалите значително времето за билдване на pull request заявки в кросплатформени мобилни CI (Continuous Integration) изпълнители?
За драстично намаляване на времето за билдване на pull request заявки в мобилни CI изпълнители, оптимизациите трябва да са насочени към разрешаването на зависимости, кеширането на компилацията и селективното изпълнение. Първо, внедрете ефективно кеширане на зависимостите, базирано на хешове от заключващи файлове (например `package-lock.json`, `Podfile.lock`, `gradle/verification-metadata.xml` или SPM package resolved файлове). Това предотвратява повторното изтегляне или разрешаване на външни пакети при чисти инстанции на runner-ите. Второ, конфигурирайте нативни кешове за компилация. За Android активирайте Gradle Build Cache (`--build-cache`) с отдалечено HTTP кеширане или устойчиво кеширане на ниво CI за `~/.gradle/caches` и директориите за билдване. За iOS кеширайте внимателно `DerivedData` от Xcode и артефактите от компилация на Swift Package / CocoaPods или използвайте съвременни инструменти за отдалечено кеширане като Tuist или Bazel. За React Native/Flutter кеширайте изходния JS пакет, node_modules и кеша на Flutter engine SDK. Трето, приложете селективно изпълнение на задачите (откриване на промени и анализ на въздействието върху тестовете). Използвайте филтри по Git пътища, за да пропускате iOS билдове, когато са променени само файлове за Android или бекенд, изпълнявайте бързи етапи за статичен анализ (lint) и модулни тестове преди скъпите UI тестове или стъпки за компилация, и избягвайте генерирането на пълни бинарни файлове за продукция (като AAB или универсални IPA) при изпълнения за PR, където е необходим само билд за симулатор/debug или изпълнение на модулни тестове.
name: PR Fast Check
on:
pull_request:
paths:
- 'android/**'
- 'shared/**'
jobs:
android-unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
# Gradle build caching for dependencies & task outputs
- uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: false
- name: Run Unit Tests & Lint Only
run: ./gradlew testDebugUnitTest lintDebug --build-cache --parallel
13Как бихте структурирали споделена функционалност, така че презентационният слой, сценариите на използване (use cases) от домейна и достъпът до данни да останат независимо тестваеми и преизползваеми между iOS и Android?
За да структурирате споделена функционалност за независимо тестване и преизползваемост между iOS и Android, приложете чиста, слоеста архитектура, организирана в слоеве Presentation, Domain и Data: 1. **Domain слой (чиста споделена логика):** Съдържа чисти бизнес същности, интерфейси на хранилища (repository contracts) и сценарии на използване / интерактори (например `GetCartUseCase`, `ApplyPromoCodeUseCase`). Този слой няма зависимости от UI библиотеки (UIKit, SwiftUI, Android Views, Jetpack Compose, Flutter widgets) или системни API интерфейси на платформите. Всеки use case капсулира единична бизнес операция, което го прави напълно податлив на модулно тестване с обикновени mock обекти. 2. **Data слой (достъп до данни и капсулация):** Имплементира repository интерфейсите от домейна. Той взаимодейства с отдалечени източници на данни (REST/GraphQL) и локално съхранение (SQLite/Key-Value). Мрежовите DTO (Data Transfer Object) обекти и базисните същности от базата данни се трансформират стриктно към чисти домейн същности, преди да напуснат този слой, предотвратявайки изтичането на външни схеми за сериализация или промени в базата данни към Domain или UI слоевете. 3. **Presentation слой (UI и управление на състоянието):** Състои се от държачи на състояние (state holders като ViewModels, Blocs или Presenters) и UI изгледи/уиджети. Държачът на състояние извиква домейн сценариите, управлява състоянието на потребителския интерфейс (зареждане, успех, грешка) и предоставя наблюдаемо състояние към изгледа. Изгледът просто наблюдава това състояние и го рендира. Тази структура позволява изолирано модулно тестване на use case логиката без UI, имитиране (mocking) на хранилищата за тестване на view model компонентите и лесна замяна на презентационните имплементации между платформите при пълно преизползване на домейн и data логиката.
[ UI View / Compose / SwiftUI ]
|
v (Observes State / Dispatches Events)
[ ViewModel / Presenter / BLoC ] (Presentation Layer)
|
v (Executes pure business operations)
[ Use Case / Interactor ] (Domain Layer - Pure Shared Logic)
|
v (Calls interface contract)
[ Repository Interface ] (Domain Layer Contract)
^
| (Implements)
[ Repository Implementation ] (Data Layer)
|---- Maps NetworkDTO -> DomainEntity
|---- Maps DatabaseEntity -> DomainEntity
[ API Client / Local DB / Storage ]
14Как преценявате кога локалното състояние на компонентите вече не е достатъчно и избирате подходящ архитектурен подход за управление на състоянието за мултиплатформена кодова база с множество разработчици?
При мултиплатформена разработка за мобилни устройства преходът от локално състояние на компонентите (`useState`/`StatefulWidget`) към цялостно архитектурно решение за управление на състоянието се определя от обхвата на състоянието, изискванията за жизнения цикъл и възможността за тестване: 1. **Кога локалното състояние не е достатъчно**: - **Споделяне между компоненти и препращане на свойства (Prop Drilling)**: Когато състоянието трябва да бъде достъпвано или променяно през различни навигационни маршрути или отдалечени клонове на дървото от компоненти. - **Устойчивост през жизнения цикъл**: Когато данните трябва да се запазят при унищожаване на екрани, преходи между маршрути или рестартиране на навигацията (напр. потребителска сесия, съдържание на количка, кеширани емисии). - **Разделение на отговорностите и тестваемост**: Когато бизнес логиката, валидацията и страничните ефекти се преплитат с елементите на потребителския интерфейс, което затруднява автоматизираното тестване на ниво модулни тестове без графична среда (headless unit testing). 2. **Избор на архитектура за екипи с множество разработчици**: - **Еднопосочен поток на данни / Предвидими контейнери (напр. BLoC, Redux, Riverpod)**: Налага стриктно разделение, при което интерфейсът изпраща явни събития/действия и рендерира неизменяемо състояние, емитирано от специализирани модули за бизнес логика. Това създава ясни договори, намалява страничните ефекти и позволява независимо модулно тестване на бизнес логиката. - **Атомарни / Обхватни реактивни хранилища (напр. Zustand, MobX, Provider)**: Предоставят по-малко шаблонен код (boilerplate) и гъвкав абонамент чрез селектори, подходящи когато екипите търсят леко отделяне и прецизирано повторно рендериране на компонентите. В екипна среда основната архитектурна цел е изолирането на чистата бизнес логика от слоевете за рендериране на интерфейса и използването на селективни абонаменти за предотвратяване на излишни прерисувания на цялото дърво от компоненти.
15Как бихте изследвали и намалили насичането в екран на кросплатформено мобилно приложение, което насича при превъртане на дълъг списък от изображения и динамично съдържание?
Първо бих възпроизвел насичането на реални устройства и бих профилирал приложението, тъй като забавянията при превъртане могат да произтичат от няколко източника: превишаване на бюджета за кадър, работа в JS или Dart, натоварване на главната нишка/потребителския интерфейс, GPU (Graphics Processing Unit)/растерна обработка, изчисляване на оформянето, декодиране на изображения, недостиг на памет или мрежово зареждане. При 60 Hz приложението разполага с около 16,7 ms на кадър, а при 120 Hz с около 8,3 ms, така че ресурсоемкото рендиране, декодиране, оформяне или синхронните изчисления по време на превъртане могат да доведат до пропуснати кадри. Бих използвал инструменти като Flutter DevTools, инструменти за производителност на React Native или Flipper, Android Studio Profiler, Xcode Instruments и графични визуализации на времевата линия на кадрите, за да идентифицирам истинското тясно място. За дългия списък бих се уверил, че използва мързеливо зареждане или виртуализация: например `FlatList`, `FlashList` или `RecyclerListView` в React Native, или `ListView.builder`, `SliverList` или подобни генератори във Flutter. Бих използвал стабилни ключове, избягвал повторно изграждане или рендиране на всеки ред при промяна на състоянието на родителския компонент, мемоизирал компонентите за редове или селекторите, където е уместно, поддържал `build`/`renderItem` максимално леки и изнесъл сортирането, филтрирането, парсването на JSON, форматирането или обработката на изображения извън процеса на превъртане. Ако размерите на елементите са предвидими, бих предоставил подсказки за оформянето като `getItemLayout` в React Native или фиксирани/прототипни размери на елементите във Flutter. За изображенията бих сервирал умалени копия с подходящ размер, кеширал ги, избягвал декодиране на изображения с пълна резолюция за малки клетки, използвал заместващи елементи и мързеливо зареждане, и следил за прекомерно натоварване на паметта от твърде много големи растерни графики. Също така бих опростил прекалено сложните оформления на редовете, намалил тежките сенки, изрязвания, препокриващо рисуване и прозрачност, пакетирал или пагинирал данните и след това повторно профилирал, за да потвърдя, че пропуснатите кадри и времената за кадри са се подобрили.
1. Record a trace while scrolling on a real device.
2. Check frame timeline: are frames exceeding 16.7 ms / 8.3 ms?
3. Identify bottleneck: JS/Dart, main/UI thread, raster/GPU, image decode, memory, or network.
4. Fix the largest measured bottleneck: virtualization, image resizing/caching, row memoization, layout simplification, moving work off scroll path.
5. Re-test on low-end and target-refresh-rate devices.
16Как функционират границите между нишките при извиквания през нативния мост (bridge) и как предотвратявате забавяния от превключване на нишки или накъсване на потребителския интерфейс (UI jank), когато нативните модули изпълняват тежка фонова работа?
Кросплатформените библиотеки и рамки използват специфични конвенции за нишките при извиквания през нативния мост: например стандартните извиквания на Flutter през `MethodChannel` пристигат в главната UI нишка на платформата, докато традиционният мост в React Native се изпълнява в отделна JavaScript нишка и препраща асинхронно заявки към нативните нишки. Когато нативен метод от моста се изпълнява в нативната UI/главна нишка, стартирането на тежки изчисления за процесора, продължителни синхронни дискови входно-изходни операции (I/O) или блокиращи мрежови задачи блокира главния цикъл на събитията. Това води до пропуснати кадри, видимо накъсване на интерфейса, а при Android – до грешки Application Not Responding (ANR) или принудително спиране от watchdog механизма на iOS. За да се предотврати това, нативните обработчици трябва да прехвърлят тежката или блокираща работа към фонови пулове за изпълнение (например Kotlin корутини с `Dispatchers.IO`/`Dispatchers.Default`, `ThreadPoolExecutor` в Android или `Task.detached` / `DispatchQueue.global()` в Swift GCD). След като работата приключи, резултатът трябва да се върне обратно в нишката на моста, очаквана от съответната рамка (например връщане на резултати в UI нишката за стандартни `MethodChannel` извиквания или използване на опашки за фонови задачи), което гарантира, че обновяването на кросплатформеното състояние не блокира цикъла на рендериране на интерфейса.
class ImageProcessorPlugin : MethodChannel.MethodCallHandler {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (call.method == "applyFilter") {
val imageBytes = call.argument<ByteArray>("image") ?: return result.error("INVALID_ARG", "Image null", null)
// Dispatch heavy computation off the main UI thread
scope.launch {
try {
val processed = processBitmapBytes(imageBytes) // CPU heavy work
withContext(Dispatchers.Main) {
result.success(processed)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
result.error("PROCESSING_FAILED", e.localizedMessage, null)
}
}
}
} else {
result.notImplemented()
}
}
}
17Как бихте проектирали стратегия за кеширане и инвалидиране за екрани със списъци и фийдове, използвайки Stale-While-Revalidate и условни HTTP (Hypertext Transfer Protocol) заглавни части?
Устойчива стратегия за кеширане и инвалидиране на фийдове и списъчни екрани комбинира шаблона Stale-While-Revalidate (SWR) с условни заглавни части на HTTP за валидация (като `ETag` / `If-None-Match` или `Last-Modified` / `If-Modified-Since`). Когато екранът се отвори, той незабавно прочита и рендерира кешираните данни от локална база данни (единствения източник на истина – Single Source of Truth, например Room или Core Data/SQLite), осигурявайки незабавно визуално зареждане на интерфейса. Едновременно с това се изпраща фонова мрежова заявка с кешираната заглавна част `If-None-Match: <etag>`. Ако сървърът отговори с `304 Not Modified`, не се прехвърлят данни, което валидира локалния кеш и пести трафик и батерия. Ако сървърът отговори с `200 OK`, новите данни и обновеният `ETag` се записват в локалната база данни в трансакция, а реактивните наблюдатели автоматично подават обновения списък към потребителския интерфейс. При пагинация токените за страници или отместванията се съхраняват заедно с кешираните записи. Инвалидирането се задейства при изтичане на TTL времето за живот, действия на потребителя (като pull-to-refresh, което заобикаля условните заглавни части) или локални странични ефекти от мутации (например създаване или изтриване на елемент локално обновява оптимистично базата данни и маркира кешираните курсори за страници за повторна валидация).
fun getFeed(): Flow<List<FeedItem>> = flow {
val cached = feedDao.getFeedItems()
if (cached.isNotEmpty()) emit(cached)
val lastEtag = feedDao.getFeedEtag()
try {
val response = api.fetchFeed(ifNoneMatch = lastEtag)
if (response.code() == 200 && response.body() != null) {
feedDao.updateFeedTransaction(response.body()!!, response.headers()["ETag"])
emit(feedDao.getFeedItems())
}
} catch (e: Exception) {
if (cached.isEmpty()) throw e
}
}
18Едно финансово мобилно приложение трябва да съхранява сигурно токени за опресняване (refresh tokens) на устройството, да поддържа биометрично отключване, да остава функционално след надграждане на OS (Operating System) и да работи сигурно по време на временни офлайн сесии. Как бихте проектирали тази архитектура за съхранение и достъп до токени?
Стабилната архитектура за съхранение на токени в мобилни устройства използва хардуерно обезпечени модули за сигурност: iOS Keychain, защитен от Secure Enclave, и Android Keystore, защитен от StrongBox или Trusted Execution Environment (TEE). Токените за опресняване трябва да бъдат криптирани при съхранение с помощта на ключове, защитени с биометричен криптографски контрол (като `kSecAccessControlBiometryCurrentSet` или `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` в iOS и `setUserAuthenticationRequired(true)` с `AUTH_BIOMETRIC_STRONG` в Android). Криптографският контрол гарантира, че криптографският ключ се декриптира от хардуера само след успешно биометрично удостоверяване, вместо да се разчита на булева проверка в кода на приложението, която може да бъде заобиколена. За да се запази работоспособността при надграждане на операционната система и същевременно да се предотврати неоторизиран достъп, ключовете се обвързват с хардуера на устройството, като се прилагат политики за промяна на биометричната регистрация, които анулират ключа или изискват повторно удостоверяване при добавяне на нови биометрични данни (например `setInvalidatedByBiometricEnrollment(true)` в Android). За временни офлайн сесии кратковременни криптирани токени за достъп (access tokens) и ограничено локално офлайн състояние могат да работят в рамките на предварително дефиниран локален TTL и ограничени права, като същевременно критичните действия за опресняване се отлагат до възстановяване на мрежовата свързаност. При повторно свързване ротацията на токена за опресняване с разпознаване на повторно използване за еднократни токени на бекенда валидира сесията; ако се засече повторно използване на токен или отнемане на достъпа на профила, бекендът връща сигнал за анулиране, който задейства клиента да изчисти хардуерно защитените ключове и офлайн хранилището.
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
"refreshTokenKeyAlias",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
.setInvalidatedByBiometricEnrollment(true)
.build()
keyGenerator.init(spec)
keyGenerator.generateKey()
19Как бихте проектирали мащабируема CI/CD (Continuous Integration/Continuous Deployment) инфраструктура за междуплатформено мобилно монорепозитори (monorepo), поддържащо множество екипи, като същевременно управлявате ограничен и скъпоструващ капацитет от macOS изпълнители (runners)?
За да се проектира мащабируема CI/CD инфраструктура за кросплатформено мобилно монорепозитори (monorepo), като същевременно се оптимизира оскъдният и скъп капацитет за macOS, архитектурата трябва да разчита на разпознаване на засегнатите компоненти чрез графа на компилация, строго разделяне на работните натоварвания между хибриден пул от изпълнители (runners) и виртуализация/ефимерна оркестрация за macOS. Първо, внедрете инструмент за графи на компилация в монорепозитори (като Bazel, Nx или Turborepo) с разпределено отдалечено кеширане. При всяка заявка за изтегляне (pull request) CI конвейерът изчислява разликата в насочения ацикличен граф (DAG) спрямо целевия клон, за да компилира и тества само засегнатите пакети и техните зависими модули, пропускайки изцяло непроменените модули. Второ, приложете хибридна стратегия за разпределение на изпълнителите: прехвърлете всички задачи, които не изискват задължително Xcode (като проверка на стила и компилация за TypeScript/Dart, модулни тестове, статичен анализ на кода, сканиране за сигурност и Gradle компилации за Android), към ценово ефективни, мащабируеми Linux/Kubernetes изпълнители. Запазете macOS машините стриктно за финалното сглобяване за iOS, Swift/Objective-C компилация, подписване на код и изпълнение на тестове в iOS Simulator. Трето, управлявайте macOS изпълнителите чрез инфраструктура за виртуализация (като Tart, Anka или AWS/MacStadium bare-metal възли, оркестрирани чрез Nomad или Kubernetes). Всяка iOS компилация се изпълнява в чиста, ефимерна виртуална машина с предварително подготвени инструменти и кеш за производни данни (derived-data), което предотвратява несъответствия в средата на изпълнителите и позволява бързо автоматично мащабиране според дължината на опашката на конвейера.
20Как бихте проектирали кросплатформена мобилна архитектура за продукт, от който се очаква да споделя по-голямата част от бизнес логиката между iOS и Android, като същевременно позволява специфичният за платформата потребителски интерфейс, навигация и нативни интеграции да се развиват независимо?
Бих проектирал приложението около споделено ядро, което управлява стабилното бизнес поведение: домейн модели, валидация, use cases (случаи на употреба), бизнес правила и договори за достъп до данни. Специфичният за всяка платформа потребителски интерфейс, навигацията, нативните SDK, разрешенията, управлението на жизнения цикъл и компонентите на обвивката на приложението (app shell) трябва да останат извън това ядро. Споделеният код не трябва да импортира UIKit, SwiftUI, Jetpack, компоненти от рамката на Android, навигация от React Native, навигация от Flutter или детайли от нативни SDK. Вместо това той трябва да зависи от тесни интерфейси като AuthRepository, SecureStorage, Analytics, CameraService или PaymentProvider, които всяка платформа имплементира. Бих организирал кода по функционалности във възможно най-голяма степен, вместо да създавам един общ споделен слой. Например функционалности като плащане, търсене, профил и съобщения могат да имат собствен споделен домейн/use-case код, договори за състояние и интерфейси за хранилища. Обвивките за iOS и Android след това поемат композицията на екраните, нативните UI шаблони, стековете за навигация, разрешенията и свързването на адаптерите. Презентационната логика може да бъде споделена, когато е напълно независима от UI рамката – например редуктори, машини на състоянията или view-model договори, които подават опростено състояние и действия, но самите изгледи и навигацията трябва да останат притежание на съответната платформа, за да може всяка да се развива независимо. Основният компромис е целенасочено да се споделя общата, стабилна бизнес логика, като същевременно се избягват абстракции, скриващи реалните различия между платформите. Бих наложил границите чрез правила за зависимости между модулите, инжектиране на зависимости, публични API, тестове на договорите, архитектурни проверки и ясно разпределение на отговорностите. Нативните интеграции трябва да използват портове и адаптери, така че споделеният код на функционалността да вижда унифицирана възможност, докато всяка платформа обработва поведението на съответния SDK, жизнения цикъл, разрешенията, грешките и разликите в потребителското изживяване в свой собствен слой.