Group Study (2024-2025)/Android

[Android] Android UI 구현 심화

hgeniee 2024. 10. 16. 16:15

"오준석의 생존코딩"의 JetpackCompose 시리즈 6-9강을 수강하였습니다. 

코드 설명은 코드 블록 내에 주석으로 설명했고 개념적인 부분들은 추가적으로 설명해두었습니다 ☺️

 

6강 (Scaffold, TextField, Button, 구조분해, SnackBar, 코루틴 스코프)

TextField(
	value = "",
    onValueChange = {},
                )

위와 같이 작성하면 텍스트를 입력받아도 value가 빈 문자열로 설정되어있으므로 동적으로 변수값을 지정해줘야함

일반적인 방법

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

        setContent {
            //remember 사용해서 기억할 수 있게끔
            val textValue = remember {
                mutableStateOf("텍스트를 입력하세요")
            }
            Column(
                //화면에 꽉 채움
                modifier = Modifier.fillMaxSize(),
                //가운데 정렬
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                //텍스트 필드에 밸류 값 저장, onvaluechange 내용 변하는 부분 작성
                TextField(
                    //여기 value = "" 이렇게 하면 아무리 키보드 입력해도 value 값이 변하지 않기에 입력 안 됨
                    //동적으로 value값 변화 시켜야 함, value 내부의 값 변수 같은 형태로
                    value = textValue.value,
                    onValueChange = {
                        //it은 사용자가 입력한 새로운 텍스트
                        textValue.value = it
                    },
                )
                //onclick으로 구현
                Button(onClick = {}) {
                    //버튼 안 글자 작성
                    Text("클릭!")
                }
            }
        }
    }
}

@Composable
fun HomeScreen(viewModel: MainViewModel = viewModel()) {
    var (text, setText) = remember {
        mutableStateOf("Hello World")
    }
    Column() {
        Text("Hello World")
        Button(onClick = { }) {
            Text("클릭")
        }
        //텍스트 지우면 onvaluechange 변경하면서 계속 setText호출
        TextField(value = text, onValueChange = setText)
    }
    Text("Hello World")
}

class MainViewModel: ViewModel() {
    //mutablestateof 쓰기 읽기 가능
    //state 읽기만 가능
    private val _value: MutableState<String> = mutableStateOf("Hello World")
    val value: State<String> = _value
}

 

코틀린의 구조분해 기법을 활용한 방법



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

        setContent {
            //remember 사용해서 기억할 수 있게끔
            val (text, setValue) = remember {
                mutableStateOf("")
            }

            //rememberScaffoldState -> 구버전
            val snackbarHostState = remember { SnackbarHostState() }
            val scope = rememberCoroutineScope()

            Scaffold(
                snackbarHost = { SnackbarHost(snackbarHostState) }
            ) { paddingValues ->
                Column(
                    //화면에 꽉 채움
                    modifier = Modifier.fillMaxSize()
                        .padding(paddingValues),
                    //가운데 정렬
                    verticalArrangement = Arrangement.Center,
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    //텍스트 필드에 밸류 값 저장, onvaluechange 내용 변하는 부분 작성
                    TextField(
                        //여기 value = "" 이렇게 하면 아무리 키보드 입력해도 value 값이 변하지 않기에 입력 안 됨
                        //동적으로 value값 변화 시켜야 함, value 내부의 값 변수 같은 형태로
                        value = text,
                        onValueChange = setValue,
                    )
                    //onclick으로 구현
                    Button(onClick = {
                        scope.launch {
                            snackbarHostState.showSnackbar("Hello $text")
                        }
                    }) {
                        //버튼 안 글자 작성
                        Text("클릭!")
                    }
                }

            }


        }
    }
}

@Composable
fun HomeScreen(viewModel: MainViewModel = viewModel()) {
    var (text, setText) = remember {
        mutableStateOf("Hello World")
    }
    Column() {
        Text("Hello World")
        Button(onClick = { }) {
            Text("클릭")
        }
        //텍스트 지우면 onvaluechange 변경하면서 계속 setText호출
        TextField(value = text, onValueChange = setText)
    }
    Text("Hello World")
}

class MainViewModel: ViewModel() {
    //mutablestateof 쓰기 읽기 가능
    //state 읽기만 가능
    private val _value: MutableState<String> = mutableStateOf("Hello World")
    val value: State<String> = _value
}

강의영상이 구버전을 사용해서 변경 된 부분들이 있습니다 

 

6강 예제 코드 실행 영상

 

 

Scaffold란?

화면 레이아웃을 구성할 때 자주 사용하는 기본적인 UI 구조

  • Top bar
  • Bottom bar
  • Content
  • Snack bar 

다양한 UI 요소 존재

Scaffold에서 Snack bar 사용하기

val snackbarHostState = remember { SnackbarHostState() } : 스낵바의 상태 기억 및 초기화

Scaffold( snackbarHost = { SnackbarHost(snackbarHostState) } )

SnackbarHost:

  • SnackbarHost는 스낵바를 표시하는 장소
  • 내부적으로 스낵바를 관리하고, 상태에 따라 스낵바를 화면에 표시
  • SnackbarHost는 snackbarHostState를 통해 스낵바의 상태를 받아옴 

snackbarHostState:

  • snackbarHostState는 스낵바의 현재 상태를 나타내는 객체
  • 스낵바가 표시될 때 snackbarHostState를 사용하여 표시할 메시지를 업데이트할 수 있음

 

7강 (Navigation)

Gradle에 다음 dependency 추가

implementation ("androidx.navigation:navigation-compose:2.7.1")

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

            NavHost(
                navController = navController,
                startDestination = "first",
            ) {
                //화면을 표시할 내용들을 composable 함수로 감싸주고 이름 지정해줌
                //startDestination = "first"라고 지정했기에 첫화면이 가장 먼저 뜸
                composable("first") {
                    FirstScreen(navController)
                }
                composable("second") {
                    SecondScreen(navController)
                }
                //넘겨받을 값 중괄호로, backStackEntry로 넘어오는 값 객체로
                composable("third/{value}") { backStackEntry ->
                    ThirdScreen(
                        navController = navController,
                        //backstackentry로 넘어옴
                        value = backStackEntry.arguments?.getString("value") ?: "",
                    )
                }
            }
        }
    }
}

@Composable
//navController 사용해서 이동 용이
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 = {
            //value 비어 있으면 이동하지 않게 처리
            if (value.isNotEmpty()) {
                navController.navigate("third/$value") //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))
        //value 받아오기
        Text(value)
        Button(onClick = {
            navController.navigateUp()
        }) {
            Text("뒤로 가기")
        }
    }
}

 

 

7강 예제 코드 실행 영상

Navigation?

화면 간의 전환을 쉽게 관리할 수 있도록 도와주는 라이브러리

NavController

val navController = rememberNavController()

  • NavController는 화면 간의 이동을 관리하는 역할
  • rememberNavController()를 호출하여 NavController 인스턴스를 생성, 이를 사용하여 다른 화면으로의 이동을 제어

NavHost

NavHost( navController = navController, startDestination = "first" ) 

  • 각 화면의 경로(이름)와 해당 경로에 대한 Composable 함수를 연결

BackStackEntry

composable("third/{value}") { backStackEntry ->
    ThirdScreen(
        navController = navController,
        value = backStackEntry.arguments?.getString("value") ?: "",
    )
}

  • 네비게이션 스택에서 현재 위치한 화면에 대한 정보를 담고 있는 객체
  • 인자로 받은 값을 꺼낼 수 있음

 

8강(ViewModel)

 

compose 내에서 ViewModel 사용하는 법(Gradle에 dependency 추가 x)


class MainActivity : ComponentActivity() {
    //생성된 viewmodel 계속해서 재사용
    private val viewModel by viewModels<MainViewModel>()

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

        setContent {
            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                Text(
                    //이 부분이 변경되어야하니깐 state로 만들어줘야함
                    viewModel.data.value,
                    fontSize = 30.sp
                )
                Button(onClick = {
                    viewModel.data.value = "World"
                }) {
                    Text("변경")
                }
            }
        }
    }
}

//viewmodel: activity, life cycle 동시에 가져가므로 remember 같은거 신경 쓰지 않아도 됨
class MainViewModel : ViewModel() {
    val data = mutableStateOf("Hello")
}

 

Gradle에 dependency 추가한 경우

implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6")

class MainActivity : ComponentActivity() {
    //생성된 viewmodel 계속해서 재사용

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

        setContent {
            val viewModel = viewModel<MainViewModel>()
            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                Text(
                    //이 부분이 변경되어야하니깐 state로 만들어줘야함
                    viewModel.data.value,
                    fontSize = 30.sp
                )
                Button(onClick = {
                    viewModel.changeValue()
                }) {
                    Text("변경")
                }
            }
        }
    }
}

//viewmodel: activity, life cycle 동시에 가져가므로 remember 같은거 신경 쓰지 않아도 됨
class MainViewModel : ViewModel() {
    //외부에서 접근 못하도록 private 사용
    //data로 변수 설정하면 private public 변수명 같아서 오류 발생해서 _data로 변경
    private val _data = mutableStateOf("Hello")
    //읽기 전용으로 state type로 공개
    val data: State<String> = _data

    //얘를 통해서만 수정 가능하도록
    fun changeValue() {
        _data.value = "World"
    }
}

 

8강 예제 코드 실행 영상

ViewModel?

Android에서 UI 관련 데이터를 저장하고 관리하는 클래스

ViewModel 인스턴스 생성

viewModel<MainViewModel>()

  • MainActivity의 생명 주기와 연결되어, Activity가 파괴되더라도 데이터가 유지
  • viewModel함수는 내부적으로 ViewModel을 관리, 재사용성을 보장

 

9강 (State 심화)

Gradle에 다음 dependency 추가

implementation("androidx.compose.runtime:runtime-livedata:1.7.3")

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

        setContent {
            HomeScreen()
        }
    }
}

//컴포저블 사용해서 화면 표시
//내용 변경 일어나면 state 활용 , state 매우 중요
//compose는 state 기반으로 동작함
@Composable
fun HomeScreen(viewModel: MainViewModel = viewModel()) {
    val text1: MutableState<String> = remember {
        mutableStateOf("Hello World")
    }

    //by 사용하면 텍스트가 스트링이 됨, 텍스트2의 값을 변경할 때
    //by setter getter 활용
    var text2: String by remember {
        mutableStateOf("Hello World")
    }

    //mutablestate 안에 T 제네릭 받아서 리턴, 그리고 unit 리턴
    //안에 있는 값을 텍스트로 취하고 세터 역할은 ..
    //여기 있는 내용 수정하려면 setter를 수정해야됨
    val (text: String, setText: (String) -> Unit) = remember {
        mutableStateOf("Hello World")
    }

    //타입이 State
    val text3: State<String> = viewModel.liveData.observeAsState("Hello")

    //글자 입력할 때마다 recomposition 일어나는 중
    Column {
        Text("Hello world")
        Button(onClick = {
            text1.value = "변경"
            print(text1.value)
            text2 = "변경"
            print(text2)
            setText("변경")
            //viewModel.value.value = "변경" -> 불가 읽기 전용이므로
            viewModel.changeValue("변경")
        }) {
            Text("클릭")
        }
        TextField(value = text, onValueChange = setText)
    }
}

class MainViewModel: ViewModel() {
    //mutablestateof 쓰기 읽기 가능
    //state 읽기만 가능
    private val _value: MutableState<String> = mutableStateOf("Hello World")
    val value: State<String> = _value

    //gradle에 livedata 위한 디펜던시 넣어줘야

    private val _liveData = MutableLiveData<String>()
    val liveData: LiveData<String> = _liveData

    fun changeValue(value: String) {
        _value.value = value
    }
}

state의 개념을 알아보기 위한 강의이므로 영상은 따로 첨부하지 않았습니다

 

state?

상태는 UI의 현재 상태를 나타내는 변수

Compose는 상태 기반 UI로 작동하므로, 상태가 변경될 때 UI가 자동으로 업데이트됨

MutableState 활용

val text1: MutableState<String> = remember {
    mutableStateOf("Hello World")
}

  • text1은 MutableState를 사용하여 정의된 상태 변수, "Hello World"로 초기화
  • remember를 사용하여 상태 저장, 구성 요소가 다시 구성될 때 동일한 값을 유지

Delegated property 사용

var text2: String by remember {
    mutableStateOf("Hello World")
}

  • text2는 by 키워드를 사용하여 MutableState의 getter와 setter를 델리게이트
  • text2의 값을 변경할 때 내부적으로 mutableStateOf의 값을 수정

Livedata와 viewmodel 사용

val text3: State<String> = viewModel.liveData.observeAsState("Hello")

  • text3는 ViewModel에서 제공하는 LiveData를 관찰하는 상태
  • observeAsState를 사용하여 UI가 LiveData의 값이 변경될 때 자동으로 업데이트되도록, 초기값: "Hello"

 

 참고자료: https://www.youtube.com/watch?v=xszyeIWFsGc&list=PLxTmPHxRH3VV8lJq8WSlBAhmV52O2Lu7n&pp=iAQB