[Compose] BMI 계산기

KSang·2024년 4월 15일
0

TIL

목록 보기
83/101
post-thumbnail

compose를 이용해서 간단한 BMI 계산기를 만들어 보자

단순 구현보다 좀더 현업에서 쓰는 코드에 가깝게 만들어 보고자 노력했다.

우선 화면은 입력 화면, 결과 화면으로 구성된다.

우선 메인 액티비티에선 두 화면을 navigate할 컴포저블로 이동하자

class MainActivity : ComponentActivity() {

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

        setContent {
            BMICalculatorTheme {
                BMIApp()
            }
        }
    }
}

기존엔 MainActivity가 있는 파일에 컴포저블도 전부 쓰고 MainActivity안에 전부 선언 했는데, 그렇게 안 쓰인다고 했다.

MainAct에선 밖으로 뺀 컴포저블을 쓰고 enableEdgeToEdge 같은 스타일 설정이나

			val windowSizeClass = calculateWindowSizeClass(this) // 디바이스 크기별
            val displayFeatures = calculateDisplayFeatures(this)

디바이스 크기 별로 ui를 구성하게 도와주는 이런 것들을 선언하고 컴포저블로 이동하는게 보통 인 것 같다.

그래서 나도 입력화면과 결과화면을 navigate할 컴포저블을 만들었다.

@Composable
fun BMIApp(
    navController: NavHostController = rememberNavController(),
) {
    NavHost(
        navController = navController,
        startDestination = Screen.Main.route,
    ){
        composable(Screen.Main.route){
            Surface(
                modifier = Modifier.fillMaxSize(),
                color = MaterialTheme.colorScheme.background
            ) {
                BMIMain{ data ->
                    navController.navigate(Screen.Result.createRoute(data))
                }
            }
        }

        composable(Screen.Result.route) { backStackEntry ->
            BMIResult(
                backStackEntry = backStackEntry
            ) {
                navController.navigate(Screen.Main.route)
            }
        }
    }
}

BMIApp에서 내비게이션을 설정하고, 입력창(Main)과 결과창을 조절 한다.

sealed Class를 이용해서 각 스크린명을 저장해 뒀다.

sealed class Screen(val route: String) {
    object Main: Screen("main")
    object Result: Screen("result/{result}") {
        fun createRoute(result: String) = "result/$result"
    }
}

createRoute를 작성해, 번들로 값을 보내 주는 함수 또한 설정해 준다.

@Composable
fun BMIMain(
    viewModel: BMIViewModel = viewModel(),
    navigateToResult: (String) -> Unit,
) {
    val (height, setHeight) = remember { mutableStateOf("") }
    val (weight, setWeight) = remember { mutableStateOf("") }

    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text(
            text = "BMI 계산기",
            fontSize = 40.sp,
            fontWeight = FontWeight.Bold,
            color = Color.Blue
        )
        Spacer(modifier = Modifier.height(20.dp))

        InPutField(label = "신장", value = height, onValueChange = setHeight, unit = "cm")
        Spacer(modifier = Modifier.height(20.dp))

        InPutField(label = "체중", value = weight, onValueChange = setWeight, unit = "kg")
        Spacer(modifier = Modifier.height(56.dp))

        Button(
            onClick = {
                viewModel.setBMI(height.toDouble(), weight.toDouble())
            }
        ) {
            Text(text = "확인하러가기")
        }
    }

    LaunchedEffect(viewModel.onClickEvent) {
        viewModel.onClickEvent.collect {
            navigateToResult(it)
        }
    }
}

@Composable
fun InPutField(
    label: String,
    value: String,
    onValueChange: (String) -> Unit,
    unit: String,
){
    Row {
        Text(
            text = label,
            modifier = Modifier.align(Alignment.CenterVertically)
        )
        Spacer(modifier = Modifier.width(16.dp))
        TextField(
            value = value,
            onValueChange = onValueChange,
            placeholder = { Text(text = "${label}을 입력 하세요") },
            singleLine = true,
            modifier = Modifier.weight(1f),
            keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number)
        )
        Spacer(modifier = Modifier.width(16.dp))
        Text(
            text = unit,
            modifier = Modifier.align(Alignment.CenterVertically)
        )
    }
}

입력 창부분이다.

Column과 Row로 구성되었다.

위 이미지를 보면 신장과, 체중을 입력하는 부분이 유사하게 생겨서 따로 함수로 빼내서 구현 했다.

여기서 버튼을 눌렀을때를 보면 뷰모델에서 로직을 처리하는데, 이를 어떻게 관찰하고 결과화면으로 넘어가야할지 고민을 많이 했다.

Compose에선 LaunchedEffect를 사용해서 데이터의 변화를 관찰 할 수 있었다.

버튼을 눌러 BMI를 계산하고 LaunchedEffect로 관찰한 뒤, 람다로 적은navigateToResult를 통해 결과화면으로 이동해준다.

BMIApp.kt

		BMIMain{ data ->
                    navController.navigate(Screen.Result.createRoute(data))
                }

BMIResult.kt

@Composable
fun BMIResult(
    backStackEntry: NavBackStackEntry,
    viewModel: ResultViewModel = viewModel(),
    onBack: () -> Unit,
) {
    viewModel.setUi(backStackEntry.arguments?.getString("result")?.toDouble()?: return)
    val state by viewModel.uiState.collectAsState()
    Column(
        modifier = Modifier
            .padding(16.dp)
            .fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text(
            text = state?.title?: return@Column,
            fontSize = 32.sp,
            color = Color.Blue,
            fontWeight = FontWeight.Bold,
            textAlign = TextAlign.Center
        )

        Spacer(modifier = Modifier.height(32.dp))
        Text(
            text = state?.result ?: "",
            fontSize = 24.sp,
            fontWeight = FontWeight.Bold,
            textAlign = TextAlign.Center,
        )

        Spacer(modifier = Modifier.height(32.dp))
        Text(
            text = state?.info?: "",
            fontSize = 24.sp,
            fontWeight = FontWeight.Bold,
        )

        Spacer(modifier = Modifier.height(56.dp))
        Button(onClick = { onBack() }) {
            Text(text = state?.buttonTitle ?: return@Button)
        }
    }
}

결과 화면에선 state로 ui를 구성한 걸 볼 수 있는데,

이는 뷰모델의 state다.

collectAsState를 통해, 뷰모델에 있는 Flow의 상태를 가져와 화면을 구성했다.

컴포즈를 이용해 프로젝트를 진행할때 코드를 어떻게 정리할까 고민을 많이 한 시간이었던 것 같다.

운좋게, 좋은 자료를 발견하게 되어서 참고해 아직 멀었지만 괜찮게 정리 된 것 같다.

참고하던 자료가 Redux 스타일을 따른다고 했는데, 이게 정확히 뭔진 아직 모르겠다.

다음에 제대로 공부하고 왜 이 스타일을 쓰는지 알아봐야겠다.

0개의 댓글