Compose는 선언적이어서 업데이트 하는 유일한 방법은 새 인수로 동일한 컴포저블을 호출하는것임.
State가 업데이트 될때마다 Recompose가 실행됨.
- 컴포지션: 컴포즈가 컴포저블을 실행할 때 빌드한 UI에 관한 설명(컴포저블의 트리 구조)
- 초기 컴포지션: 처음 생성된 컴포지션
- 리컴포지션: 데이터가 변경될때 컴포지션을 업데이트 하기 위해 컴포저블을 다시 실행하는 것.
- 리컴포지션 때 똑같은거는 다시 빌드 안하는데 이걸 식별하는데에 도와주는게 key값, 데이터의 고유한 값을 컴포즈 api의 key 값에 매핑해주면 스마트 리컴포지션에 도움이 된다.
remember
: 상태를 메모리에 객체를 저장할 수 있음. 초기 컴포지션때 저장된 값을 리컴포지션 때 유지되게해줌.
remember
삭제는 언제되냐?
remember를 호출한 컴포저블이 컴포지션에서 삭제되면 삭제됨.
mutableState
타입으로 생성하는 이유?
컴포즈 내부에서는 상태를State<T>
형태로 저장하고있는데 이 상태는 immutable임.
그래서 mutable한 state 형태로 컴포저블의 상태를 선언함.interface MutableState<T> : State<T> { override var value: T }
MutableState의 내부
interface MutableState<T> : State<T> {
override var value: T
}
by
키워드 사용을 위해서는 아래 두가지 Import구문을 추가해야함.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
전체 컴포저블 컴포넌트로 보면 현재 컴포즈가 없어지면 당연히 상태 유지가 안됨..
이때 사용되는게 rememberSaveable
Bundle에 저장됨.
MutableState는 상태 변경으로 State를 변경하기 위해 사용하는거고
꼭 mutableState
타입으로 생성해야되냐? 아님!
프로젝트에서 필요에 의해 Flow나 Rx등을 사용할 때 Jetpack과 호환해서 사용할 수 있는거? 당연히 제공함미다.(링크 클릭해서 보면 내부 구조랑 사용예시, 설명을 볼수있어요~)
- Flow: collectAsState(), collectAsStateWithLifecycle()
- LiveData: observaAsState()
- RxJava2: subscribeAsState()
핵심은?
Compose는 State 객체를 읽어오는 과정에서 자동으로 리컴포저블 되고,
관찰가능한 다양한 형태(rx, live data, flow)의 데이터를 컴포즈에서 다룰 수 있는 state 형태로 변환해서 사용한다는게 핵심임미다~
그렇기 때문에 의도하는대로 State로 변환되는지 확인 하고 사용해야함
호이스팅 = 끌어올리다?
=> Stateless를 유지하기 위해서 상태를 끌어올려 컴포저블 호출자를 상위로 옮기는 패턴.
호이스팅된 상태의 속성(끌어올려진 상태의 속성)
예시
@Composable
fun HelloScreen() {
var name by rememberSaveable { mutableStateOf("") }
HelloContent(name = name, onNameChange = { name = it })
}
// 상태 저장박식과 분리되고, 테스트가 용이해짐.
// ex> HelloScreen을 수정하거나 교체 시, HelloContent의 구현방식을 변경할 필요 없음.
@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. 가장낮은 공통 상위 요소로 끌어올리기(읽기)
2. 최소한 변경될 수 있는 가장 높은 수준으로 끌어올리기(쓰기)
3. 동일한 이벤트에 대한 응답으로 두 상태가 변경되는 경우 두 상태를 함께 끌어올려야 함
Activity 혹은 프로세스가 다시 생성된 후 `rememberSaveable`을 사용하여 UI 상태를 복원함.
Bundle에는 int, double, long, String 부터 FloatArray, StringArrayList, Serializable, Parcelable까지 저장가능
@Parcelize
data class City(val name: String, val country: String) : Parcelable
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable {
mutableStateOf(City("Madrid", "Spain"))
}
}
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"))
}
}
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"))
}
}
간단한 호이스팅은 컴포저블함수 자체에서 관리가 가능함.
하지만 추적할 양이 늘어나거나 컴포저블 함수에서 실행할 로직이 발생하면 상태 책임을 다른 클래스(=> 상태 홀더)에 위임하는것이 좋음.
상태 홀더 = 호이스팅한 상태 객체
remember는 calculation 람다 매개 변수를 취함.
remember를 사용하여 초기화 하거나 계산하는데 비용이 많이 드는 객체 또는 작업의 결과를 컴포지션 중에 저장할 수 있음. 그러나 안하는게 좋다~
remember API는 key 또는 keys 매개변수도 취합니다. 이러한 키 중 하나라도 변경될 경우 다음번에 함수가 재구성될 때 remember는 캐시를 무효화하고 계산 람다 블록을 다시 실행함 그래서 이 키를 잘 활용하면 된다~
@Composable
fun BackgroundBanner(
@DrawableRes avatarRes: Int,
modifier: Modifier = Modifier,
res: Resources = LocalContext.current.resources
) {
val brush = remember(key1 = avatarRes) {
ShaderBrush(
BitmapShader(
ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
Shader.TileMode.REPEAT,
Shader.TileMode.REPEAT
)
)
}
Box(
modifier = modifier.background(brush)
) {
// ...
}
}
remember의 키값을 windowSizeClass로 받고있음.
@Composable
fun rememberMyAppState(
windowSizeClass: WindowSizeClass
): MyAppState {
return remember(windowSizeClass) {
MyAppState(windowSizeClass)
}
}
@Stable
class MyAppState(
private val windowSizeClass: WindowSizeClass
) { ... }
Compose 클래스가 equals 함수를 구현해서 키가 변경된 것을 확인하고 저장된 값을 무효화 함(= 리컴포저블을 안함).
derivesStateOf와의 차이점 => 상태를 압축
val isEnabled = remember {
derivedStateOf { lazyListState.firstVisibleItemIndex > 0 }
}
val output = remember(input) { expensiveCalculation(input) }
rememberSaveable은 이 키로 input매개변수를 받음. input이 변경되면 캐시가 무 효화하고 함수가 재구성 될 때 계산 람다 블록을 재실행함.
// rememberSaveable는 typedQuery가 변경될 때까지 userTypedQuery를 저장
var userTypedQuery by
rememberSaveable(inputs = typedQuery, stateSaver = TextFieldValue.Saver) {
mutableStateOf(
TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length))
)
}
컴포저블 함수 범위 밖에서 발생하는 앱 상태에 관한 변경사항
컴포저블 수명주기 및 속성으로 인해 컴포저블에는 부수 효과가 없는것이 좋은데,
필요한 경우도 있음. 상태에 따른 스낵바 같은 단발성 이벤트
아래 API 들은 이 필요한 경우에 사용하는 것들임.
Effect: UI를 내보내지 않으며 컴포지션이 완료될 때 부수효과를 실행하는 컴포저블 함수. 예측 가능한 방식으로 실행됨
@Composable
fun MyScreen(
state: UiState<List<Movie>>,
scaffoldState: ScaffoldState = rememberScaffoldState()
) {
// If the UI state contains an error, show snackbar
if (state.hasError) {
// `LaunchedEffect` will cancel and re-launch if
// `scaffoldState.snackbarHostState` changes
LaunchedEffect(scaffoldState.snackbarHostState) {
// Show snackbar using a coroutine, when the coroutine is cancelled the
// snackbar will automatically dismiss. This coroutine will cancel whenever
// `state.hasError` is false, and only start when `state.hasError` is true
// (due to the above if-check), or if `scaffoldState.snackbarHostState` changes.
scaffoldState.snackbarHostState.showSnackbar(
message = "Error message",
actionLabel = "Retry message"
)
}
}
Scaffold(scaffoldState = scaffoldState) {
/* ... */
}
}
@Composable
fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {
// Creates a CoroutineScope bound to the MoviesScreen's lifecycle
val scope = rememberCoroutineScope()
Scaffold(scaffoldState = scaffoldState) {
Column {
/* ... */
Button(
onClick = {
// Create a new coroutine in the event handler to show a snackbar
scope.launch {
scaffoldState.snackbarHostState.showSnackbar("Something happened!")
}
}
) {
Text("Press me")
}
}
}
}
@Composable
fun LandingScreen(onTimeout: () -> Unit) {
// This will always refer to the latest onTimeout function that
// LandingScreen was recomposed with
val currentOnTimeout by rememberUpdatedState(onTimeout)
// Create an effect that matches the lifecycle of LandingScreen.
// If LandingScreen recomposes, the delay shouldn't start again.
LaunchedEffect(true) {
delay(SplashWaitTimeMillis)
currentOnTimeout()
}
/* Landing screen content */
}
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// Safely update the current lambdas when a new one is provided
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
// If `lifecycleOwner` changes, dispose and reset the effect
DisposableEffect(lifecycleOwner) {
// Create an observer that triggers our remembered callbacks
// for sending analytics events
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
currentOnStart()
} else if (event == Lifecycle.Event.ON_STOP) {
currentOnStop()
}
}
// Add the observer to the lifecycle
lifecycleOwner.lifecycle.addObserver(observer)
// When the effect leaves the Composition, remove the observer
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
/* Home screen content */
}
- SideEffect: Compose 상태를 비 Compose 코드에 게시
@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
val analytics: FirebaseAnalytics = remember {
/* ... */
}
// On every successful composition, update FirebaseAnalytics with
// the userType from the current User, ensuring that future analytics
// events have this metadata attached
SideEffect {
analytics.setUserProperty("userType", user.userType)
}
return analytics
}
@Composable
fun loadNetworkImage(
url: String,
imageRepository: ImageRepository
): State<Result<Image>> {
// Creates a State<T> with Result.Loading as initial value
// If either `url` or `imageRepository` changes, the running producer
// will cancel and will be re-launched with the new inputs.
return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {
// In a coroutine, can make suspend calls
val image = imageRepository.load(url)
// Update State with either an Error or Success result.
// This will trigger a recomposition where this State is read
value = if (image == null) {
Result.Error
} else {
Result.Success(image)
}
}
}
@Composable
fun loadNetworkImage(
url: String,
imageRepository: ImageRepository
): State<Result<Image>> {
// Creates a State<T> with Result.Loading as initial value
// If either `url` or `imageRepository` changes, the running producer
// will cancel and will be re-launched with the new inputs.
return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {
// In a coroutine, can make suspend calls
val image = imageRepository.load(url)
// Update State with either an Error or Success result.
// This will trigger a recomposition where this State is read
value = if (image == null) {
Result.Error
} else {
Result.Success(image)
}
}
}
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
LaunchedEffect, produceState, DisposableEffect와 같은 Compose의 일부 효과에서 실행 중인 Effect를 취소하는 데 사용되는 가변적인 수의 인수를 취하고 새 키로 새 Effect를 시작
Effect에 사용되는 변수는 Effect 컴포저블의 매개변수로 추가하거나 rememberUpdatedState를 사용해야함.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// These values never change in Composition
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
/* ... */
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}