Compose의 SideEffects (2)

 

지난 포스팅

https://jinudmjournal.tistory.com/148

 

Compose의 SideEffects (1)

Composable에서의 non-composable SideEffects란 Composable 함수의 범위 밖에서 발생하는 앱 상태에 관한 변경사항입니다. Composable의 수명 주기 및 속성으로 인해 SideEffects를 최소화하는 것이 좋습니다. 하지

jinudmjournal.tistory.com

 

지난포스팅에 이어서 Compose의 SideEffects에 대하여 학습하는 시간을 가지게 되었습니다.

이번 시간에는 Compose 상태를 Non - Compose 상태로 변환하는 방법과 반대의 경우를 살펴보겠습니다.

 

Non - Compose -> Conpose

Compose에서는 STATE<T>를 통해 상태를 관찰하고 Composable을 업데이트 합니다.

따라서 Flow, LiveData, Rxjava 등의 관찰가능한 타입들을 Compose에서 사용하려면 State로 변환하는 과정이 필요하며,

이를 위해서 productState를 활용할 수 있습니다.

 

produceState는 관찰하고 있는 값이 변경되면 State<T>를 반환합니다.
Compose에서는 Flow, LiveData를 위해 State로 변환하는 함수를 지원합니다.

 

produceState가 컴포지션을 시작하면 프로듀서가 실행되고 컴포지션을 종료하면 취소합니다.반환 된 State는 동일한 값을 설정해도 리컴포지션이 트리거되지 않습니다.

 

produceState가 코루틴을 만드는 경우에도 정지되지 않는 데이터 소스를 관찰하는 데 사용할 수 있습니다.

이 소스의 구독을 삭제하는 경우 awaitDispose 함수를 사용할 수 있는데요, 아래 문서를 참고할 수 있습니다.

 

https://developer.android.com/reference/kotlin/androidx/compose/runtime/ProduceStateScope#awaitDispose(kotlin.Function0)

 

ProduceStateScope  |  Android Developers

androidx.compose.desktop.ui.tooling.preview

developer.android.com

 

아래 예는 produceState를 사용하여 네트워크에서 이미지를 로드하는 방법을 사용하였습니다.

loadNetworkImage는 Composable 함수로 다른 컴포저블에서 사용할 수 있는 State를 반환합니다.

 

Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository
): State<Result<Image>> {

    // Result.Loading을 초기값으로 사용하여 State<T>를 생성합니다.
    // `url` 또는 `imageRepository`가 변경되면 실행 중인 생산자는
    // 취소되고 새 입력으로 다시 시작됩니다.
    return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {
		// 코루틴에서는 정지 호출을 할 수 있습니다.
        val image = imageRepository.load(url)

		// 오류 또는 성공 결과로 상태를 업데이트합니다.
        // 이 상태를 읽는 재구성이 트리거됩니다.
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}

 

주의할 점은 반환 유형이 있는 Composable 함수는 이름을 소문자로 시작하여 지정해야 합니다. (loadNetworkImage)

 

Compose State -> Non - Compose State

Compose 상태를 Compose에서 관리하지 않은 객체와 공유하려면 리컴포지션 성공 시마다 호출되는 SiedEffect를 활용합니다.

이전에 학습한 LaunchedEffect와 비슷한 방식으로 동작하는데요, SideEffect 컴포저블을 활용합니다.

 

@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        /* ... */
    }


	// 구성이 성공할 때마다 FirebaseAnalytics를 다음으로 업데이트합니다.
    // 현재 사용자의 userType을 가져와 향후 분석을 보장합니다.
    // 이벤트에는 이 메타데이터가 첨부되어 있습니다.
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

 

SideEffect는 쉽게 사용될 수 있지만, key 값에 따라 제어하거나 Composable이 Dispose되는 경우에는 사용하기 어렵습니다.

따라서 LaunchedEffect나 DisposableEffect를 활용하는 것을 권장한다고 합니다.

 

derivedStateOf

derivedStateOf는 하나 이상의 상태 객체를 다른 상태로 변환할 때 사용합니다.스크롤이 경계값을 통과하는지, 리스트의 항목이 임계값보다 큰지 등의 유효성 검사에 활용합니다.composable 함수 안에서 사용해야 하며, recomposition이 일어나도 살아남아야 합니다. 따라서 remember을 활용합니다.

 

val result = remember(state1, state2) { calculation(state1, state2) }
val result1 = remember { derivedStateOf { calculation(state1, state2) } }

 

위와 같이 사용할 경우, remember에 key를 지정하는 방식은

key가 변경되는 만큼 업데이트가 되어야 하는 경우에 사용하며, key마다 다른 값의 remember를 반환합니다.

 

@Composable
fun TodoList(highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")) {

    val todoTasks = remember { mutableStateListOf<String>() }

	// todoTasks 또는 highPriorityKeywords가 있는 경우에만 우선순위가 높은 작업을 계산합니다.
    // 재구성할 때마다 변경하는 것이 아니라 변경합니다.
    val highPriorityTasks by remember(highPriorityKeywords) {
        derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } }
    }

    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
       /* 사용자가 목록에 요소를 추가할 수 있는 나머지 UI */
    }
}

 

위 코드에서의 동작 방식은 아래와 같습니다.

- derivedStateOf가 todoTasks가 변경될 때마다, highPriorityTasks에 대한 계산을 실행

- 그에 따라 UI를 업데이트 합니다.

- highPriorityKeywords가 변경되면 remember 블록이 실행됩니다.

- 이전 파생 상태 객체 대신 새로운 객체를 생성하고 기억합니다.

 

결론적으로 highPriorityTasks를 계산하기 위한 필터링이 비용이 많이 들 수 있으므로,

매 리컴포지션 시가 아닌 목록이 변경될 때만 실행하도록 동작할 수 있습니다.

 

snapshotFlow

Compose의 상태를 Flow로 변환합니다.

snapshotFlow를 활용하여 State<T> 객체를 Cold Flow로 변환합니다.

snapshotFlow 블록 내에서 읽은 State 객체 하나가 변경되었을 때,

새 값이 이전에 내보낸 값과 같지 않은 경우 Flow에서 새 값을 수집기에 내보냅니다.

 

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

 

위 코드에서 listState.firstVisibleItemIndex는 Flow 연산자의 이점을 활용할 수 있는 Flow로 변환 됩니다.

 

 

리뷰

Compose에서 자주 사용하는 효과에 대해 알아보았습니다.

개발을 진행하면서 급하게 compose를 학습하였기 때문에 어떤 의미를 가지고 사용되어지는지 모른채 무작정 사용하게 되었는데요.

이번 리뷰를 통해서 제대로 학습할 수 있었던 것 같습니다.