본문 바로가기

안드로이드/클린 아키텍처

[안드로이드 클린 아키텍처 시리즈] UI Layer 구현 3편 (with Jetpack Compose) #10


1. 안드로이드 클린 아키텍처 UI Layer 3편

안녕하세요. 이전 포스팅에서는 Hilt를 통한 의존성 주입에 대해 살펴보았습니다. 이번 포스팅에서는 UI Layer의 마지막 부분으로, UI 컴포넌트와 ViewModel을 연결하고 사용자 인터페이스를 통해 데이터를 표시하기 위해서 Jetpack Compose를 활용한 화면 구성부터 구현하겠습니다. 본 시리즈는 클린 아키텍처에 대해 다루고 있기 때문에 Jetpack Compose를 자세히 설명하지는 않겠습니다.

영화 정보를 사용자 화면에 보여준 모습
Data Layer로부터 불러온 데이터를 화면에 보여준 모습

2. Jetpack Compose를 이용한 UI 구현

Jetpack Compose는 최신 안드로이드 UI 개발 툴킷으로, 선언적 UI 프로그래밍을 가능하게 해 줍니다. 이를 통해 더 간결하고 직관적인 코드로 UI를 구성할 수 있습니다. 우리는 Jetpack Compose를 사용하여 사용자 인터페이스를 구성하고, ViewModel에서 제공하는 데이터를 화면에 바인딩하는 과정을 구현할 예정입니다.

2-1. Compose Navigation 구현

먼저, 첫 시작 화면이나 화면 이동을 편하게 하기 위해서 jetpack compose navigation 세팅을 해주겠습니다. navigation은 크게 3가지로 구분됩니다. 아래의 코드를 보면 NavHost의 startDestination에 Page.MoveDetail.route를 전달했기 때문에 시작 화면은 MovieDetailScreen이 됩니다.

  • NavGraph : 앱 내에서 접근 가능한 모든 화면(destination)과 이 화면 사이를 이동할 수 있는 경로를 정의
  • NavHost : NavController를 통해 사용자의 명령을 실행할 수 있는 컨테이너. 즉, 화면을 보여주는 컨테이너
  • NavController : 사용자가 앱 내에서 화면을 이동할 때, NavHost에 적절한 화면을 표시하도록 관리
sealed class Page(val route: String) {
    data object MoveDetail: Page("movie_detail")
}
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val navController = rememberNavController()
            CleanarchitectureexampleTheme {
                MainGraph(mainNavController = navController)
            }
        }
    }
}
@Composable
fun MainGraph(
    mainNavController: NavHostController
) {
    NavHost(navController = mainNavController, startDestination = Page.MoveDetail.route) {
        composable(
            route = Page.MoveDetail.route
        ) {
            val viewModel = hiltViewModel<MovieDetailViewModel>()
            MovieDetailScreen(
                mainNavController = mainNavController,
                viewModel = viewModel
            )
        }
    }
}

2-2. MovieDetailViewModel 구현

MovieDetailViewModel에서는 GetMovieDetail UseCase를 호출해서 Data Layer로부터 영화 정보를 가져오겠습니다. 원래는 영화 리스트 항목에서 선택한 movieId를 받아와서 호출을 해야하지만, 영화 리스트 화면이 없기 때문에 '466420'라는 영화 고유값을 강제로 넣어줘서 해당 영화의 상세 정보를 가져오겠습니다.

@HiltViewModel
class MovieDetailViewModel @Inject constructor(
    private val getMovieDetail: GetMovieDetail,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    val movieId = mutableStateOf(savedStateHandle.get<Int>("movieId") ?: 466420)
    val movie = mutableStateOf<MovieEntity?>(null)
    init {
        viewModelScope.launch {
            getMovieById(movieId.value).onSuccess {
                Log.d("MovieDetailViewModel", "MovieDetailViewModel: $it")
                movie.value = it
            }
        }

    }
    private suspend fun getMovieById(movieId: Int): ApiResult<MovieEntity> {
        return getMovieDetail(movieId)
    }
}

2-3. MovieDetailScreen 구현

MovieDetailViewModel에서 영화에 대한 정보를 movie라는 상태변수에 저장했습니다. 해당 데이터가 정상적으로 불러와졌는지 확인하기 Screen에서 viewModel의 movie 정보를 Text Composeable에 전달하겠습니다. 그러면 화면에서 영화 정보를 확인할 수 있습니다.

@Composable
fun MovieDetailScreen(
    mainNavController: NavHostController,
    viewModel: MovieDetailViewModel
) {
    Column {
        Text(text = viewModel.movie.value.toString())
    }
}

3. Compose navigation 및 Screen, ViewModel 정리

이번 시간에는 각 화면의 이동, 시작화면 등의 화면 구성을 위해 compose navigation을 구현했고 영화 세부 정보를 볼 수 있는 MovieDetailScreen과 MovieDetailViewModel를 간단하게 구현했습니다.

ViewModel에서는 Domain Layer에 해당되는 getMovieDetail UseCase를 호출하고 Domain Layer에서 Repository에 영화 정보를 요청하여 다시 ViewModel로 데이터가 전달되는 과정을 통해 Screen에 영화 정보를 볼 수 있었습니다.

다음 포스팅에서는 Screen을 좀 더 이쁘게 꾸며보고 클린 아키텍처 시리즈를 마무리 하겠습니다.