Mobile Developer -haastatteluun valmistautuminen

Mobile Developer -haastattelukysymykset

20 usein kysyttyä Mobile Developer -haastattelukysymystä. Mobiilikehitys on kysytty ala, jossa rakennetaan luotettavia iOS-, Android- ja monialustasovelluksia ja ratkaistaan tuote-, suorituskyky-, julkaisu-, offline- ja laiteintegraatiotehtäviä. Kysymykset kattavat eri tasot, ja voit harjoitella niihin vastaamista ääneen haastatteluharjoittajassamme.

Aloita Mobile Developer -AI-haastatteluEi vaadi luottokorttia. 1 ilmainen sessio saatavilla.
Teknisten haastattelujen harjoittelu englanniksiTila, jossa ei-äidinkieliset puhujat voivat harjoitella teknisen haastattelun läpäisemistä.

Kysymykset aloittelijoille

1Mitä eroa on todennustunnisteen tallentamisella tavalliseen avain-arvo-tallennustilaan, kuten SharedPreferences- tai UserDefaults-ratkaisuun, verrattuna alustan suojattuun tallennustilaan, kuten Android Keystoreen tai iOS Keychainiin?

Vakiomuotoiset tallennusmekanismit, kuten Androidin `SharedPreferences` ja iOS:n `UserDefaults`, on suunniteltu kevyille, ei-sensitiivisille asetuksille. Ne tallentavat tiedot salaamattomina selkokielitiedostoina (XML tai plist) sovelluksen hiekkalaatikkohakemistoon (sandbox). Kuka tahansa, jolla on fyysinen pääsy rootattuun tai jailbreakattuun laitteeseen, salaamattomaan laitevarmuuskopioon tai tiedostojärjestelmään, voi lukea nämä tunnisteet suoraan. Sen sijaan alustan suojatut tallennustilat – erityisesti iOS Keychain ja Android Keystore (jota käytetään usein `EncryptedSharedPreferences`-rajapinnan kautta) – tarjoavat tallennetun tiedon salauksen (data-at-rest encryption). iOS Keychain salaa tallennetut kohteet laitteistoon sidotuilla avaimilla (kuten Secure Enclave) ja mahdollistaa tarkat pääsykäytännöt. Android Keystore luo ja säilyttää kryptografiset avaimet laitteistoeristetyissä turvamoduuleissa (TEE, Trusted Execution Environment tai StrongBox), mikä varmistaa, etteivät salausavaimet koskaan altistu sovelluksen muistissa eikä niitä voi viedä tiedostojärjestelmästä.

// --- 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)
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

2Mikä on mobiilisovellusten CI/CD-putken (Continuous Integration / Continuous Delivery) ensisijainen tarkoitus, ja mitkä ainutlaatuiset vaiheet ja rajoitteet erottavat sen verkko- tai taustajärjestelmien julkaisuputkista?

Mobiilisovellusten CI/CD-putken ensisijainen tarkoitus on automatisoida mobiiliasiakassovellusten kääntäminen, testaaminen, allekirjoittaminen ja jakelu tasaisen laadun ja toistettavien julkaisujen takaamiseksi. Jatkuva integrointi (CI) validoi koodimuutokset automaattisten käännösten, staattisen analyysin ja yksikkötestauksen avulla. Jatkuva toimitus ja julkaisu (CD) paketoi, allekirjoittaa ja jakelee binaaritestiversiot testikanaviin (kuten TestFlightiin tai Firebase App Distributioniin) tai sovelluskauppoihin. Mobiili-CI/CD eroaa verkko- ja taustajärjestelmäputkista useilla keskeisillä tavoilla: 1. Julkaisuobjektin tyyppi: Käännökset tuottavat käännettyjä asiakasbinaareja (.ipa, .apk, .aab) sen sijaan, että ne suoritettaisiin suoraan palvelimilla tai kontti-imagessa. 2. Kääntäjäinfrastruktuurin laitteistorajoitteet: iOS-kääntäminen vaatii macOS-laitteistoa ja Xcoden komentorivityökaluja, kun taas taustajärjestelmäputket ajetaan tyypillisesti kevyissä Linux-konteissa. 3. Julkaisuviive ja hyväksynnät: Julkaiseminen loppukäyttäjille vaatii kolmannen osapuolen sovelluskauppojen hyväksyntäprosessin (Apple App Store / Google Play Store), mikä tarkoittaa, että julkaisuja ei voi perua välittömästi palvelinpään uudelleenjulkaisulla. Korjaukset vaativat uuden allekirjoitetun binaarin toimittamisen tai ajonaikaisten feature flag -lippujen hyödyntämisen.

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
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

3Mikä on esitys- (presentation), sovelluslogiikka- (domain) ja datakerroksen (data) erottamisen tarkoitus monialustaisessa mobiilisovelluksessa, ja miten tämä rakenne edistää koodin jakamista iOS:n ja Androidin välillä?

Sovelluksen jakaminen esitys-, domain- ja datakerroksiin luo selkeät vastuualueiden rajat: 1. **Esityskerros (Presentation Layer):** Käsittelee käyttöliittymän renderöinnin, käyttäjän syötteet ja näkymätason tilan (esim. View-, Widget-, ViewModel- tai Presenter-komponentit). 2. **Domain-kerros (Domain Layer):** Kapseloi puhtaan liiketoimintalogiikan, domain-mallit/entiteetit ja käyttötapaukset. Se pysyy riippumattomana käyttöliittymästä ja alustakohtaisista kehyksistä. 3. **Datakerros (Data Layer):** Hallitsee datan noutamista ja pysyvyyttä etä-API-rajapinnoista, paikallisista tietokannoista tai laitteen välimuistista repository- ja data source -rakenteiden kautta. Monialustaisessa mobiilikehityksessä tämä rakenne edistää koodin jakamista, koska domain- ja datakerrokset ovat alustariippumattomia. Keskeiset liiketoimintasäännöt, datamuunnokset ja verkkoliikenteen käsittely voidaan jakaa sataprosenttisesti iOS:n ja Androidin kesken, mikä mahdollistaa joko koko pinon jakamisen (kuten Flutterissa tai React Nativessa) tai liiketoiminta- ja datalogiikan jakamisen säilyttäen samalla alustakohtaiset natiivit esityskerrokset (kuten Kotlin Multiplatformissa).

[Presentation Layer] (UI, Screens, ViewModels)
         │
         ▼ calls
[Domain Layer]       (Use Cases, Business Rules, Entities)
         ▲
         │ implements interfaces
[Data Layer]         (Repositories, API Clients, Local Storage)
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

4Mitkä ovat keskeisimmät erot imperatiivisen ja deklaratiivisen UI-kehitysparadigman (User Interface) välillä mobiiliohjelmoinnissa, ja miten Flutter-widgetit ja React Native -komponentit toteuttavat deklaratiivista mallia?

Imperatiivisessa UI-kehityksessä kehittäjät kirjoittavat eksplisiittisiä, vaiheittaisia ohjeita UI-elementtien luomiseen, muokkaamiseen ja tuhoamiseen (kuten näkymän etsiminen tunnisteen perusteella ja metodien, kuten setText tai setVisibility, suora kutsuminen). Sen sijaan deklaratiivisissa UI-malleissa kuvataan, miltä käyttöliittymän tulisi näyttää tietyssä tilassa (usein muodossa UI = f(state)). Kun tila muuttuu, sovelluskehys määrittää tehokkaan tavan päivittää visuaalinen esitys. Sekä Flutter että React Native toteuttavat tätä deklaratiivista paradigmaa. Flutterissa widgetit ovat muuttumattomia konfiguraatiokuvauksia; kun dynaaminen data muuttuu, `setState()`-kutsun tekeminen StatefulWidget-luokassa ajastaa uudelleenrakennuksen, jossa `build()`-metodi palauttaa uuden widget-puun kuvauksen, jonka Flutter täsmäyttää Element- ja RenderObject-puihinsa. React Nativessa komponentit ovat funktioita tai luokkia, jotka palauttavat JSX-koodia; tilan tai prop-arvojen päivittäminen käynnistää uudelleenrenderöinnin, jossa React täsmäyttää virtuaalisen elementtipuun ja tekee tarvittavat minimaaliset natiivipäivitykset sillan (bridge) tai natiiviajoympäristön kautta.

class CounterWidget extends StatefulWidget {
  const CounterWidget({super.key});
  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: _increment,
      child: Text('Count: $_count'),
    );
  }
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

5Mitä toiminnallista eroa on kylmä-, lämmin- ja kuumakäynnistyksellä mobiilisovelluksissa, ja miksi kylmäkäynnistys on suorituskykybudjettien kriittisin mittari?

Mobiilisovelluksen elinkaaressa käynnistystilat eroavat sen perusteella, ovatko sovelluksen prosessi ja muistitila jo valmiina käyttöjärjestelmässä: - Kylmäkäynnistys (Cold Start): Sovellus käynnistyy täysin alusta, koska käyttöjärjestelmä ei ole vielä luonut sen prosessia tai prosessi on aiemmin suljettu. Käyttöjärjestelmän täytyy varata uusi prosessi, ladata binaarit/ajoympäristöt, alustaa Application-/ajonaikainen konteksti ja luoda ensimmäinen näkymä. Tässä tilassa viive on suurin. - Lämmin käynnistys (Warm Start): Sovelluksen prosessi on yleensä jo muistissa, mutta aktiviteetti- tai näkymähierarkia on tuhottu tai vapautettu (esimerkiksi taustalla tapahtuvan uudelleenluonnin tai takaisin-navigoinnin vuoksi). Käyttöjärjestelmä luo käyttöliittymän/aktiviteetin uudelleen ilman tarvetta luoda uutta prosessia alusta alkaen. - Kuumakäynnistys (Hot Start): Sovellus ja sen käyttöliittymätila ovat edelleen kokonaan muistissa (esimerkiksi käyttäjä painoi Koti-painiketta ja palasi heti takaisin). Käyttöjärjestelmä vain tuo olemassa olevan näkymähierarkian etualalle lähes olemattomalla alustusviiveellä. Kylmäkäynnistys on suorituskykybudjettien kriittisin mittari, koska se edustaa hitainta ensivaikutelmaa käyttäjälle. Pitkät kylmäkäynnistykset korreloivat suoraan sovelluksen hylkäämisen, heikomman käyttäjien pysyvyyden ja sovelluskauppojen huonompien sijoitusten kanssa (kuten Android Vitals -kynnysarvot).

Cold Start: [OS Process Spawn] -> [App Runtime Init] -> [Activity/UI Creation] -> [First Frame Drawn]
Warm Start: [Existing Process]  -> [Activity/UI Recreation] -> [First Frame Drawn]
Hot Start:  [Existing Process]  -> [Bring Existing View to Foreground] (Instant)
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

6Mikä on natiivisilta (native bridge) alustariippumattomassa mobiilisovelluksessa, ja milloin sellaista käytetään sen sijaan, että toiminnallisuus toteutettaisiin kokonaan Flutter- tai React Native -koodilla?

Natiivisilta on kommunikaatiokerros, jonka avulla alustariippumaton koodi voi kutsua alustakohtaista iOS- tai Android-koodia, ja natiivikoodi voi palauttaa tuloksia tai lähettää tapahtumia takaisin. Flutterissa tämä tehdään yleensä alustakanavien (platform channels) tai liitännäisten (plugins) avulla. React Nativessa se toteutetaan yleensä natiivimoduuleilla (native modules), TurboModules-moduuleilla tai natiiveilla käyttöliittymäkomponenteilla. Siltaa käytetään silloin, kun ominaisuus vaatii jotain, mihin pelkkä Flutter- tai React Native -koodi ei pääse kunnolla käsiksi, kuten alustan API (Application Programming Interface) -rajapintoja, laiteominaisuuksia, natiivia SDK-pakettia (Software Development Kit), suorituskykykriittistä natiivitoteutusta tai natiivia käyttöliittymäkomponenttia. Tyypillisiä esimerkkejä ovat kameraominaisuudet, Bluetooth, push-ilmoitukset, maksut, suojattu tallennustila, taustapalvelut, terveysrajapinnat tai SDK:t, jotka tarjoavat integraation vain Swift-/Objective-C- tai Kotlin-/Java-kielelle. Hyvä silta tarjoaa yleensä pienen ja selkeän rajapinnan alustariippumattomalle kerrokselle (kuten getBatteryLevel, startBluetoothScan tai openNativePaymentSheet), kun taas alustakohtainen toteutus huolehtii varsinaisista iOS- ja Android-yksityiskohdista.

Cross-platform screen:
  calls NativePayments.openPaymentSheet(orderId)

iOS implementation:
  uses Apple Pay / native payment SDK

Android implementation:
  uses Google Pay / native payment SDK
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

7Mitä offline-first-arkkitehtuuri tarkoittaa mobiilikehityksessä, ja miten se eroaa pohjimmiltaan tavallisesta HTTP-vastausten (Hypertext Transfer Protocol) välimuistituksesta?

Mobiilikehityksessä offline-first-arkkitehtuuri käsittelee paikallista tallennustilaa ensisijaisena totuuden lähteenä (source of truth) sekä luku- että kirjoitusoperaatioille. Sen sijaan että sovellus odottaisi verkkopyyntöjen valmistumista ennen käyttöliittymän renderöintiä tai käyttäjän toimintojen sallimista, se toimii suoraan paikallisen tietokannan tai tallennuskerroksen kanssa. Taustalla toimivat synkronointiprosessit huolehtivat paikallisten muutosten sovittamisesta etäpalvelimen kanssa, kun verkkoyhteys on käytettävissä. Tämä eroaa pohjimmiltaan tavallisesta HTTP-vastausten välimuistituksesta usealla tavalla: 1. **Tietomalli**: HTTP-välimuisti tallentaa raakoja verikkovastauksia (kuten JSON-dataa), joiden avaimina toimivat pyynnön URL-osoitteet ja otsakkeet. Offline-first-arkkitehtuuri tallentaa strukturoituja toimialueen entiteettejä paikalliseen tietokantaan (kuten Room, SQLite tai SwiftData/Core Data). 2. **Kirjoitukset vs. lukemiset**: HTTP-välimuisti on ensisijaisesti lukutoimintojen optimointimekanismi, eikä se luonnostaan tue offline-tilassa tehtäviä kirjoitustransaktioita tai mutaatioita. Offline-first sallii täysimittaiset paikalliset kirjoitukset ja asettaa muutokset jonoon myöhempää palvelinsynkronointia varten. 3. **Kyselyt ja elinkaari**: Välimuistiin tallennetut HTTP-vastaukset ovat alttiina automaattisille välimuistin poistosäännöille, eikä niihin voi kohdistaa mielivaltaisia kyselyitä, suodatuksia tai liitoksia. Offline-first-pysyvyys mahdollistaa täydet kyselyominaisuudet, indeksoinnin ja deterministisen elinkaarenhallinnan riippumatta verkon tilasta.

/* 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
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

8Mitä eroa on etä-push-ilmoituksella ja paikallisella ilmoituksella mobiilisovelluksessa?

Paikallisten ilmoitusten ja etä-push-ilmoitusten keskeinen ero liittyy niiden alkuperään ja käynnistystapaan: 1. Paikalliset ilmoitukset luodaan, ajastetaan ja laukaistaan kokonaan laitteessa sovelluksen toimesta käyttöjärjestelmän rajapintojen avulla (kuten iOS:n UserNotifications tai Androidin AlarmManager/WorkManager/NotificationManager). Ne laukeavat laitekohtaisten ehtojen perusteella, kuten tiettynä päivämääränä tai kellonaikana, ajastimen päätyttyä tai maantieteellisen sijainnin (geofencing) perusteella. Koska ne toimivat paikallisesti käyttöjärjestelmän taustaprosessissa, ne eivät vaadi taustapalvelinta tai aktiivista internetyhteyttä toimitushetkellä. 2. Etä-push-ilmoitukset ovat peräisin ulkoiselta sovelluspalvelimelta, ja ne välitetään internetin yli alustan push-yhdyskäytävien kautta (APNs Apple-laitteille, FCM Androidille). Yhdyskäytävä toimittaa hyötykuorman laitteen käyttöjärjestelmätason taustaprosessille, joka herättää sovelluksen tai näyttää ilmoitusbannerin. Etäilmoituksia tarvitaan reaaliaikaisiin ulkoisiin tapahtumiin, kuten saapuviin chat-viesteihin, kaveripyyntöihin tai uutisiin.

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)")
    }
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

9Mikä on rakenteellinen ja toiminnallinen ero Android App Bundlen (.aab), APK:n (Android Package) ja iOS:n IPA-arkiston välillä, kun sovelluksia jaellaan sovelluskauppojen kautta?

APK (Android Package) on suoritettava pakettiformaatti, joka sisältää käännetyn DEX-tavukoodin, resurssit, assetit ja natiivikirjastot, ja se voidaan asentaa ja suorittaa suoraan Android-laitteella. Universaali APK paketoi resurssit kaikille näyttötiheyksille, CPU-arkkitehtuureille ja kielille, mikä kasvattaa latauskokoa. Android App Bundle (.aab) on Google Play -sovelluskauppajakelussa vaadittava julkaisuformaatti. AAB-tiedostoa ei voi asentaa suoraan laitteelle, vaan Google Play luo bundlen ja Play App Signing -palvelun avulla optimoituja split-APK-paketteja, jotka räätälöidään dynaamisesti tietyn laitteen arkkitehtuurin, näyttötiheyden ja kielen mukaan (Dynamic Delivery). iOS:n IPA (.ipa) on sovellusarkistotiedosto (zip-säilö, joka sisältää Payload-hakemiston, `.app`-bundlen, allekirjoitusresurssit ja metatiedot). Kun se ladataan App Store Connectiin, Apple suorittaa App Thinning -optimoinnin (kuten App Slicing), jotta lataavalle laitteelle toimitetaan vain sen tarvitsemat assetit ja binaarit. Toiminnallisesti APK- ja IPA-tiedostot voidaan asentaa suoraan fyysisille testilaitteille (allekirjoituksen ja provisioinnin ehdoin), kun taas AAB on ensin muunnettava split-APK-paketeiksi (esimerkiksi `bundletool`-työkalulla) ennen paikallista laiteasennusta.

# 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
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

10Minkä suojan TLS (Transport Layer Security) tarjoaa mobiilisovelluksen verkkopyynnöille, ja miten nykyaikaiset mobiilikäyttöjärjestelmät pakottavat turvalliset siirtokerroksen oletusasetukset?

TLS (Transport Layer Security) suojaa mobiiliverkkokutsuja tarjoamalla kolme olennaista takuuta: luottamuksellisuuden (siirrettävän datan salaaminen, jotta ulkopuoliset eivät voi lukea hyötykuormaa tai otsikoita), datan eheyden (siirron aikana tapahtuvien pyyntöjen ja vastausten peukaloinnin tai muuttamisen havaitseminen) ja palvelimen todennuksen (palvelimen digitaalisen varmenteen vahvistaminen luotettuja varmentajia vasten väliintulohyökkäysten estämiseksi). Nykyaikaiset mobiilikäyttöjärjestelmät pakottavat suojatun tiedonsiirron oletusarvoisesti estämällä salaamattoman selkokielisen HTTP-liikenteen: 1. iOS pakottaa ATS-ominaisuuden (App Transport Security), joka vaatii verkkoyhteyksiltä (kuten URLSession-kautta tehtäviltä) HTTPS-protokollaa vähintään TLS 1.2 -versiolla, ellei verkkotunnuskohtaisia poikkeuksia ole erikseen määritelty Info.plist-tiedostossa. 2. Android (API 28+) poistaa selkokielisen HTTP-liikenteen käytöstä oletuksena (`cleartextTrafficPermitted=false`). Salaamattoman HTTP:n salliminen vaatii nimenomaisia poikkeuksia Network Security Configuration -tiedostossa (`network_security_config.xml`) tai manifest-attribuutissa `android:usesCleartextTraffic`.

<!-- Android: res/xml/network_security_config.xml -->
<network-security-config>
    <!-- Cleartext HTTP blocked by default. Exceptions require explicit declaration -->
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.example.com</domain>
    </domain-config>
</network-security-config>

<!-- iOS: Info.plist ATS configuration -->
<!-- ATS blocks HTTP by default; exceptions must be declared inside NSAppTransportSecurity -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <false/>
</dict>
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Kysymykset jatkaville

11Miten toteutat biometrisen todennuksen (Face ID / sormenjälki) alustan suojattua tallennustilaa käyttäen niin, että kryptografiset avaimet avataan vain onnistuneen biometrisen vahvistuksen jälkeen?

Kryptografisten avainten suojaamiseksi biometrialla sovellus ei saa nojata pelkkään biometrisen kehotteen palauttamaan totuusarvoiseen käyttöliittymätason takaisinkutsuun. Sen sijaan salausavaimet on luotava ja tallennettava laitteistopohjaiseen suojattuun tallennustilaan (iOS:ssä Secure Enclave, Androidissa Android Keystore / StrongBox), johon on määritelty pääsynhallintakäytännöt, jotka vaativat biometrisen todennuksen ennen avaimen käyttöä. - iOS:ssä avaimet tai Keychain-tietueet konfiguroidaan käyttämällä funktiota `SecAccessControlCreateWithFlags` lipuilla kuten `.biometryCurrentSet` (tai `.userPresence`). Kun yksityistä avainta pyydetään allekirjoitukseen tai salauksen purkuun, käyttöjärjestelmä pyytää käyttäjältä automaattisesti Face ID- tai Touch ID -todennuksen. - Androidissa avaimet luodaan käyttämällä `KeyGenParameterSpec.Builder`-rakentajaa ja kutsumalla `.setUserAuthenticationRequired(true)`. Salaustoiminto (kuten `Cipher` tai `Signature`) alustetaan ja välitetään `BiometricPrompt.CryptoObject`-oliona metodille `BiometricPrompt.authenticate()`. Avain avataan ja se on käytettävissä vain `onAuthenticationSucceeded`-käsittelijässä kyseisen todennetun `CryptoObject`-olion kautta. Lipun `.biometryCurrentSet` (iOS) tai metodin `setInvalidatedByBiometricEnrollment(true)` (Android) käyttäminen varmistaa, että jos laitteeseen rekisteröidään uusi sormenjälki tai biometrinen tunniste, aiemmat avaimet mitätöidään pysyvästi. Tämä estää luvattoman pääsyn tilanteessa, jossa laitteen pääsykoodi on vaarantunut.

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()
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

12Mitä välimuististrategioita ja liukuhihnaoptimointeja voit toteuttaa lyhentääksesi merkittävästi PR (Pull Request) -koontiaikoja alustariippumattomissa mobiilialustan CI (Continuous Integration) -ajajissa?

PR-koontiaikojen merkittävä lyhentäminen mobiili-CI-ajajissa edellyttää optimointeja riippuvuuksien ratkaisemisessa, käännösvälimuistin hyödyntämisessä ja valikoivassa suorituksessa. Ensimmäiseksi otetaan käyttöön tehokas riippuvuuksien välimuistitus, joka perustuu lukitustiedostojen tiivisteisiin (esim. `package-lock.json`, `Podfile.lock`, `gradle/verification-metadata.xml` tai SPM:n resolved-tiedostot). Tämä estää ulkoisten pakettien tarpeettoman lataamisen ja riippuvuusselvityksen puhtaissa ajajaympäristöissä. Toiseksi konfiguroidaan natiivien käännösten välimuistit. Androidilla otetaan käyttöön Gradle Build Cache (`--build-cache`) joko etävälimuistina HTTP:n yli tai CI-ympäristön pysyvänä välimuistina hakemistoille `~/.gradle/caches` ja koontihakemistoille. iOS:llä välimuistitetaan Xcoden `DerivedData` sekä Swift Package- ja CocoaPods-käännösartefaktit huolellisesti tai hyödynnetään moderneja etävälimuistityökaluja, kuten Tuistia tai Bazelia. React Native- ja Flutter-projekteissa välimuistitetaan JS-paketin tuloste, `node_modules` ja Flutter-moottorin SDK-välimuisti. Kolmanneksi sovelletaan tehtävien valikoivaa suoritusta (muutosten tunnistus ja testien vaikutusanalyysi). Git-polkusuodattimilla ohitetaan iOS-käännökset, jos vain Android- tai taustajärjestelmätiedostoja on muutettu, suoritetaan nopeat staattiset analyysit ja yksikkötestit ennen raskaita käyttöliittymätestejä tai käännösvaiheita, ja vältetään täysien julkaisubinäärien (kuten AAB- tai universaalien IPA-pakettien) luomista PR-ajoissa, joissa tarvitaan vain simulaattori-, debug- tai yksikkötestikoonti.

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
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

13Miten rakentaisit jaetun ominaisuuden siten, että esityskerros, domain-käyttötapaukset ja tiedonhaku pysyvät itsenäisesti testattavina ja uudelleenkäytettävinä iOS- ja Android-alustoilla?

Jotta jaettu ominaisuus olisi itsenäisesti testattava ja uudelleenkäytettävä sekä iOS- että Android-alustoilla, kannattaa soveltaa kerrosarkkitehtuuria (Clean Architecture), joka on jaettu esitys- (Presentation), domain- ja datakerroksiin: 1. **Domain-kerros (puhdas jaettu logiikka):** Sisältää puhtaat liiketoimintaentiteetit, repository-rajapinnat (sopimukset) ja käyttötapaukset eli interaktorit (esim. `GetCartUseCase`, `ApplyPromoCodeUseCase`). Tällä kerroksella ei ole riippuvuuksia käyttöliittymäkirjastoihin (UIKit, SwiftUI, Android Views, Jetpack Compose, Flutter widgets) eikä alustakohtaisiin rajapintoihin. Jokainen käyttötapaus kapseloi yhden liiketoimintaoperaation, mikä tekee siitä täysin yksikkötestattavan pelkillä valelogiikoilla (mocks). 2. **Datakerros (tiedonhaku ja kapselointi):** Toteuttaa domain-kerroksen repository-rajapinnat. Se kommunikoi etäpalveluiden (REST/GraphQL) ja paikallisen tallennustilan (SQLite/avain-arvovarastot) kanssa. Verkon DTO-oliot ja tietokantaentiteetit muunnetaan puhtaiksi domain-entiteeteiksi ennen kuin ne poistuvat tästä kerroksesta, mikä estää ulkoisten serialisointirakenteiden tai tietokantamuutosten vuotamisen domain- tai esityskerrokseen. 3. **Esityskerros (käyttöliittymä ja tilanhallinta):** Koostuu tilanhaltijoista (ViewModel, Bloc tai Presenter) sekä käyttöliittymänäkymistä ja -komponenteista. Tilanhaltija kutsuu domain-käyttötapauksia, hallitsee käyttöliittymän tilaa (lataus, onnistuminen, virhe) ja välittää havainnoitavaa tilaa näkymälle. Näkymä vain havainnoi tätä tilaa ja piirtää sen. Tämä rakenne mahdollistaa käyttötapausten yksikkötestauksen ilman käyttöliittymää, repositoryiden korvaamisen testeissä ja esityskerroksen toteutusten vaihtamisen alustojen välillä samalla kun koko domain- ja datalogiikka käytetään uudelleen.

[ 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 ]
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

14Miten arvioit, milloin paikallinen komponentin tila ei enää riitä, ja miten valitset sopivan arkkitehtuurisen tilanhallintaratkaisun usean kehittäjän monialustaisessa koodikannassa?

Monialustaisessa mobiilikehityksessä siirtymistä paikallisesta komponentin tilasta (`useState`/`StatefulWidget`) arkkitehtuuriseen tilanhallintaratkaisuun ohjaavat tilan näkyvyysalue, elinkaarivaatimukset ja testattavuus: 1. **Milloin paikallinen tila ei riitä**: - **Komponenttien välinen jakaminen ja prop drilling**: Kun tilaan täytyy päästä käsiksi tai sitä on muokattava erillisissä navigointireiteissä tai kaukana toisistaan olevissa widget-/komponenttipuun haaroissa. - **Elinkaaren pysyvyys**: Kun datan on säilyttävä näkymien purkamisen, reittisiirtymien tai navigoinnin nollaamisen yli (esim. käyttäjäistunto, ostoskorin sisältö, välimuistiin tallennetut syötteet). - **Vastuiden eriyttäminen ja testattavuus**: Kun liiketoimintalogiikka, validointi ja sivuvaikutukset kietoutuvat käyttöliittymäkomponentteihin, mikä tekee automatisoidusta yksikkötestauksesta ilman käyttöliittymää vaikeaa. 2. **Arkkitehtuurin valinta usean kehittäjän koodikantoihin**: - **Yksisuuntainen tiedonkulku / ennustettavat säiliöt (esim. BLoC, Redux, Riverpod)**: Pakottaa tarkan jaon, jossa käyttöliittymä välittää eksplisiittisiä tapahtumia/toimintoja ja renderöi erillisten liiketoimintalogiikkayksiköiden tuottamaa muuttumatonta tilaa. Tämä luo selkeät sopimukset, vähentää sivuvaikutuksia ja mahdollistaa liiketoimintalogiikan itsenäisen yksikkötestauksen. - **Atomiset / rajatut reaktiiviset tilasäilöt (esim. Zustand, MobX, Provider)**: Tarjoavat vähemmän toistoleikettä (boilerplate) ja joustavat valitsintilaukset (selector subscriptions), mikä sopii tilanteisiin, joissa tiimi haluaa kevyttä irrottamista toisistaan ja kohdennettuja komponenttien uudelleenrenderöintejä. Tiimiympäristössä ensisijainen arkkitehtuurinen tavoite on puhtaan liiketoimintalogiikan eristäminen käyttöliittymän renderöintikerroksista sekä selektiivisten tilauksien käyttäminen koko puun tarpeettomien uudelleenrenderöintien estämiseksi.

// Zustand store: pure state container isolated from React UI hierarchy
import { create } from 'zustand';

interface CartState {
  items: string[];
  addItem: (item: string) => void;
  clearCart: () => void;
}

export const useCartStore = create<CartState>((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ items: [...state.items, item] })),
  clearCart: () => set({ items: [] }),
}));

// Usage in components uses selective subscriptions to avoid unnecessary re-renders:
// const itemCount = useCartStore((state) => state.items.length);
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

15Miten tutkisit ja vähentäisit nykimistä (jank) monialustaisessa mobiilisovelluksessa, jonka näkymä pätkii selattaessa pitkää kuvia ja dynaamista sisältöä sisältävää listaa?

Toistaisin pätkimisen ensin oikeilla laitteilla ja profiloitsisin sen, koska vierityksen nykiminen voi johtua useista syistä: kehysbudjetin (frame budget) ylittymisestä, JS- tai Dart-koodin suorituksesta, pää-/UI-säikeen kuormituksesta, GPU-/rasterointityöstä, asettelusta, kuvien dekoodauksesta, muistipaineesta tai verkkolatauksista. 60 Hz:n taajuudella sovelluksella on noin 16,7 ms kehystä kohti ja 120 Hz:n taajuudella noin 8,3 ms, joten raskas renderöinti, dekoodaus, asettelu tai synkroninen laskenta vierityksen aikana voi aiheuttaa kehysten putoamista (dropped frames). Käyttäisin työkaluja kuten Flutter DevTools, React Nativen suorituskykytyökalut tai Flipper, Android Studio Profiler, Xcode Instruments sekä kehysaikajanojen näkymiä todellisen pullonkaulan tunnistamiseksi. Pitkän listan osalta varmistaisin, että se on laiskasti latautuva tai virtualisoitu: esimerkiksi `FlatList`, `FlashList` tai `RecyclerListView` React Nativessa tai `ListView.builder`, `SliverList` tai vastaavat rakentajat Flutterissa. Käyttäisin vakaita avaimia (keys), välttäisin jokaisen rivin uudelleenrakentamista tai uudelleenrenderöintiä vanhemman tilan muuttuessa, memoisoisin rivikomponentit tai valitsimet tarvittaessa, pitäisin `build`- / `renderItem`-funktion kevyenä ja siirtäisin lajittelun, suodatuksen, JSON-jäsennyksen, muotoilun tai kuvankäsittelyn pois vierityksen kriittiseltä suorituspolulta. Jos kohteiden koot ovat ennakoitavissa, määrittäisin asetteluvihjeet, kuten `getItemLayout` React Nativessa tai kiinteät/prototyyppikoot (`itemExtent`) Flutterissa. Kuvien osalta tarjoaisin oikeankokoisia pikkukuvia (thumbnails), välimuistittaisin ne, välttäisin täyden resoluution kuvien dekoodausta pieniin soluihin, käyttäisin paikkamerkkejä ja laiskaa latausta sekä tarkkailisin liiallisten suurten bittikarttojen aiheuttamaa muistin kuormitusta. Yksinkertaistaisin myös liian monimutkaisia riviasetteluita, vähentäisin raskaita varjostuksia, leikkauksia (clipping), päällekkäispiirtoa (overdraw) ja läpinäkyvyyttä (opacity) tarvittaessa, eräajaisin tai sivuttaisin datan ja profiloitsisin sovelluksen uudelleen varmistaakseni, että pudonneiden kehysten määrä ja kehysajat paranivat.

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.
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

16Miten säierajat toimivat natiivisiltakutsuissa (native bridge), ja miten estät säikeenvaihdosta johtuvat pysähdykset tai käyttöliittymän (UI, User Interface) nykimisen, kun natiivimoduulit suorittavat raskasta taustatyötä?

Monialustaiset sovelluskehykset noudattavat siltakutsuissa tiettyjä säikeistyskäytäntöjä: esimerkiksi Flutterin standardit `MethodChannel`-kutsut saapuvat alustan pääkäyttöliittymäsäikeeseen (UI thread), kun taas perinteiset React Nativen siltakutsut suoritetaan erillisessä JavaScript-säikeessä ja lähetetään asynkronisesti natiivisäikeille. Kun natiivi siltametodi suoritetaan natiivissa UI-/pääsäikeessä, raskaiden laskentaoperaatioiden, pitkän synkronisen levyn I/O:n tai estävien verkkotoimintojen suorittaminen lukitsee pääsuoritussilmukan. Tämä johtaa ruutujen putoamiseen (dropped frames), näkyvään käyttöliittymän nykimiseen sekä Androidilla ANR-virheisiin (Application Not Responding) tai iOS:ssä watchdog-prosessin tekemään sovelluksen sulkemiseen. Tämän estämiseksi natiivien siltakutsujen käsittelijöiden tulisi siirtää raskas tai estävä työ taustasäiepooleihin (esimerkiksi Kotlin Coroutines `Dispatchers.IO`- tai `Dispatchers.Default`-määrityksellä, Androidin `ThreadPoolExecutor` tai Swiftin `Task.detached` / GCD:n `DispatchQueue.global()`). Kun työ valmistuu, tulos on palautettava takaisin kehyksen odottamaan siltasäikeeseen (esimerkiksi tulosten palauttaminen UI-säikeessä tavanomaisille MethodChanneleille tai taustatehtäväjonoja käyttäen), mikä varmistaa, etteivät alustojen väliset tilapäivitykset estä käyttöliittymän renderöintisykliä.

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()
        }
    }
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

17Miten suunnittelisit välimuisti- ja mitätöintistrategian lista- ja syötenäkymille käyttäen SWR (Stale-While-Revalidate) -mallia ja ehdollisia HTTP-otsikoita (Hypertext Transfer Protocol)?

Vankka välimuisti- ja mitätöintistrategia syöte- ja listanäkymille yhdistää Stale-While-Revalidate (SWR) -mallin ja ehdolliset HTTP-validaatio-otsikot (kuten `ETag` / `If-None-Match` tai `Last-Modified` / `If-Modified-Since`). Kun näkymä avautuu, se lukee ja renderöi välimuistissa olevan datan välittömästi paikallisesta tietokannasta (Single Source of Truth, esim. Room tai Core Data/SQLite), mikä tarjoaa käyttäjälle välittömän käyttöliittymänäkymän. Samanaikaisesti taustalla lähetetään verkkopyyntö, joka sisältää välimuistissa olevan `If-None-Match: <etag>` -otsikon. Jos palvelin vastaa tilakoodilla `304 Not Modified`, dataa ei siirretä, mikä validoi paikallisen välimuistin säästäen samalla kaistanleveyttä ja akkua. Jos palvelin vastaa tilakoodilla `200 OK`, uusi data ja päivitetty `ETag` kirjoitetaan paikalliseen tietokantaan transaktiossa, ja reaktiiviset tietokantatarkkailijat (observers) lähettävät päivitetyn listan automaattisesti käyttöliittymään. Sivutusta (pagination) varten sivutunnisteet (tokens) tai siirtymät (offsets) tallennetaan välimuistitietueiden rinnalle. Mitätöinti laukaistaan TTL-elinkaaren (Time-To-Live) päättymisellä, käyttäjän toimilla (kuten pyyhkäisyllä tehtävä päivitys eli pull-to-refresh, joka ohittaa ehdolliset otsikot) tai paikallisten muutosten sivuvaikutuksilla (esim. tietueen luominen tai poistaminen paikallisesti päivittää tietokannan optimistisesti ja merkitsee välimuistissa olevat sivukursorit uudelleenvalidoitaviksi).

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
    }
}
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

Kysymykset kokeneille

18Finanssialan mobiilisovelluksen on tallennettava virkistystokenit (refresh token) turvallisesti laitteelle, tuettava biometristä avausta, säilyttävä toimintakuntoisena käyttöjärjestelmäpäivityksissä (OS, Operating System) ja toimittava turvallisesti tilapäisten offline-istuntojen aikana. Miten suunnittelisit tämän tokenien tallennus- ja käyttöoikeusarkkitehtuurin?

Vankka mobiilitokenien tallennusarkkitehtuuri hyödyntää laitteistopohjaisia turvamoduuleja: iOS Keychainia Secure Enclaven tukemana ja Android Keystorea StrongBoxin tai luotetun suoritusympäristön (TEE, Trusted Execution Environment) tukemana. Refresh tokenit on salattava levossa avaimilla, jotka on suojattu biometrisellä kryptografisella kytkennällä (kuten `kSecAccessControlBiometryCurrentSet` tai `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` iOS:ssä, ja `setUserAuthenticationRequired(true)` arvolla `AUTH_BIOMETRIC_STRONG` Androidissa). Kryptografinen kytkentä varmistaa, että laitteisto purkaa salausavaimen kääreen vain onnistuneen biometrisen todennuksen jälkeen sen sijaan, että luotettaisiin ohitettavissa olevaan boolean-tarkistukseen sovelluskoodissa. Jotta avaimet säilyvät OS-päivityksissä mutta estävät luvattoman käytön, ne sidotaan laiterautaan ja niille asetetaan rekisteröintimuutoskäytännöt, jotka mitätöivät avaimet tai vaativat uudelleentunnistautumisen, jos uusia biometrisiä tunnisteita lisätään (esim. `setInvalidatedByBiometricEnrollment(true)` Androidissa). Tilapäisissä offline-istunnoissa lyhytikäiset salatut pääsyoikeustokenit ja rajatut paikalliset offline-istuntotilat toimivat määritettyjen paikallisten TTL-aikojen ja rajattujen oikeuksien puitteissa siirtäen korkeampia oikeuksia vaativat virkistykset siihen asti, kun verkkoyhteys palaa. Yhteyden palautuessa taustajärjestelmä validoi istunnon refresh tokenien rotaatiolla ja kertakäyttöisellä toistohyökkäysten tunnistuksella (replay detection); jos tokenin uudelleenkäyttö tai tilin mitätöinti havaitaan, taustajärjestelmä palauttaa mitätöintisignaalin, joka laukaisee laitteistopohjaisten avainten poistamisen ja paikallisen tallennustilan tyhjentämisen asiakassovelluksessa.

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()
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

19Miten suunnittelisit skaalautuvan CI/CD (Continuous Integration / Continuous Deployment) -infrastruktuurin alustariippumattomalle mobiilimonorepolle useiden tiimien tueksi samalla halliten rajallista ja kallista macOS-suorittajien (runner) kapasiteettia?

Skaalautuvan CI/CD-infrastruktuurin suunnittelu alustariippumattomalle mobiilimonorepolle rajallisen ja kalliin macOS-kapasiteetin optimoimiseksi perustuu riippuvuusgraafin huomioivaan muutoskohtaiseen kääntämiseen (affected build detection), työkuormien tarkkaan jakamiseen hybridisuorittajien kesken sekä macOS-ympäristöjen virtualisointiin ja orkestrointiin. Ensinnäkin monorepolle otetaan käyttöön koontigraafityökalu (kuten Bazel, Nx tai Turborepo), jossa on hajautettu etävälimuisti. Jokaisessa pull requestissa CI-putki laskee suunnatun syklittömän graafin (DAG, Directed Acyclic Graph) erotuksen kohdehaaraan nähden kääntääkseen ja testatakseen vain ne paketit ja niiden riippuvuudet, joihin muutokset vaikuttavat, ohittaen muuttumattomat moduulit kokonaan. Toiseksi toteutetaan hybridi suorittajien allokointistrategia: kaikki tehtävät, jotka eivät ehdottomasti vaadi Xcodea (kuten TypeScript/Dart-koodin staattinen tarkistus ja kääntäminen, yksikkötestit, tietoturvatarkistukset ja Androidin Gradle-käännökset), siirretään kustannustehokkaille, skaalautuville Linux/Kubernetes-suorittajille. macOS-suorittajat varataan ainoastaan lopulliseen iOS-koostamiseen, Swift/Objective-C-kääntämiseen, koodin allekirjoittamiseen ja iOS-simulaattoritestien ajamiseen. Kolmanneksi macOS-suorittajia hallitaan virtualisointi-infrastruktuurilla (kuten Tart, Anka tai AWS/MacStadium bare-metal -solmut, joita orkestroidaan Nomadin tai Kubernetesin avulla). Jokainen iOS-käännös suoritetaan puhtaassa, tilapäisessä virtuaalikoneessa, jossa on esilämmitetyt työkaluketjut ja `derived-data`-välimuistit, mikä estää suorittajien tilan ajautumista (runner drift) ja mahdollistaa nopean automaattisen skaalauksen putken jonon pituuden mukaan.

name: Cross-Platform Monorepo CI
on: [pull_request]
jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      ios_affected: ${{ steps.filter.outputs.ios }}
      android_affected: ${{ steps.filter.outputs.android }}
    steps:
      - uses: actions/checkout@v4
      - id: filter
        run: |
          # Determine affected targets via monorepo tool (e.g. nx / bazel)
          echo "ios=$(./tools/affected.sh ios)" >> $GITHUB_OUTPUT
          echo "android=$(./tools/affected.sh android)" >> $GITHUB_OUTPUT

  build-android:
    needs: detect-changes
    if: needs.detect-changes.outputs.android_affected == 'true'
    runs-on: ubuntu-latest-16core
    steps:
      - uses: actions/checkout@v4
      - run: ./gradlew assembleRelease

  build-ios:
    needs: detect-changes
    if: needs.detect-changes.outputs.ios_affected == 'true'
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - run: bundle exec fastlane ios build_and_test
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa

20Miten suunnittelisit alustariippumattoman mobiiliarkkitehtuurin tuotteelle, jonka odotetaan jakavan suurimman osan liiketoimintalogiikasta iOS:n ja Androidin välillä, samalla kun alustakohtaisen UI (User Interface) -käyttöliittymän, navigaation ja natiivien integraatioiden annetaan kehittyä itsenäisesti?

Suunnittelisin sovelluksen jaetun ytimen ympärille, joka omistaa vakaan liiketoimintakäyttäytymisen: toimialuemallit, validoinnin, käyttötapaukset (use cases), liiketoimintasäännöt ja datan käsittelyn sopimukset. Alustakohtaisen käyttöliittymän, navigaation, natiivien SDK-kirjastojen, käyttöoikeuksien, elinkaaren hallinnan ja sovelluskuoren (app shell) vastuut tulisi pitää tämän ytimen ulkopuolella. Jaetun koodin ei tule tuoda riippuvuuksia UIKit-, SwiftUI-, Jetpack- tai Android-kehysrajapintoihin, React Native -navigaatioon, Flutter-navigaatioon tai natiiveihin SDK-yksityiskohtiin. Sen sijaan sen tulisi riippua kapeista rajapinnoista, kuten AuthRepository, SecureStorage, Analytics, CameraService tai PaymentProvider, jotka kukin alusta toteuttaa. Järjestäisin koodin mahdollisimman pitkälle ominaisuuksittain yhden suuren jaetun kerroksen luomisen sijaan. Esimerkiksi maksutapahtumalla (checkout), haulla, tilinhallinnalla ja viestinnällä voi kullakin olla jaettua toimialue- ja käyttötapauskoodia, tilasopimuksia sekä repositoriorajapintoja. iOS- ja Android-kuoret omistavat tällöin näkymien koostamisen, natiivit käyttöliittymämallit, navigaatiopinot, käyttöoikeudet ja sovittimien kytkennät. Esityslokiikkaa voidaan jakaa, kun se on täysin riippumaton UI-kehyksestä – kuten reducerit, tilakoneet tai view-model-sopimukset, jotka tuottavat yksinkertaista tilaa ja toimintoja – mutta varsinaisten näkymien ja navigaation tulisi säilyä alustan omistuksessa, jotta kukin alusta voi kehittyä itsenäisesti. Pääasiallinen kompromissi on jakaa yhteinen, vakaa liiketoimintalogiikka tehokkaasti välttäen samalla abstraktioita, jotka piilottavat todellisia alustaeroja. Valvoisin rajoja moduuliriippuvuussäännöillä, riippuvuuksien injektoinnilla, julkisilla API-rajapinnoilla, sopimustesteillä, arkkitehtuuritarkistuksilla ja selkeällä omistajuudella. Natiivien integraatioiden tulisi käyttää portti- ja sovitinmallia (ports/adapters), jolloin jaettu ominaisuuskoodi näkee yhteisen kyvykkyyden samalla kun kukin alusta käsittelee SDK-käyttäytymisen, elinkaaren, oikeudet, virheet ja UX-erot omassa kerroksessaan.

shared-core/
  checkout/
    domain/          # Money, Cart, CheckoutRules, PlaceOrderUseCase
    ports/           # PaymentGateway, TaxRepository, Analytics
    state/           # CheckoutStateMachine or ViewModel contract

ios-app/
  checkout-ui/       # SwiftUI/UIKit screens and iOS navigation
  adapters/          # ApplePayPaymentGateway, KeychainStorage

android-app/
  checkout-ui/       # Compose/XML screens and Android navigation
  adapters/          # GooglePayPaymentGateway, EncryptedSharedPrefsStorage
Kokeile vastata tähän kysymykseen tekoälyvalmentajan kanssa