Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Intelligence View
⚡ tsecurity.de Intelligence

Run Your MVVM Android App on iOS — KMP Magic ✨

In this article, we’ll take an existing Android MVVM app and gradually convert it into a Kotlin Multiplatform (KMP) app — without rewriting the business logic. The goal is simple: Share business logic (repository, networking, database) a…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

In this article, we’ll take an existing Android MVVM app and gradually convert it into a Kotlin Multiplatform (KMP) app — without rewriting the business logic.



The goal is simple:




Share business logic (repository, networking, database) across Android and iOS, while keeping UI native.







Out of scope for this article




  • To keep things simple and focused, we will not cover:


  • Common UI (Compose Multiplatform)


  • Common ViewModel


  • Publishing shared module remotely (KMMBridge / SPM)





We want clarity first, not complexity.







Outline of our article




  • Android only MVVM app (Starting point)


    - Feature of sample app


    - Basic outline of android project


    - How data flows (mental model)


    - Clone and run the android app


  • Setup the KMP world


    - What’s important from the official guide (summary)


  • Beginning the KMP magic


    - Step 1: Move data folder to the shared module


    - Step 2: Replace Hilt with Koin (Multiplatform DI)


    - Step 3: Replace Retrofit with Ktor


    - Step 4: Replace Room with Room KMP


    - Step 5: Create UI and ViewModel alternative for IOS


  • Final words







Android only MVVM app (Starting point)



We begin with an Android-only MVVM application that follows a clean and well-structured architecture.



Source code (master branch) — Link






Features of the sample app




  • Movies app using OMDb free API


  • Offline-first architecture (Database acts as a single source of truth, refer MovieRepository.kt file for implementation)


  • Two screens: Movie list screen (Uses MovieViewModel and shows a list of movies), Movie detail screen (simple UI, no ViewModel)


  • Uses: Retrofit → Networking; Room → Local caching; Hilt → Dependency Injection; Jetpack Compose → UI







Basic outline of the android project







└── com.khush.moviesapp
├── data
│ ├── database
│ │ ├── DatabaseService.kt
│ │ ├── MovieDao.kt
│ │ ├── MovieDatabase.kt
│ │ └── MovieDatabaseService.kt
│ ├── model
│ │ └── MovieData.kt
│ ├── network
│ │ └── ApiInterface.kt
│ ├── repository
│ │ └── MovieRepository.kt
│ └── utils
│ └── Const.kt
├── di
│ └── module
│ └── ApplicationModule.kt
├── ui
│ ├── viewmodel
│ │ ├── MovieViewModel.kt
│ │ └── UIState.kt
│ ├── DetailScreen.kt
│ ├── MovieActivity.kt
│ └── MovieScreen.kt
└── MovieApplication.kt









How data flows (mental model)




  • UI observes ViewModel


  • ViewModel talks to Repository


  • Repository decides: fetch from DB or fetch from API


  • UI reacts to changes in UIState





This mental model is important — we’ll preserve it even after KMP.







Clone and run the Android app



Before touching KMP, always verify the base app works.




  1. Clone the repo (master branch)


  2. Generate an API key from https://www.omdbapi.com/


  3. Add the API key inside Const.kt


  4. Run the Android app




We should see the movie list screen working correctly.






Setup up the KMP world



Before converting our Android MVVM app to a KMP MVVM app, we must ensure the KMP environment is correctly set up.



We will follow the official Kotlin guide for integrating KMP into an existing Android app — https://kotlinlang.org/docs/multiplatform/multiplatform-integrate-in-existing-app.html



You can follow this guide end-to-end. At this stage:


- Nothing meaningful will be shown on iOS yet


- Our goal is only to ensure the project builds and runs on both platforms





What’s important from the official guide (summary)




  • Create a shared module for cross-platform code — Link
    - Once created, we’ll see three important folders:



- commonMain - common kotlin code, accessible by both android and ios
- androidMain - android specific implementations, can use android sdk
- iosMain - ios specific implementations, can use ios platform apis






  • Add code to the shared module — Link
    - This allows Android code to access shared Kotlin code. We can see the use of expect/actual in the example, let’s understand the basics




KMP allows us to declare what we need in common code, and define how it works per platform.



- expect class — written in commonMain — method declaration without implementation


- actual class — written in androidMain or iosMain with platform specific implementation



Whenever common code needs different behavior on Android and iOS, we use expect/actual.





  • Add a dependency on the shared module to your Android application — Link


  • Now we will run our cross-platform application on Android


  • Make your cross-platform application work on iOS — Link


    - Follow all four steps and finally verify that shared module can be imported in Swift and iOS app builds and runs successfully




Optional (but recommended): Install Kotlin Multiplatform plugin




  • Android Studio Settings → Plugins → Marketplace → Kotlin Multiplatform


  • This helps with: Environment checks, Project configuration, Boilerplate generation (We can do KMP without the plugin, but it smoothens the experience)






Beginning the KMP magic ✨



At this point:


• Android app works


• iOS app builds


• Shared module is ready


• KMP pipeline is validated



Now we are finally ready to convert our Android MVVM app into a KMP MVVM app.




At any point, you can refer to the full conversion Pull Request for comparison.



PR — Android → KMP conversion




Highlights of what we will be doing




  • Moving our data folder from app module to shared module


  • Replace Android only libraries to KMP compatible libraries


    - Hilt → Koin (Dependency Injection)


    - Retrofit → Ktor (Networking) (Additionally Gson → Kotlin Serialization)


    - Room → Room KMP (Database)


  • Create native UI and ViewModel in swift for IOS






Step 1: Move data folder to the shared module



The data layer is the most natural candidate for sharing.



What we move



Move the entire data package from:




app/src/main/java






to:




shared/src/commonMain/kotlin






This includes: database, network, repository, model, utils



At this point, the project will break — this is expected.



We’ll fix things step by step.



Clean up Android-only dependencies



Since data logic is now shared, we must remove Android-only dependencies from app.



Remove from app/build.gradle.kts: Hilt, Retrofit, Room, kapt (we’ll use KSPinstead)






Step 2: Replace Hilt with Koin (Multiplatform DI)



Hilt is Android-only, so we replace it with Koin, which works on both Android and iOS.



Remove all Hilt annotations




@HiltViewModel, @AndroidEntryPoint, @HiltAndroidApp, @Inject constructor






Also delete: ApplicationModule.kt (Hilt module)



Create Android-specific Koin module in App module folder, and Shared module in commonMain folder.




#AppModule.kt

val appModule = module {
viewModel {
MovieViewModel(
movieRepository = get()
)
}
}

###################################

#SharedModule.kt

val sharedModule = module {
single<MovieRepository> {
MovieRepository(
apiInterface = get(),
databaseService = get()
)
}
}






Now we will add koin related dependencies




#libs.version.toml

koin-bom = "4.1.1"

koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
koin-core = { module = "io.insert-koin:koin-core" }
koin-android = { module = "io.insert-koin:koin-android" }
androidx-startup-runtime = { group = "androidx.startup", name = "startup-runtime", version.ref = "startupRuntime" }

###################################

#App Module build.gradle.kts

dependencies {
implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.android)
}

###################################

#Shared Module build.gradle.kts

commonMain {
dependencies {
implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.core)
}
}






Sync Gradle and then update MovieApplication.kt and MovieActivity.kt files




#MovieApplication.kt

class MovieApplication: Application() {
override fun onCreate() {
super.onCreate()
// Initialize Koin on Android
startKoin {
androidContext(this@MovieApplication)
modules(listOf(appModule, sharedModule))
}
}
}

###################################

#MovieActivity.kt

class MovieActivity : ComponentActivity() {

// Inject ViewModel
private val movieViewModel: MovieViewModel by inject()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MainScreen(movieViewModel = movieViewModel, ...)
...
}
}









Step 3: Replace Retrofit with Ktor



Retrofit is Android-only.



We replace it with Ktorfit (built upon Ktor), which gives us a Retrofit-like API but works on KMP.



To do it we will add Ktor, Ktorfit and Kotlin Serialization dependency (We will replace with Gson used in android project). We will also need KSP for this.




#libs.version.toml

ktor = "3.3.3"
ktorfit = "2.7.2"
ksp = "2.3.4"

ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktorfit-lib = {module = "de.jensklingenberg.ktorfit:ktorfit-lib", version.ref = "ktorfit" }

[plugins]

ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
ktorfit = { id = "de.jensklingenberg.ktorfit", version.ref = "ktorfit" }
kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

###################################

#Project level build.gradle.kts

alias(libs.plugins.ksp) apply false
alias(libs.plugins.ktorfit) apply false
alias(libs.plugins.kotlinxSerialization) apply false

###################################

#Shared module build.gradle.kts

alias(libs.plugins.ksp)
alias(libs.plugins.ktorfit)
alias(libs.plugins.kotlinxSerialization)

commonMain {
dependencies {
// Ktor
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization)

// Ktorfit
implementation(libs.ktorfit.lib)
}






We will use the same ApiInterface.kt class just replace the imports




#ApiInterface.kt

import de.jensklingenberg.ktorfit.http.GET
import de.jensklingenberg.ktorfit.http.Query

interface ApiInterface {
@GET(".")
suspend fun getMoviesData(
@Query("s") s: String = "army",
@Query("apikey") apikey: String = Const.API_KEY
): ApiResponse
}






We will add the dependency in shared module class for Ktorfit




#SharedModule.kt

val sharedModule = module {
single<ApiInterface> {
val ktorfit = Ktorfit
.Builder()
.baseUrl(Const.BASE_URL)
.httpClient(
HttpClient {
install(ContentNegotiation) {
json(
Json {
isLenient = true
ignoreUnknownKeys = true
prettyPrint = true
}
)
}
}
)
.build()

ktorfit.createApiInterface()
}
}






To use Kotlin Serialization, inside MovieData.kt replace @Serializable with @SerialName, add @Serializable over data classes






Step 4: Replace Room with Room KMP



We will now replace room with KMP supported Room version.



We will add the dependency for room and all necessary configurations.




#libs.version.toml

room = "2.8.4"
sqlite = "2.6.2"
koin-bom = "4.1.1"
startupRuntime = "1.2.0"

androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
androidx-startup-runtime = { group = "androidx.startup", name = "startup-runtime", version.ref = "startupRuntime" }

[plugins]

androidx-room = { id = "androidx.room", version.ref = "room" }

###################################

#Project level build.gradle.kts

alias(libs.plugins.androidx.room) apply false

###################################

#Shared Module build.gradle.kts

alias(libs.plugins.androidx.room)

commonMain {
dependencies {
// Room
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.sqlite.bundled)
}
}
androidMain {
dependencies {
implementation(libs.androidx.startup.runtime)
}
}
room {
schemaDirectory("$projectDir/schemas")
}
dependencies {
add("kspAndroid", libs.androidx.room.compiler)
add("kspIosSimulatorArm64", libs.androidx.room.compiler)
add("kspIosX64", libs.androidx.room.compiler)
add("kspIosArm64", libs.androidx.room.compiler)
}

###################################

#proguard-rules.pro

-keep class * extends androidx.room.RoomDatabase { <init>(); }






We need to modify MovieDatabase.kt class definition




#MovieDatabase.kt

@Database(entities = [MovieData::class], version = 1, exportSchema = false)
@ConstructedBy(MovieDatabaseConstructor::class)
abstract class MovieDatabase : RoomDatabase() {

abstract fun getMovieDao(): MovieDao

}

// The Room compiler generates the `actual` implementations.
@Suppress("KotlinNoActualForExpect")
expect object MovieDatabaseConstructor : RoomDatabaseConstructor<MovieDatabase> {
override fun initialize(): MovieDatabase
}






Now we will create the instance inside SharedModule.kt




val sharedModule = module {
single<DatabaseService> {
MovieDatabaseService(
getDatabaseBuilder()
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.build()
)
}
}






You can see getDatabaseBuilder() which is needed for Room to create Database instance, we need to create the RoomDatabase.Builder instance separately for platform specific modules (android and ios).



Before creating expect actual method for database builder, we need app context for creating it for android platform module, we will create it using app startup library that we have added in dependency in *androidMain*module




#androidMain folder AndroidManifest.xml file

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.khush.shared.data.database.AppContextInitializer"
android:value="androidx.startup" />
</provider>
</application>
</manifest>

###################################

#AppContextInitializer.kt

class AppContextInitializer: Initializer<Context> {

companion object {
lateinit var applicationContext: Context
}

override fun create(context: Context): Context {
applicationContext = context.applicationContext
return context
}

override fun dependencies(): List<Class<out Initializer<*>?>?> {
return emptyList()
}

}






Now let’s create Database builder instance




#MovieDatabase.kt

expect fun getDatabaseBuilder(): RoomDatabase.Builder<MovieDatabase>

###################################

#MovieDatabase.android.kt

actual fun getDatabaseBuilder(): RoomDatabase.Builder<MovieDatabase> {
val appContext = AppContextInitializer.applicationContext
val dbFile = appContext.getDatabasePath("main_db.db")
return Room.databaseBuilder<MovieDatabase>(
context = appContext,
name = dbFile.absolutePath
)
}

###################################

#MovieDatabase.ios.kt

actual fun getDatabaseBuilder(): RoomDatabase.Builder<MovieDatabase> {
val dbFilePath = documentDirectory() + "/main_db.db"
return Room.databaseBuilder<MovieDatabase>(
name = dbFilePath,
)
}

@OptIn(ExperimentalForeignApi::class)
private fun documentDirectory(): String {
val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
directory = NSDocumentDirectory,
inDomain = NSUserDomainMask,
appropriateForURL = null,
create = false,
error = null,
)
return requireNotNull(documentDirectory?.path)
}






That’s it, rest room structure remains the same, no changes in DAO or Entity class.



Run Android app: Congrats, Android app now runs fully using KMP shared module



Let’s run it on IOS now.






Step 5: Create UI and ViewModel alternative for IOS



We will first add Skie dependency. Skie library helps between Kotlin and Swift interoperability.



Skie allows Kotlin Flow → Swift async/await.




#libs.version.toml

skie = "0.10.9"

[plugins]

skie = { id = "co.touchlab.skie", version.ref = "skie" }

###################################

#Project level build.gradle.kts

alias(libs.plugins.skie) apply false

###################################

#Shared Module build.gradle.kts

alias(libs.plugins.skie)






We need to start koin and access movie repository from ios swift files, we will create a mediator class in iosMain folder inside shared module that will do the work, we can directly access it from Swift file




#SharedIosInterop.kt

object SharedIosInterop: KoinComponent {

val movieRepository: MovieRepository by inject()

fun initKoin() {
startKoin {
modules(sharedModule)
}
}
}






Let’s create the UI and ViewModel for IOS in iosApp folder. Delete existing Swift file and create the below files.




#iosApp.swift

import SwiftUI
import sharedKit

@main
struct iosAppApp: App {

private let shared = SharedIosInterop()
// Initialize Koin on app start
init() {
shared.doInitKoin()
}

var body: some Scene {
WindowGroup {
MovieScreen(
vm: MovieViewModel(repo: shared.movieRepository)
)
}
}
}

###################################

#MovieScreen.swift

struct MovieScreen: View {
@StateObject var vm: MovieViewModel

var body: some View {
NavigationStack {
content
}
}

@ViewBuilder
private var content: some View {
switch vm.state {
case .loading:
ProgressView()

case .failure(let msg):
VStack(spacing: 12) {
Text(msg).multilineTextAlignment(.center)
Button("Retry") { vm.start() }
}
.padding()

case .success(let movies):
List {
ForEach(movies, id: \.title) { movie in
NavigationLink {
DetailScreen(title: movie.title, poster: movie.poster)
} label: {
MovieRow(movie: movie)
}
}
}
}
}
}

struct MovieRow: View {
let movie: MovieData

var body: some View {
HStack(spacing: 12) {
AsyncImage(url: URL(string: movie.poster)) { phase in
switch phase {
case .empty:
Rectangle().opacity(0.2)
case .success(let image):
image.resizable().scaledToFill()
default:
Rectangle().opacity(0.2)
}
}
.frame(width: 80, height: 80)
.clipped()

Text(movie.title)
.font(.headline)

Spacer()
}
.padding(.vertical, 6)
}
}

###################################

#MovieViewModel.swift

import Foundation
import Combine
import sharedKit

@MainActor
final class MovieViewModel: ObservableObject {

enum ScreenState {
case loading
case success([MovieData])
case failure(String)
}

@Published private(set) var state: ScreenState = .loading

private let repo: MovieRepository
private var task: Task<Void, Never>?

init(repo: MovieRepository) {
self.repo = repo
start()
}

func start() {
task?.cancel()
state = .loading

task = Task {
do {
for try await value in repo.getMoviesData() {
let movies = (value as? [MovieData]) ?? []
if movies.isEmpty {
state = .failure("Empty Data")
} else {
state = .success(movies)
}
}
} catch {
state = .failure(error.localizedDescription)
}
}
}

deinit {
task?.cancel()
}
}

###################################

#DetailScreen.swift

import SwiftUI

struct DetailScreen: View {
let title: String
let poster: String

var body: some View {
VStack(spacing: 16) {
Text(title).font(.title2)

AsyncImage(url: URL(string: poster)) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image.resizable().scaledToFit()
default:
Text("Image load failed")
}
}
}
.padding()
}
}






We will add few files on .gitignore.




#.gitignore

shared/build
xcuserdata
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings






Great, now run the IOS project it should run the MVVM app in IOS using shared module



Finally after all, your structure should look something like this




├── app
│ ├── src
│ │ ├── main
│ │ │ ├── java
│ │ │ │ └── com.khush.moviesapp
│ │ │ │ ├── di.module
│ │ │ │ │ └── AppModule.kt
│ │ │ │ ├── ui
│ │ │ │ │ ├── viewmodel
│ │ │ │ │ │ ├── MovieViewModel.kt
│ │ │ │ │ │ └── UIState.kt
│ │ │ │ │ ├── DetailScreen.kt
│ │ │ │ │ ├── MovieActivity.kt
│ │ │ │ │ └── MovieScreen.kt
│ │ │ │ └── MovieApplication.kt
│ │ │ └── AndroidManifest.xml
│ ├── build.gradle.kts
│ └── proguard-rules.pro
├── gradle
│ └── libs.versions.toml
├── iosApp
│ ├── iosApp
│ │ ├── DetailScreen.swift
│ │ ├── IosApp.swift
│ │ ├── MovieScreen.swift
│ │ └── MovieViewModel.swift
│ └── iosApp.xcodeproj
├── shared
│ ├── src
│ │ ├── androidMain
│ │ │ ├── kotlin
│ │ │ │ └── com.khush.shared
│ │ │ │ └── data
│ │ │ │ └── database
│ │ │ │ ├── AppContextInitializer.kt
│ │ │ │ └── MovieDatabase.android.kt
│ │ │ └── AndroidManifest.xml
│ │ ├── commonMain
│ │ │ └── kotlin
│ │ │ └── com.khush.shared
│ │ │ ├── data
│ │ │ │ ├── database
│ │ │ │ │ ├── DatabaseService.kt
│ │ │ │ │ ├── MovieDao.kt
│ │ │ │ │ ├── MovieDatabase.kt
│ │ │ │ │ └── MovieDatabaseService.kt
│ │ │ │ ├── model
│ │ │ │ │ └── MovieData.kt
│ │ │ │ ├── network
│ │ │ │ │ └── ApiInterface.kt
│ │ │ │ ├── repository
│ │ │ │ │ └── MovieRepository.kt
│ │ │ │ └── utils
│ │ │ │ └── Const.kt
│ │ │ └── di
│ │ │ └── SharedModule.kt
│ │ └── iosMain
│ │ └── kotlin
│ │ └── com.khush.shared
│ │ ├── data
│ │ │ └── database
│ │ │ └── MovieDatabase.ios.kt
│ │ └── interop
│ │ └── SharedIosInterop.kt
│ └── build.gradle.kts
├── build.gradle.kts
└── settings.gradle.kts









Final words



Congratulations! Now we are able to run same MVVM app on IOS and it is just the beginning, this base understanding will help you integrate more complex project into IOS later.





Source code (master-multiplatform branch) — Link



Check out the pull request for the conversion for better understanding — Link



Contact Me: LinkedIn, Twitter



Happy coding ✌️

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Run Your MVVM Android App on iOS — KMP Magic ✨

Thematisch verwandte Begriffe: Your, MVVM, Android, Magic · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY Kritische Sicherheitsmeldung
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick