[Android] Jetpack Compose (1)

아래 블로그 포스팅을 참고했습니다.

https://blog.mathpresso.com/jetpack-compose-%EB%A1%9C-%EA%B8%B0%EB%8A%A5%EC%A0%84%EC%B2%B4%EB%A5%BC-refactoring-%ED%95%B4%EB%B3%B4%EC%9E%90-2b921c624e80

 

요즘 핫🔥-한 Jetpack Compose 로 기능 하나를 통째로 Refactoring 해보기

곧 몰아칠 새로운 UI 개발 패러다임에 대비해보자

blog.mathpresso.com

Jetpack Compose 

SwiftUI, Flutter 와 같은 선언형 UI 

기존의 안드로이드의 XML이나 IOS의 StoryBoard로 작성하는 방식은

특정 상태에 따라 UI를 어떻게 보여줄 지를 구상하여 구현하게 된다.

 

반면에 Jectpack Compose와 같은 선언형 UI는 이와 반대로

특정 상태에 따라 UI가 무엇을 보여주면 되는지에 대해 구현하게 된다.

 

이러한 방식으로 구현하는 경우 기존의 UI 작성 방식보다 훨씬 적은 코드를 사용할 수 있으며,

뷰 계층의 트리를 탐색하여 수동으로 조작하는 기존 방식보다 유지보수에 이점을 얻게 된다.

< 트리 탐색 기법 : view.findViewById() 등의 매칭 방식 >

기존 언어의 유연성을 그대로 활용할 수 있으며, 개발자가 뷰의 속성 등을 구현할 경우 상세하게

작성하지 않아도 되므로 재사용성, 확장성에 이점을 가진다.

 

Jetpack의 렌더링 방식

특정 상태에 따라 무엇을 보여줄 지 함수를 선언한다면, 특정 상태가 변경되면 Composable 함수

전체가 다시 호출되어 화면이 재렌더링 되어, 호출 비용이 오히려 증가하지는 않는지 생각하게 된다.

 

이를 해결하기 위해서 jetpack Compose는 새로운 상태에 따라 변경해야할 UI만 지능적으로

Recomposition 하는 렌더링 방식을 가지고 있다.

 

- Composable 함수는 순서와 관계없이 실행될 수 있다.

- Composable 함수는 동시에 실행될 수 있다.

- Recomposition 될 때 특정 Composable 함수와 람다 함수들을 건너뛸 수 있다.

- Recomposition은 새 상태에 따라서 취소되고 다시 시작할 수 있다.

- Composable 함수는 프레임별로 굉장히 빈도수가 높게 실행될 수 있다.

 

Jetpack을 적용하기 위한 액티비티 생성하기

https://developer.android.com/jetpack/compose/tutorial?hl=ko 

 

Android Compose 튜토리얼  |  Android 개발자  |  Android Developers

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Jetpack Compose는 네이티브 Android UI를 빌드하기 위한 최신 도구 키트입니다. Jetpack Compose는 더 적은 수의 코드,

developer.android.com

jetpack을 프로젝트에 적용해보기 위해서 공식 문서를 확인했다.

jetpack compose를 사용하기 위해서는 Empty Activity가 아닌 Empty Compose Activity로 

생성해야 @Composable과 같은 어노테이션을 적용할 수 있다.

class JetPackActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Greeting(name = "Android")
        }
    }
}

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

프로젝트를 생성하면 setContent에서 xml 관련 된 UI를 생성할 수 있다.

IOS 프로그래밍을 할 때 선언형으로 UI를 그린 것처럼 코틀린 코드 내에서 UI를 그릴 수 있게 된다.

추가적인 xml 작성 없이, Text View를 그릴 수 있게 되었다.

State + remember (rememberSaveable)

Jetpack Compose에서 데이터 상태 관리에 도움을 주는 API

Recomposition 될 때마다 관리하는데 도움을 주며, remember 를 사용해서 특정 상태 

변경으로 인해 Recomposition 이 되더라도 데이터를 보존시켜줄 수 있다.

class JetPackActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
//            Greeting(name = "Android")
            Home()
        }
    }
}

@Composable
fun Home(){
    var workStatus by remember { mutableStateOf("") }
    Harry(workStatus, onWorkStatusChanged = {workStatus = it})
}

@Composable
fun Harry(workStatus:String,onWorkStatusChanged: (String)-> Unit){
    Column {
        Text(text = workStatus.toString())
        TextField(value = workStatus,
            label = { Text(text = "업무 상태")},
            onValueChange = {onWorkStatusChanged(it)})
    }
}

Harry 함수를 통해서 업무 상태를 작성하는 TextField를 선언하는 과정을 예로 들었다.

Harry 함수가 다른 상태 변경이 발생했을 경우, workStatus에 TextField의 마지막 입력 값이 그대로 저장된다.

remember를 통해서 현재 Composable 함수 내에서 Recomposition 시에 데이터를 보존할 수 있는 효과를 얻었다.

하지만 Configuration이 변경되거나 전화가 오는 상황 등에서는 데이터의 보존이 어려우므로,

Bundle에 값을 저장할 수 있는 rememberSaveable를 추가로 사용해야 한다.

 

CompoistionLocal

Composable 함수로 UI를 작성한다면, 데이터는 Parameter로 전달해야 한다.

재활용이 쉽도록 UI Component 들을 함수로 잘 짜놓으면,

이때 이 Component에 필요한 상태나 데이터를 주고받는데 어려움이 생길 수 있다.

이런 경우 CompoistionLocal를 활용해서 Composable 함수 내에서 효과적으로 데이터를 주고받을 수 있다.

 

[CompoistionLocal 사용 안한 코드]

fun onCreate() {
    setContent {
        val currentUser by viewModel.user.collectAsState()
        Home()
    }
}
// ...
@Composable
fun Home(user : User) {
    Body(user)
    Footer(user)
}

@Composable
fun Body(user : User) {
    // ...
    Greeting(user)
}
@Composable
fun Footer(user : User) {
    // ...
    Greeting(user)
}
@Composable
fun Greeting(user : User) {
    // ...
    Tex

위와 같이 코드를 작성할 경우 Home() 함수에서는 user가 필요 없지만,

Body, Footer 함수에서 user를 필요로 하므로 불필요한 데이터를 Home에 전달해야 하는 경우가 발생한다.

 

만약에 함수의 깊이가 매우 깊어진다면, 이런 컴포넌트 구조가 늘어나서 복잡한 코드가 된다.

어떤 함수에 어떤 인자가 필요한지 하나씩 확인해야하며,

UI에 필요한 데이터들을 모든 컴포넌트들의 Composable 함수에 파라미터로 넘겨야한다.

이를 개미굴 구조라고 하며, 코드의 가독성이 떨어지고 데이터 상태 관리가 어려워진다.

 

[CompoistionLocal 사용 코드]

fun onCreate() {
    setContent {
        val currentUser by viewModel.user.collectAsState()
        CompositionLocalProvider(LocalUser provide currentUser) {
            Home()
        }
    }
}
// ...
@Composable
fun Home() {
    Body()
    Footer()
}

@Composable
fun Body() {
    // ...
    Greeting()
}
@Composable
fun Footer() {
    // ...
    Greeting()
}
@Composable
fun Greeting() {
    // ...
    val user = LocalUser.current
    Text(text = user.name)
}

미리 Provide 하는 코드를 작성해서 User가 필요한 Composable 함수에만 가져다 쓰는 방식이다.

User가 필요한 Composable 함수만 가져다 쓸 수 있으며, 가독성이 훨씬 좋아진 것을 확인할 수 있다.

 

@Preview 어노테이션 활용

Jetpack Compose에서는 함수 미리보기 기능을 지원한다.

함수를 어디에서도 구현하지않고, preview 어노테이션을 추가한 상태로 작성해두는 방식이다.

이 방식으로 직접 함수를 프로젝트에 적용하지 않고 미리보기 기능을 지원한다.

카드뷰를 생성하는 MessageCard 코드를 통해서 해당 기능을 적용했다.

// preview 어노테이션 작성
@Composable
fun MessageCard(name : String){
    Text(text = "hello !! $name")
}
@Preview
@Composable
fun PreviewMessageCard(){
    MessageCard(name = "Android test")
}
///////////////

앱을 빌드하면, 기존 방식과 다르게 kotlin 코드를 작성하는 액티비티에서 xml처럼 view를 볼 수 있다. 

아래 화면처럼 Split 탭에서 적용되는 뷰를 미리 확인할 수 있다.

 

여러 어노테이션을 적용하는 방법과 Jetpack Compose의 기초 문법을 공부했다.

지금 배운 내용으로는 프로젝트에 적용하기 어려워보인다.

다음 스터디에서는 기본적인 레이아웃 UI를 구성하는 방식에 대해서 포스팅해야 겠다.