- Today
- Total
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- jlpt
- Android
- posting
- blog
- 일본어문법
- 코틀린
- 학습지
- github
- ai
- 일본어기초
- 진짜학습지
- n3문법
- 안드로이드
- PR
- 책추천
- coroutine
- Kotlin
- errorhandling
- rxjava
- webflux
- 책리뷰
- 인공지능
- androidstudio
- suspend
- 진짜학습지후기
- KotlinInAction
- 진짜일본어
- pullrequest
- GIT
- CustomTab
코딩하는 개굴이
[Android] Jetpack Compose 한 입 찍먹하기(List/Navigation/Dialog) 본문
Jetpack Compose 는 간단한 코드와 직관적인 API, 빠른 개발, 강력한 성능이라는 장점을 내세운 A
ndroid 의 네이티브 UI 빌드 도구 키트이다. 근래에서는 Wear OS 또한 전용 Compose를 전면으로 내세우고 있기 때문에
Google 의 Compose 사랑이 어느정도인지 감이 잡힐 것이다. (아, 이건 각이구나)
Airbnb, twitter, zoom, grab, dropbox 등 compose 통합을 이룬 앱들이 많아지고 있는 추세이다.
대체 어떤 것이기에 이렇게 좋다고 하는가? 한번 찍먹 해보도록 하자.
(코드랩을 통한 찍먹 또한 가능하다. 5 과정을 거쳐 잘 구성되어있으니 살펴보아도 좋을 것 같다.)
Compose 의 이해
지금까지 Android 의 뷰 계층 구조는 위젯 트리와 같아서 사용자의 상호작용 등으로 앱의 상태가 변경될 경우 변경된 데이터를 표시하기 위해 UI 계층 구조를 업데이트해야했다. UI 를 업데이트하는 가장 일반적인 방법으로는 findViewById 와 같은 함수들을 사용해 트리를 탐색하고 찾은 뷰에 대해 setText 등으로 위젯의 내부 상태를 변경하는 것이 있었다.
그러나, 이렇게 뷰를 수동으로 조작할 경우 오류가 발생할 가능성이 커지는데, 데이터를 여러 위치에서 렌더링한다면, 데이터를 표시하는 뷰의 업데이트를 누락하거나 충돌할 수 있다. 따라서, 이러한 단점을 해결하기 위해 선언형 UI 모델의 전환이 시작되었는데, 이는 처음부터 화면 전체를 개념적으로 재생성한 뒤 필요한 변경사항들만 적용하는 방식을 기본으로한다.
화면 전체를 재생성하는 데 있어 시간, 컴퓨팅 성능 및 배터리 사용량 측면이 우려될 수 있다. 그러나, Compose 는 특정 시점에 UI 의 어떤 부분을 다시 그려야하는지를 지능적으로 선택함으로써 이 비용을 줄였다.
간단하게 함수를 구성하기
그러면 어떻게 컴포즈를 사용할 수 있을까?
예를들어 로직을 통해 데이터를 받아 UI 요소를 내보내는 composable 함수를 정의한다고 가정하자.
@Composable
fun Greeting(name: String) {
Text("Hello $name")
}
끝이다.
본래 viewmodel 을 통해 값을 xml 에서 지정한다거나, activity 등에서 text 의 id 를 찾아 name 을 이용한 세팅이 필요한 작업이지만 몇 줄로 끝나버렸다. (wow)
간단하게 보이는 이 몇 줄 내에는 꽤나 많은 의미가 내포되어있는데, 하나하나 알아보도록하자.
- 모든 Composable 함수에는
@Composable
annotation 이 있어야하며 이는 함수가 데이터를 UI 로 변환하기 위한 함수라는 것을 Compose 컴파일러에게 알리기 위함이다. - 함수는
name
과 같은 데이터들을 받는데, composable 함수는 매개변수를 받을 수 있으며, 이를 통해 앱 로직이 UI 를 형성할 수 있다. - 위 함수는 UI 에 Text 를 표시하기 위해 Text() 라는 composable 함수를 호출한다. composable 함수는 다른 composable 함수를 호출해 UI 계층 구조를 구성한다.
- 함수는 아무것도 반환하지 않는다. UI 를 내보내는 Composable 함수는 UI 위젯을 구성하는 대신 화면 상태를 설명하고 있으므로, 반환의 필요가 없다.
- 함수는 동일한 인수로 여러번 호출 될 때 동일한 방식으로 동작하며 전역변수 혹은 random 호출과 같은 다른 값을 사용하지 않는다.
자, 여기까지 이해했다면 찍먹하는데 필요한 내용들은 어느정도 준비 된 것 같다.
개발자라면 글보다는 실제로 해보면서 와닿는 법, 국룰의 기본인 TextView / EditText / Button / ListView 등을 활용한 예시를 만들어보면서 더 파악해보도록 하자.
기본 반찬들 만들어보기 (tv, et, btn, list…)
김밥천국이나 분식집 혹은 어떤 식당에 가도 한국이라면 기본 반찬이 나오듯, 안드로이드 책을 펼치면 제일 처음 나오는 기본 반찬과 같은 아이들을 먼저 구성해보도록 하자.
위 화면과 같이 TextView 를 상단에, id/pwd 를 받는 edit text, checkbox 그리고 버튼 하나. 골고루 한번 맛보아보자.
New Project 를 누르면 구글의 사랑을 받는 Compose 답게 제일 상단에 올라와 있는 Compose 용 Empty Activity 를 생성해준다.
그러면 언제나 그랬듯 컴퓨터 팬이 쏴라락 돌아가면서 열심히 만드는데,
그동안 잠시 어떤게 기본으로 생성되었는지 살펴보자.
(Screen 들은 추후에 생성한 것이다.) Color/Theme/Type 가 ui.theme 아래에 들어있는 것을 볼 수 있는데, 들어가본다면 이는 Color/Type 이 Theme 에서 쓰이는 것을 볼 수 있다. 그리고 Theme 은 ClassmateTheme (project 명 + Theme) 이라는 Composable 로, MainActivity 에서 Surface 의 상위 Composable 로 사용되고 있는 것을 볼 수 있을 것이다.
이는, 테마 설정을 구현하는 Composable 인 MaterialTheme 을 래핑한 Composable 함수를 자동적으로 생성해준 것이며 이 안의 모든 컴포넌트들에 대한 색상, 유형 및 모양에 대한 커스터마이징을 용이하게 만들어준다.
MainActivity
ClassmateTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
BaseLayout() // Layout 을 구성하는 Composable
}
}
아까의 그 화면을 만들기 위해 BaseLayout 이라는 Composable 을 하나 생성해보도록 하자.
xml 도 특정 부모를 두고 배치를 했다면 Compose 도 마찬가지이다. Column, Row 를 이용할 수 있고 각 composable 들은 modifier 등을 두어 너비/높이를 조절, 테두리를 만드는 것과 같이 구성 요소들을 꾸미 거나 행동들을 추가할 수 있다. 이를 염두 해두고 생성한 BaseLayout 을 살펴보자.
@Composable
fun BaseLayout(navController: NavController) {
Column(modifier = Modifier.padding(16.dp)){
Text(
text = "CLASS MATE",
fontSize = 40.sp,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(8.dp))
InputBox("ID : ", false)
InputBox("PWD : ", true)
Spacer(modifier = Modifier.height(24.dp))
CheckBoxGroupForThree() // 다음 단계에서 자세히 살펴볼 것이다 :)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
){
Button(
onClick ={},
modifier = Modifier
.wrapContentSize()
.padding(top = 16.dp)
){
Text(text = "LOGIN")
}
}
}
}
- modifier 를 이용해 약 16dp 를 주거나, match parent 와 유사한 fillMaxWidth 를 설정하거나, height 를 조절하기도 한다.
- 간격을 주기 위해 Spacer 라는 Composable 을 사용했다.
- InputBox 는 EditText 의 역할을 하는 composable 로, pwd 의 *** 표시가 되는 입력을 위해 isPassword 설정 값을 true 로 설정하고 tit 로 좌측의 텍스트를 지정했다.
- Button 은 파라미터로 onClick 을 지정할 수 있으며, 하위에 Text Composable 을 지니도록 구성했다.
익숙하지 않지만 직관적이기에 이해에 무리가 없을 것으로 생각이 되지만, @Preview
를 넣은 Composable 을 하나 추가하여, BaseLayout 을 호출하도록하면, 실시간으로 바뀌는 UI 를 볼 수 있기 때문에 이해하기 더 용이할 것이다.
@Preview
@Composable
fun PreviewMethod() {
ClassmateTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
BaseLayout()
}
}
}
값을 받는 Composable 함수 생성하기
위의 코드에서 CheckBoxGroupForThree
라는 함수가 있었을텐데, 이는 체크박스 그룹에 해당하는 함수이다.
지금 만드는 구성에서 우리는 checkbox 에서 입력을 받아 login 을 누르면 다른 화면으로 이동해 눌렀던 값의 리스트를 보여주는 구현을 만들 것이기 때문에 이 함수가 부모에게 입력 받은 값을 알릴 수 있도록 구성할 것이다.
그러기 위해서 우선 BaseLayout 상단에 composable 이 넘겨준 값을 받아 버튼 onclick 에 넘겨줄 수 있도록 Map 을 사용해보자.
@Composable
fun BaseLayout(navController: NavController?) {
val CLASS_A = "ClassA"
val CLASS_B = "ClassB"
val CLASS_C = "ClassC"
val classStateMap: MutableMap<String, Boolean> = mutableMapOf()
classStateMap[CLASS_A] = false
classStateMap[CLASS_B] = false
classStateMap[CLASS_C] = false
...
그리고, checkboxForThree 는 Select all class 를 선택하면 모든 checkbox 를 선택하고 해제하면 모든 것을 해제할 수 있도록 onClick 에 설정하고 변경된 check 값을 상위 composable 에게 알릴 수 있도록 stateChanged 를 호출하게 구성한다.
@Composable
fun CheckBoxGroupForThree(
firstItem: String,
secondItem: String,
thirdItem: String,
stateChanged: (Boolean, Boolean, Boolean) -> Unit
) {
val (isFirstItemSelected, selectFirstItem) = remember{mutableStateOf(false)}
val (isSecondItemSelected, selectSecondItem) = remember{mutableStateOf(false)}
val (isThirdItemSelected, selectThirdItem) = remember{mutableStateOf(false)}
val allSelected = (isFirstItemSelected && isSecondItemSelected && isThirdItemSelected)
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier
.border(
width = 2.dp,
color = colorResource(id = R.color.purple_500),
shape =RoundedCornerShape(3.dp)
)
){
CheckBoxRow(text = "Select all class", checkedState = allSelected, onClick ={
if (allSelected) {
selectFirstItem(false)
selectSecondItem(false)
selectThirdItem(false)
stateChanged(false, false, false)
} else {
selectFirstItem(true)
selectSecondItem(true)
selectThirdItem(true)
stateChanged(true, true, true)
}
})
CheckBoxRow(
text = firstItem,
checkedState = isFirstItemSelected,
onClick ={
stateChanged(!isFirstItemSelected, isSecondItemSelected, isThirdItemSelected)
selectFirstItem(!isFirstItemSelected)
})
CheckBoxRow(
text = secondItem,
checkedState = isSecondItemSelected,
onClick ={
stateChanged(isFirstItemSelected, !isSecondItemSelected, isThirdItemSelected)
selectSecondItem(!isSecondItemSelected)
})
CheckBoxRow(
text = thirdItem,
checkedState = isThirdItemSelected,
onClick ={
stateChanged(isFirstItemSelected, isSecondItemSelected, !isThirdItemSelected)
selectThirdItem(!isThirdItemSelected)
})
}
}
위에서 적당히 이해가 가지만 한가지 처음보는 키워드가 있을 것이다. remember
이것은 무엇일까?
remember
Composable UI 는 내부에 가지고 있는 메모리가 존재한다.
Composable functions can store a single object in memory by using the remember composable.
즉, composable 함수는 메모리 내에 single object 를 저장하거나 불러올 수 있고 이는 remember composable 을 통해 가능하다. 따라서 우리는 주로 mutable 상태의 타입 인스턴스 값을 만들어 사용하는 것이다.
@Composable
fun BaseLayout() {
val mContext = LocalContext.current
val CLASS_A = "ClassA"
val CLASS_B = "ClassB"
val CLASS_C = "ClassC"
val classStateMap: MutableMap<String, Boolean> =mutableMapOf()
classStateMap[CLASS_A] = false
classStateMap[CLASS_B] = false
classStateMap[CLASS_C] = false
Column(modifier = Modifier.padding(16.dp)){
Text(
text = "CLASS MATE",
fontSize = 40.sp,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(8.dp))
InputBox("ID : ", false)
InputBox("PWD : ", true)
Spacer(modifier = Modifier.height(24.dp))
CheckBoxGroupForThree(
firstItem = "ClassA",
secondItem = "ClassB",
thirdItem = "ClassC",
stateChanged ={c1, c2, c3->
classStateMap[CLASS_A] = c1
classStateMap[CLASS_B] = c2
classStateMap[CLASS_C] = c3
})
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
){
Button(
onClick ={
val bundle = Bundle()
bundle.putBoolean(CLASS_A, classStateMap[CLASS_A] ?: false)
bundle.putBoolean(CLASS_B, classStateMap[CLASS_B] ?: false)
bundle.putBoolean(CLASS_C, classStateMap[CLASS_C] ?: false)
val intent = Intent(mContext, SecondActivity::class.java)
intent.putExtras(bundle)
mContext.startActivity(intent)
}, modifier = Modifier
.wrapContentSize()
.padding(top = 16.dp)
){
Text(text = "LOGIN")
}
}
}
}
자, 이렇게 만든 것을 BaseLayout 에서 호출 시, stateChanged 로부터 값을 받아 중간자 역할을 하는 map 에 저장하게하고, Button의 onClick 시 이를 가져다 SecondActivity 를 start 할 수 있게 한다.
(Manifest 추가 및 Activity 생성을 완료 후 돌려보자. 이번에는 startActivity 를 사용했지만 밑에 navigation 을 사용한 방법 또한 알아보자.)
List
두번째 화면에서는 첫번째 화면에서 선택한 값을 넘겨받아 이에 대해 리스트로 보여주고, 랜덤 색상의 아이콘을 옆에 배치해보도록 하자.
기존이라면 RecyclerView/ListView 등을 썼을텐데, Compose 에서는 어떤 것을 써야할까?
LazyColumn 은 현재 보이는 아이템만 구성, 배치하는 세로 형태의 스크롤 리스트이며,
LazyListScope.item 을 써서 단일, LazyListScope.items 를 써서 아이템 리스트를 추가할 수 있다.
설명을 보면 RecyclerView 와 비슷해보인다. 그러나, RecyclerView 와 같이 하위 항목을 재사용하지 않고 스크롤에 따라 새로운 composable들을 emit 하는데, Android view 를 인스턴스화하는 것보다 상대적으로 효율적이기 때문에 성능이 좋다고 한다.
@Composable
fun LazyColumnContent(list: MutableList<String>) {
val itemLists = remember{list}
LazyColumn(
contentPadding = PaddingValues(16.dp, 8.dp),
modifier = Modifier
.padding(4.dp)
){
items(
items = itemLists,
itemContent ={
LazyColumnItem(
itemText =it,
)
})
}
}
@Composable
fun LazyColumnItem(
itemText: String
) {
val context = LocalContext.current
val itemColor: MutableState<Color> = remember {
mutableStateOf(Color.random())
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.border(
width = 2.dp,
color = colorResource(id = R.color.purple_500),
shape = RoundedCornerShape(3.dp)
)
.padding(4.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = itemText)
IconButton(onClick = {
}) {
Icon(
imageVector = Icons.Filled.AccountCircle,
tint = itemColor.value,
contentDescription = itemText
)
}
}
Spacer(modifier = Modifier.padding(4.dp))
}
fun Color.Companion.random(): Color {
val red = Random.nextInt(256)
val green = Random.nextInt(256)
val blue = Random.nextInt(256)
return Color(red, green, blue)
}
위를 보면 기존에 UI 에서 구현하고 adapter 와 viewholder 등을 생성했던 것을 생각하면 확연히 보일러플레이트 코드들이 줄었다는 것을 볼 수 있다. 또한 항목을 많이 넘기면 지연이 전혀 없는 것을 확인할 수 있는데 이는 LazyColumn 이 화면에 보이는 composable 만을 로딩하기 때문이다.
찍먹이기 때문에 가볍게 기본을 구성해보았지만 효율적인 recompose 및 custom 한 클래스 설정 등을 알아보고 싶은 사람들을 위해 참고 링크에 관련 블로그들을 첨부해두었다.
참고 링크
- https://www.charlezz.com/?p=45695
- https://kotlinworld.com/191
- https://hodie.tistory.com/125
- https://velog.io/@blue-sky/Compose-목록-및-그리드#itmesindexed
Navigation 사용하기
방금 우리는 화면 이동에서 startActivity 를 수행하고 activity 또한 2개를 생성했었다. 그러나, 이번에는 navigation 을 통해 이동할 수 있도록 해보자. 우선 gradle 에서 아래와 같이 implementation 을 추가하자.
implementation 'androidx.navigation:navigation-compose:2.7.6'
변경을 제대로 진행하기 전 우리가 해야할 일에 대해 한번 정리해보자.
지금 화면 1에서 2로 선택한 체크 박스의 값을 보내야하는 상황이다. 따라서 두 가지가 포인트가 될 텐데,
하나는 화면 이동을 위해 navController 를 넘기는 것, 다른 하나는 한 화면에서 다른 화면으로의 값을 보내는 내용이 될 것이다.
MainActivity 를 재구성해보자.
기존에 setContent 하위에 화면을 구성하던 애들은 싸그리 모아 FirstScreen.kt
, SecondScreen.kt
라는 클래스를 생성해 모은다.
class FirstScreen {
@Composable
fun FirstView(navController: NavController) {
ClassmateTheme{
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
){
BaseLayout(navController)
}
}
}
...
class SecondScreen {
@Composable
fun SecondView(
navController: NavController
) {
ClassmateTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
BaseLayoutForSecond(
navController
)
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent{
val navController = rememberNavController()
Nav(navController)
}
}
@Composable
fun Nav(navController: NavHostController) {
NavHost(navController = navController, startDestination = "main"){
composable("main"){FirstScreen().FirstView(navController = navController)}
composable(route = "list?ClassA={a_value}&ClassB={b_value}&ClassC={c_value}",
arguments =listOf(
navArgument("a_value"){
type = NavType.BoolType
},
navArgument("b_value"){
type = NavType.BoolType
},
navArgument("c_value"){
type = NavType.BoolType
}
)
){backStackEntry->
val classA = backStackEntry.arguments?.getBoolean("a_value") ?: false
val classB = backStackEntry.arguments?.getBoolean("b_value") ?: false
val classC = backStackEntry.arguments?.getBoolean("c_value") ?: false
SecondScreen().SecondView(navController = navController, classA, classB, classC)
}
}
}
}
그리고 화면 이동을 위한 navController 를 MainActivity 에서 넘길 수 있도록 onCreate 의 setContent 에서 rememberNavController() 를 수행하여 Nav 에 이를 넘긴다.
여기서 rememberNavController 가 생성한 것은 앱의 화면과 각 상태를 구성하는 composable 의 backstack 을 추적하며 각 screen 들의 state 를 관리해주는 역할을 한다. 이를 NavigationGraph 와 연결시켜주면, 특정한 destination 의 Composable 로 갈 수 있게 된다.
NavHost(navController = navController, startDestination = "main"){
composable(route = "main"){FirstScreen().FirstView(navController = navController)}
이것을 기준으로 자세히 보자.
이와 같이 NavHost 는 composable 함수들을 넣을 수 있으며, 필수 인자인 route 가 위에서는 main 이 되며 이는 composable 로 향하는 path 를 뜻한다. 각 Screen/Navigation 상의 Destination 들은 유니크한 값이어야하며 해당 작업이 끝나면 성공적으로 Navigation Graph 가 생성된다.
Desination 간의 이동 시, navController.navigate("main"
) 으로 지정한 라우트로의 이동이 가능하다.
값 넘기기
Destination 이동 시에 값을 전달할 경우 아래와 같이 composable 에 2가지 인자를 넣어줄 수 있다.
- route
- “list/{value}” : value 라는 dynamic 한 값을 전달한다. 예를 들어 list/1234 로 넘기면 1234라는 값을 전달할 수 있게 된다.
- arguments
- 라우트를 통해 전달받은 값들의 list
navArgument(”value”){type = NavType.IntType}
여기서 argument 는 value 로 넘기는 값의 이름이 되고, type 은 넘기는 데이터 타입을 지정해주는 것이다.
이를 활용해 위에서는 "list?ClassA={a_value}&ClassB={b_value}&ClassC={c_value}"
의 형태로 각 ClassA, ClassB, ClassC 라는 argument들을 boolean 타입으로 지정해두었다.
이제 적용해서 보내는 쪽을 수정해보자. 변경된 FirstScreen 에서 startActivity 하던 위치에서 지정한 형태에 맞게 값을 넘겨줄 수 있도록 하자.
//FirstScreen.kt
....
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
){
Button(
onClick ={
// 아래 코드를 주석 처리한다.
// val bundle = Bundle()
// bundle.putBoolean(CLASS_A, classStateMap[CLASS_A] ?: false)
// bundle.putBoolean(CLASS_B, classStateMap[CLASS_B] ?: false)
// bundle.putBoolean(CLASS_C, classStateMap[CLASS_C] ?: false)
// val intent = Intent(mContext, SecondActivity::class.java)
// intent.putExtras(bundle)
// mContext.startActivity(intent)
navController?.navigate("list?$CLASS_A=${classStateMap[CLASS_A]}&$CLASS_B=${classStateMap[CLASS_B]}&$CLASS_C=${classStateMap[CLASS_C]}")
}, modifier = Modifier
.wrapContentSize()
.padding(top = 16.dp)
){
Text(text = "LOGIN")
}
....
참고 링크
- https://mypark.tistory.com/entry/JETPACK-COMPOSE-영화-앱-만들기-1-Navigation-Component-Scaffold-LazyColumn-Passing-Data-Between-Screens#google_vignette
- https://velog.io/@ehgus8621/AndroidKotlin-Compose의-NavController-활용하여-Splash화면-만들기
- https://developer.android.com/jetpack/compose/navigation?hl=ko#navigate-from-Compose
- https://velog.io/@lifeisbeautiful/Android-Jetpack-Compose-Navigation-구현하기
- https://developer88.tistory.com/entry/Navigation-총정리-nested-Jetpack-Compose
- https://pluu.github.io/blog/android/2022/02/04/compose-pending-argument-part-2/
Dialog
화면 이동까지 찍먹 해보았다. 다음은 Dialog 이다.
사실 별로 신경쓰지 않고 끄적였지만 Composable 이 메모리를 사용해 객체를 저장하기 때문에 단점이 느껴졌을 수 있다.
Composable UI 를 호출하는 곳에서 State 값을 변경할 수 없기에 구구절절 넘김을 받는 등의 처리가 필요하며 재사용성이 떨어지고 테스트를 하기 어렵기도 하다.
이러한 장점을 극복하기 위해 State Hoisting pattern 을 통해 상태를 갖지 않는 Composable 로 변경이 가능하다고 하다. 아래와 같이 호출부에서 Content 를 다루고 있고, Content 의 동작이 호출부로 전파되며 호출부는 변경된 State 에 따라 변경 내역을 반영해 나간다.
단방향의 데이터 흐름도 지키면서 분리가 되므로 코드 관리가 용이해지는 방향이 될 수 있다.
( 아까의 CheckBox 를 구현하는 부분에서는 호출부에 state 를 두지 않고 content 쪽에서 가지고 있고 호출부는 넘기기 위한 단기적인 value 만 갖고 있었기에 그렇게 수행하지 않은 것을 알 수 있는데, 이와 같이 수정해 볼 수 있을 것이다. )
이번에는 이를 참고하여 아까 구현한 SecondScreen 의 profile 아이콘을 클릭 시 관련 정보를 다이얼로그로 노출시켜보도록 하자.
호출부인 LazyColumnContent 는 각 dialogState (다이얼로그의 노출을 결정), dialogInfo (다이얼로그에서 노출할 값) 를 가지고 있으며, 그 값의 변화에 따라 AlertDialog 를 노출시켜 줄 수 있도록 처리한다.
호출되는 쪽인 LazyColumnItem 은 state 들을 받아, ProfileIcon 이 클릭되는 시점에 각각 true 와 아이템의 정보를 넘겨준다. 따라서, 호출부에서는 이를 인식해 recomposition 이 이루어지고 바로 Dialog 가 노출될 수 있는 것이다.
@Composable
fun LazyColumnContent(list: MutableList<String>) {
val dialogState: MutableState<Boolean> = remember{
mutableStateOf(false)
}
val dialogInfo: MutableState<ClassInfo> = remember{
mutableStateOf(ClassInfo("", Color.Red))
}
// Code to Show and Dismiss Dialog
if (dialogState.value) {
AlertDialog(
onDismissRequest ={dialogState.value = false},
title ={
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
){
Icon(
imageVector = Icons.Filled.AccountCircle,
tint = dialogInfo.value.iconColor,
contentDescription = dialogInfo.value.className
)
}
},
text ={
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
text = dialogInfo.value.className
)
},
dismissButton ={
Button(onClick ={dialogState.value = false}){
Text(text = "CLOSE")
}
},
confirmButton ={
Button(onClick ={dialogState.value = false}){
Text(text = "OK")
}
}
)
} else {
Log.d("GAEGUL", "Dialog is closed")
}
val itemLists = remember{list}
LazyColumn(
contentPadding =PaddingValues(16.dp, 8.dp),
modifier = Modifier
.padding(4.dp)
){
items(
items = itemLists,
itemContent ={
LazyColumnItem(
itemText =it,
dialogState = dialogState,
dialogInfo = dialogInfo
)
})
}
}
@Composable
fun LazyColumnItem(
itemText: String,
dialogState: MutableState<Boolean>,
dialogInfo: MutableState<ClassInfo>
) {
val context =LocalContext.current
val itemColor: MutableState<Color> = remember{
mutableStateOf(Color.random())
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.border(
width = 2.dp,
color = colorResource(id = R.color.purple_500),
shape =RoundedCornerShape(3.dp)
)
.padding(4.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
){
Text(text = itemText)
IconButton(onClick ={
// or else show dialog instead
dialogState.value = true
dialogInfo.value = ClassInfo(itemText, itemColor.value)
}){
Icon(
imageVector = Icons.Filled.AccountCircle,
tint = itemColor.value,
contentDescription = itemText
)
}
}
Spacer(modifier = Modifier.padding(4.dp))
}
- https://medium.com/@karthik.dusk/dialog-in-android-jetpack-compose-with-mvvm-4be48c5948fc
- https://leevisual.tistory.com/m/272
Lifecycle
위에서 호출된 composable 에서 호출부의 state 를 변경하자마자 dialog 가 바뀐 것을 볼 수 있었다.
이는 Composable 이 연결 된 state 에 대해 별도의 lifecycle 을 돌리고 있기 때문이다.
Composition 이 시작되고, 최소 0번 이상의 recomposition 이 수행되며 composition 이 종료되는 심플한 플로우이다.
그렇다면 이것과 Activity Lifecycle 과는 어떤 관계가 되는 것일까?
Compose Lifecycle 은 Jetpack Compose 의 UI 구성을 위한 도구로 액티비티 내부에 생성되지만 액티비티 생명주기에 종속적이지 않다. 자체적인 생명주기이며 이는 Activity 생명주기가 앱의 전반적인 상태와 동작을 관리하는 것과는 다르게 Composable UI 의 화면 표시와 사라지는 등 세부적인 부분과 관련이 된다.
그렇다면 관련이 아예 없을까?
그것도 아니다. 여전히 UI 의 리소스 해제 등의 작업이 연관되어있을 수 있으나, 특정 생명주기에서 기존에 필요했던 작업들은 생략될 수 있다.
1. onCreate():
- setContent 가 수행되며 이를 통해 Composable 함수들이 실행되어 UI가 구성된다.
2. onStart() / onResume():
- Composable UI는 이미
setContent()
에서 설정되었으며, UI의 상태가 변경되면 자체적인 lifecycle 을 통해 자동으로 업데이트(recomposition)되기 때문에 별도의 Composable 관련 작업이 필요하지 않다.
3. onPause() / onStop() / onDestroy():
- Composable UI는 자동으로 업데이트되므로 이 생명주기에서도 특별한 작업이 필요하지 않지만, 필요한 경우 리소스 해제와 같은 작업을 수행할 수 있다.
- Activity 생명주기 메서드에서 수동으로 UI 업데이트 작업을 수행했던 로직들이 불필요하다.
- UI를 제외한 시스템 리소스 해지, 딥링크 정보 수신 등의 작업이 주로 수행된다.
참고 링크
- https://velog.io/@beokbeok/ViewCompositionStrategy
- https://parade621.tistory.com/m/55
- https://velog.io/@ricky_0_k/Compose-무작정-맛보기-2.-State-와-Composable-의-LifeCycle-이야기
- https://medium.com/@VolodymyrSch/android-simple-mvi-implementation-with-jetpack-compose-5ee5d6fc4908
- https://velog.io/@dev-junku/Android-Naver-Android-Jetpack-Compose-적용-후기-발표-정리
지금까지 Jetpack Compose 의 찍먹이었다.
Compose 는 기존의 방식과는 현저히 다르기에 다소 으잉? 하는 부분이 있었을지 모르겠다.
Build 속도 및 APK File Size 의 개선 등의 장점으로 Google 에서 전면으로 내세우고 있는만큼 머지않아 필수가 될 가능성이 크니 꿀단지를 끌어안고 먹어볼 예정이다.
'안드로이드 > KOTLIN' 카테고리의 다른 글
[Kotlin] Coroutine, 잠시 멈춰! (0) | 2024.03.03 |
---|---|
[Kotlin] Serializable 과 Parcelable (1) | 2024.02.04 |
코드로 Coroutine 동작 파악하기 (0) | 2023.09.17 |
[Kotlin] Coroutine Dispatchers.Main 의 동작 순서 보장 (feat. Dispatchers.Main.immediate) (1) | 2023.06.18 |
[Kotlin] Kotlin <--> Java 의 예외처리 (+ 지정 키워드 escape 하여 사용하기) (0) | 2023.05.07 |