본문 바로가기

IT/안드로이드 관련

[안드로이드] - Compose 문서 읽어보기 -1

안녕하세요 남갯입니다

 

오늘은 Compose 문서 읽어보면서 내용을 정리 해보려고 합니다.

 

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

 

명령형 선언형의 차이
선언형
ViewB(
    color : red,
    child: viewC()
)

명령형

View = b = new View()
b.setColor(Red)
b.clearChildren()
ViewC c3 = new ViewC()
b.add(c3)



명령형에서 findViewById를 통해 트리를 탐색하고
뷰를 가져와서 setText() , addChild등과 같이 메서드를 호출해서 노드를 변경하게 되는데, 뷰를 수동으로 조작하게 되면 오류가 발생할 가능성이 커지고 여러 데이터를 표시하는경우 뷰를 업데이트하기는것을 잊어버리기 쉬움.

StateLess
선언형 접근방식이라 stateLess상태이며 getter랑 setter함수를 노출하지 않습니다.


동적 작성
for문을 통해 동적으로 뷰를 구성 가능

@Composable
fun Greeting(names: List<String>) {
    for (name in names) {
        Text("Hello $name")
    }
}


재구성 
상태가 없으므로 위젯에서 setter 를 호출해서 내부상태를 변경합니다.

@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("I've been clicked $clicks times")
    }
}


위의 형태처럼 함수가 재구성되고 함수에게 뷰를 위젯이 새 데이터로 다시 그려진다.

기존방식은 트리를 재구성하는 작업의 형태라 많은 컴퓨터의 성능을 처리하는데, Compose는 지능적 재구성을 이용한다. 재구성은 입력이 변경될 때 구성 가능한 함수를 다시 호출하는 프로세스. 매개변수가 변경되지 않은 함수 또는 람다를 모두 건너뛰면서 효율적으로 이루어짐

재구성의 동작은 호출순서와 상관없이 동시에 호출될 수 있음

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}


위의 코드는 start - middle - end 순으로 호출될거라 예상하지만 다르게 호출 가능
따라서

@Composable
fun ListComposable(myList: List<String>) {
    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
            }
        }
        Text("Count: ${myList.size}")
    }
}

위의 코드는 정상적으로 불리지만

@Composable
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
    var items = 0

    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
                items++ // Avoid! Side-effect of the column recomposing.
            }
        }
        Text("Count: $items")
    }
}



위와같은 코드는 스레드 세이프하지 않아서 기대와는 다른 동작이 발생할수 있다
 

재구성은 가능한 한 많이 건너뜀
재구성시에 변경되지 않은 데이터들은 최대한 넘어가도록 할 수 있음.

 

/**
 * Display a list of names the user can click with a header
 */
@Composable
fun NamePicker(
    header: String,
    names: List<String>,
    onNameClicked: (String) -> Unit
) {
    Column {
        // this will recompose when [header] changes, but not when [names] changes
        Text(header, style = MaterialTheme.typography.h5)
        Divider()

        // LazyColumn is the Compose version of a RecyclerView.
        // The lambda passed to items() is similar to a RecyclerView.ViewHolder.
        LazyColumn {
            items(names) { name ->
                // When an item's [name] updates, the adapter for that item
                // will recompose. This will not recompose when [header] changes
                NamePickerItem(name, onNameClicked)
            }
        }
    }
}

/**
 * Display a single name the user can click.
 */
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
    Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}

header가 변경될때 나머지는 변경시키지 않고 람다로 스킵가능, 

LazyColumn을 통해 name이 변경되지 않았다면 LazyColumnItems를 건너 뛰도록 가능

 

컴포저블 함수는 자주 실행가능

컴포저블 함수는 자주 실행될 수 있으므로, ui 애니메이션과 같이 모든프레임 실행이 가능하다. 

초당 수백번 읽기도 가능하며, 성능에 치명적 영향을 줄 수 있어서, 비용이 많은 동작은 서브스레드로

작업데이터를 구성하고 mutableStateOf 혹은 LiveData를 통해 Compose에게 데이터를 전달 할 수 있습니다.

 

 

상태 및 컴포지션

Compose는 선언형 ui이므로 업데이트를 하기 위한 방법은 새 인수를 통해 동일한 컴포저블을 호출하는 방법이다.

 

@Composable
fun HelloContent() {
   Column(modifier = Modifier.padding(16.dp)) {
       Text(
           text = "Hello!",
           modifier = Modifier.padding(bottom = 8.dp),
           style = MaterialTheme.typography.h5
       )
       OutlinedTextField(
           value = "",
           onValueChange = { },
           label = { Text("Name") }
       )
   }
}

이 코드를 실행해도, textField가 자체적으로 업데이트가 되지않는다. value가 변경될 때 업데이트 된다.

 

 

Remember Composable

remember composable을 사용하여 메모리에 단일 객체를 저장할 수 있습니다.

remember는 객체를 컴포지션에 저장하고 remember를 호출한 Composable이 컴포지션에서 삭제되면

객체를 잊습니다.

 

MutableState

MutableState 객체를 선언하는 데는 세가지 방법이 있습니다.

  • val mutableState = remember { mutableStateOf(default) }
  • var value by remember { mutableStateOf(default) }
  • val (value, setValue) = remember { mutableStateOf(default) }

 

if 구문을 을 통해 조건형태의 인사말을 표현 가능합니다.

@Composable
fun HelloContent() {
   Column(modifier = Modifier.padding(16.dp)) {
       var name by remember { mutableStateOf("") }
       if (name.isNotEmpty()) {
           Text(
               text = "Hello, $name!",
               modifier = Modifier.padding(bottom = 8.dp),
               style = MaterialTheme.typography.h5
           )
       }
       OutlinedTextField(
           value = name,
           onValueChange = { name = it },
           label = { Text("Name") }
       )
   }
}

remember가 재구성 과정 상태유지는 도움되지만 구성 변경 전반에서 상태유지가 되지 않아서

 rememberSaveable을 사용해야합니다. bundle에 저장할 수 있는 모든 값을 자동으로 저장한다.

 

 

mutableStata말고 다른유형의 데이터를 읽을 수 있음. State<T>로 변환해야 합니다.

- LiveData 

- Flow

- RxJava2

 

요점: Compose는 State<T> 객체를 읽어오면서 자동으로 재구성됩니다.

Compose에서 LiveData 같은 관찰 가능한 또 다른 유형을 사용할 경우 컴포저블에서 LiveData<T>.observeAsState() 같은 구성 가능한 확장 함수를 사용하여 그 유형을 읽어오려면 유형을 State<T>로 변환해야 합니다.

 

 

stateFul과 stateLess

remember를 사용하면 statefult하게 만든다.

stateLess Composable은 상태를 갖기 않는 컴포저블

 

재사용 가능한 컴포저블을 개발할 때는 동일한 컴포저블의 스테이트풀(Stateful) 버전과 스테이트리스(Stateless) 버전을 모두 노출해야 하는 경우가 있습니다. 스테이트풀(Stateful) 버전은 상태를 염두에 두지 않는 호출자에 편리하며, 스테이트리스(Stateless) 버전은 상태를 제어하거나 끌어올려야 하는 호출자에 필요합니다.

 

상태가 필요하냐 하지않냐에 따라 구분해서 사용

 

 

상태 호이스팅

컴포저블을 stateLess로 만들기 위해 상태를 컴포저블의 호출자로 옮기는 패턴

상태가 변경되었음을 하기위한 패턴은 두개의 매개변수를 바꾸는것

1. value : T , 2. onValueChange : (T) -> Unit , 상황에따라 onExpand, onCollapse

 

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

위와 같이 짤경우

1. HelloContent의 상태를 변경시켜(끌어올림)으로 여러 상황을 테스트 가능

2. HelloScreen이 변경되더라도 HelloContent를 변경할 필요가 없음.

 

 

상태가 내려가고 이벤트가 올라가는 패턴을 단방향 데이터 흐름이라고 한다.

 

핵심 사항: 상태를 끌어올릴 때 상태의 이동 위치를 쉽게 파악할 수 있는 세 가지 규칙이 있습니다.

  1. 상태는 적어도 그 상태를 사용하는 모든 컴포저블의 가장 낮은 공통 상위 요소로 끌어올려야 합니다(읽기).
  2. 상태는 최소한 변경될 수 있는 가장 높은 수준으로 끌어올려야 합니다(쓰기).
  3. 동일한 이벤트에 대한 응답으로 두 상태가 변경되는 경우 두 상태를 함께 끌어올려야 합니다.

이러한 규칙에서 요구하는 것보다 상태를 더 높은 수준으로 끌어올릴 수 있습니다. 하지만 상태를 끌어내리면 단방향 데이터 흐름을 따르기가 어렵거나 불가능할 수 있습니다.

 

 

Compose에서 상태 복원

activity나 process가 다시 생성된 이후 rememberSavable 을 사용하여 UI 상태를 복원

rememberSavable은 bundle 데이터만 저장된다고 했는데, Parcelize Annotation을 통해

클래스 유형을 만들어 상태를 저장 가능합니다.

혹은 Parcelize가 적합하지 않을경우 mapSaver라는 시스템으로 객체를 변화하는 규칙을 정의 가능

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

 

ListSaver를 사용해서 저장도 가능

data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

 

Compose 에서 상태관리

 

  • 컴포저블 - 간단한 UI 요소 상태 관리 목적.
  • 상태 홀더 - 복잡한 UI 요소 상태 관리 목적. 상태 홀더는 UI 요소의 상태와 UI 로직을 소유합니다.
  • aac ViewModel - 비즈니스 로직 및 화면 상태나 UI 상태에 대한 액세스 권한을 제공하는 특수한 유형의 상태 홀더.

 

 

상태 및 로직유형

UI 요소 상태 : UI요소를 호이스팅한 상태, ScaffoldState는 Scaffold 컴포저블의 상태를 처리합니다.

화면상태 : 장바구니 항목, 사용자에게 표시할 메세지 또는 로드 플래그

 

@Composable
fun MyApp() {
    MyTheme {
        val scaffoldState = rememberScaffoldState()
        val coroutineScope = rememberCoroutineScope()

        Scaffold(scaffoldState = scaffoldState) {
            MyContent(
                showSnackbar = { message ->
                    coroutineScope.launch {
                        scaffoldState.snackbarHostState.showSnackbar(message)
                    }
                }
            )
        }
    }
}

 

scaffoldState에 변경 가능한 속성이 포함되므로 모든 상호작용은 MyApp 컴포저블에서 발생해야함

그럼 버그추적도 어렵도 단일정보 소스 원칙도 위배된다.

따라서 여러 요소의 상택 있는 복잡한 로직이 포함된 Composable은 책임을 상태홀더에 위임해야한다.

관심사 분리 원측을 따라서, 컴포저블이 UI요소를 빼버리고, 상태홀더가 해당 상태들을 포함한다.

 

// Plain class that manages App's UI logic and UI elements' state
class MyAppState(
    val scaffoldState: ScaffoldState,
    val navController: NavHostController,
    private val resources: Resources,
    /* ... */
) {
    val bottomBarTabs = /* State */

    // Logic to decide when to show the bottom bar
    val shouldShowBottomBar: Boolean
        get() = /* ... */

    // Navigation logic, which is a type of UI logic
    fun navigateToBottomBarRoute(route: String) { /* ... */ }

    // Show snackbar using Resources
    fun showSnackbar(message: String) { /* ... */ }
}

@Composable
fun rememberMyAppState(
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    navController: NavHostController = rememberNavController(),
    resources: Resources = LocalContext.current.resources,
    /* ... */
) = remember(scaffoldState, navController, resources, /* ... */) {
    MyAppState(scaffoldState, navController, resources, /* ... */)
}

MyApp은 UI요소를 빼버리고 모든 UI로직과 상태를 MyAppState에 위임합니다.

@Composable
fun MyApp() {
    MyTheme {
        val myAppState = rememberMyAppState()
        Scaffold(
            scaffoldState = myAppState.scaffoldState,
            bottomBar = {
                if (myAppState.shouldShowBottomBar) {
                    BottomBar(
                        tabs = myAppState.bottomBarTabs,
                        navigateToRoute = {
                            myAppState.navigateToBottomBarRoute(it)
                        }
                    )
                }
            }
        ) {
            NavHost(navController = myAppState.navController, "initial") { /* ... */ }
        }
    }
}

 

 

ViewModel

* ViewModel은 컴포지션보다 라이프사이클이 깁니다. (수명이 길다) 따라서 viewModel의 수명에 바인딩 된 상태에서

참조를 오랫동안 보유하면 메모리 릭이 발생할 수 있음.

 

 

data class ExampleUiState(
    dataToDisplayOnScreen: List<Example> = emptyList(),
    userMessages: List<Message> = emptyList(),
    loading: Boolean = false
)

class ExampleViewModel(
    private val repository: MyRepository,
    private val savedState: SavedStateHandle
) : ViewModel() {

    var uiState by mutableStateOf<ExampleUiState>(...)
        private set

    // Business logic
    fun somethingRelatedToBusinessLogic() { ... }
}

@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {

    val uiState = viewModel.uiState
    ...

    Button(onClick = { viewModel.somethingRelatedToBusinessLogic() }) {
        Text("Do something")
    }
}

ExampleScreen에서 작동하는 ViewModel과 일반 상태 홀더

 

private class ExampleState(
    val lazyListState: LazyListState,
    private val resources: Resources,
    private val expandedItems: List<Item> = emptyList()
) { ... }

@Composable
private fun rememberExampleState(...) { ... }

@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {

    val uiState = viewModel.uiState
    val exampleState = rememberExampleState()

    LazyColumn(state = exampleState.lazyListState) {
        items(uiState.dataToDisplayOnScreen) { item ->
            if (exampleState.isExpandedItem(item) {
                ...
            }
            ...
        }
    }
}