관리 메뉴

나만을 위한 블로그

[Android Compose] observeAsState란? 본문

Android/Compose

[Android Compose] observeAsState란?

참깨빵위에참깨빵 2024. 2. 16. 21:49
728x90
반응형

틈틈이 Compose 공부용으로 뉴스 api를 사용한 뉴스 조회 앱을 만들고 있는데, 뷰모델에 함수를 생성한 후에 액티비티에서 호출하면 ApiResult.Success가 호출되지 않는 현상이 발생했다.

아래는 오류가 발생하던 메인 액티비티의 구현으로, 아직 LazyColumn을 사용하기 전의 코드다.

 

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    private val newsViewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeNewsAppTheme {
                newsViewModel.getKoreaNewsHeadline()
                newsViewModel.koreaNews.observe(this) {
                    when (it) {
                        is ApiResult.Loading -> {
                            // 프로그레스 바
                            Timber.e("## 뉴스 데이터 불러오는 중...")
                        }
                        is ApiResult.Success -> {
                            // 화면에 데이터 표시
                            Timber.e("## 뉴스 데이터 불러오기 성공 : ${it.data}")
                            it.data?.articles?.forEach { newsData ->
                                // 뉴스 데이터 각각 접근
                            }
                        }
                        is ApiResult.Error -> {
                            // 에러 토스트 표시
                            Timber.e("## 뉴스 데이터 불러오기 실패 : ${it.message}")
                        }
                    }
                }
            }
        }
    }
}

 

그러나 이 방식은 XML을 사용하는 전통적인 방식에서 작동하는 것이고, Compose에선 이렇게 사용하면 안 된다.

수정 포인트는 2곳이다.

 

  • LaunchedEffect에서 뷰모델의 함수 호출
  • observeAsState()를 사용해서 LiveData를 관찰하고, 변경될 때마다 Composable 함수를 자동 업데이트하게 변경

 

LaunchedEffect는 컴포저블 함수 안에서 suspend 함수를 호출하기 위한 장치라서 반드시 사용해야 하지만, observeAsState 함수는 처음 보는 함수다. 이 함수는 뭐하는 함수인가?

 

https://developer.android.com/jetpack/compose/state?hl=ko#use-other-types-of-state-in-jetpack-compose

 

상태 및 Jetpack Compose  |  Android Developers

상태 및 Jetpack Compose 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 앱의 상태는 시간이 지남에 따라 변할 수 있는 값을 의미합니다. 이는 매우 광범위한 정

developer.android.com

- LiveData.observeAsState() : LiveData를 관찰하기 시작하고 State를 통해 값을 나타낸다

 

https://developer.android.com/reference/kotlin/androidx/compose/runtime/livedata/package-summary#(androidx.lifecycle.LiveData).observeAsState(kotlin.Any)

 

androidx.compose.runtime.livedata  |  Android Developers

androidx.compose.desktop.ui.tooling.preview

developer.android.com

해당 LiveData를 관찰하기 시작하고 State를 통해 값을 나타낸다. LiveData에 새 값이 게시될 때마다 리턴된 State가 업데이트되어 모든 State.value 사용이 재구성(recomposition)된다. 초기값은 이 LiveData가 아직 초기화되지 않았을 때만 사용된다. T가 null이 아닌 타입인 경우, 이 LiveData에 설정한 모든 값도 null이 아닌지 확인하는 것은 사용자의 책임이다. 내부 옵저버는 이 컴포저블이 폐기(disposes)되거나 현재 LifecycleOwner가 Lifecycle.State.DESTROYED 상태로 이동하면 자동 제거된다

 

정리하면 Compose에서 LiveData를 observe해서 값이 바뀌면 자동 갱신하기 위해 써야 하는 요소다.

위 2가지 요소를 적용하고 LazyColumn까지 사용해서 변경된 메인 액티비티의 코드는 아래와 같다.

 

class MainActivity : ComponentActivity() {

    private val newsViewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeNewsAppTheme {
                LaunchedEffect(Unit) {
                    newsViewModel.getKoreaNewsHeadline()
                }
                val newsData by newsViewModel.koreaNews.observeAsState()
                newsData?.let { result ->
                    when (result) {
                        is ApiResult.Loading -> {
                            // 프로그레스 바 표시
                            Timber.e("## 뉴스 데이터 불러오는 중...")
                        }
                        is ApiResult.Success -> {
                            // 화면에 데이터 표시
                            Timber.e("## 뉴스 데이터 불러오기 성공 : ${result.data}")
                            LazyColumn(
                                verticalArrangement = Arrangement.spacedBy(10.dp)
                            ) {
                                items(result.data?.articles ?: listOf()) { newsItem: NewsData ->
                                    NewsItem(
                                        title = newsItem.title,
                                        description = newsItem.description,
                                        url = newsItem.url,
                                        imageUrl = newsItem.urlToImage,
                                        onUrlClick = { url ->
                                            openWebViewWith(url)
                                        }
                                    )
                                }
                            }
                        }
                        is ApiResult.Error -> {
                            // 에러 토스트 표시
                            Timber.e("## 뉴스 데이터 불러오기 실패 : ${result.message}")
                        }
                    }
                }
            }
        }
    }

    private fun openWebViewWith(url: String) =
        Intent(Intent.ACTION_VIEW, Uri.parse(url)).also {
            startActivity(it)
        }
}

 

뷰모델의 구현은 특별할 게 없는 흔한 형태를 하고 있다.

 

@HiltViewModel
class NewsViewModel @Inject constructor(
    private val newsRepository: NewsRepository
): ViewModel() {
    private val _koreaNews = MutableLiveData<ApiResult<BaseNewsData<NewsData>>?>()
    val koreaNews: LiveData<ApiResult<BaseNewsData<NewsData>>?> = _koreaNews

    fun getKoreaNewsHeadline() = viewModelScope.launch {
        _koreaNews.value = ApiResult.Loading()
        val result = newsRepository.getKoreaNewsHeadline()
        _koreaNews.value = result
    }
}

 

반응형
Comments