본문 바로가기
개발/Android

[Android] Clean Architecture With Pokedex (feat. Jetpack Compose, Hilt) - 4

by du.it.ddu 2023. 1. 1.
반응형

이번 포스팅은 Presentation 레이어를 구현한다.
즉, presentation 모듈을 구현하게 된다.

presentation 레이어는 앱의 UI, UI에 보여줄 데이터를 획득하는 기능을 담당한다.
UI를 그리기 위해 Compose를 활용할 것이며,
UI의 데이터, 즉 상태를 관리하는 기능은 ViewModel을 활용할 것이다.

먼저 포켓몬 리스트 데이터를 담당하는 ViewModel을 구현해보자.

@HiltViewModel
class PokemonListViewModel @Inject constructor(
    getPokemonListUseCase: GetPokemonListUseCase
) : ViewModel() {

    val pokemonList = getPokemonListUseCase
        .invoke(POKEMON_LIST_LIMIT)
        .cachedIn(viewModelScope)

    private val _isLoading: MutableState<Boolean> = mutableStateOf(false)
    val isLoading: State<Boolean> = _isLoading

    companion object {
        private const val POKEMON_LIST_LIMIT = 50
    }
}

클린 아키텍처에서 데이터의 획득은 UseCase를 통해 획득한다.
그러므로 ViewModel에서 UseCase를 통해 포켓몬 리스트를 획득할 수 있도록 구현한다.

포켓몬의 상세 데이터 획득의 ViewModel도 거의 동일하다.

@HiltViewModel
class PokemonDetailViewModel @Inject constructor(
    arguments: SavedStateHandle,
    private val getPokemonUseCase: GetPokemonUseCase
) : ViewModel() {
    private val _uiState: MutableState<PokemonDetailState> = mutableStateOf(PokemonDetailState.Loading)
    val uiState: State<PokemonDetailState> = _uiState

    init {
        arguments.get<String>("name")?.let { loadPokemon(it) }
    }

    private fun loadPokemon(name: String?) {
        if (!name.isNullOrEmpty()) {
            viewModelScope.launch(Dispatchers.Main) {
                runCatching {
                    getPokemonUseCase.invoke(name)
                }.onSuccess {
                    _uiState.value = PokemonDetailState.Success(it)
                }.onFailure {
                    _uiState.value = PokemonDetailState.Failure
                }
            }
        } else {
            _uiState.value = PokemonDetailState.Failure
        }
    }
}

UseCase를 통해 UI의 데이터(상태)를 획득하고, 이를 UI에서 획득하도록 구현된다.

이제 UI 부분을 살펴보자.
UI 코드는 상당히 길기 때문에, ViewModel의 상태를 통해 구성하는 정도의 코드만 작성한다.

먼저 포켓몬 리스트 화면의 UI 코드를 보자.

@Composable
fun PokemonListScreen(
    viewModel: PokemonListViewModel,
    onPokemonClick: (PokemonEntity) -> Unit
) {
    Surface(
        color = MaterialTheme.colors.surface,
        modifier = Modifier.fillMaxSize()
    ) {
        val pokemonList = viewModel.pokemonList.collectAsLazyPagingItems()
        PokemonGrid(
            pokemonList,
            onPokemonClick
        )
        PokemonListLoadStateView(pokemonList.loadState)
    }
}

굉장히 단순하다.
UI에 해당하는 ViewModel을 통해, 포켓몬 리스트의 상태를 참조하여 UI를 그려낸다.

포켓몬의 상세화면 UI도 거의 유사하다.

@Composable
fun PokemonDetailScreen(
    viewModel: PokemonDetailViewModel
) {
    Surface(
        color = MaterialTheme.colors.surface
    ) {
        when (val state = viewModel.uiState.value) {
            is PokemonDetailState.Loading -> {
                LoadingIndicator()
            }
            is PokemonDetailState.Success -> {
                PokemonDetailView(state.pokemon)
            }
            is PokemonDetailState.Failure -> {

            }
        }
    }
}

포켓몬 상세 ViewModel의 상태에 따라 UI를 분기한다.

중요한 부분은 어떻게 UseCase와 상호작용하여 UI 데이터를 획득하고, 이를 통해 UI를 구성하는지이다.
presentation 모듈의 자세한 구현은 아래 Github에서 참고하면 된다.
https://github.com/DuItDDu/Android_CleanArchitecture_Pokedex/tree/master/presentation

지금까지 클린 아키텍처로 어떻게 앱을 구성하는지 알아보았다.
Domain, Data, Presenter로 레이어를 구분하고 기능을 분리함으로써 더욱 유연하고 명확한 설계를 적용할 수 있었다.
팀 단위로 많은 사람이 작업하거나, 규모가 큰 앱을 개발한다면 클린 아키텍처가 좋은 선택이 될 수 있다.

반응형