Beeeam

컴포즈 공부 2일차 본문

Android

컴포즈 공부 2일차

Beamjun 2023. 11. 26. 23:57

컴포저블 함수

컴포즈로 사용자 인터페이스를 만들 때 사용하는 특수한 코틀린 함수이다. @composable 애너테이션을 붙여서 선언한다.

컴포저블 함수에서는 일반 코틀린 함수를 호출할 수 있지만 일반 코틀린 함수에서는 컴포저블 함수를 호출하는 것이 불가능하다.

상태 컴포저블, 비상태 컴포저블 함수로 구분된다. 여기서 상태는 앱 실행 중에 변경될 수 있는 모든 값들을 의미한다. ex) 텍스트 필드에 입력된 문자열, 슬라이더의 위치 값, 체크 박스의 현재 설정 상태 등등…

상태

컴포즈 같은 선언형 언어에서 “상태”는 시간에 따라 변경될 수 있는 값을 의미한다. 우리가 일반적으로 사용하는 표준 변수와 다를게 없다고 생각할 수 있지만 2가지 차이점이 있다.

  1. 컴포저블 함수의 상태 변수는 “기억”되어야 한다. 함수를 호출할 때마다 초기화되는 표준 변수와 달리 상태 변수는 이전에 호출되었을 때의 상태 값을 기억해야한다.
  2. 상태 변수의 변경은 컴포저블 함수 계층 트리 전체에 영향을 미친다.

상태 변수의 값은 MutableState 객체로 감싸야 한다. MutableState 객체는 옵저블 타입이라서 관찰이 가능하다.

var textState by remember { mutableStateOf("") }

상태 호이스팅

@Composable
fun MyTextField() {
    var textState by remember { mutableStateOf("") }

    val onTextChange = { text: String ->
        textState = text
    }

    TextField(
        value = textState,
        onValueChange = { onTextChange(it) }
    )
}

위의 MyTextField 컴포저블은 상태, 값 변경을 위한 람다, TextField 컴포저블을 포함하고 있다. 이 컴포저블은 재사용성이 떨어지는데 TextField를 통해 입력 받은 값을 다른 함수들로 전달할 수 없기 때문이다. 이런 문제를 해결하기 위해 “상태 호이스팅”을 사용한다.

상태 호이스팅을 사용하면 위의 컴포저블을 다음과 같이 정의할 수 있다.

@Composable
 fun DemoScreen() {
    var textState by rememberSaveable { mutableStateOf("") }

    val onTextChange = { text: String ->
        textState = text
    }

    MyTextField(textState = textState, onTextChange = onTextChange)
 }

 @Composable
 fun MyTextField(textState: String, onTextChange: (String) -> Unit) {
    TextField(
        value = textState,
        onValueChange = { onTextChange(it) }
    )
 }

환경 설정 변경을 통한 상태 저장

앱 실행 중 화면이 가로 ↔ 세로 돌아가거나 시스템 전체 폰트 설정 변경 등의 상황이 발생하면 액티비티를 삭제하고 다시 생성해야 한다. 이런 경우 remember 키워드로 저장한 상태들은 다 지워지는데 rememberSaveable 키워드를 사용하면 이를 막을 수 있다.

var textState by rememberSaveable { mutableStateOf("") }

CompositionLocal

위와 같은 계층의 컴포저블이 있을 때 Composable1에 정의되어 있는 상태가 Composable8에서만 필요한 경우가 있다. 그러면 이를 다른 컴포저블들을 통해서 전달해야한다. 이런 번거로움을 해결할 수 있는 것이 CompositionLocal이다. CompositionLocal을 사용하면 가장 높은 노드에 선언되어 있는 상태를 중간 노드를 거치지 않고도 사용할 수 있다.

 

CompositionLocal은 compositionLocalOf 함수 또는 staticCompositionLocalOf 함수를 호출해서 얻을 수 있다. compositionLocalOf 변경이 잦은 상태를 다룰 때, staticCompositionLocalOf 자주 변경되지 않는 상태를 다룰 때 사용하면 된다.

그 다음 CompositionLocalProvider를 사용하여 하위 컴포저블로 전달하면된다.

ex)

val LocalColor = staticCompositionLocalOf { Color(0xFFffdbcf) }

@Composable
fun Composable1() {
    var color = if (isSystemInDarkTheme()) {
        Color(0xFFa08d87)
    } else {
        Color(0xFFffdbcf)
    }
    Column {
        Composable2()

			// 하위 컴포저블에서 배경 색 상태를 사용할 수 있게 CompositionLocal 블록에서 실행
        CompositionLocalProvider(LocalColor provides color) {
            Composable3()
        }
    }
}

...

@Composable
fun Composable8() {
    Text("Composable8", modifier = Modifier.background(LocalColor.current))
}

Slot API

@Composable
fun SlotDemo() {
    Column {
        Text(text = "Top")
        Text(text = "Mid")
        Text(text = "Bottom")
    }
}

위와 같은 컴포저블은 3개의 Text를 하나의 Column으로 묶어서 화면에 보여준다. 근데 만약에 중간에 보여지는 컴포저블을 비워뒀다가 특정 시점에 보여주려면 어떻게 해야 할까?

이때 사용하는 것이 Slot API다.

@Composable
fun SlotDemo(
    midContent: @Composable () -> Unit
) {
    Column {
        Text(text = "Top Content")
        midContent()
        Text(text = "Bottom Content")
    }
}

@Composable
fun SlotButton() {
    Button(onClick = { }) {
        Text("Click Me!!")
    }
}

ComposePracticeTheme {
    SlotDemo(
        midContent = { SlotButton() }
    )
}

midContent 인자가 Slot이고 이를 통해 외부에서 인자로 받은 컴포저블을 화면에 보여줄 수 있다.

다음과 같이 컴포저블을 인자로 전달할 때 상태에 따라 조건문을 거쳐 각각 다른 컴포저블을 전달하게 되면 상태에 따라 다른 컴포저블을 화면에 보여줄 수 있다.

@Composable
fun MainScreen() {
    ...
    ScreenContent(
        linearSelected = linearSelected,
        imageSelected = imageSelected,
        onLinearClick = onLinearClick,
        onImageClick = onImageClick,
        titleContent = {
            if (imageSelected) {
                TitleImage(drawing = R.drawable.baseline_cloud_download_24)
            } else {
                Text(text = "DownLoading", modifier = Modifier.padding(30.dp))
            }
        },
        progressContent = {
            if (linearSelected) {
                LinearProgressIndicator(Modifier.height(40.dp))
            } else {
                CircularProgressIndicator(Modifier.height(200.dp), strokeWidth = 18.dp)
            }
        }
    )
}

@Composable
fun ScreenContent(
    linearSelected: Boolean,
    imageSelected: Boolean,
    onLinearClick: (Boolean) -> Unit,
    onImageClick: (Boolean) -> Unit,
    titleContent: @Composable () -> Unit,
    progressContent: @Composable () -> Unit,
) {
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.SpaceBetween
    ) {
        titleContent()
        progressContent()
        CheckBoxes(
            linearSelected = linearSelected,
            imageSelected = imageSelected,
            onLinearClick = onLinearClick,
            onImageClick = onImageClick
        )
    }
}

위의 코드를 실행하면 각 체크박스의 값을 클릭할 때마다 같은 위치에 뷰가 다른 뷰로 바뀌어 나타나는 것을 확인할 수 있다.

Modifier

모디파이어는 컴포저블에 적용할 수 있는 설정들을 저장한다. ex) 테두리, 패딩, 배경, 크기, 이벤트 핸들러 등…

val modifier = Modifier.border(width = 2.dp, color = Color.Black).padding(10.dp)

위와 같이 만들 수 있다. (border는 테두리, padding은 패딩)

만들어진 모디파이어를 컴포저블에 넣으면 해당 속성이 적용되는 것을 확인할 수 있다.

fun DemoScreen() {
    val modifier = Modifier.border(width = 2.dp, color = Color.Black).padding(10.dp)
    Text(
        text = "Hello Compose",
        modifier = modifier,
        fontSize = 40.sp,
        fontWeight = FontWeight.Bold
    )
}

근데 모디파이어를 생성할 때 연결 순서도 중요하다. 만약 위의 코드의 모디파이어 연결 순서를 반대로 바꾸면 다음과 같은 결과를 확인할 수 있다.

커스텀 컴포저블을 만들 때 모디파이어를 지원하면 해당 컴포저블을 더 다양하게 설정할 수 있다.

커스텀 컴포저블에 모디파이어를 지원하게 할 때 주의해야 할 점은 모디파이어가 없어도 해당 함수가 실행될 수 있게 해야 한다는 점이다. 그래서 선택적인 파라미터로 만들어야 한다.

@Composable
fun customImage(drawing: Int, modifier: Modifier = Modifier) {
    Image(
        painter = painterResource(drawing),
        modifier = modifier,
        contentDescription = null
    )
}

모디파이어를 선택적인 파라미터로 만들기 위해서 파라미터의 기본 값으로 빈 Modifier를 넣어서 구현하였다.

'Android' 카테고리의 다른 글

컴포즈 공부 4일차 (code lab)  (0) 2023.12.01
컴포즈 공부 3일차  (1) 2023.11.28
컴포즈 공부 1일차 (코드 형식 및 연습)  (1) 2023.11.26
RecyclerView 마스터하기  (1) 2023.11.24
Android CountDownTimer  (0) 2023.11.07