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.