Skip to main content

Advanced KMP + CMP Techniques

Overview

This guide covers advanced techniques for handling complex but common scenarios in Kotlin Multiplatform with Compose Multiplatform. These patterns solve real-world problems that developers frequently encounter.

Platform-Specific UI Adaptations

Responsive Design with Platform Detection

@Composable
fun AdaptiveLayout() {
val platform = Platform.current
val windowSize = rememberWindowSizeClass()

when {
platform == Platform.Android || platform == Platform.IOS -> {
// Mobile layout
MobileLayout()
}
platform == Platform.Desktop && windowSize.widthSizeClass == WindowWidthSizeClass.Expanded -> {
// Desktop wide layout
DesktopWideLayout()
}
platform == Platform.Desktop -> {
// Desktop compact layout
DesktopCompactLayout()
}
platform == Platform.Web -> {
// Web layout
WebLayout()
}
}
}

@Composable
private fun MobileLayout() {
Column(
modifier = Modifier.fillMaxSize()
) {
TopAppBar(title = { Text("Mobile App") })
LazyColumn(
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(16.dp)
) {
items(100) { index ->
ListItem(index = index)
}
}
BottomNavigation()
}
}

@Composable
private fun DesktopWideLayout() {
Row(
modifier = Modifier.fillMaxSize()
) {
NavigationRail(
modifier = Modifier.width(80.dp)
) {
// Navigation items
}
Column(
modifier = Modifier.weight(1f)
) {
TopAppBar(title = { Text("Desktop App") })
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 200.dp),
contentPadding = PaddingValues(16.dp)
) {
items(100) { index ->
GridItem(index = index)
}
}
}
SidePanel(
modifier = Modifier.width(300.dp)
) {
// Details panel
}
}
}

Platform-Specific Navigation

@Composable
fun PlatformAwareNavigation() {
val platform = Platform.current

when (platform) {
Platform.Android, Platform.IOS -> {
// Use bottom navigation for mobile
BottomNavigationApp()
}
Platform.Desktop -> {
// Use navigation rail for desktop
NavigationRailApp()
}
Platform.Web -> {
// Use top navigation for web
TopNavigationApp()
}
}
}

@Composable
fun BottomNavigationApp() {
var selectedTab by remember { mutableStateOf(0) }

Scaffold(
bottomBar = {
BottomNavigation {
BottomNavigationItem(
selected = selectedTab == 0,
onClick = { selectedTab = 0 },
icon = { Icon(Icons.Default.Home, "Home") },
label = { Text("Home") }
)
BottomNavigationItem(
selected = selectedTab == 1,
onClick = { selectedTab = 1 },
icon = { Icon(Icons.Default.Search, "Search") },
label = { Text("Search") }
)
}
}
) { paddingValues ->
when (selectedTab) {
0 -> HomeScreen()
1 -> SearchScreen()
}
}
}

Advanced State Management

Multi-Platform State Synchronization

class CrossPlatformStateManager {
private val _state = MutableStateFlow(SharedState())
val state: StateFlow<SharedState> = _state.asStateFlow()

private val platformSpecificStates = mutableMapOf<Platform, MutableStateFlow<PlatformState>>()

init {
// Initialize platform-specific states
Platform.values().forEach { platform ->
platformSpecificStates[platform] = MutableStateFlow(PlatformState())
}
}

fun updateSharedState(update: (SharedState) -> SharedState) {
_state.value = update(_state.value)
}

fun updatePlatformState(platform: Platform, update: (PlatformState) -> PlatformState) {
platformSpecificStates[platform]?.value = update(platformSpecificStates[platform]?.value ?: PlatformState())
}

fun getPlatformState(platform: Platform): StateFlow<PlatformState> {
return platformSpecificStates[platform]?.asStateFlow() ?: MutableStateFlow(PlatformState()).asStateFlow()
}
}

data class SharedState(
val user: User? = null,
val theme: Theme = Theme.SYSTEM,
val language: String = "en"
)

data class PlatformState(
val navigationState: String = "",
val platformSpecificData: String = ""
)

@Composable
fun CrossPlatformApp() {
val stateManager = remember { CrossPlatformStateManager() }
val sharedState by stateManager.state.collectAsState()
val platformState by stateManager.getPlatformState(Platform.current).collectAsState()

// Use both shared and platform-specific state
AppContent(
sharedState = sharedState,
platformState = platformState,
onSharedStateUpdate = { stateManager.updateSharedState(it) },
onPlatformStateUpdate = { stateManager.updatePlatformState(Platform.current, it) }
)
}

Complex Form State Management

data class FormState(
val fields: Map<String, FieldState> = emptyMap(),
val isValid: Boolean = false,
val isSubmitting: Boolean = false,
val errors: List<FormError> = emptyList()
)

data class FieldState(
val value: String = "",
val isValid: Boolean = true,
val errorMessage: String? = null,
val isDirty: Boolean = false
)

data class FormError(
val field: String? = null,
val message: String,
val type: ErrorType = ErrorType.VALIDATION
)

enum class ErrorType {
VALIDATION, NETWORK, SERVER
}

class FormManager(
private val validators: Map<String, (String) -> ValidationResult>
) {
private val _state = MutableStateFlow(FormState())
val state: StateFlow<FormState> = _state.asStateFlow()

fun updateField(fieldName: String, value: String) {
val currentState = _state.value
val fieldState = currentState.fields[fieldName] ?: FieldState()

val validationResult = validators[fieldName]?.invoke(value) ?: ValidationResult.Valid

val updatedFieldState = fieldState.copy(
value = value,
isValid = validationResult.isValid,
errorMessage = validationResult.errorMessage,
isDirty = true
)

val updatedFields = currentState.fields.toMutableMap()
updatedFields[fieldName] = updatedFieldState

val updatedState = currentState.copy(
fields = updatedFields,
isValid = updatedFields.values.all { it.isValid }
)

_state.value = updatedState
}

fun submit(onSubmit: suspend (Map<String, String>) -> Result<Unit>) {
if (!_state.value.isValid) return

viewModelScope.launch {
_state.value = _state.value.copy(isSubmitting = true)

val fieldValues = _state.value.fields.mapValues { it.value.value }

when (val result = onSubmit(fieldValues)) {
is Result.Success -> {
// Handle success
}
is Result.Error -> {
_state.value = _state.value.copy(
errors = listOf(FormError(message = result.message, type = ErrorType.NETWORK))
)
}
}

_state.value = _state.value.copy(isSubmitting = false)
}
}
}

data class ValidationResult(
val isValid: Boolean,
val errorMessage: String? = null
) {
companion object {
val Valid = ValidationResult(true, null)
}
}

@Composable
fun ComplexForm(
formManager: FormManager = remember { FormManager(createValidators()) }
) {
val formState by formManager.state.collectAsState()

Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
FormField(
name = "email",
label = "Email",
value = formState.fields["email"]?.value ?: "",
isValid = formState.fields["email"]?.isValid ?: true,
errorMessage = formState.fields["email"]?.errorMessage,
onValueChange = { formManager.updateField("email", it) }
)

FormField(
name = "password",
label = "Password",
value = formState.fields["password"]?.value ?: "",
isValid = formState.fields["password"]?.isValid ?: true,
errorMessage = formState.fields["password"]?.errorMessage,
onValueChange = { formManager.updateField("password", it) }
)

Button(
onClick = { formManager.submit { submitForm(it) } },
enabled = formState.isValid && !formState.isSubmitting
) {
if (formState.isSubmitting) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
color = Color.White
)
} else {
Text("Submit")
}
}

// Display form errors
formState.errors.forEach { error ->
Text(
text = error.message,
color = Color.Red,
modifier = Modifier.padding(top = 8.dp)
)
}
}
}

private fun createValidators(): Map<String, (String) -> ValidationResult> {
return mapOf(
"email" to { value ->
if (value.isEmpty()) {
ValidationResult(false, "Email is required")
} else if (!value.contains("@")) {
ValidationResult(false, "Invalid email format")
} else {
ValidationResult.Valid
}
},
"password" to { value ->
if (value.length < 8) {
ValidationResult(false, "Password must be at least 8 characters")
} else {
ValidationResult.Valid
}
}
)
}

Advanced Animation Techniques

Platform-Specific Animations

@Composable
fun PlatformAwareAnimation() {
val platform = Platform.current
var isAnimating by remember { mutableStateOf(false) }

val animationSpec = when (platform) {
Platform.Android -> tween<Float>(durationMillis = 300, easing = FastOutSlowInEasing)
Platform.IOS -> spring<Float>(dampingRatio = 0.8f, stiffness = 300f)
Platform.Desktop -> tween<Float>(durationMillis = 200, easing = LinearEasing)
Platform.Web -> tween<Float>(durationMillis = 250, easing = EaseInOutCubic)
}

val scale by animateFloatAsState(
targetValue = if (isAnimating) 1.2f else 1f,
animationSpec = animationSpec
)

Box(
modifier = Modifier
.size(100.dp)
.scale(scale)
.background(Color.Blue)
.clickable { isAnimating = !isAnimating }
) {
Text(
text = platform.name,
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}

Complex Animation Sequences

@Composable
fun ComplexAnimationSequence() {
var animationState by remember { mutableStateOf(AnimationState.IDLE) }

val infiniteTransition = rememberInfiniteTransition()

val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)

val scale by animateFloatAsState(
targetValue = when (animationState) {
AnimationState.IDLE -> 1f
AnimationState.LOADING -> 1.2f
AnimationState.SUCCESS -> 1.5f
AnimationState.ERROR -> 0.8f
},
animationSpec = spring(dampingRatio = 0.6f)
)

val alpha by animateFloatAsState(
targetValue = when (animationState) {
AnimationState.IDLE -> 1f
AnimationState.LOADING -> 0.7f
AnimationState.SUCCESS -> 1f
AnimationState.ERROR -> 0.5f
}
)

Box(
modifier = Modifier
.size(100.dp)
.graphicsLayer(
rotationZ = rotation,
scaleX = scale,
scaleY = scale,
alpha = alpha
)
.background(
when (animationState) {
AnimationState.IDLE -> Color.Gray
AnimationState.LOADING -> Color.Blue
AnimationState.SUCCESS -> Color.Green
AnimationState.ERROR -> Color.Red
}
)
.clickable {
animationState = when (animationState) {
AnimationState.IDLE -> AnimationState.LOADING
AnimationState.LOADING -> AnimationState.SUCCESS
AnimationState.SUCCESS -> AnimationState.ERROR
AnimationState.ERROR -> AnimationState.IDLE
}
},
contentAlignment = Alignment.Center
) {
Text(
text = animationState.name,
color = Color.White,
fontWeight = FontWeight.Bold
)
}
}

enum class AnimationState {
IDLE, LOADING, SUCCESS, ERROR
}

Advanced Data Handling

Offline-First Architecture

class OfflineFirstRepository<T>(
private val localDataSource: LocalDataSource<T>,
private val remoteDataSource: RemoteDataSource<T>,
private val networkMonitor: NetworkMonitor
) {

fun getData(): Flow<DataState<T>> = flow {
// Always emit loading first
emit(DataState.Loading)

// Try to get from local first
val localData = localDataSource.getData()
if (localData.isNotEmpty()) {
emit(DataState.Success(localData))
}

// If online, try to sync with remote
if (networkMonitor.isOnline()) {
try {
val remoteData = remoteDataSource.getData()
localDataSource.saveData(remoteData)
emit(DataState.Success(remoteData))
} catch (e: Exception) {
if (localData.isEmpty()) {
emit(DataState.Error(e.message ?: "Unknown error"))
}
}
} else if (localData.isEmpty()) {
emit(DataState.Error("No internet connection and no local data"))
}
}

suspend fun refreshData(): Result<List<T>> {
return try {
val remoteData = remoteDataSource.getData()
localDataSource.saveData(remoteData)
Result.success(remoteData)
} catch (e: Exception) {
Result.failure(e)
}
}
}

sealed class DataState<out T> {
object Loading : DataState<Nothing>()
data class Success<T>(val data: List<T>) : DataState<T>()
data class Error(val message: String) : DataState<Nothing>()
}

@Composable
fun OfflineFirstList<T>(
repository: OfflineFirstRepository<T>,
itemContent: @Composable (T) -> Unit
) {
val dataState by repository.getData().collectAsState(initial = DataState.Loading)

when (val state = dataState) {
is DataState.Loading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
is DataState.Success -> {
LazyColumn {
items(state.data) { item ->
itemContent(item)
}
}
}
is DataState.Error -> {
ErrorView(
message = state.message,
onRetry = { repository.refreshData() }
)
}
}
}

Advanced Caching Strategies

class SmartCache<T>(
private val maxSize: Int = 100,
private val expirationTime: Duration = Duration.hours(1)
) {
private val cache = mutableMapOf<String, CacheEntry<T>>()

fun get(key: String): T? {
val entry = cache[key] ?: return null

if (entry.isExpired()) {
cache.remove(key)
return null
}

// Update access time for LRU
entry.lastAccessed = Clock.System.now()
return entry.data
}

fun put(key: String, data: T) {
// Remove oldest entries if cache is full
if (cache.size >= maxSize) {
val oldestKey = cache.minByOrNull { it.value.lastAccessed }?.key
oldestKey?.let { cache.remove(it) }
}

cache[key] = CacheEntry(
data = data,
createdAt = Clock.System.now(),
lastAccessed = Clock.System.now()
)
}

fun clear() {
cache.clear()
}

fun removeExpired() {
cache.entries.removeIf { it.value.isExpired() }
}

private data class CacheEntry<T>(
val data: T,
val createdAt: Instant,
var lastAccessed: Instant
) {
fun isExpired(): Boolean {
return Clock.System.now() - createdAt > expirationTime
}
}
}

class CachedRepository<T>(
private val cache: SmartCache<T>,
private val dataSource: DataSource<T>
) {
suspend fun getData(key: String): T? {
// Try cache first
cache.get(key)?.let { return it }

// If not in cache, get from data source
val data = dataSource.getData(key)
data?.let { cache.put(key, it) }

return data
}

suspend fun refreshData(key: String): T? {
val data = dataSource.getData(key)
data?.let { cache.put(key, it) }
return data
}
}

Performance Optimization

Lazy Loading with Pagination

class PaginatedDataSource<T>(
private val pageSize: Int = 20,
private val dataFetcher: suspend (Int, Int) -> List<T>
) {
private var currentPage = 0
private var hasMoreData = true
private val loadedData = mutableListOf<T>()

suspend fun loadNextPage(): List<T> {
if (!hasMoreData) return emptyList()

val newData = dataFetcher(currentPage * pageSize, pageSize)

if (newData.size < pageSize) {
hasMoreData = false
}

loadedData.addAll(newData)
currentPage++

return newData
}

fun getLoadedData(): List<T> = loadedData.toList()

fun hasMoreData(): Boolean = hasMoreData

fun reset() {
currentPage = 0
hasMoreData = true
loadedData.clear()
}
}

@Composable
fun PaginatedList<T>(
dataSource: PaginatedDataSource<T>,
itemContent: @Composable (T) -> Unit
) {
var isLoading by remember { mutableStateOf(false) }
val loadedData = remember { mutableStateListOf<T>() }

LaunchedEffect(Unit) {
loadNextPage()
}

LazyColumn {
items(loadedData) { item ->
itemContent(item)
}

if (dataSource.hasMoreData()) {
item {
if (isLoading) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else {
Button(
onClick = { loadNextPage() },
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text("Load More")
}
}
}
}
}

suspend fun loadNextPage() {
isLoading = true
val newData = dataSource.loadNextPage()
loadedData.addAll(newData)
isLoading = false
}
}

Memory Management

@Composable
fun MemoryOptimizedList<T>(
items: List<T>,
itemContent: @Composable (T) -> Unit
) {
LazyColumn {
items(
items = items,
key = { item -> item.hashCode() } // Use stable keys
) { item ->
itemContent(item)
}
}
}

// Use remember for expensive computations
@Composable
fun ExpensiveComputation(data: List<ComplexData>) {
val processedData by remember(data) {
derivedStateOf {
data.map { complexData ->
// Expensive processing
processComplexData(complexData)
}
}
}

LazyColumn {
items(processedData) { processed ->
ProcessedDataItem(processed)
}
}
}

Error Handling and Recovery

Comprehensive Error Handling

sealed class AppError : Exception() {
data class NetworkError(override val message: String, val code: Int? = null) : AppError()
data class ValidationError(val field: String, override val message: String) : AppError()
data class DatabaseError(override val message: String) : AppError()
data class UnknownError(override val message: String) : AppError()
}

class ErrorHandler {
fun handleError(error: AppError): ErrorAction {
return when (error) {
is AppError.NetworkError -> {
when (error.code) {
401 -> ErrorAction.NavigateToLogin
403 -> ErrorAction.ShowPermissionDenied
404 -> ErrorAction.ShowNotFound
else -> ErrorAction.ShowRetryDialog(error.message)
}
}
is AppError.ValidationError -> {
ErrorAction.ShowFieldError(error.field, error.message)
}
is AppError.DatabaseError -> {
ErrorAction.ShowDatabaseError(error.message)
}
is AppError.UnknownError -> {
ErrorAction.ShowGenericError(error.message)
}
}
}
}

sealed class ErrorAction {
object NavigateToLogin : ErrorAction()
object ShowPermissionDenied : ErrorAction()
object ShowNotFound : ErrorAction()
data class ShowRetryDialog(val message: String) : ErrorAction()
data class ShowFieldError(val field: String, val message: String) : ErrorAction()
data class ShowDatabaseError(val message: String) : ErrorAction()
data class ShowGenericError(val message: String) : ErrorAction()
}

@Composable
fun ErrorBoundary(
onError: (AppError) -> Unit,
content: @Composable () -> Unit
) {
var hasError by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<AppError?>(null) }

if (hasError && error != null) {
ErrorView(
error = error!!,
onRetry = {
hasError = false
error = null
}
)
} else {
content()
}
}

Testing Advanced Patterns

Complex UI Testing

@Test
fun testComplexFormValidation() {
composeTestRule.setContent {
ComplexForm()
}

// Test email validation
composeTestRule.onNodeWithText("Email")
.performTextInput("invalid-email")

composeTestRule.onNodeWithText("Invalid email format")
.assertIsDisplayed()

// Test password validation
composeTestRule.onNodeWithText("Password")
.performTextInput("123")

composeTestRule.onNodeWithText("Password must be at least 8 characters")
.assertIsDisplayed()

// Test valid form submission
composeTestRule.onNodeWithText("Email")
.performTextInput("valid@email.com")

composeTestRule.onNodeWithText("Password")
.performTextInput("validpassword123")

composeTestRule.onNodeWithText("Submit")
.assertIsEnabled()
.performClick()
}

@Test
fun testOfflineFirstBehavior() {
// Mock network state
val networkMonitor = mockk<NetworkMonitor>()
every { networkMonitor.isOnline() } returns false

composeTestRule.setContent {
OfflineFirstList(
repository = OfflineFirstRepository(mockk(), mockk(), networkMonitor)
) { item ->
Text(item.toString())
}
}

// Verify offline message is shown
composeTestRule.onNodeWithText("No internet connection and no local data")
.assertIsDisplayed()
}

This comprehensive guide covers advanced techniques for handling complex scenarios in KMP + CMP development, providing practical solutions for real-world challenges.