Compose - 기초 - 예제 - BMI

황준하·2023년 2월 5일
0

Android-Kotlin-Compose

목록 보기
9/9
전체 메인 코드
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlin.math.pow

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val viewModel = viewModel<BmiViewModel>()

            // 화면 전환을 할 수 있는 객체
            val navController = rememberNavController()

            // viewModel.bmiCalculate(height, weight)을 통해 계산 후에 리컴포즈 되면서 값을 갖게 됨
            val bmi = viewModel.bmi.value

            // 화면 구성
            NavHost(navController = navController, startDestination = "home") {
                composable(route = "home") {
                    HomeScreen() { height, weight ->
                        Log.d("콜백 여기!", "콜백해서 여기로 옴!"+height)
                        viewModel.bmiCalculate(height, weight)
                        navController.navigate("result")
                    }
                }
                composable(route = "result") {
                    ResultScreen(
                        navController,
                        bmi = bmi,
                    )
                }
            }
        }
    }
}

@Composable
fun HomeScreen(onResultClicked: (Double, Double) -> Unit) {
    val (height, setHeight) = rememberSaveable {
        mutableStateOf("")
    }
    val (weight, setWeight) = rememberSaveable {
        mutableStateOf("")
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(text = "비만도 계산기") }
            )
        }
    ) {
        Column(
            modifier = Modifier
                .padding(it)
                .padding(16.dp)
        ) {
            OutlinedTextField(
                value = height,
                onValueChange = setHeight,
                label = { Text(text = "키") },
                modifier = Modifier.fillMaxWidth(),
                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
            )
            OutlinedTextField(
                value = weight,
                onValueChange = setWeight,
                label = { Text(text = "몸무게") },
                modifier = Modifier.fillMaxWidth(),
                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
            )
            Spacer(modifier = Modifier.height(8.dp))
            Button(
                onClick = {
                    if (height.isNotEmpty() && weight.isNotEmpty()) {
                        onResultClicked(height.toDouble(), weight.toDouble())
                    }
                },
                modifier = Modifier.align(Alignment.End)
            ) {
                Text(text = "결과")
            }
        }
    }
}

@Composable
fun ResultScreen(
    navController: NavController,
    bmi: Double,
) {
    val text = when {
        bmi >= 35 -> "고도 비만"
        bmi >= 30 -> "2단계 비만"
        bmi >= 25 -> "1단계 비만"
        bmi >= 23 -> "과체중"
        bmi >= 18.5 -> "정상"
        else -> "저체중"
    }

    val imageRes = when {
        bmi >= 23 -> R.drawable.baseline_sentiment_very_dissatisfied_24
        bmi >= 18.5 -> R.drawable.baseline_sentiment_satisfied_24
        else -> R.drawable.baseline_sentiment_dissatisfied_24
    }

    Scaffold(topBar = {
        TopAppBar(
            title = { Text(text = "비만도 계산기") },
            navigationIcon = {
                Icon(
                    imageVector = Icons.Default.ArrowBack,
                    contentDescription = "home",
                    modifier = Modifier.clickable {
                        navController.popBackStack()
                    }
                )
            }
        )
    }) {
        Column(
            modifier = Modifier
                .padding(it)
                .fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Text(text = text, fontSize = 30.sp)
            Spacer(modifier = Modifier.height(50.dp))
            Image(
                painter = painterResource(id = imageRes),
                contentDescription = null,
                modifier = Modifier.size(100.dp),
                colorFilter = ColorFilter.tint(color = Color.Black),
            )
        }
    }
}

//@Preview
//@Composable
//fun Preview() {
////    ResultScreen(bmi = 35.0)
//}

class BmiViewModel : ViewModel() {
    private val _bmi = mutableStateOf(0.0)
    // State의 값이 변경되었을 때 화면이 다시 그려짐
    val bmi: State<Double> = _bmi   // 외부에서 읽는 곳

    fun bmiCalculate(
        height: Double,
        weight: Double
    ) {
        _bmi.value = weight / (height / 100.0).pow(2.0)
    }
}
Gradle(Module)
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
    
// Navigation
implementation "androidx.navigation:navigation-compose:2.5.3"
drawble

baseline_sentiment_very_dissatisfied_24.xml
baseline_sentiment_satisfied_24.xml
baseline_sentiment_dissatisfied_24.xml

핵심 코드

  • 구조분해
    val (height, setHeight) = rememberSaveable {
        mutableStateOf("")
    }
    val (weight, setWeight) = rememberSaveable {
        mutableStateOf("")
    }
    구조분해를 이용하여 객체가 가지고 있는 여러 값을 분해해서 여러 변수에 한꺼번에 초기화해서 사용한다.
  • AppBar

    topBar = {
                TopAppBar(
                    title = { Text(text = "비만도 계산기") }
                )
            }

    AppBar를 컨트롤 하는 부분이다. title를 지정할 땐 꼭 대괄호로 해주어야된다.

  • 텍스트 입력
    OutlinedTextField(
            value = height,
            onValueChange = setHeight,
            label = { Text(text = "키") },
            modifier = Modifier.fillMaxWidth(),
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
    )
    맨 위의 구조분해로 적어준 것을 적용하여 텍스트 입력을 할 수 있게한다.
    바뀌는 텍스트바다 수신하여 적용시킨다.
    또한, keyboardOptions 부분은 숫자만 입력할 수 있도록 한 곳이다.
  • 버튼
    Button(
        onClick = {
            if (height.isNotEmpty() && weight.isNotEmpty()) {
                 onResultClicked(height.toDouble(), weight.toDouble())
            }
        },
        modifier = Modifier.align(Alignment.End)
    ) {
        Text(text = "결과")
    }
    결과 버튼을 구현한 코드이다.
    onClick 부분에 height와 weight의 텍스트 입력칸이 비어있는지 확인하고 콜백함수로
    HomeScreen(){height, weight->
        viewModel.bmiCalculate(height, weight)
        navController.navigate("result")
    }
    이 부분 작업을 진행하게 된다.
  • ViewModel & State
    class BmiViewModel : ViewModel() {
        private val _bmi =mutableStateOf(0.0)
        // State의 값이 변경되었을 때 화면이 다시 그려짐
        val bmi: State<Double> = _bmi   // 외부에서 읽는 곳
    
        fun bmiCalculate(
            height: Double,
            weight: Double
        ) {
            _bmi.value = weight / (height / 100.0).pow(2.0)
        }
    }
    // viewModel.bmiCalculate(height, weight)을 통해 계산 후에 리컴포즈 되면서 값을 갖게 됨
    val bmi = viewModel.bmi.value
    위 콜백 함수를 통해 bmi 계산 함수(bmiCalculate)가 실행되면 _bmi의 값은 변경되고 변경된 값은 State로 선언해준 bmi에 들어가게 된다.
    변경된 값은 val bmi = viewModel.bmi.value의 bmi로 들어오면서 화면이 다시 그려지면서 UI에 있는 값이 업데이트 된다.
    fun <T> mutableStateOf(
        value: T,
        policy: SnapshotMutationPolicy<T> =structuralEqualityPolicy()
    ): MutableState<T> =createSnapshotMutableState(value, policy)
    mutableStateOf부분을 뜯어보면 value와 policy를 받아 createSnapshotMutableState을 통해SnapshotMutableState를 생성하고 MutableState 인터페이스의 구현체가 된다.
profile
Xlnt한 날까지 노력하는 개발자

0개의 댓글