Skip to content

Presentation Layer

The Presentation layer contains the UI components and state management. It uses Jetpack Compose for the UI and ViewModels for state management.

Location

presentation/
├── key/               # Key generation screen
├── encryption/        # Encryption/decryption screen
├── encoding/          # Encoding conversion screen
├── main/              # Main navigation
└── common/            # Shared UI components

Principles

  • UI only: Contains only UI-related code
  • State management: Uses ViewModels and StateFlow
  • Compose: Uses Jetpack Compose for UI
  • Reactive: UI reacts to state changes

ViewModels

ViewModels manage UI state and coordinate with application layer:

KeyGenerationViewModel

@HiltViewModel
class KeyGenerationViewModel @Inject constructor(
    private val generateKeyCommand: AesGenerateAndSaveKeyCommandHandler,
    private val readAllKeysQuery: ReadAllKeysQueryHandler,
    private val deleteKeyCommand: DeleteKeyCommandHandler
) : ViewModel() {

    private val _uiState = MutableStateFlow(KeyGenerationUiState())
    val uiState: StateFlow<KeyGenerationUiState> = _uiState.asStateFlow()

    fun generateKey(algorithm: EncryptionAlgorithm) {
        viewModelScope.launch {
            _uiState.value = _uiState.value.copy(isLoading = true)

            generateKeyCommand.handle(
                AesGenerateAndSaveKeyCommand(algorithm)
            ).fold(
                onSuccess = { keyView ->
                    _uiState.value = _uiState.value.copy(
                        isLoading = false,
                        generatedKey = keyView,
                        error = null
                    )
                    loadKeys()
                },
                onFailure = { error ->
                    _uiState.value = _uiState.value.copy(
                        isLoading = false,
                        error = error.message
                    )
                }
            )
        }
    }

    fun loadKeys() {
        viewModelScope.launch {
            readAllKeysQuery.handle(ReadAllKeysQuery())
                .fold(
                    onSuccess = { keys ->
                        _uiState.value = _uiState.value.copy(savedKeys = keys)
                    },
                    onFailure = { error ->
                        _uiState.value = _uiState.value.copy(error = error.message)
                    }
                )
        }
    }
}

UI State

data class KeyGenerationUiState(
    val isLoading: Boolean = false,
    val generatedKey: KeyView? = null,
    val savedKeys: List<KeyView> = emptyList(),
    val error: String? = null
)

Compose Screens

KeyGenerationScreen

@Composable
fun KeyGenerationScreen(viewModel: KeyGenerationViewModel) {
    val uiState by viewModel.uiState.collectAsState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        KeyGenerationScreenTitle()

        AlgorithmSelectionCard(
            selectedAlgorithm = uiState.selectedAlgorithm,
            onAlgorithmSelected = { /* ... */ }
        )

        GenerateKeyButton(
            isLoading = uiState.isLoading,
            onClick = { viewModel.generateKey(uiState.selectedAlgorithm) }
        )

        uiState.generatedKey?.let { key ->
            GeneratedKeyCard(key = key)
        }

        SavedKeysSection(keys = uiState.savedKeys)

        uiState.error?.let { error ->
            KeyGenerationErrorCard(error = error)
        }
    }
}

UI Components

Reusable Compose components:

GeneratedKeyCard

@Composable
fun GeneratedKeyCard(
    key: KeyView,
    clipboard: Clipboard
) {
    Card(
        modifier = Modifier.fillMaxWidth()
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Text(
                text = "Generated Key",
                style = MaterialTheme.typography.titleLarge
            )

            Text(
                text = key.keyBase64,
                style = MaterialTheme.typography.bodyMedium
            )

            OutlinedButton(
                onClick = {
                    // Copy to clipboard
                }
            ) {
                Text("Copy")
            }
        }
    }
}

Navigation between screens:

@Composable
fun MainScreen() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = "key_generation"
    ) {
        composable("key_generation") {
            KeyGenerationScreen(viewModel = hiltViewModel())
        }
        composable("encryption") {
            EncryptionScreen(viewModel = hiltViewModel())
        }
        composable("encoding") {
            EncodingScreen(viewModel = hiltViewModel())
        }
    }
}

State Management

StateFlow

ViewModels expose state via StateFlow:

val uiState: StateFlow<KeyGenerationUiState> = _uiState.asStateFlow()

Collecting State

UI collects state in Compose:

val uiState by viewModel.uiState.collectAsState()

Error Handling

Errors are displayed in UI:

uiState.error?.let { error ->
    KeyGenerationErrorCard(error = error)
}

Learn More