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

2024. 2. 15. 11:30·안드로이드/클린 아키텍처
반응형

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

안녕하세요. 이전 포스팅에서는 Jetpack Compose를 활용한 화면 구성과 Compose Navigation, 그리고 ViewModel을 이용한 데이터 바인딩 과정에 대해 살펴보았습니다. 이번 포스팅은 클린 아키텍처 시리즈의 마지막 편으로, UI Layer에서 화면 이쁘게 꾸며보고 지금까지 포스팅한 클린 아키텍처를 정리하면서 시리즈를 마무리하겠습니다.

MovieDetailScreen 실행 모습
MovieDetailScreen 실행 모습

2. MovieDetailScreen 꾸미기

이전 포스팅에서는 서버로부터 불러온 영화에 대한 Entity의 정보를 문자열로만 출력했습니다. 이번 시간에는 해당 데이터를 StateFlow에 저장하고 MovieDetailScreen에서 compose를 활용해서 UI를 구현하겠습니다.

2-1. MovieDetailViewModel

MovieDetailViewmodel에서 UseCase로부터 데이터를 가져오고, 가져온 데이터를 StateFlow에 저장하겠습니다. 클린 아키텍처에서는 LiveData보다 StateFlow를 많이 사용합니다.

대표적인 이유는 LiveData는 안드로이드 플랫폼에 종속적이기 때문입니다. Domain Layer는 순수 Java, Kotlin 코드로 작성되어야 한다고 설명드린 적이 있습니다. 그래서 LiveData는 Domain Layer에서 사용이 불가능했기 때문에 Kotlin Flow를 사용해야만 했습니다.

하지만 Flow는 상태가 없어 ViewModel에서 사용하기 어려웠습니다. 그래서 kotlin 1.41 버전에 StateFlow가 등장했고 LiveData와 마찬가지로 value를 통해 상태 값을 저장하고 읽을 수 있습니다. 구글에서도 Kotlin Flow를 사용하도록 권장하고 있습니다.

@HiltViewModel
class MovieDetailViewModel @Inject constructor(
    private val getMovieDetail: GetMovieDetail,
    savedStateHandle: SavedStateHandle,
) : ViewModel() {
    private val movieId = mutableStateOf(savedStateHandle.get<Int>("movieId") ?: 466420)
    val movieData = MutableStateFlow<MovieEntity>(MovieEntity())

    init {
        viewModelScope.launch(Dispatchers.Main.immediate) {
            getMovieById(movieId.value).onSuccess {
                movieData.value = it
            }
        }

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

2-2. MovieDetailScreen

MovieDetailViewModel에서 관리하는 StateFlow를 활용하여, Jetpack Compose의 다양한 컴포저블 함수를 사용해서 화면을 만들었습니다.

@Composable
fun MovieDetailScreen(
    mainNavController: NavHostController,
    viewModel: MovieDetailViewModel
) {
    val state = viewModel.movieData.collectAsState().value
    MovieDetailsScreen(state, mainNavController)
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MovieDetailsScreen(
    state: MovieEntity,
    appNavController: NavHostController
) {
    Scaffold(
    	floatingActionButton = {
            FloatingActionButton(onClick = { /*TODO*/ }) {
                Image(
                    painter = painterResource(id = favoriteIcon),
                    contentDescription = null,
                    Modifier.size(24.dp),
                )
            }
        },
        topBar = {
            TopAppBar(
                title = { Text(text = "Overview") },
                navigationIcon = {
                    IconButton(onClick = { appNavController.popBackStack() }) {
                        Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null)
                    }
                }
            )
        }
    ) { paddingValues ->
        Column(
            Modifier
                .fillMaxSize(1f)
                .padding(paddingValues)
        ) {
            SubcomposeAsyncImage(
                model = state.backgroundUrl,
                loading = { MovieItemPlaceholder() },
                error = { MovieItemPlaceholder() },
                contentDescription = null,
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .height(280.dp)
                    .fillMaxWidth(1f)
            )
            Text(
                text = state.title,
                style = MaterialTheme.typography.titleLarge,
                modifier = Modifier
                    .padding(16.dp, 16.dp, 16.dp, 0.dp)
                    .fillMaxWidth(1f),
            )
            Text(
                text = state.description,
                style = MaterialTheme.typography.bodyMedium,
                modifier = Modifier
                    .padding(16.dp, 8.dp)
                    .fillMaxWidth(1f),
            )
        }
    }
}

@Composable
private fun MovieItemPlaceholder() {
    Image(
        painter = painterResource(id = drawable.bg_image),
        contentDescription = "",
        contentScale = ContentScale.Crop,
    )
}

3. 안드로이드 클린 아키텍처 시리즈 정리 및 마무리

Domain Layer의 비즈니스 로직 구성, Data Layer의 데이터 관리 및 처리, UI Layer의 Screen 개발까지, 각 계층별 구현 방법과 클린 아키텍처의 원칙을 따르는 중요성 등 클린 아키텍처의 이론부터 실습까지 살펴보았습니다.

클린 아키텍처의 적용은 앱의 유지보수성, 확장성, 그리고 테스트 용이성을 높이는데 기여합니다. 각 계층을 명확히 분리함으로써, 변경에 더욱 유연하게 대응할 수 있고, 팀원 간의 협업 시에도 앱의 구조를 더욱 쉽게 이해할 수 있기 때문입니다.

최근에는 swift, react, flutter 등 모두 선언형 UI를 채택하고 있습니다. 저는 compose가 나오기 전부터 안드로이드 개발을 해서 xml기반의 UI 개발에 더 익숙하지만, 선언형 UI를 활용하는 방식이 더욱 효율적이고 생산성이 높아진다고 생각합니다.

추후에 영화 목록 화면도 구성하고 Github에 프로젝트도 공개할 예정입니다.

감사합니다.

저작자표시 비영리 변경금지 (새창열림)

'안드로이드 > 클린 아키텍처' 카테고리의 다른 글

[안드로이드 클린 아키텍처 시리즈] UI Layer 구현 3편 (with Jetpack Compose) #10  (0) 2024.02.14
[안드로이드 클린 아키텍처 시리즈] UI Layer 구현 2편 (with Hilt) #9  (0) 2024.02.13
[안드로이드 클린 아키텍처 시리즈] UI Layer 구현 1편 (with Hilt) #8  (0) 2024.02.12
[안드로이드 클린 아키텍처 시리즈] Data Layer 구현 2편 #7  (0) 2024.02.11
[안드로이드 클린 아키텍처 시리즈] Data Layer 구현 1편 #6  (0) 2024.02.10
'안드로이드/클린 아키텍처' 카테고리의 다른 글
  • [안드로이드 클린 아키텍처 시리즈] UI Layer 구현 3편 (with Jetpack Compose) #10
  • [안드로이드 클린 아키텍처 시리즈] UI Layer 구현 2편 (with Hilt) #9
  • [안드로이드 클린 아키텍처 시리즈] UI Layer 구현 1편 (with Hilt) #8
  • [안드로이드 클린 아키텍처 시리즈] Data Layer 구현 2편 #7
코딩덕
코딩덕
안드로이드, 리액트 등의 개발 노하우와 최신 AI 기술을 다루는 기술 블로그입니다. 실무 중심의 경험을 바탕으로 마주한 문제와 해결 과정을 체계적으로 기록하며, 개발자에게 실질적으로 도움 되는 프로그래밍 팁과 인사이트를 쉽고 명확하게 공유하고자 합니다.
  • 코딩덕
    개발자가 들려주는 IT 이야기
    코딩덕
  • 전체
    오늘
    어제
    • 분류 전체보기 (66)
      • 안드로이드 (62)
        • 멀티 모듈 (11)
        • 클린 아키텍처 (11)
        • 트러블 슈팅 (5)
        • 코틀린 (3)
        • 코루틴 (2)
        • Compose (1)
        • Compose UI (6)
        • Compose Dialog (8)
        • Compose Paging3 (11)
        • Compose State (2)
        • Util (1)
      • Github (3)
        • PR Template (2)
        • AI Code Review (1)
      • 리액트 (1)
        • NextJs (1)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    jsonadapter
    트러블슈팅
    코틀린
    Usecase
    Clean Architecture
    클린 아키텍처
    pr template
    multi module
    Gradle
    pager
    LazyRow
    안드로이드
    recyclerview
    dynamic json
    ViewModel
    OnBackPressedDispatcher
    데이터 레이어
    멀티 모듈
    MutableState
    Dialog
    Github
    ai code review
    Jetpack Compose
    flow
    paging3
    코루틴
    enum class
    sealed class
    ScrollView
    UI Layer
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
코딩덕
[안드로이드 클린 아키텍처 시리즈] UI Layer 구현 4편 (with Jetpack Compose) #11
상단으로

티스토리툴바