기본 생성된 코드를 보았을 때, Compose는 총 3가지의 구성 요소를 가지는 것으로 추측할 수 있다.
위젯을 포함하는 Composable 함수
Preview를 하기 위한 Preview Composable 함수
setContent 람다 표현식으로 실제 화면에 노출하는 코드
일반적으로 우리가 아는 Activity의 라이프사이클 콜백 onCreate()에서 setContentView(Int) 함수를 호출하던것이 setContent() 함수로 바뀐것이 가장 큰 특징으로 보여진다.
3. Composable Function
Composable Function은 어노테이션을 이용한 기술이다. 함수위에 @Composable 어노테이션을 붙이게 되면 함수 안 다른 함수를 호출할 수 있게된다. 아래 코드를 보자.
1 2 3 4 5 6
@Composable funGreeting(names: List<String>) { for (name in names) { Text("Hello $name") } }
단순하게 내부에는 Text라는 함수가 존재하는데, 이를 통해 UI계층 별 요구하는 컴포넌트를 생성해준다. 기본적으로 보이는 text 파라미터는 내부 속성에서 받는 일부 중 하나이다.
4. TextView 만들기
위 코드를 실행시켜보면 당연하게도 Hello로 시작하는 TextView가 화면에 그려질것을 암시한다.
1 2 3 4 5 6 7 8
setContent { BasicsCodelabTheme { // A surface container using the 'background' color from the theme Surface(color = MaterialTheme.colors.background) { Greeting("Android") } } }
5. @Preview
말 그대로 어노테이션을 이용하여 IDE에서 Preview를하기 위한 용도이다. 아래 코드와 같이 @Preview 어노테이션을 추가하면 다음 결과를 볼 수 있다.
setContent { BasicsCodelabTheme { // A surface container using the 'background' color from the theme Surface(color = MaterialTheme.colors.background) { Greeting("Android") } } }
기존에 onCreate시점에 화면을 그려주기 위한 필수적인 요소를 정리해보자면
setContent : Activity에서 setContentView함수를 사용하는 것과 동일한 동작을 하는 확장함수이다. 다만, setContent의 경우 (@Composable) -> Unit 타입의 컴포즈 UI를 구현해주어야한다.
XXXTheme : Theme정보를 의미한다. 해당 프로젝트에서는 Theme.kt에 여러 테마에 필요한 정보를 정리하고, 컴포즈 UI 구현을 위한 코드를 작성해두었다.
노란색 배경을 입혀 기존 TextView에 추가해보았다. 또한, Greeting에는 Modifier라는 것을 이용하여 Padding을 추가했다. 아래와 같은 결과가 나오게 되었다.
1 2 3 4 5 6
BasicsCodelabTheme { // A surface container using the 'background' color from the theme Surface(color = Color.Yellow) { Greeting("Android") } }
1 2 3 4 5 6 7 8 9 10 11 12 13
@Composable funGreeting(name: String) { var isSelected by remember { mutableStateOf(false) } val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)
선언형 UI의 장점은 말 그대로 내가 UI를 정의한대로 시각적으로 표현이 가능하다는 장점이 있다. 기존에는 속성을 매번 On/Off와 같은 옵션을 통해 변경하는 것이 다반사였지만, 이제는 매번 속성에 변경이 생길때마다 새로 그려주게 되는것이다.
8. 재사용
Compose의 장점 중 하나는 재사용성이 뛰어난것인데, XML에서 우리가 include 태그를 통해 여러곳에서 갖다쓸 수 있던것처럼, 함수를 통해 여러곳에서 정의하여 사용이 가능하다.
참고해야할 점은 Compose 컴포넌트 확장 시 @Composable 어노테이션을 붙여야 한다.
9. Container 작성
MyApp이라는 이름으로 컴포즈 컴포넌트를 구횬하여 여러곳에서 공통으로 사용할 수 있는 Composable을 구현하였다. 내부적으로 Container내 내가 원하는 컴포넌트를 넣어주려면 아래와 같이 인자로 @Composable () -> Unit 타입을 넘겨받아 처리해주면 된다.
remember라는 함수를 사용하여 기존에 존재하는 컴포넌트의 상태값을 기억하게 하는 함수가 있다.
remember 함수의 내부를 살펴보자.
1 2 3 4 5 6 7 8
/** * Remember the value produced by [calculation]. [calculation] will only be evaluated during the composition. * Recomposition will always return the value produced by composition. */ @OptIn(ComposeCompilerApi::class) @Composable inlinefun<T>remember(calculation: @DisallowComposableCalls () -> T): T = currentComposer.cache(false, calculation)
매 호출마다 Recomposition(재조합)하게되는 경우 컴포넌트에 값을 다시 제공하는 것을 알 수 있다. @Composable 어노테이션에 들어간 함수는 매번 해당 상태를 구독하고, 상태가 변경될때마다 알림을 받아 기존 화면을 갱신해준다.
그리고, 아래 Counter를 보면 Button을 이용하여 이벤트를 받아 처리하도록 했다.
updateCount(Int) 함수릉 통해 매번 값을 업데이트 해주는데, 이를 통해 counterState에 값을 넣어주면서 해당 컴포넌트가 매번 변경이 되는것이다.
따라서 결과를 보면, 다음과 같다. Count가 5가 넘어가면 초록색으로 바뀐다.
그 외에도 여러형태의 모양을 구성할수 있도록 옵션이 제공되어 있다. 자세한 정보는 나중에 Codelabs에 더 나와 있으니 보도록하고, 이번에 setContent에 대한 동작원리를 함께 고민해보자.
12. Activity에서의 View 생성 방식과의 비교
Compose를 안드로이드 앱에서 사용하려면 Activity, Fragment와 같은곳에서 contentView로 뿌려줘야한다. 기존에 우리가 사용하던 함수를 보자.
1 2 3 4 5 6 7 8 9 10 11 12 13
/** * Set the activity content from a layout resource. The resource will be * inflated, adding all top-level views to the activity. * * @param layoutResID Resource ID to be inflated. * * @see #setContentView(android.view.View) * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams) */ publicvoidsetContentView(@LayoutResint layoutResID){ getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }
UI 컴포넌트에서 화면을 붙일 수 있는 Window라는 녀석에서 Layout Resource Id를 통해 기존에 등록되어있던 Layout XML 파일을 로드하여 인플레이터에서 파싱하고, 이를통해 레이아웃 계층에 있는 뷰객체를 생성하여 순차적으로 ViewGroup, View를 만들어 넣어주게 된다.
PhoneWindow를 보면 자세하게 알 수 있는데, Window를 구현한 setContentView에서 처음에 생성되는 최상위 레이아웃 그 위에 따로 없다면 installDecor() 함수를 통해 mContentParent(레이아웃 리소스가 붙게될 ViewGroup)를 생성하고, 하위에 넣어주게 된다.
그러면 기존 방식은 이정도로 설명을하고, 이번엔 Compose에서 setContent() 라는 함수를 어떻게 사용하는지 보자.
/** * Composes the given composable into the given activity. The [content] will become the root view * of the given activity. * * This is roughly equivalent to calling [ComponentActivity.setContentView] with a [ComposeView] * i.e.: * * ``` * setContentView( * ComposeView(this).apply { * setContent { * MyComposableContent() * } * } * ) * ``` * * @param parent The parent composition reference to coordinate scheduling of composition updates * @param content A `@Composable` function declaring the UI contents */ publicfun ComponentActivity.setContent( parent: CompositionContext? = null, content: @Composable () -> Unit ) { val existingComposeView = window.decorView .findViewById<ViewGroup>(android.R.id.content) .getChildAt(0) as? ComposeView
if (existingComposeView != null) with(existingComposeView) { setParentCompositionContext(parent) setContent(content) } else ComposeView(this).apply { // Set content and parent **before** setContentView // to have ComposeView create the composition on attach setParentCompositionContext(parent) setContent(content) // Set the view tree owners before setting the content view so that the inflation process // and attach listeners will see them already present setOwners() setContentView(this, DefaultActivityContentLayoutParams) } }
이녀석도 마찬가지로 window.decorView.findViewById<ViewGroup>(android.R.id.content) 함수를 호출하여 decorView를 가져온다. 만약 compose를 통해 만들어진 최상위 레이아웃이 존재하면, 기존에 inflator에서 ViewGroup, View를 생성해서 넣어주던것 처럼 setContent() => window가 Activity/Fragment에 붙으면 createComposition()를 호출하여 검증 후 ensureCompsositionCreated() 함수를 호출한다. 현재는 내부적으로 ViewGroup.setContent() 를 사용하고 있는데, 곧 교체 될 예정이라고 한다. 이코드도 보면 기존에 있는 ViewGroup에 확장함수로 구현한 녀석인데, 쉽게 말해 ViewGroup에 하위 View, ViewGroup에 Composable로 구현된 함수로 컴포넌트를 넣어줄 때 AndroidComposeView라는 객체를 꺼내오거나 없다면 새로 생성하여 넣어준다.
/** * Composes the given composable into the given view. * * The new composition can be logically "linked" to an existing one, by providing a * [parent]. This will ensure that invalidations and CompositionLocals will flow through * the two compositions as if they were not separate. * * Note that this [ViewGroup] should have an unique id for the saved instance state mechanism to * be able to save and restore the values used within the composition. See [View.setId]. * * @param parent The [Recomposer] or parent composition reference. * @param content Composable that will be the content of the view. */ internalfun ViewGroup.setContent( parent: CompositionContext, content: @Composable () -> Unit ): Composition { GlobalSnapshotManager.ensureStarted() val composeView = if (childCount > 0) { getChildAt(0) as? AndroidComposeView } else { removeAllViews(); null } ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) } return doSetContent(composeView, parent, content) }
/** * A [android.view.View] that can host Jetpack Compose UI content. * Use [setContent] to supply the content composable function for the view. * * This [android.view.View] requires that the window it is attached to contains a * [ViewTreeLifecycleOwner]. This [androidx.lifecycle.LifecycleOwner] is used to * [dispose][androidx.compose.runtime.Composition.dispose] of the underlying composition * when the host [Lifecycle] is destroyed, permitting the view to be attached and * detached repeatedly while preserving the composition. Call [disposeComposition] * to dispose of the underlying composition earlier, or if the view is never initially * attached to a window. (The requirement to dispose of the composition explicitly * in the event that the view is never (re)attached is temporary.) */ classComposeView@JvmOverloadsconstructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AbstractComposeView(context, attrs, defStyleAttr) {
/** * Set the Jetpack Compose UI content for this view. * Initial composition will occur when the view becomes attached to a window or when * [createComposition] is called, whichever comes first. */ funsetContent(content: @Composable () -> Unit) { shouldCreateCompositionOnAttachedToWindow = true this.content.value = content if (isAttachedToWindow) { createComposition() } } }
결론적으로 AbstractComposeView 라는 녀석은 ViewGroup을 상속받은 녀석이며, 모든 composable의 상태가 변화 되었을 때 이를 감지하는 중요한 녀석이다.
setContent()라는 함수는 위에서 설명했으니 넘어가고, 이번에는 Content라는 녀석을 보자. 이녀석은 추상 메소드로, createComposition() 이라는 함수가 호출 되었을 때, 가장 먼저 불리는 함수이다. 아까 언급되었던 ensureCompsositionCreated() 함수에서 tree계층의 ComposeView가 다 붙었다면, 이후에 즉시 Content함수가 호출이된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
@Suppress("DEPRECATION")// Still using ViewGroup.setContent for now privatefunensureCompositionCreated() { if (composition == null) { try { creatingComposition = true composition = setContent( parentContext ?: findViewTreeCompositionContext() ?: windowRecomposer ) { Content() // 이곳에서 뷰가 다 window에 붙게되면 콜백을 호출한다. } } finally { creatingComposition = false } } }
그러면 아래 ComposeView의 오버라이딩 된 Content가 호출되면서, 기존에 생성된 View에 UI속성과 같은 Content가 붙게된다.
1 2 3 4 5 6 7 8
/** * The Jetpack Compose UI content for this view. * Subclasses must implement this method to provide content. Initial composition will * occur when the view becomes attached to a window or when [createComposition] is called, * whichever comes first. */ @Composable abstractfunContent()
Content는 설명에서 보는것과 같이 createComposition() 함수 호출 후 View가 Window에 붙은 이후 즉시 호출된다.
최종적으로 ComponentActivity.setContent(CompositionContext?, @Composable () -> Unit) 함수에서 구현된 ComposeView 인스턴스를 ContentLayout을 widht/height를 wrapContent크기로 정하여 ContentView를 Set해주게 된다.
/** * Composes the given composable into the given activity. The [content] will become the root view * of the given activity. * * This is roughly equivalent to calling [ComponentActivity.setContentView] with a [ComposeView] * i.e.: * * ``` * setContentView( * ComposeView(this).apply { * setContent { * MyComposableContent() * } * } * ) * ``` * * @param parent The parent composition reference to coordinate scheduling of composition updates * @param content A `@Composable` function declaring the UI contents */ publicfun ComponentActivity.setContent( parent: CompositionContext? = null, content: @Composable () -> Unit ) { ... else ComposeView(this).apply { // Set content and parent **before** setContentView // to have ComposeView create the composition on attach setParentCompositionContext(parent) setContent(content) // Set the view tree owners before setting the content view so that the inflation process // and attach listeners will see them already present setOwners() setContentView(this, DefaultActivityContentLayoutParams) } }
13. ComposeView
android.view.View 는 Jetpack Compose UI 콘텐츠를 사용할 수 있도록 해줍니다. setContent 를 사용하면 composable function content 를 뷰에 제공할 수 있다.
Compose 의 계층 구조는 아래와 같으며. ComposeView 를 통해 androidx.compose.materia 에 정의된 다양한 컴포넌트를 조합하여 Composable function 콘텐츠를 구성할 수 있다.