Compose에서 view 사용하기
Compose는 기존 view 시스템과의 상호 운용을 위해 ``AndroidView`` composable을 제공한다. Compose 네이티브 컴포넌트가 없는 WebView 등을 사용할 때 활용할 수 있다.
내부적으로는 composition 트리 안에 view 노드를 직접 만드는 방식으로 구현되어 있다.
오버로딩 1
@Composable
@UiComposable
fun <T : View> AndroidView(
factory: (Context) -> T,
modifier: Modifier = Modifier,
update: (T) -> Unit = NoOpUpdate
)
``factory``는 view를 만드는 람다이다. ``factory`` 블럭은 composition 단계에서 view를 만들기 위해 단 한 번만 실행되며, 항상 UI 스레드에서 실행된다. 따라서 ``factory`` 블럭 안에서 view를 초기화하거나 property를 지정하는 작업을 수행해도 된다.
공식 문서에서는 ``remember``로 view를 감싸지 말고, ``factory`` 블럭 안에서 모든 걸 해결하라고 권장하고 있다.
Note: Prefer to construct a View in the AndroidView factory lambda instead of using remember to hold a View reference outside of AndroidView.
``update`` 블럭은 recomposition이 발생할 때 UI 스레드에서 실행되며, view의 속성을 업데이트할 때 사용할 수 있다. View가 ``factory``에 의해 초기화된 직후에 한번 실행되며, 이후에도 recomposition 때마다 실행될 수 있다.
``AndroidView``는 레이아웃 경계를 넘어가는 UI를 clip하지 않는다. 경계 바깥으로 넘어가는 UI를 자르고 싶다면, ``View.setClipToOutline`` 함수를 호출해야 한다.
WebView(context).apply {
clipToOutline = true
}
``AndroidView``는 오버로딩이 2개 있다. 이 오버로딩은 view가 재활용되지 않을 때 사용해야 한다. ``LazyRow``나 ``LazyColumn`` 등 컴포넌트를 재사용할 수 있는 container 안에서 사용되더라도, 내부 view는 항상 다시 만들어진다. 즉 ``AndroidView`` composable 자체는 재활용되지만, 내부의 view는 재활용되지 않는 것.
``AndroidView``가 재사용될 수 있는 경우에는 아래에서 소개할 다른 오버로딩을 활용해야 한다. View를 만드는 작업은 매우 비싸기 때문에, 아래의 오버로딩을 사용하는 것을 권장한다.
오버로딩 2
@Composable
@UiComposable
fun <T : View> AndroidView(
factory: (Context) -> T,
modifier: Modifier = Modifier,
onReset: ((T) -> Unit)? = null,
onRelease: (T) -> Unit = NoOpUpdate,
update: (T) -> Unit = NoOpUpdate
): Unit
``onReset``과 ``onRelease`` 람다가 눈에 띈다.
``onReset`` 람다가 null이 아니라면, composable을 재사용할 수 있는 container 안에서 view가 재사용될 수 있다. 위에서 말했던 ``LazyRow``에서 ``AndroidView``를 재사용하는 경우에서, ``AndroidView``와 내부 view가 모두 재사용될 수 있는 것이다.
``onReset``에서는 view를 초기화하는 로직을 실행하면 된다. 다른 블럭과 마찬가지로 UI 스레드에서 실행된다.
물론 상황에 따라 reset된 view가 즉시 재사용되지 않을 수도 있다. 대기 중이던 view가 재사용된다면 ``update`` 블럭이 실행되며, 재사용되지 않고 삭제된다면 대기 상태에서 ``onReset``이 호출된 후 view가 삭제된다.
View가 composition hierarchy에 붙어 있는지 확인하고 싶다면, ``View.addOnAttachStateChangeListener``를 등록할 수 있다. View의 생명주기가 궁금하다면 ``findViewTreeLifecycleOwner`` 함수를 실행하면 된다. 이 함수는 Compose의 ``LocalLifecycleOwner``를 반환한다.
View가 composition에서 완전히 제거되는 경우에는 ``onRelease``가 UI 스레드에서 실행된다. ``onRelease``가 return한 후에는 view가 절대로 재사용되지 않는다. 나중에 view가 다시 필요한 경우에도 새 view를 생성하지, release된 view를 재사용하지는 않는다.
View Binding?
View binding이 필요하다면 ``AndroidViewBinding`` composable을 사용할 수 있다.
@Composable
fun AndroidViewBindingExample() {
AndroidViewBinding(ExampleLayoutBinding::inflate) {
exampleView.setBackgroundColor(Color.GRAY)
}
}
Example: WebView
Compose에 없는 WebView를 사용해 보자. 먼저 다음과 같이 ``WebView``를 생성하는 ``createWebView`` 함수를 정의한다.
private fun createWebView(context: Context, onSetProgress: (Int) -> Unit) = WebView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView, newProgress: Int) {
onSetProgress(newProgress)
super.onProgressChanged(view, newProgress)
}
}
settings.apply {
builtInZoomControls = false
domStorageEnabled = true
javaScriptEnabled = true
loadWithOverviewMode = true
blockNetworkLoads = false
setSupportZoom(false)
}
}
이제 ``AndroidView``에서 ``createWebView``를 호출하자.
var progress by remember { mutableIntStateOf(0) }
AndroidView(
factory = { context ->
createWebView(context) { progress = it }
},
update = {
it.loadUrl(url)
},
)
공식 문서에서 언급한 대로, view를 remember하지 않고 ``factory`` 안에서 만들었다.
앱을 실행해 보니 웹뷰가 잘 보인다. 성공!
참고문헌