상호 운용성 API

앱에 Compose를 채택하는 동안 Compose와 뷰 기반 UI를 결합할 수 있습니다. 다음에는 Compose로의 전환을 보다 쉽게 할 수 있는 API, 추천, 팁이 나와 있습니다.

뷰의 Compose

뷰 기반 디자인을 사용하는 기존 앱에 Compose 기반 UI를 추가할 수 있습니다.

전적으로 Compose에 기반하는 새로운 화면을 생성하려면 활동에서 setContent() 메서드를 호출하고 원하는 구성 가능한 함수를 전달하도록 합니다.

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

        setContent { // In here, we can call composables!
            MaterialTheme {
                Greeting(name = "compose")
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

이 코드는 Compose 전용 앱에서 찾을 수 있는 코드와 비슷합니다.

ComposeView의 ViewCompositionStrategy

기본적으로 Compose는 뷰가 창에서 분리될 때마다 컴포지션을 삭제합니다. Compose UI View 유형(예: ComposeView, AbstractComposeView)은 이 동작을 정의하는 ViewCompositionStrategy를 사용합니다.

기본적으로 Compose는 DisposeOnDetachedFromWindowOrReleasedFromPool 전략을 사용합니다. 하지만 다음과 같은 Compose UI View 유형이 사용되는 일부 상황에서는 이 기본값이 바람직하지 않을 수 있습니다.

  • 프래그먼트. 컴포지션은 Compose UI View 유형을 위한 프래그먼트의 뷰 수명 주기에 따라 상태를 저장해야 합니다.

  • 전환. Compose UI View가 전환의 일부로 사용될 때마다 전환이 끝날 때가 아닌 전환이 시작될 때 창에서 분리되므로 컴포저블이 화면에 계속 표시되는 동안 상태를 삭제하게 됩니다.

  • 자체 수명 주기 관리 맞춤 View

상황에 따라 AbstractComposeView.disposeComposition을 수동으로 호출하지 않으면 앱이 컴포지션 인스턴스의 메모리를 천천히 유출할 수 있습니다.

더 이상 필요하지 않은 컴포지션을 자동으로 삭제하려면 다른 전략을 설정하거나 setViewCompositionStrategy 메서드를 호출하여 전략을 직접 만듭니다. 예를 들어 DisposeOnLifecycleDestroyed 전략은 lifecycle이 끝나면 컴포지션을 삭제합니다. 이 전략은 알려진 LifecycleOwner와 1:1 관계를 공유하는 Compose UI View 유형에 적합합니다. LifecycleOwner를 알 수 없는 경우 DisposeOnViewTreeLifecycleDestroyed를 사용할 수 있습니다.

프래그먼트의 ComposeView에서 이 API의 실제 동작을 확인하세요.

프래그먼트의 ComposeView

Compose UI 콘텐츠를 프래그먼트 또는 기존 뷰 레이아웃에 통합하려면 ComposeView를 사용하고 setContent() 메서드를 호출합니다. ComposeView는 Android View입니다.

다음과 같이 다른 View와 마찬가지로 ComposeView를 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>

Kotlin 소스 코드에서 XML로 정의된 레이아웃 리소스의 레이아웃을 확장합니다. 그런 다음 XML ID를 사용하여 ComposeView를 가져오고, 호스트 View에 가장 적합한 컴포지션 전략을 설정한 다음, setContent()를 호출하여 Compose를 사용합니다.

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(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                // In Compose world
                MaterialTheme {
                    Text("Hello Compose!")
                }
            }
        }
        return view
    }

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

위, 아래로 배치되어 있으며 서로 약간 다른 두 텍스트 요소

그림 1. 뷰 UI 계층 구조에서 Compose 요소를 추가하는 코드의 출력을 보여줍니다. 'Hello Android!' 텍스트는 TextView 위젯에 의해 표시됩니다. 'Hello Compose!' 텍스트는 Compose 텍스트 요소에 의해 표시됩니다.

또한 전체 화면이 Compose를 사용하여 빌드된 경우 프래그먼트에 ComposeView를 직접 포함할 수도 있습니다. 이렇게 하면 전적으로 XML 레이아웃 파일을 사용하지 않아도 됩니다.

class ExampleFragmentNoXml : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return ComposeView(requireContext()).apply {
            // Dispose of the Composition when the view's LifecycleOwner
            // is destroyed
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                MaterialTheme {
                    // In Compose world
                    Text("Hello Compose!")
                }
            }
        }
    }
}

동일한 레이아웃의 여러 ComposeView

동일한 레이아웃에 ComposeView 요소가 여러 개 있다면 savedInstanceState가 작동하기 위해서는 각 요소에 고유 ID가 있어야 합니다.

class ExampleFragmentMultipleComposeView : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View = LinearLayout(requireContext()).apply {
        addView(
            ComposeView(requireContext()).apply {
                setViewCompositionStrategy(
                    ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
                )
                id = R.id.compose_view_x
                // ...
            }
        )
        addView(TextView(requireContext()))
        addView(
            ComposeView(requireContext()).apply {
                setViewCompositionStrategy(
                    ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
                )
                id = R.id.compose_view_y
                // ...
            }
        )
    }
}

다음과 같이 ComposeView ID는 res/values/ids.xml 파일에 정의되어 있습니다.

<resources>
    <item name="compose_view_x" type="id" />
    <item name="compose_view_y" type="id" />
</resources>

Compose의 뷰

Compose UI에 Android 뷰 계층 구조를 포함할 수 있습니다. 이 접근 방식은 AdView와 같이 Compose에서 아직 사용할 수 없는 UI 요소를 사용하려는 경우에 특히 유용합니다. 이 접근 방식을 사용하면 직접 디자인한 맞춤 뷰를 재사용할 수도 있습니다.

뷰 요소 또는 계층 구조를 포함하려면 AndroidView 컴포저블을 사용해야 합니다. AndroidView에는 View를 반환하는 람다가 전달됩니다. 또한 AndroidView는 뷰가 확장될 때 호출되는 update 콜백도 제공합니다. AndroidView는 콜백 내에서 읽은 State가 변경될 때마다 재구성됩니다. 여타 기본 제공 컴포저블과 마찬가지로 AndroidView는 상위 컴포저블에서 위치를 설정하는 등의 목적으로 사용할 수 있는 Modifier 매개변수를 취합니다.

@Composable
fun CustomView() {
    var selectedItem by remember { mutableStateOf(0) }

    // Adds view to Compose
    AndroidView(
        modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
        factory = { context ->
            // Creates view
            MyView(context).apply {
                // Sets up listeners for View -> Compose communication
                setOnClickListener {
                    selectedItem = 1
                }
            }
        },
        update = { view ->
            // View's been inflated or state read in this block has been updated
            // Add logic here if necessary

            // As selectedItem is read here, AndroidView will recompose
            // whenever the state changes
            // Example of Compose -> View communication
            view.selectedItem = selectedItem
        }
    )
}

@Composable
fun ContentExample() {
    Column(Modifier.fillMaxSize()) {
        Text("Look at this CustomView!")
        CustomView()
    }
}

XML 레이아웃을 삽입하려면 androidx.compose.ui:ui-viewbinding 라이브러리에서 제공하는 AndroidViewBinding API를 사용합니다. 그렇게 하려면 프로젝트에서 뷰 결합을 사용 설정해야 합니다.

@Composable
fun AndroidViewBindingExample() {
    AndroidViewBinding(ExampleLayoutBinding::inflate) {
        exampleView.setBackgroundColor(Color.GRAY)
    }
}

Compose의 프래그먼트

AndroidViewBinding 컴포저블을 사용하여 Compose에 Fragment를 추가합니다. AndroidViewBinding에는 컴포저블이 컴포지션을 종료할 때 프래그먼트를 삭제하는 등 프래그먼트별 처리 기능이 있습니다.

이렇게 하려면 FragmentContainerView가 포함된 XML을 Fragment의 홀더로 확장하면 됩니다.

예를 들어 my_fragment_layout.xml을 정의한 경우 android:name XML 속성을 Fragment의 클래스 이름으로 대체하면서 다음과 같은 코드를 사용할 수 있습니다.

<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" />

다음과 같이 Compose에서 이 프래그먼트를 확장합니다.

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

동일한 레이아웃에서 여러 프래그먼트를 사용해야 하는 경우 각 FragmentContainerView에 고유 ID를 정의했는지 확인하세요.

Compose에서 Android 프레임워크 호출

Compose는 Android 프레임워크 클래스 내에서 작동합니다. 예를 들어 Compose는 Activity 또는 Fragment 같은 Android 뷰 클래스에서 호스팅되며, Context, 시스템 리소스, Service 또는 BroadcastReceiver 같은 Android 프레임워크 클래스를 사용해야 할 수 있습니다.

시스템 리소스에 관해 자세히 알아보려면 Compose의 리소스 문서를 참고하세요.

컴포지션 로컬

CompositionLocal 클래스를 사용하면 구성 가능한 함수를 통해 암시적으로 데이터를 전달할 수 있습니다. 일반적으로 이 클래스는 UI 트리의 특정 노드의 값과 함께 제공됩니다. 이 값은 구성 가능한 함수에서 CompositionLocal을 매개변수로 선언하지 않아도 구성 가능한 하위 요소에 사용할 수 있습니다.

CompositionLocal은 Compose에서 Context, Configuration 또는 View 같은 Android 프레임워크 유형의 값을 전파하는 데 사용됩니다. 이러한 프레임워크에서 Compose 코드가 해당 LocalContext, LocalConfiguration 또는 LocalView와 함께 호스팅됩니다. IDE에서 자동 완성 기능을 사용할 때 검색 가능성을 높이기 위해 CompositionLocal 클래스에 Local 접두사가 지정됩니다.

current 속성을 사용하여 CompositionLocal의 현재 값에 액세스합니다. 예를 들어 아래 코드는 Toast.makeToast 메서드에 LocalContext.current를 제공하여 토스트 메시지를 표시합니다.

@Composable
fun ToastGreetingButton(greeting: String) {
    val context = LocalContext.current
    Button(onClick = {
        Toast.makeText(context, greeting, Toast.LENGTH_SHORT).show()
    }) {
        Text("Greet")
    }
}

더욱 자세한 예시는 이 문서 끝에 있는 우수사례: BroadcastReceivers 섹션을 참고하세요.

기타 상호작용

필요한 상호작용에 관해 정의된 유틸리티가 없다면 일반적인 Compose 가이드라인인 데이터는 아래로 흐르고 이벤트는 위로 흐름을 따르는 것이 좋습니다(Compose 이해에서 더 자세히 논의). 예를 들어 다음 컴포저블은 다른 활동을 시작합니다.

class OtherInteractionsActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // get data from savedInstanceState
        setContent {
            MaterialTheme {
                ExampleComposable(data, onButtonClick = {
                    startActivity(Intent(this, MyActivity::class.java))
                })
            }
        }
    }
}

@Composable
fun ExampleComposable(data: DataExample, onButtonClick: () -> Unit) {
    Button(onClick = onButtonClick) {
        Text(data.title)
    }
}

우수사례: BroadcastReceivers

Compose에 이전하거나 구현하고자 하는 기능과 관련해 보다 실질적인 예를 보여주고 CompositionLocal부작용을 보여주기 위해 구성 가능한 함수에서 BroadcastReceiver를 등록해야 한다고 가정하겠습니다.

이 방법에서는 LocalContext를 이용해 현재 컨텍스트와 rememberUpdatedStateDisposableEffect 부수 효과를 사용합니다.

@Composable
fun SystemBroadcastReceiver(
    systemAction: String,
    onSystemEvent: (intent: Intent?) -> Unit
) {
    // Grab the current context in this part of the UI tree
    val context = LocalContext.current

    // Safely use the latest onSystemEvent lambda passed to the function
    val currentOnSystemEvent by rememberUpdatedState(onSystemEvent)

    // If either context or systemAction changes, unregister and register again
    DisposableEffect(context, systemAction) {
        val intentFilter = IntentFilter(systemAction)
        val broadcast = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                currentOnSystemEvent(intent)
            }
        }

        context.registerReceiver(broadcast, intentFilter)

        // When the effect leaves the Composition, remove the callback
        onDispose {
            context.unregisterReceiver(broadcast)
        }
    }
}

@Composable
fun HomeScreen() {

    SystemBroadcastReceiver(Intent.ACTION_BATTERY_CHANGED) { batteryStatus ->
        val isCharging = /* Get from batteryStatus ... */ true
        /* Do something if the device is charging */
    }

    /* Rest of the HomeScreen */
}