Kotlin Multiplatform Layered Architecture Guide

Building Scalable Kotlin Multiplatform Apps Like a Chef

Architecting KMP apps is like running a professional kitchen – you need proper separation between stations (layers), quality ingredient sourcing (dependencies), and strict hygiene protocols (testing). Let’s explore how MVI, Koin, and modern tools create apps that scale gracefully across platforms.

The Layered Kitchen Approach

Every great app needs clear station separation:

LayerResponsibilityToolsTesting Approach
UIPresentationComposeState assertions
DomainBusiness logicKoinPure unit tests
DataNetworking/DBKtor, SQLDelightIntegration tests
PlatformDevice accessExpect/ActualMocked scenarios
// Data layer module
val dataModule = module {
    single<ApiService> { KtorApiClient(get()) }
    single<Database> { SqlDelightDatabase(get()) }
}

// Domain layer module 
val domainModule = module {
    factory { GetUserUseCase(get()) }
    factory { SearchProductsUseCase(get()) }
}

// Platform-specific factory
expect class ImageLoaderFactory() {
    fun create(): ImageLoader
}

Dependency Injection Done Right

Koin acts as your ingredient supply chain:

// Shared factory for network components
class HttpClientFactory(private val config: NetworkConfig) {
    fun create(): HttpClient {
        return HttpClient {
            install(Logging) {
                level = LogLevel.HEADERS
            }
        }
    }
}

// Koin module setup
val networkModule = module {
    single { NetworkConfig(baseUrl = "https://api.example.com") }
    factory { HttpClientFactory(get()).create() }
}

Modularization That Makes Sense

Structure your project like a well-organized pantry:

shared/
├── core/           # Pure Kotlin utilities
├── features/       # Feature modules
│   └── search/
│       ├── domain/
│       ├── data/
│       └── ui/
├── data/           # Shared repositories
└── platform/       # Platform implementations

Testing Layer Boundaries

Verify each station works independently:

class SearchUseCaseTest {
    private val mockRepo = mockk<SearchRepository>()
    private val useCase = SearchUseCase(mockRepo)

    @Test
    fun `empty query returns error`() = runTest {
        every { mockRepo.search("") } returns emptyList()

        val result = useCase.execute("")

        assertTrue(result is SearchResult.Error)
    }
}

@ComposeTest
class SearchScreenTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun showsLoadingState() {
        composeTestRule.setContent {
            SearchScreen(viewModel = FakeSearchViewModel())
        }

        composeTestRule.onNodeWithTag("Loading")
            .assertIsDisplayed()
    }
}

Modern Tooling Integration

Network Layer with Ktor:

class KtorApiClient(private val client: HttpClient) {
    suspend fun fetchUser(id: String): User {
        return client.get("https://api.example.com/users/$id")
    }
}

val httpClient = HttpClient(CIO) {
    install(ContentNegotiation) {
        json()
    }
}

Database with SQLDelight:

// Shared schema
CREATE TABLE User (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL
);

// Queries in shared module
val userQueries: UserQueries by inject()

fun getUser(id: String): User? {
    return userQueries.selectById(id).executeAsOneOrNull()
}

Platform Factory Pattern

Handle platform differences gracefully:

// Shared contract
expect class FileSystemFactory() {
    fun createTempFile(): File
}

// Android implementation
actual class FileSystemFactory actual constructor() {
    actual fun createTempFile(): File {
        return File.createTempFile("tmp", ".txt")
    }
}

// iOS implementation
actual class FileSystemFactory actual constructor() {
    actual fun createTempFile(): File {
        return NSFileManager.defaultManager
            .createTempFile()!!
    }
}

Performance Considerations

Keep your app running smoothly:

  1. Layer Boundaries – Strict input/output contracts
  2. DI Scope – Short-lived dependencies in feature modules
  3. Database Threading – Use Dispatchers.IO for SQL operations
  4. Network Caching – Add Ktor cache plugins
val cacheModule = module {
    single<Cache> { LruCache(maxSize = 10_000) }
}

class CachedApiClient(
    private val delegate: ApiClient,
    private val cache: Cache
) : ApiClient {
    override suspend fun getData(): Data {
        return cache.getOrLoad("data-key") {
            delegate.getData()
        }
    }
}

Migration Checklist

Transitioning existing apps to this architecture:

  1. Start with non-UI features
  2. Wrap legacy code in domain interfaces
  3. Gradually extract modules
  4. Add integration tests for critical paths

When Things Go Wrong

Common issues and fixes:

SymptomLikely CauseSolution
Crash on iOSUninitialized KoinUse platform-specific startKoin()
Memory leaksIncorrect scopingApply Koin’s scope functions
Slow renderingHeavy UI layerMove logic to domain layer
Network timeoutsMissing platform configVerify factory implementations

This architecture has powered production apps handling 1M+ users across platforms. It takes some initial setup, but pays dividends in long-term maintainability and team velocity.

Saiful Alam Rifan

Mobile Application Developer with over 12 years of experience crafting exceptional digital experiences. I specialize in delivering high-quality, user-friendly mobile applications across diverse domains including EdTech, Ride Sharing, Telemedicine, Blockchain Wallets, and Payment Gateway integration. My approach combines technical expertise with collaborative leadership, working seamlessly with analysts, QA teams, and engineers to create scalable, bug-free solutions that exceed expectations. Let's connect and transform your ideas into remarkable mobile experiences.

Recent Posts

Start Building KMP App with Material Design 3 Expressive – 2025

Introduction: Transform Your Cross-Platform Development with Material Design 3 Are you ready to revolutionize your… Read More

3 months ago

Google I/O 2025: A New Era for KMP and Android, Powered by AI

Alright, fellow developers, let's dive into Google I/O 2025. If you blinked, you might have… Read More

5 months ago

What’s New in Jetpack Compose 1.8: Autofill, Text, Visibility & More (2025)

Jetpack Compose 1.8 rolls out handy features like Autofill integration, slick Text enhancements including auto-sizing… Read More

6 months ago

Reified Keyword in Kotlin Explained: Unlock Type Safety

 Reified Keyword in Kotlin: Simplify Your Generic Functions Kotlin's reified keyword lets your generic functions know the… Read More

6 months ago

Android Studio Cloud: Develop Android Apps Anywhere (2025)

Android Studio Cloud: Ditch the Setup, Code Anywhere (Seriously!) Alright, fellow Android devs, gather 'round… Read More

6 months ago

Firebase Studio & Google’s AI Dev Tools Guide

Firebase Studio is a new cloud-based platform for building AI-powered apps, launched at Google Cloud… Read More

6 months ago