Group Study (2023-2024)/Android 심화

[Android 심화] 2주차 스터디 - Jetpack Compose 입문(2)

푸리우 2023. 11. 13. 22:42

6. Scaffold, TextField, Button, 구조 분해, SnackBar, 코루틴 스코프

위와 같이 TextField에 문자를 입력한 다음 버튼을 누를 시 snackbar가 뜨는 화면을 만들어 보겠다.

먼저 textField를 만든다.

setContent {
    val textValue=remember{
        mutableStateOf("")
    }

    Column(
        modifier=Modifier.fillMaxSize(),
        verticalArrangement= Arrangement.Center,
        horizontalAlignment= Alignment.CenterHorizontally,
    ){
        TextField(
            value=textValue.value,
            onValueChange={
                textValue.value=it
            },
        )
    }
}

위 코드에서 구조 분해를 이용할 경우 아래와 같이 수정한다.

//수정 전
val textValue=remember{
	mutableStateOf("")
}

TextField(
	value=textValue.value,
	onValueChange={
		textValue.value=it
	},
)

//수정 후
val (text,setValue)=remember{
	mutableStateOf("")
}

TextField(
	value=text,
	onValueChange=setValue
)

다음은 Snackbar를 이용하기 위해 Scaffold와 버튼을 추가한다.

  • Scaffold: 앱의 기본 레이아웃을 정의하고, 앱 바 (AppBar), 하단 내비게이션 바, 드로어 (Drawer), 스낵바 (Snackbar) 및 기타 구성 요소를 관리하기 위한 도구를 제공한다.
  • SnackBar: 사용자에게 간단한 메시지를 표시하거나 사용자 작업을 알리는 데 사용되는 경고 또는 알림 메시지를 나타내는 UI 요소

이때 코루틴을 사용한다.

  • 코루틴 : 코루틴은 비동기적으로 실행되는 코드를 간소화하기 위해 Android에서 사용할 수 있는 동시 실행 설계 패턴
  • 코루틴 스코프: 코루틴의 실행 수명 주기와 범위를 정의하는 데 사용된다.
setContent {
    //구조 분해 사용
    val (text,setValue)=remember{
        mutableStateOf("")
    }

    val scaffoldState = rememberScaffoldState()
    val scope=rememberCoroutineScope() //코루틴 스코프
    val keyboardController=LocalSoftwareKeyboardController.current

    Scaffold(
        scaffoldState = scaffoldState,
    ) {
        Column(
            modifier=Modifier.fillMaxSize(),
            verticalArrangement= Arrangement.Center,
            horizontalAlignment= Alignment.CenterHorizontally,
        ){
            TextField(
                value=text,
                onValueChange=setValue,
            )
            Button(onClick={
                keyboardController?.hide() //키보드 숨기기
                scope.launch{
                    scaffoldState.snackbarHostState.showSnackbar("Hello $text")
                }
            }){
                Text("클릭")
            }
        }
    }
}

7. Navigation

위와 같이 버튼을 눌렀을 때 다른 화면으로 이동하고 이동할 때 이전 화면에 입력한 글자를 다음 화면에 보이도록 만들어보겠다.

우선 navigation을 사용하기 위해서 dependencies에 아래 코드를 추가한다.

def nav_version = "2.5.3"
implementation "androidx.navigation:navigation-compose:$nav_version"

Activity에 아래와 같은 코드를 작성한다.

setContent {
    val navController=rememberNavController()

    NavHost(
        navController=navController,
        startDestination="first",
    ){
        composable("first"){
            FirstScreen(navController)
        }
        composable("second"){
            SecondScreen(navController)
        }
        composable("third/{value}"){ backStackEntry->
            ThirdScreen(
                navController=navController,
                value=backStackEntry.arguments?.getString("value") ?:""
            )
        }
    }
}

이제 각 화면 별 Compose를 만든다. 첫 번째 화면에서 "두 번째" 버튼 클릭 시 다음 화면으로 넘어갈 수 있도록 FirstScreen을 다음과 같이 작성한다.

@Composable
fun FirstScreen(navController: NavController){
    Column(
        //중략
    ){
    	//중략
        Button(onClick={navController.navigate("second")}){Text("두 번째")}
        //중략
    }
}

두 번째 화면에서 "뒤로 가기" 버튼을 누를 때 다시 첫 번째 화면으로 돌아가려면 SecondScreen을 아래와 같은 코드를 작성한다.

@Composable
fun SecondScreen(navController: NavController){
    Column(
        //중략
    ){
        //중략
        Button(onClick={navController.navigateUp()}) {Text("뒤로 가기")}
    }
}

이때 navController.navigateUp() 대신 navController.popBackStack()을 이용해도 된다.

이번에는 첫 번째 화면에서 글자를 입력한 후 "세 번째" 버튼을 클릭 시 세 번째 화면에 글자가 보이도록 하기 위해서 FirstScreen과 ThirdScreen을 다음과 같이 작성한다. 세 번째 화면에서도 "뒤로 가기" 버튼을 누르면 첫 번째 화면으로 돌아가도록 코드를 작성한다.

fun FirstScreen(navController: NavController){
    val(value,setValue)=remember{
        mutableStateOf("")
    }
    Column(
        //중략
    ){
        //중략
        TextField(value=value,onValueChange=setValue)
        Button(onClick={
            if(value.isNotEmpty()){
                navController.navigate("third/$value")
            }
        }){Text("세 번째")}
    }
}
@Composable
fun ThirdScreen(navController: NavController,value:String){
    Column(
        //중략
    ){
        //중략
        Text(value)
        Button(onClick={
            navController.navigateUp()
        }) {Text("뒤로 가기")}
    }
}

전체 코드는 아래와 같다.

class SeventhActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val navController=rememberNavController()

            NavHost(
                navController=navController,
                startDestination="first",
            ){
                composable("first"){
                    FirstScreen(navController)
                }
                composable("second"){
                    SecondScreen(navController)
                }
                composable("third/{value}"){ backStackEntry->
                    ThirdScreen(
                        navController=navController,
                        value=backStackEntry.arguments?.getString("value") ?:""
                    )
                }
            }
        }
    }
}

@Composable
fun FirstScreen(navController: NavController){
    val(value,setValue)=remember{
        mutableStateOf("")
    }
    Column(
        modifier=Modifier.fillMaxSize(),
        verticalArrangement=Arrangement.Center,
        horizontalAlignment= Alignment.CenterHorizontally,
    ){
        Text(text="첫 번째 화면")
        Spacer(modifier=Modifier.height(16.dp))
        Button(onClick={
            navController.navigate("second")
        }){
            Text("두 번째")
        }
        Spacer(modifier=Modifier.height(16.dp))
        TextField(value=value,onValueChange=setValue)
        Button(onClick={
            if(value.isNotEmpty()){
                navController.navigate("third/$value")
            }
        }){
            Text("세 번째")
        }
    }
}

@Composable
fun SecondScreen(navController: NavController){
    Column(
        modifier=Modifier.fillMaxSize(),
        verticalArrangement=Arrangement.Center,
        horizontalAlignment= Alignment.CenterHorizontally,
    ){
        Text(text="두 번째 화면")
        Spacer(modifier=Modifier.height(16.dp))
        Button(onClick={
            navController.navigateUp()
        }) {
            Text("뒤로 가기")
        }
    }
}

@Composable
fun ThirdScreen(navController: NavController,value:String){
    Column(
        modifier=Modifier.fillMaxSize(),
        verticalArrangement=Arrangement.Center,
        horizontalAlignment= Alignment.CenterHorizontally,
    ){
        Text(text="세 번째 화면")
        Spacer(modifier=Modifier.height(16.dp))
        Text(value)
        Button(onClick={
            navController.navigateUp()
        }) {
            Text("뒤로 가기")
        }
    }
}

 

8. ViewModel

ViewModel 클래스는 수명 주기를 고려하여 UI 관련 데이터를 저장하고 관리하도록 설계되었다. ViewModel 클래스를 사용하면 화면 회전과 같이 구성을 변경할 때도 데이터를 유지할 수 있다.

위와 같이 버튼을 클릭했을 때 텍스트가 바뀌는 화면을 만들어보겠다.

setContent {
    val data=mutableStateOf("Hello")

    Column(
        modifier=Modifier.fillMaxSize(),
        verticalArrangement= Arrangement.Center,
        horizontalAlignment= Alignment.CenterHorizontally,
    ){
        Text(
            data.value,
            fontSize=30.sp,
        )
        Button(onClick={
            data.value="World"
        }){
            Text("변경")
        }
    }
}

우선 위와 같이 코드를 만들었을 때 버튼을 클릭했을 때 텍스트 값이 바뀌지 않는다. 왜냐하면 버튼을 클릭 시 리컴포지션이 일어나 data의 value 값이 다시 "Hello"가 되게 하기 때문이다.

이때 remember를 사용한다. remember는 현재 컴포넌트가 다시 그려질 때 상태를 보존하는 방법이다. 이것은 앱이 화면 방향 변경 또는 구성 변경과 같은 이벤트로부터 데이터를 보존할 수 있게 한다. remember 함수를 사용하여 상태를 보존하면 버튼을 눌렀을 때 텍스트가 변경되고 화면이 업데이트된다.

val data=remember{mutableStateOf("Hello")}

이번에는 ViewModel을 사용해서 화면을 만들어 보겠다. 아래와 같이 ViewModel을 상속받은 클래스를 생성한다.

class MainViewModel: ViewModel(){
    val data=mutableStateOf("Hello")
}

ViewModel을 사용할 때는 remember를 사용하지 않아도 된다. 아래 코드를 Activity 안에 추가한다.

private val viewModel by viewModels<MainViewModel>()

그러면 ViewModel을 통해 data 값을 바꿀 수 있게 된다.

Text(
    viewModel.data.value,
    fontSize=30.sp,
)
Button(onClick={
    viewModel.data.value="World"
}){
    Text("변경")
}

Compose 안에서 ViewModel을 사용하려면 dependencies에 아래 코드를 추가해 주고

def lifecycle_version = "2.5.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"

SetContent 안에 아래 코드를 넣어준다.

val viewModel = viewModel<MainViewModel>()

 

추가로 ViewModel을 이용할 때 외부에 데이터를 공개하지 않는다. 따라서 아래 코드처럼 view에서 데이터 값을 조작하지 않는 것이 좋다.

viewModel.data.value="World"

따라서 데이터를 외부에 공개하지 않고 읽기 전용 변수(_data)를 만들어준다.

class MainViewModel: ViewModel(){
    private val _data = mutableStateOf("Hello")
    val data:State<String> = _data
}

데이터 값을 바꾸고 싶다면 class 안에 함수를 따로 만들어준다.

fun changeValue(){
    _data.value="world"
}

따라서 텍스트 값 변경과 관련된 코드를 아래와 같이 바꿔준다.

Button(onClick={
    viewModel.changeValue()
}){Text("변경")}

9. State 심화

State는 시간이 지남에 따라 변할 수 있는 값을 의미한다. 이는 매우 광범위한 정의로서 Room 데이터베이스부터 클래스 변수까지 모든 항목이 포함된다.

mutableStateOf는 관찰 가능한 MutableState<T>를 생성하는데, 이는 런타임 시 Compose에 통합되는 관찰 가능한 유형이다. value가 변경되면 value를 읽는 구성 가능한 함수의 리컴포지션이 예약된다. MutableState 내부 구조는 아래와 같다.

interface MutableState<T> : State<T> {
    override var value: T
    operator fun component1(): T
    operator fun component2(): (T) -> Unit
}

컴포저블에서 MutableState 객체를 선언하는 데는 세 가지 방법이 있다.

//text1은 MutableState<String> 타입
val text1= remember{
    mutableStateOf("Hello world")
}

//text2는 String 타입
var text2 by remember{
    mutableStateOf("Hello World")
}

//text는 String 타입, setText는 (String)->Unit
val(text,setText)=remember{
    mutableStateOf("Hello world")
}

값을 수정할 때는 아래처럼 코드를 작성한다.

text1.value="변경"
text2="변경"
setText("변경")

 

참고자료

https://www.youtube.com/playlist?list=PLxTmPHxRH3VV8lJq8WSlBAhmV52O2Lu7n: 6~9강

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