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!
# 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:
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()
}
}
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()
}
}