[android] compose 의 상호 운용성 API

sundays·2022년 11월 9일

안녕하세요. 제가 앱을 만들때 처럼 외부에서 만든 API의 UI의 레이아웃을 어쩔수 없이 반영해야 할 때가 있습니다. custom fragment를 사용해야 하는데 compose에서 쓰고 싶어서 뷰 기반 UI도 같이 사용하려고 사용하는 것이 상호운용성 API 입니다. FragmentContainerView 라이브러리를 사용하는 프로젝트도 별로 없는거 같은데 compose 에다 적용하는 예제는 정말 드문 것 같습니다.

Compose in Fragment

fragment 안에서 compose를 사용하고 싶을 때가 있습니다. 정확히 kotlin 앱에서 xml 기반의 ui 안에 compose를 넣는 방식입니다. 이것은 ComposeView라는 API를 적용하는 방법이 있습니다.

layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/hello_world"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello Android!" />

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

기존의 UI들이 LinearLayout에 있는 구조에 ComposeView만 추가된 모습입니다.

ExampleFragment.kt

class ExampleFragment : Fragment() {

    private var _binding: FragmentExampleBinding? = null
    // This property is only valid between onCreateView and onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentExampleBinding.inflate(inflater, container, false)
        val view = binding.root
        binding.composeView.apply {
            // Dispose of the Composition when the view's LifecycleOwner
            // is destroyed
            setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                // In Compose world
                MaterialTheme {
                    Text("Hello Compose!")
                }
            }
        }
        return view
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

기존의 프래그먼트에서 composeview를 적용 해준 방식입니다

		binding.composeView.apply {
            // Dispose of the Composition when the view's LifecycleOwner
            // is destroyed
            setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                // In Compose world
                MaterialTheme {
                    Text("Hello Compose!")
                }
            }
        }

위에 소스에서 이부분만 보면 알수있는데요. databinding 된 composeview의 apply 함수의 내용이 반영 되게 될것입니다. 내부에있는 setcontent 부분에 compose source를 넣으시면 composeview 에 반영이 됩니다.

이렇게요.

물론 컴포즈 뷰가 여러개 첨부될 가능성도 있기 때문에, 이렇게 하시는 것보다도 다음과 같이 id를 여러개 배분해주는 쪽으로 하는 것이 좋습니다.

override fun onCreateView(...): View = LinearLayout(...).apply {
      addView(ComposeView(...).apply {
          id = R.id.compose_view_x
          ...
      })
      addView(TextView(...))
      addView(ComposeView(...).apply {
          id = R.id.compose_view_y
          ...
      })
    }
  }

Fragment in Compose

이번에는 Compose 내부에서 fragment 를 추가하려고 합니다. 접근 방식은 AdView와 같이 Compose에서 아직 사용할 수 없는 UI 요소를 사용하려는 경우에 특히 유용합니다. 이 접근 방식을 사용하면 직접 디자인한 맞춤 뷰를 재사용할 수도 있습니다.

Library

XML 레이아웃을 삽입하려면 androidx.compose.ui:ui-viewbinding 라이브러리에서 제공하는 AndroidViewBinding API를 사용합니다.

implementation 'androidx.compose.ui:ui-viewbinding:1.3.0'

my_fragment_layout.xml

AndroidViewBinding 컴포저블을 사용하여 Compose에 Fragment를 추가하려면 FragmentContainerView를 띄워 레이아웃을 생성합니다.

<androidx.fragment.app.FragmentContainerView
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/fragment_container_view"
  android:layout_height="match_parent"
  android:layout_width="match_parent"
  android:name="com.example.MyFragment" />

이 FragmentContainerView 레이아웃에서 원하는 fragment가 위에 올라갈 것입니다. 기존에 있는 레이아웃을 바로 띄우지는 못한다는 뜻입니다. 꼭 참고하세요

ExampleFragmentActivity.kt

기존에 fragment의 내용을 actvity에서 출력하게 될 경우 fragmentmanager 에서 transaction을 호출하는 등 그러한 방식을 사용하였지만 compose이기 때문에 framgent를 호출할때는 Composable annotation 과 함께 AndroidViewBinding API를 적용합니다.

@Composable
fun FragmentInComposeExample() {
    AndroidViewBinding(MyFragmentLayoutBinding::inflate) {
        val myFragment = fragmentContainerView.getFragment<MyFragment>()
        // ...
    }
}

MyFragmentLayoutBinding은 fragment가 data-binding 이 된 것으로 그것을 inflate 하는 방식입니다. 이부분은 AndroidViewBinding이 지원해주는 fragment inflating방식 이더라구요. 저에게는 가장 새로웠던 부분이었습니다.

그리고 가장 중요한건 fragmentActivity를 상속해야 합니다. 그렇지 않으면 다음 과 같은 오류가 발생합니다.

FragmentContainerView must be within a FragmentActivity to be instantiated from XML

참고로 이 방법 말고 compose코드 안에서 customview를 생성하는방식도 있습니다.

Using

저의 경우에는 Youtube API Library UI 를 그대로 가져와야 할때 사용하였습니다. 제가 예전에 개발할때는 fragmentActivity를 사용해본 경험이 없었습니다. 그래서 조금 더 파봤는데 androidx 에서 나온 것 같네요 제가 앱 배포할때에는 jetpack이 없던 시절이라.... 하하하

제가 말씀드린 이 방법들 말고도 service, broadcastreceiver 같은 android framework를 사용하는 CompositionLocal 클래스 들도 상호운용성에 해당합니다. 그건 제가 사용하고자 하는 내용과는 좀 다른 부분인지라 자세한건 android developer가 더 상세하게 설명해주고 있으니 꼭 한번씩 봐주세요.

Reference

profile
develop life

0개의 댓글