프레젠테이션 계층 - 도메인 계층 - 데이터 계층에서 벌어지게 되는 흐름에 맞춰서 단계를 나눠 아키텍처를 정의합니다. 사용자 관점에서 보았을때 아키텍처의 순서는 다음과 같습니다.
데이터 계층에서 도메인 계층의 인테페이스를 상속받아 적용하지만 실제로는 도메인과 데이터 계층은 철저히 분리 되어있습니다. 그렇다면 언제 의존성이 주입될까요? 그것에 대해서는 수동으로 DI를 적용해주는 방법도 있고, Hilt를 사용해서 Annotation을 적용하게 될 수도 있습니다.
interface CoinRepository {
suspend fun getCoins(): List<CoinDto>
suspend fun getCoinById(coindId: String): CoinDetailDto
}
class GetCoinUseCase @Inject constructor(private val repository: CoinRepository) {
operator fun invoke(coinId : String): Flow<Resource<CoinDetail>> = flow {
try {
emit(Resource.Loading<CoinDetail>())
val coins = repository.getCoinById(coinId).toCoinDetail()
emit(Resource.Success<CoinDetail>(coins))
} catch (e: HttpException) {
emit(Resource.Error<CoinDetail>(e.localizedMessage ?: "An Expected error occured"))
} catch (e: IOException) {
emit(Resource.Error<CoinDetail>("Couldn't reach server. check your intract internet"))
}
}
}
class CoinRepositoryImpl @Inject constructor(
private val api: CoinPaprikaApi
) : CoinRepository {
override suspend fun getCoins(): List<CoinDto> {
return api.getCoins()
}
override suspend fun getCoinById(coindId: String): CoinDetailDto {
return api.getCoinById(coinId = coindId)
}
}
data class CoinDto(
val id: String,
val is_active: Boolean,
val is_new: Boolean,
val name: String,
val rank: Int,
val symbol: String,
val type: String
)
// 도메인 계층에서 사용할 모델로 변경
fun CoinDto.toCoin(): Coin {
return Coin(
id = id,
is_active = is_active,
name = name,
is_new = is_new,
rank = rank,
symbol = symbol,
type = type
)
}
@Composable
fun CoinListScreen(
navController: NavController,
viewModel: CoinListViewModel = hiltViewModel()
) {
val state = viewModel.state.value
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(state.coins) {
CoinListItem(coin = it, onItemClick = {
navController.navigate(Screen.CoinDetailScreen.route + "/${it.id}")
})
}
}
}
}
@HiltViewModel
class CoinListViewModel @Inject constructor(
private val getCoinsUseCase: GetCoinsUseCase
) : ViewModel() {
private val _state = mutableStateOf(CoinListState())
val state: State<CoinListState> = _state
init {
getCoins()
}
private fun getCoins() {
getCoinsUseCase().onEach { result ->
when (result) {
is Resource.Error -> {
_state.value = CoinListState(
error = result.message ?: "An unexpected error occured"
)
}
is Resource.Success -> {
_state.value = CoinListState(coins = result.data ?: emptyList())
}
is Resource.Loading -> {
_state.value = CoinListState(istLoading = true)
}
}
}.launchIn(viewModelScope)
}
}
해당 예시 코드는 github에 전부 업로드하였습니다. 저도 구조를 더 한번에 보기 위해서 포스팅 하였습니다. 해당 내용과 맞는 내용으로 저의 개인프로젝트도 리팩토링을 하게 되었습니다 리팩토링을 하고 보니 저의 프로젝트는 중간과정이 도메인 계층이 실종된 체라 생성해야 할 코드들이 매우 증가할 것 같군요
다음포스팅에서는 저의 이전 코드들과 비교해서 개선점을 포스팅하고 그 이후에는 테스트 코드까지 추가하는 방식으로 포스팅하겠습니다