Skip to main content
Library modules are small, focused, reusable components that provide fundamental functionality across the application. These modules are framework-agnostic and can be used in any layer of the architecture.

Module Overview

Real Clean Architecture includes several library modules:

cache

Local data persistence abstraction

foundations

Core domain types like Answer

httpclient

HTTP client configuration

viewmodel

ViewModel base interfaces

designsystem

Reusable UI components

cache-test

Test utilities for cache

flow-test-observer

Flow testing utilities

coroutines-test-dispatcher

Coroutines testing support

cache

The cache module provides abstractions for local data persistence using the Multiplatform Settings library.

Module Configuration

plugins {
    alias(libs.plugins.kotlin.multiplatform)
    alias(libs.plugins.kotlin.serialization)
}

kotlin {
    jvmToolchain(17)
    jvm()
    iosX64()
    iosArm64()
    iosSimulatorArm64()
    sourceSets {
        commonMain {
            dependencies {
                implementation(libs.multiplatform.settings)
                implementation(libs.multiplatform.settings.serialization)
                implementation(libs.kotlin.serialization)
                implementation(libs.coroutines.core)
            }
        }
    }
}

Core Interfaces

1

CachedObject - Basic Cache Interface

Provides simple get/put operations for cached data.
interface CachedObject<T : Any> {
    fun put(value: T)
    fun get(): T
}
2

FlowCachedObject - Reactive Cache

Extends CachedObject with Flow-based observation.
interface FlowCachedObject<T : Any> : CachedObject<T> {
    fun observe(): Flow<T>
}
3

CacheProvider - Factory Interface

Creates cached objects with serialization support.
interface CacheProvider {
    fun <T : Any> getCachedObject(
        fileName: String,
        serializer: KSerializer<T>,
        defaultValue: T
    ): CachedObject<T>

    fun <T : Any> getFlowCachedObject(
        fileName: String,
        serializer: KSerializer<T>,
        defaultValue: T
    ): FlowCachedObject<T>
}

Implementation

The module provides RealCachedObject and RealFlowCachedObject implementations:
class RealCachedObject<T : Any>(
    private val fileName: String,
    private val settings: Settings,
    private val serializer: KSerializer<T>,
    private val defaultValue: T
) : CachedObject<T> {
    override fun put(value: T) {
        settings.encodeValue(serializer, fileName, value)
    }

    override fun get(): T {
        return settings.decodeValue(serializer, fileName, defaultValue)
    }
}
The cache module is multiplatform and supports JVM, iOS (x64, Arm64, Simulator Arm64), making it perfect for sharing cache logic across platforms.

Platform-Specific Implementation

In the app module, Android-specific implementation uses SharedPreferences:
class AndroidCacheProvider(
    private val applicationContext: Context
) : CacheProvider {

    private val settings by lazy {
        val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
        SharedPreferencesSettings(sharedPrefs)
    }

    override fun <T : Any> getCachedObject(
        fileName: String,
        serializer: KSerializer<T>,
        defaultValue: T
    ): CachedObject<T> {
        return RealCachedObject(fileName, settings, serializer, defaultValue)
    }

    override fun <T : Any> getFlowCachedObject(
        fileName: String,
        serializer: KSerializer<T>,
        defaultValue: T
    ): FlowCachedObject<T> {
        return RealFlowCachedObject(getCachedObject(fileName, serializer, defaultValue))
    }
}

foundations

The foundations module contains fundamental domain types used across the application.

Answer Type

A sealed class representing the result of an operation that can succeed or fail:
sealed class Answer<out T, out E> {
    data class Success<out T>(
        val data: T,
    ) : Answer<T, Nothing>()

    data class Error<out E>(
        val reason: E,
    ) : Answer<Nothing, E>()

    fun <C> fold(
        success: (T) -> C,
        error: (E) -> C,
    ): C =
        when (this) {
            is Success -> success(data)
            is Error -> error(reason)
        }
}
The Answer type provides a type-safe way to handle success and error cases, similar to Result but with custom error types. Use the fold function to elegantly handle both cases.

Usage Example

From user-component/src/commonMain/kotlin/com/denisbrandi/androidrealca/user/domain/usecase/UserUseCases.kt:6:
fun interface Login {
    suspend operator fun invoke(loginRequest: LoginRequest): Answer<Unit, LoginError>
}

httpclient

The httpclient module provides a configured Ktor HTTP client for network requests.

HttpClientProvider Interface

interface HttpClientProvider {
    fun getClient(): HttpClient
}

RealHttpClientProvider Implementation

object RealHttpClientProvider : HttpClientProvider {

    private val httpClient by lazy {
        createClient(CIO.create())
    }

    fun createClient(engine: HttpClientEngine): HttpClient {
        return HttpClient(engine) {
            install(ContentNegotiation) {
                json(
                    Json {
                        isLenient = true
                        ignoreUnknownKeys = true
                    }
                )
            }
            install(HttpTimeout) {
                requestTimeoutMillis = 3000L
            }
        }
    }

    override fun getClient(): HttpClient {
        return httpClient
    }
}
The HTTP client is configured with:
  • ContentNegotiation for JSON serialization/deserialization
  • HttpTimeout with 3-second request timeout
  • CIO engine for Kotlin/Native compatibility

viewmodel

The viewmodel module provides base interfaces and delegates for implementing the presentation layer.

Core Interfaces

interface StateViewModel<State> {
    val state: StateFlow<State>
}

interface EventViewModel<ViewEvent> {
    val viewEvent: Flow<ViewEvent>
}

Delegate Implementations

Manages UI state with StateFlow:
class StateDelegate<State> : StateViewModel<State> {
    private lateinit var _state: MutableStateFlow<State>
    override val state: StateFlow<State>
        get() = _state.asStateFlow()

    fun setDefaultState(state: State) {
        _state = MutableStateFlow(state)
    }

    fun updateState(block: (State) -> State) {
        _state.update {
            block(it)
        }
    }
}
Manages one-time view events with SharedFlow:
class EventDelegate<ViewEvent> : EventViewModel<ViewEvent> {
    private val _viewEvent = MutableSharedFlow<ViewEvent>()
    override val viewEvent: Flow<ViewEvent> = _viewEvent.asSharedFlow()

    fun sendEvent(scope: CoroutineScope, newEvent: ViewEvent) {
        scope.launch {
            _viewEvent.emit(newEvent)
        }
    }
}

Usage in ViewModels

From cart-ui/src/main/java/com/denisbrandi/androidrealca/cart/presentation/viewmodel/CartViewModel.kt:6:
internal interface CartViewModel : StateViewModel<CartScreenState> {
    fun updateCartItemQuantity(cartItem: CartItem)
}

internal data class CartScreenState(val cart: Cart)
Always use StateDelegate for UI state that should survive configuration changes, and EventDelegate for one-time events like navigation or showing toasts.

designsystem

The designsystem module contains reusable UI components, theme definitions, and design tokens.

Components

The module includes:
  • Buttons.kt - Button components with consistent styling
  • Colors.kt - Color palette definitions
  • Dimens.kt - Spacing and dimension values
  • EmptyContents.kt - Empty state views
  • ErrorDialogs.kt - Error dialog components
  • ErrorViews.kt - Error state views
  • Labels.kt - Text label components
  • Loadings.kt - Loading indicators
  • ModalEvent.kt - Modal event handling
  • Theme.kt - Material theme configuration
  • TopBar.kt - App bar components
  • Typography.kt - Text styles
The design system provides a centralized location for all UI components, ensuring consistency across the entire application.

Testing Modules

Three specialized modules support testing across the application.

cache-test

Provides test implementations for cache abstractions.

TestCacheProvider

class TestCacheProvider(
    private val expectedFileName: String,
    private val expectedDefaultValue: Any
) : CacheProvider {

    lateinit var providedCachedObject: InMemoryCachedObject<*>

    override fun <T : Any> getCachedObject(
        fileName: String,
        serializer: KSerializer<T>,
        defaultValue: T
    ): CachedObject<T> {
        return if (expectedFileName == fileName && expectedDefaultValue == defaultValue) {
            InMemoryCachedObject(defaultValue).also {
                providedCachedObject = it
            }
        } else {
            throw IllegalStateException("getCachedObject not stubbed")
        }
    }

    override fun <T : Any> getFlowCachedObject(
        fileName: String,
        serializer: KSerializer<T>,
        defaultValue: T
    ): FlowCachedObject<T> {
        return RealFlowCachedObject(getCachedObject(fileName, serializer, defaultValue))
    }
}

flow-test-observer

Provides utilities for testing Kotlin Flows.
class FlowTestObserver<T : Any>(
    private val flow: Flow<T>,
    coroutineScope: CoroutineScope
) {
    private val emittedValues = mutableListOf<T>()
    private val job: Job = flow.onEach {
        emittedValues.add(it)
    }.launchIn(coroutineScope)

    fun getValues() = emittedValues

    fun stopObserving() {
        job.cancel()
    }

    fun getFlow() = flow
}

fun <T : Any> Flow<T>.test(
    coroutineScope: CoroutineScope = CoroutineScope(UnconfinedTestDispatcher())
) = FlowTestObserver(this, coroutineScope)
Usage:
val observer = myFlow.test()
// Trigger emissions
assertEquals(expectedValues, observer.getValues())

coroutines-test-dispatcher

Provides JUnit rule for testing coroutines on Android.
class MainCoroutineRule : TestWatcher() {

    private val testDispatcher = UnconfinedTestDispatcher()

    override fun starting(description: Description?) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description?) {
        Dispatchers.resetMain()
    }
}
Usage:
class MyViewModelTest {
    @get:Rule
    val mainCoroutineRule = MainCoroutineRule()

    @Test
    fun `test coroutine execution`() {
        // Coroutines will run on test dispatcher
    }
}
Use these testing modules together to write comprehensive tests for your components:
  • cache-test for repository tests
  • flow-test-observer for use case tests
  • coroutines-test-dispatcher for ViewModel tests

Best Practices

1

Keep Modules Small and Focused

Each library module should have a single responsibility and minimal dependencies.
2

Favor Multiplatform Modules

When possible, create multiplatform modules to share code across Android and iOS.
3

Provide Test Utilities

Create corresponding test modules (like cache-test) to make testing easier for consumers.
4

Use Clear Abstractions

Define interfaces (CacheProvider, HttpClientProvider) that can be easily mocked or replaced.

Build docs developers (and LLMs) love