Sobes.tech
Middle+

How do you plan to use the Presentation Layer pattern in your project?

sobes.tech AI

Answer from AI

The Presentation Layer (view layer) will be responsible for displaying data to the user and handling their actions. It will consist of UI elements (Activity, Fragment, Composables), Presenters/ViewModels, and adapters.

Main principles of usage:

  • Separation of responsibilities: UI displays data, and Presenter/ViewModel manages display logic, interacting with the domain.
  • Testability: The presentation logic is moved to Presenter/ViewModel, simplifying unit testing.
  • Independence from Android API: Ideally, Presenter/ViewModel should not contain references to Android SDK (Context, View, etc.), using interfaces and DI.

Example structure using Jetpack Compose and ViewModel:

com.example.myapp.presentation
├── common // Common UI components
├── home // Feature Home
│   ├── HomeViewModel.kt // ViewModel for Home screen
│   └── HomeScreen.kt // Composable for Home screen
├── details // Feature Details
│   ├── DetailsViewModel.kt
│   └── DetailsScreen.kt
└── navigation // Navigation between screens

In case of using XML/Fragments, the structure will be similar, but instead of Composable, there will be Fragment and corresponding XML layouts. The Presenter or ViewModel will work with View through an interface or Data Binding.

Interaction between layers:

  • UI (Activity/Fragment/Composable) -> ViewModel: sending user actions (clicks, input).
  • ViewModel -> Domain: getting/sending data.
  • Domain -> ViewModel: providing data/results of operations.
  • ViewModel -> UI: updating UI state (via StateFlow, LiveData, or other observation mechanisms).

Example of using ViewModel in Composable:

// HomeScreen.kt
@Composable
fun HomeScreen(
    viewModel: HomeViewModel = hiltViewModel() // DI for ViewModel
) {
    val uiState by viewModel.uiState.collectAsState() // Observing UI state

    when (uiState) {
        is HomeUiState.Loading -> LoadingIndicator()
        is HomeUiState.Success -> {
            val data = (uiState as HomeUiState.Success).data
            // Display data
        }
        is HomeUiState.Error -> {
            val message = (uiState as HomeUiState.Error).message
            ErrorView(message)
        }
    }
}

// HomeViewModel.kt
@HiltViewModel
class HomeViewModel @Inject constructor(
    private val getItemsUseCase: GetItemsUseCase // UseCase from Domain layer
) : ViewModel() {

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

    init {
        loadItems()
    }

    private fun loadItems() {
        viewModelScope.launch {
            getItemsUseCase.execute() // Call UseCase
                .onStart { _uiState.value = HomeUiState.Loading }
                .catch { e -> _uiState.value = HomeUiState.Error(e.message ?: "Unknown error") }
                .collect { items -> _uiState.value = HomeUiState.Success(items) }
        }
    }
}

// HomeUiState.kt
sealed class HomeUiState {
    object Loading : HomeUiState()
    data class Success(val data: List<Item>) : HomeUiState()
    data class Error(val message: String) : HomeUiState()
}

This approach ensures clear separation of layers, improves testability, and scalability of the project.