Compose를 기존 앱 아키텍처와 통합

UDF(단방향 데이터 흐름) 아키텍처 패턴은 Compose에서 원활하게 작동합니다. 이 대신 앱에 MVP(Model View Presenter) 같은 다른 유형의 아키텍처 패턴을 사용하는 경우에는 Compose 채택 전이나 채택 과정에서 UI의 관련 부분을 UDF로 이전하는 것이 좋습니다.

Compose에서의 ViewModel

아키텍처 구성요소 ViewModel 라이브러리를 사용하면 viewModel() 함수를 호출하여 모든 컴포저블에서 ViewModel에 액세스할 수 있습니다. 이에 관한 설명은 Compose와 일반 라이브러리와의 통합 문서에 나와 있습니다.

Compose를 채택할 때 서로 다른 컴포저블에 동일한 ViewModel 유형을 사용하는 것에 주의해야 합니다. ViewModel 요소가 뷰 수명 주기 범위를 따르기 때문입니다. 이 범위는 호스트 활동, 프래그먼트 또는 탐색 그래프(탐색 라이브러리가 사용되는 경우)가 됩니다.

예를 들어 활동에서 컴포저블이 호스팅되면 viewModel()은 항상 동일한 인스턴스를 반환하고 이 인스턴스는 활동이 끝날 때만 지워집니다. 다음 예에서는 동일한 GreetingViewModel 인스턴스가 호스트 활동 아래의 모든 컴포저블에 재사용되기 때문에 동일한 사용자에게 인사말이 두 번 표시됩니다. 생성된 첫 번째 ViewModel 인스턴스가 다른 컴포저블에 재사용됩니다.

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MaterialTheme {
                Column {
                    Greeting("user1")
                    Greeting("user2")
                }
            }
        }
    }
}

@Composable
fun Greeting(userId: String) {
    val greetingViewModel: GreetingViewModel = viewModel(
        factory = GreetingViewModelFactory(userId)
    )
    val messageUser by greetingViewModel.message.observeAsState("")

    Text(messageUser)
}

class GreetingViewModel(private val userId: String) : ViewModel() {
    private val _message = MutableLiveData("Hi $userId")
    val message: LiveData<String> = _message
}

class GreetingViewModelFactory(private val userId: String): ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return GreetingViewModel(userId) as T
    }
}

또한 탐색 그래프에 따라 ViewModel 요소의 범위가 지정되므로 탐색 그래프에서 대상에 해당하는 컴포저블은 ViewModel의 다른 인스턴스를 갖습니다. 이 경우 ViewModel은 관련 대상의 수명 주기로 범위가 국한되고 대상이 백스택에서 삭제되면 지워집니다. 다음 예에서는 사용자가 프로필 화면으로 이동하면 GreetingViewModel의 새 인스턴스가 생성됩니다.

@Composable
fun MyScreen() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            Greeting(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

@Composable
fun Greeting(userId: String) {
    val greetingViewModel: GreetingViewModel = viewModel(
        factory = GreetingViewModelFactory(userId)
    )
    val messageUser by greetingViewModel.message.observeAsState("")

    Text(messageUser)
}

상태 정보 소스

Compose를 UI의 특정 부분에 채택할 경우 Compose와 뷰 시스템 코드 간에 데이터를 공유해야 할 수 있습니다. 가능한 경우 두 플랫폼 모두에 적용된 UDF 권장사항을 따르는 또 다른 클래스에 공유 상태를 캡슐화하는 것이 좋습니다. 예를 들면 공유된 데이터의 스트림을 노출해 데이터 업데이트를 내보내는 ViewModel에 캡슐화하는 것이 좋습니다.

하지만 이 방법은 공유할 데이터가 변경 가능한 요소이거나 UI 요소와 긴밀히 연결된 경우에는 가능하지 않을 수도 있습니다. 이 경우 어느 한 시스템이 정보 소스여야 하고, 그 시스템이 데이터 업데이트를 다른 시스템과 공유해야 합니다. 일반적으로 정보 소스는 UI 계층 구조의 루트에 더 가까운 요소가 소유해야 합니다.

정보 소스로서의 Compose

SideEffect 컴포저블을 사용하여 Compose 상태를 비 Compose 코드에 게시할 수 있습니다. 이 경우 정보 소스는 상태 업데이트를 전송하는 컴포저블에 보관됩니다.

예를 들어 애널리틱스 라이브러리를 사용하면 커스텀 메타데이터(이 예에서는 사용자 속성)를 이후의 모든 애널리틱스 이벤트에 연결하여 사용자 인구를 분류할 수 있습니다. 현재 사용자의 사용자 유형을 애널리틱스 라이브러리에 전달하려면 SideEffect를 사용하여 값을 업데이트합니다.

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        /* ... */
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

자세한 내용은 부수 효과 문서를 참고하세요.

정보 소스로의 뷰 시스템

뷰 시스템이 상태를 소유하고 이를 Compose와 공유하는 경우 mutableStateOf 객체에 상태를 래핑하여 Compose에서 상태가 스레드로부터 안전하도록 만들어야 합니다. 이 접근 방법을 사용하는 경우 구성 가능한 함수는 더 이상 정보 소스를 갖지 않기 때문에 단순해집니다. 하지만 뷰 시스템이 변경 가능한 상태와, 그 상태를 사용하는 뷰를 업데이트해야 합니다.

다음 예에서 CustomViewGroup은 내부에 TextField 컴포저블이 있는 TextViewComposeView를 포함하고 있습니다. TextView는 사용자가 TextField에 입력하는 내용을 표시해야 합니다.

class CustomViewGroup @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {

    // Source of truth in the View system as mutableStateOf
    // to make it thread-safe for Compose
    private var text by mutableStateOf("")

    private val textView: TextView

    init {
        orientation = VERTICAL

        textView = TextView(context)
        val composeView = ComposeView(context).apply {
            setContent {
                MaterialTheme {
                    TextField(value = text, onValueChange = { updateState(it) })
                }
            }
        }

        addView(textView)
        addView(composeView)
    }

    // Update both the source of truth and the TextView
    private fun updateState(newValue: String) {
        text = newValue
        textView.text = newValue
    }
}