안드로이드 Compose Preview를 잘 활용하는 방법은? - 함수를 잘 분리하자.



개인 광고 영역

Android XML은 기본 Preview가 잘 나온다. 하지만 Compose는 Preview를 하기 위한 기법이 필요하다.

  • Preview를 잘하기 위해서는 Primitive types 또는 class 객체를 넘겨준다.
  • DI를 이용하는 경우 Preview가 힘들다.
  • DI를 활용하는 경우라면 @Preview 함수 내에서 DI에서 만들어주는 객체를 manual dependency injection을 통해 생성해 초기화 시켜야 한다.

그래서 추천하는 방식은 아래와 같다.

  1. DI를 활용한 ViewModel에서 전달받아 값을 처리하는 @Composable fun을 하나 만든다.
  2. Primitive types/data class/@Composable () -> Unit를 파라메터로 가지는 함수를 생성한다.
    1. 번을 Preview 하는 함수를 작성한다.
// 1번에 대한 구현
@Composable
internal fun MainContainer(viewModel: ViewModel) {
  val value by remember { viewModel.value }
  MainContainer(value)
}

// 2번에 대한 구현
@Composable
private fun MainContainer(value: String) {
  Text(text = value)
}

// 3번에 대한 구현
@Preview
@Composable
internal fun PreviewMainContainer() {
  SampleThemePreview {
    MainContainer(
      value = "value",
    )
  }
}


이 글에서 알아볼 내용

  • Compose Preview를 잘 그릴 수 있는 방법
  • ViewModel을 쉽게 활용하기 위한 방법


Preview 잘 그리는 방법

앞에서 소개한 3가지 함수를 구분하는 방식으로 Preview를 그리고 있다.

참고 : Preview 함수는 private이라도 상관은 없지만, detekt와 같은 코드 컨벤션을 활용하고 있다면 최소 internal을 사용해야 한다.

Preview를 잘 그리는 방식을 별거 없다.

  • 함수를 최대한 분리하라!
    • 함수를 많이 분리하다 보면 생기는 파라메터를 해결하기 위해 ViewModel 활용 시 ProvidableCompositionLocal을 활용하는 걸 고려하라.
      • 뒤에서 소개한다.
      • 함수를 분리하고 파일을 함께 분리해두면 Preview 조건을 최소화할 수 있다.
  • Preview를 활용해 예측 사항을 미리 만들어두는 편이 좋다.
    • switch 상태를 미리 정의
    • A 케이스와 B 케이스의 동작에 대한 Preview를 미리 만들어둔다.
    • Column/Row를 활용해 미리 다양한 형태를 만들어둔다.
  • Preview 용 Theme를 만들어두고, Custom CompositionLocal 값을 등록해 활용한다.

결국 Compose는 kotlin을 잘 활용하고, 함수 분리를 잘 한다면 유용하게 활용할 수 있다.


Preview 가능한 형태로 함수를 분리해 보자

만약 아래와 같이 함수를 분리해두었다고 생각해 보자. Header()는 Preview가 바로 불가능하기 때문에 Primitive types 용 함수를 별도로 만들었다.

@Composable
internal fun Header() {
  val title by remember { viewModel.title }
  Header(title)
}

@Composable
internal fun Header(title: String) {
  Text(text = title)
}

@Preview
@Composable
internal fun PreviewHeader() {
  SampleThemePreview {
    Header(title = "text")
  }
}

그럼 이를 가져다 쓰는 함수는 어떻게 처리하면 좋을까?


Preview 하기 위한 조합

역시 총 3개의 함수가 생성된다.

Header() 함수를 바로 가져다 사용하면 Preview 불가능하다. 그래서 Preview 가능한 형태로 한 번 더 감싸준다.

// Preview는 동작하지 않지만 외부에서 활용할 코드
@Composable
internal fun SampleContainer() {
  val buttonTextOne by remember { viewModel.buttonTextOne }
  val buttonTextTwo by remember { viewModel.buttonTextTwo }
  SampleContainer(
    buttonTextOne = buttonTextOne,
    buttonTextTwo = buttonTextTwo,
    header = {
      Header() // Header() 함수 호출
    }
  )
}

// Preview 동작 가능한 형태로 구성
@Composable
internal fun SampleContainer(
  header: @Composable () -> Unit,
  buttonTextOne: String,
  buttonTextTwo: String,
) {
  Column {
    header() // Hight-Order function call
    Row {
      Button(text = buttonTextOne)
      Button(text = buttonTextTwo)
    }
  }
}

// Preview 가능한 함수를 활용한 Preview 구현
@Preview
@Composable
internal fun PreviewSampleContainer() {
  SampleThemePreview {
    SampleContainer(
      buttonTextOne = "Preview button one",
      buttonTextTwo = "Preview button two",
      header = {
        Header(text = "Preview title")
      }
    )
  }
}

이렇게 하면 SampleContainer()는 외부에서 가져다 쓰기만 하면 활용이 가능하고, SampleContainer(header, buttonTextOne, buttonTextTwo) 함수는 Preview + 구현체로 활용한다. 2 번째 함수를 그대로 Preview에서 활용하여 Preview를 구현한다.

함수를 나누는 것이 쉬운 건 아니다. 이런 형태로의 코드를 가능하도록 만들려면 고민도 필요하지만 그냥 작성하다 보면 자연스럽게 흘러오기도 한다.

필자의 경우는 후자에 가깝다. 하지만 이미 이런 형태를 가진 오픈 소스도 있으니 참고하시길

Github - chrisbanes - tivi


Preview

앞에서 작성한 코드처럼 하더라도 Preview가 쉽지 않다. 이렇게 작성한다고 하더라도 오류 없이 Preview가 잘 동작하지는 않는다.

비즈니스 로직이 수정되어도 Preview를 위한 리빌드가 필요한데, 그만큼 rebuild 해줘야 한다.

새로운 파일을 추가하면 rebuild는 필수인데, 아래와 같이 노출된다.

image_01

작업 중에 Preview가 어려운 경우라면 오류도 발생한다.

image_02

정말 단순한 Preview는 즉시 반영되지만 로직이 수정되거나, 타이밍에 따라 빌드 실패가 자주 일어난다.

그래도 Preview가 있으면 효율적이긴 하다.


실시간으로 기기에서 Preview를 보려면

Android Studio 최신 버전을 활용하고 있다면 아래 옵션을 활성화할 수 있다.

Live Edit of literals을 켜면 기기에서도 실시간 반영을 해준다.

단, 실패할 가능성이 크기 때문에 OFF 해두고 활용하는 게 좋고, 그만큼 잦은 빌드가 일어나고, 코드 치다 보면 앱이 그냥 종료되는 경우도 많다.

image_03

로직에 따라 앱이 종료되는 경우도 많으니 Preview는 그냥 Android Studio에서 Preview를 잘 구성해서 미리 보기 형태로 보는 걸 추천한다.


Android Studio Preview 활용

Preview는 최대 3개의 버튼이 활성화된다.

image_04

  1. 첫 번째 아이콘은 애니메이션을 플레이해볼 수 있다.
  • 가장 최신 기린 버전의 Beta New UI에서는 View가 잘 안 보이는 버그가 있다.

image_05

  1. 두 번째 아이콘은 가장 기본으로 보이는 아이콘으로 현재 화면에서 Preview를 볼 수 있다.

Preview 내에서 버튼을 눌러 다른 이벤트를 처리해 볼 수도 있지만 이렇게 구성하는 시간에 로직과 화면을 구성하는 편이 이득이다.

  1. 마지막 아이콘은 실제 기기에서 Preview 할 수 있다. 별도의 Preview 전용 앱이 설치되고 실행된다.


Preview가 많으면

많으면 화면에 노출하기까지의 시간이 추가로 필요하다.

아래는 3개의 Preview 상태를 보여준다.

image_06


ViewModel을 CompositionLocal 활용하는 방법

hilt를 활용하는 경우라면 아래 글을 참고하세요.

StackOverflow - hiltViewModel

필자의 경우 CompositionLocal에 viewModel을 담아두고, 파라메터로 넘겨주지 않고, 작업한다.

viewModel을 inject 하는 위치는 Activity이다. 이를 하위 함수들에게 넘겨주면 수정 포인트가 너무 많이 생긴다.

이에 따라 아래와 같은 코드를 만들어 활용하고 있다.

@Composable
internal fun SampleActivity.ComposeLocalProvider(content: @Composable () -> Unit) {
    CompositionLocalProvider(
        SampleViewModelProvider(this.sampleViewModel),
        content = content
    )
}

아래 코드도 필요하다.

private val SampleViewModel: ProvidableCompositionLocal<SampleViewModel?> =
    compositionLocalOf<SampleViewModel?> { null }

internal fun SampleViewModelProvider(sampleViewModel: SampleViewModel) =
    SampleViewModel provides sampleViewModel

internal val sampleViewModel: SampleViewModel
    @Composable
    get() = SampleViewModel.current!!

View에서는 아래의 코드를 한번 감싸줘야 한다.

class SampleActivity {

  fun onCreate() {
    setContent {
      SampleTheme {
        ComposeLocalProvider { // ViewModel 자동 등록
          // 여기에서 하위 함수 호출
          SampleContainer()
        }
      }
    }
  }
}

다른 함수에서 활용 시에는 아래와 같다.

@Composable
fun SampleContainer() {
  val data by remember { sampleViewModel.data }
  // data 활용
}

외부에서는 sampleViewModel.xxx로 접근할 수 있다.


라이브러리 소개

이런 코드를 모든 feature에서 생성할 순 없다. 그렇기 때문에 이를 가져다 쓴다.

github - android-alatan/LifecycleComponents에 보면

아래 두 가지가 있다. 이를 활용하면 쉽게 접근할 수 있다.

implementation("com.github.android-alatan.lifecyclecomponents:provided-compose-local-api:$version")
implementation("com.github.android-alatan.lifecyclecomponents:provided-compose-local-ksp:$version")

기존 코드에 @ProvidedComposeLocal을 추가하여 활용할 수 있다.

주의 : 전체 코드 활용법을 소개하는 것이 아니기 때문에 동작하지는 않을 수 있다. 이 코드는 ksp를 필요로 한다.

class SampleActivity {

  @ProvidedComposeLocal
  lateinit var viewModel: SampleViewModel

  fun onCreate() {
    setContent {
      SampleTheme {
        ComposeLocalProvider { // ViewModel 자동 등록
          // 여기에서 하위 함수 호출
          SampleContainer()
        }
      }
    }
  }
}

@ProvidedComposeLocal에서 위에서 작성했던 SampleViewModelProvider 부분과 ComposeLocalProvider의 코드를 자동으로 만들어준다.

리빌드 한번 해줘야 하는 단점은 있지만 이를 잘 활용하면 유용한 개발이 가능하다.


마무리

Compose가 좋긴 하지만 Preview는 여전히 힘들다.

Compose를 1년 넘게 활용하고 있다 보니 이런 Preview 형태도 만들어지게 되었다.

결국 함수를 잘 나눠 활용해야 유용해진다는 점이다.



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments