[Android] Jetpack Compose - 상태 호이스팅(State Hoisting)

알린·2024년 2월 23일
0

Android

목록 보기
9/21

Compose의 상태

  • 선언형 UI 프레임워크인 Compose는 stateless함이 가장 큰 장점

    💡 Compose Stateless의 장점

    1. UI 재사용성
    2. UI 테스트 가능성 있음

    💡 Compose를 Stateful할 때는?

    1. 재사용성은 줄어듦
    2. 테스트 가능성 사라짐
  • 위와 같은 한계점을 극복하기 위해 State Hoisting 사용

상태 호이스팅(State Hoisting)

  • Stateful한 컴포저블을 Stateless 하도록 만들기 위한 디자인 패턴
  • 자식 컴포저블의 state를 해당 컴포저블을 호출하는 컴포저블 쪽으로 끌어올림으로써 자식 컴포저블을 stateless하게 만드는 것
    즉, 자식 컴포저블의 state를 호출부로 끌어올리는 것
  • 여러 함수가 읽거나 수정하는 상태가 공통의 상위 항목에 위치하는 프로세스

사용 방법

  • State를 다음과 같은 두 변수로 나누는 방식으로 진행
    • value: T: 값
    • onValueChangd: (T) -> Unit: 새 값이 들어왔을 때 값을 변경하도록 요청하는 함수(이벤트)

사용되는 곳

  • 내부에서 상태를 저장해야 할 때

구현

MainActivity에 다음 코드 추가

@Composable
fun OnboardingScreen(modifier: Modifier = Modifier) {
    // by : = 대신 사용, 매번 .value를 입력할 필요가 없도록 해주는 속성
    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = { shouldShowOnboarding = false }
        ) {
            Text("Continue")
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    JetpackCompose_StudyTheme {
        OnboardingScreen()
    }
}

여기서 OnboardingScreen에서 만든 상태를 MyApp 컴포저블과 공유해야 상태에 엑세스 할 수 있다.
이는 상태 값에 액세스해야 하는 공통 상위 요소로 상태 값을 이동하는 것이다.

먼저 MyApp의 콘텐츠를 Greetings라는 새 컴포저블을 생성해 이동한다.
미리보기에서도 Greetings 메서드를 호출하도록 수정한다.

@Composable
fun MyApp(modifier: Modifier = Modifier) {
     Greetings()
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingsPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

최상위 MyApp Composable Preview를 추가

@Preview
@Composable
fun MyAppPreview() {
    JetpackCompose_StudyTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

MyApp에 OnboardingScreen외에도 다른 화면을 보여주는 로직 추가 구현한다.

  • 이벤트는 사용자가 버튼을 클릭했을 때 콜백(다른 함수에 인수로 전달되는 함수)을 실행해 전달한다.
  • MyApp 컴포저블에서 매번 값을 사용하지 않도록 by 속성 위임을 사용한댜.
@Composable
fun MyApp(
    modifier: Modifier = Modifier,  // 빈 수정자가 할당되는 수정자 매개변수를 포함
) {
    // by : = 대신 사용, 매번 .value를 입력할 필요가 없도록 해주는 속성
    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {  // onBoarding이 보여질 때 (true일 때)
            // OnboardingScreen 실행해 Button이 클릭되면 false로 바꿔주기
            OnboardingScreen(onContinueClicked = {shouldShowOnboarding = false})  
        } else {  // onBoarding이 보여지지 않을 때 (false일 때)
            Greetings()  // 다른 화면 출력
        }
    }
}

@Composable
fun OnboardingScreen(
    modifier: Modifier = Modifier,
    onContinueClicked: () -> Unit
) {
    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    JetpackCompose_StudyTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

위와 같은 방식은 상태가 아닌 함수를 OnboardingScreen에 전달하는 방식이다.
장점은 다음과 같다.

  1. 컴포저블의 재사용에 용이
  2. 다른 컴포저블이 상태를 변경하지 않도록 보호
  3. 작업을 간단하게 유지

동작 화면

전체 코드

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.jetpackcompose_study.ui.theme.JetpackCompose_StudyTheme
import androidx.compose.runtime.remember
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material3.Button
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment


class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {  // 레이아웃 정의
            JetpackCompose_StudyTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(
    modifier: Modifier = Modifier,  // 빈 수정자가 할당되는 수정자 매개변수를 포함
) {
    // by : = 대신 사용, 매번 .value를 입력할 필요가 없도록 해주는 속성
    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {  // onBoarding이 보여질 때 (true일 때)
            // OnboardingScreen 실행해 Button이 클릭되면 false로 바꿔주기
            OnboardingScreen(onContinueClicked = {shouldShowOnboarding = false})
        } else {  // onBoarding이 보여지지 않을 때 (false일 때)
            Greetings()  // 다른 화면 출력
        }
    }
}

@Composable
fun OnboardingScreen(
    modifier: Modifier = Modifier,
    onContinueClicked: () -> Unit
) {
    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

// 구성 가능한 함수: 함수가 내부에서 다른 @Composable 함수 호출 가능
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
     var expanded by remember { mutableStateOf(false) }
    val extraPadding = if (expanded) 48.dp else 0.dp
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(
                modifier = Modifier
                    .weight(1f)
                    .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded },
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

@Preview
@Composable
fun MyAppPreview() {
    JetpackCompose_StudyTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    JetpackCompose_StudyTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    JetpackCompose_StudyTheme {
        Greetings()
    }
}
profile
Android 짱이 되고싶은 개발 기록 (+ ios도 조금씩,,👩🏻‍💻)

0개의 댓글