Arhitectura Aplicațiilor Mobile Moderne cu Kotlin Multiplatform (KMP): Clean Architecture între Android și iOS în Kotlin

Ghid arhitectural complet pentru Kotlin Multiplatform (KMP): separarea straturilor Domain, Data și Presentation între Android și iOS, managementul memoriei în Kotlin/Native, interoperabilitate cu Swift și Swift Concurrency (async/await, SKIE).

Sintaxă
commonMain { dependencies { implementation(libs.ktor.client.core) } }

Timp de ani de zile, dezvoltarea mobilă multiplataformă a fost împărțită între abordarea nativă duplicată (două echipe separate scriind același cod în Kotlin și Swift) și framework-uri de tip „cutie neagră” hibride (React Native, Flutter) care impun propriul motor de randare și abstractizează forțat platforma nativă.

Kotlin Multiplatform (KMP) reprezintă soluția optimă a industriei moderne: în loc să înlocuiască UI-ul nativ, KMP permite partajarea exclusivă a logicii de business (Networking, Caching local, Validări de afaceri, Modelare de date și ViewModel-uri), păstrând interfața 100% nativă: Jetpack Compose pe Android și SwiftUI pe iOS!

Arhitectura Clean Architecture în Kotlin Multiplatform
# Stratul UI (Nativ & Separat):
#    ┌──────────────────────────┐         ┌──────────────────────────┐
#    │      Android (Kotlin)    │         │         iOS (Swift)      │
#    │      Jetpack Compose     │         │           SwiftUI        │
#    └────────────┬─────────────┘         └────────────┬─────────────┘
#                 │                                    │
# ════════════════╪════════════════════════════════════╪════════════════════════════
# Stratul Partajat (KMP - shared module):              │
#                 ▼                                    ▼
#    ┌───────────────────────────────────────────────────────────────┐
#    │    Presentation Layer: Shared ViewModels & StateFlow (SKIE)   │
#    └──────────────────────────────┬────────────────────────────────┘
#                                   ▼
#    ┌───────────────────────────────────────────────────────────────┐
#    │    Domain Layer: Use Cases, Domain Models, Business Rules     │
#    └──────────────────────────────┬────────────────────────────────┘
#                                   ▼
#    ┌───────────────────────────────────────────────────────────────┐
#    │    Data Layer: Repositories, Ktor Client (HTTP), SQLDelight   │
#    └───────────────────────────────────────────────────────────────┘

Stiva Tehnologică Recomandată în Producție pentru KMP

Responsabilitate Arhitecturală Librărie Multiplatform Oficială Avantaj Cheie în Producție
Client HTTP & Rețea io.ktor:ktor-client-core Motor adaptiv: OkHttp pe Android, NSURLSession / Darwin pe iOS.
Bază de Date Locală app.cash.sqldelight sau androidx.room Interogări SQL verificate la compilare cu generare automată de cod Kotlin.
Serializare JSON org.jetbrains.kotlinx:kotlinx-serialization-json Performanță maximă fără reflexie (reflection-free bytecode).
Data & Oră org.jetbrains.kotlinx:kotlinx-datetime Suport standardizat ISO-8601 peste Instant și TimeZone pe toate platformele.
Interoperabilitate Swift co.touchlab.skie (SKIE) Transformă automat Kotlin Flow și suspend functions în AsyncSequence și async/await native în Swift!
Injecție de Dependințe io.insert-koin:koin-core Service Locator ușor și idiomatic fără generare complexă de adnotări.

Implementare Practică: ViewModel Partajat cu StateFlow și Consum în SwiftUI

Iată cum arată un State Holder partajat în modulul shared și modul elegant în care este consumat direct în SwiftUI pe iOS:

shared/src/commonMain/kotlin/ProfileViewModel.kt
package ro.dexblog.kmp.profile

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

@kotlinx.serialization.Serializable
data class UserProfile(val id: String, val name: String, val email: String)

sealed interface ProfileUiState {
    data object Loading : ProfileUiState
    data class Success(val profile: UserProfile) : ProfileUiState
    data class Error(val message: String) : ProfileUiState
}

class ProfileViewModel(
    private val repository: ProfileRepository,
    private val coroutineScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
) {
    private val _uiState = MutableStateFlow(ProfileUiState.Loading)
    val uiState: StateFlow = _uiState.asStateFlow()

    fun loadProfile(userId: String) {
        coroutineScope.launch {
            _uiState.value = ProfileUiState.Loading
            try {
                val user = repository.fetchUserProfile(userId)
                _uiState.value = ProfileUiState.Success(user)
            } catch (e: Exception) {
                _uiState.value = ProfileUiState.Error(e.message ?: "Eroare necunoscută la încărcare")
            }
        }
    }

    // Curățare la distrugerea ecranului (Lifecycle dealloc)
    fun onCleared() {
        coroutineScope.cancel()
    }
}
iosApp/iosApp/ProfileView.swift – Consum Direct în SwiftUI cu Swift Concurrency
import SwiftUI
import SharedModule // Cadrul KMP exportat ca XCFramework

struct ProfileView: View {
    @StateObject private var observableViewModel: ObservableProfileViewModel

    init(userId: String) {
        _observableViewModel = StateObject(wrappedValue: ObservableProfileViewModel(userId: userId))
    }

    var body: some View {
        NavigationView {
            VStack {
                switch observableViewModel.state {
                case is ProfileUiStateLoading:
                    ProgressView("Se încarcă profilul...")
                case let successState as ProfileUiStateSuccess:
                    VStack(alignment: .leading, spacing: 12) {
                        Text(successState.profile.name)
                            .font(.title)
                            .bold()
                        Text(successState.profile.email)
                            .font(.subheadline)
                            .foregroundColor(.gray)
                    }
                    .padding()
                case let errorState as ProfileUiStateError:
                    Text("Eroare: \(errorState.message)")
                        .foregroundColor(.red)
                default:
                    EmptyView()
                }
            }
            .navigationTitle("Profil Utilizator")
            .task {
                await observableViewModel.observeState()
            }
        }
    }
}

// Wrapper Swift pentru observarea StateFlow-ului prin SKIE (Swift AsyncSequence)
@MainActor
class ObservableProfileViewModel: ObservableObject {
    private let viewModel: ProfileViewModel
    @Published var state: ProfileUiState = ProfileUiStateLoading()

    init(userId: String) {
        self.viewModel = ProfileViewModel(repository: ProfileRepositoryImpl())
        self.viewModel.loadProfile(userId: userId)
    }

    func observeState() async {
        // SKIE convertește StateFlow direct în AsyncSequence Swift!
        for await updatedState in viewModel.uiState {
            self.state = updatedState
        }
    }

    deinit {
        viewModel.onCleared()
    }
}