๐ก์ฐ์ํํ ํฌ์ฝ์ค ๊ณผ์ ์์ ์ ๊ด์ฉํ ์ํ ๊ด๋ฆฌ์ UIState์ ๋ํ์ฌ ์ ๋ฆฌํ์์ต๋๋ค.
์ํ ๊ด๋ฆฌ
- UIState๋ ์ํ๋ฅผ ์ ์ฅํ๊ณ ๊ด๋ฆฌํ๋ ํจํด์ ํ์ฉํ๋ ์ค์ํ ๊ฐ๋ ์ ๋๋ค.
Android์์ ์ ํ๋ฆฌ์ผ์ด์ ์์ UI ๋ฐ ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌํ๋ ์ค์ํ ๊ฐ๋ ์ ๋๋ค. ํนํ MVVM ํจํด์ด๋ Jetpack Compose ๋ฑ์์ ์ํ ๊ด๋ฆฌ๋ ํ์์ ์ธ ์์์ด๋ฉฐ, ์ด๋ฅผ ํตํด UI๊ฐ ์ผ๊ด์ฑ ์๊ฒ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ํ๊ณ ์ฌ์ฉ์์ ์ก์ ์ ๋ฐ๋ผ ๋์ ์ผ๋ก ์ ๋ฐ์ดํธ ํ ์ ์์ต๋๋ค.
State ManageMent
- ์๋๋ก์ด๋ ์ ํ๋ฆฌ์ผ์ด์
์ ์ฌ์ฉ์ ์ธํฐํ์ด์ค๊ฐ ์๋์ ๊ฐ์ด ๋ค์ํ ์ํ๋ฅผ ๊ฐ์ง ์ ์์ต๋๋ค.
- Loading
- Success
- Error
- Init
- UI ์ํ๋ฅผ ๋ํ๋ด๋ ํด๋์ค๋ ์ํ ๊ฐ์ฒด๋ฅผ ํ์ฉํ๋ฉด UI์ ViewModel์ด ์ํธ์์ฉํ์ฌ ์ํ์ ๋ฐ๋ฅธ UI๋ฅผ ๊ฐฑ์ ํ ์ ์์ต๋๋ค.
UI State ํจํด
- ๊ฐ ์ํ๋ฅผ ๋ช ์์ ์ผ๋ก ์ ์ํ๊ณ , ์ด๋ฅผ ํตํด UI๊ฐ ์ด๋ค ์ํ์ ์๋์ง ํ์ธํ ์ ์์ต๋๋ค.
Sealed interface
- Sealed interface์ Data Object๋ง์ ํ์ฉํด์ UI State๋ฅผ ๊ตฌํํ ์ ์์ต๋๋ค.
sealed interface ClubListUiState {
data object Init : ClubListUiState
data object NotData : ClubListUiState
data object NotAddress : ClubListUiState
data object Error : ClubListUiState
}
- sealed interface๋ฅผ ํ์ฉํ ์์์ฑ๋ฅผ ๊ตฌํํ๊ณ , ๋ถ๊ธฐ ์ฒ๋ฆฌ๋ฅผ ํตํด์ ๊ฐ ์ํฉ์ ๋ง๋ UI๋ฅผ ๋์ํฉ๋๋ค.
viewModel.uiState.observe(viewLifecycleOwner) { state ->
when (state) {
ClubListUiState.Init -> applyViewVisibility(binding.includeClubList.rcvClubListClub)
ClubListUiState.NotAddress -> applyViewVisibility(binding.includeClubAddress.linearLayoutClubNotAddress)
ClubListUiState.NotData -> applyViewVisibility(binding.includeClubData.linearLayoutClubNotData)
ClubListUiState.Error -> applyViewVisibility(binding.includeClubError.linearLayoutClubError)
}
}
- sealed Interface์ data object๋ฅผ ํ์ฉํ๋ฉด ์ํฉ์ ๋ง๋ ๋ทฐ๋ฅผ ์ ๋ฐ์ดํธํ๋ ์ฅ์ ์ด ์์ง๋ง, ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๊ณ ๊ด๋ฆฌํ๊ธฐ ์ด๋ ต์ต๋๋ค.
- ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ Sealed Class์ data Class๋ฅผ ํจ๊ป ํ์ฉํ ์ ์์ต๋๋ค.
Sealed Class
- sealed Class๋ data Class์ ํจ๊ป ์ฌ์ฉํ์ฌ ์ํ๋ฅผ ๊ฐ์ฒด๋ก ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
- Sealed Class๋ก UI ์ํ๋ฅผ ์ ์ํ๋ฉด ์ํ์ ๋ง๋ ๊ฐ์ ์ ์ฅํ ์ ์์ต๋๋ค.
sealed class UIState<out T> {
object Loading : UIState<Nothing>()
data class Success<out T>(val data: T) : UIState<T>()
data class Error(val exception: Throwable) : UIState<Nothing>()
object Empty : UIState<Nothing>()
}
- ๊ฐ ์ํ์ ๋ง๋ ๊ฐ์ ์ ์ฅํ๊ณ , ์ํ ๋ณํ ์ View๋ก data๋ฅผ ์ ๋ฌํ ์ ์์ต๋๋ค.
ViewModel ํ์ฉ
- ViewModel์์ LiveData๋ StateFlow๋ฅผ ์ฌ์ฉํ์ฌ ์์ฑํ UIState๋ฅผ ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
- ์ด๋ฅผ ํตํด์ ์ฌ์ฉ์์ ์ก์ ์ ๋ฐ๋ผ ๋ฐ์ดํฐ๋ฅผ ๋ก๋ํ๊ณ , UIState๋ฅผ ์ ๋ฐ์ดํธํ ์ ์์ต๋๋ค.
class MyViewModel : ViewModel() {
private val _uiState = MutableLiveData<UIState<List<String>>>()
val uiState: LiveData<UIState<List<String>>> get() = _uiState
fun fetchData() {
_uiState.value = UIState.Loading
viewModelScope.launch {
try {
val data = repository.getData() // ๋ฐ์ดํฐ ๋ก๋
if (data.isEmpty()) {
_uiState.value = UIState.Empty
} else {
_uiState.value = UIState.Success(data)
}
} catch (e: Exception) {
_uiState.value = UIState.Error(e)
}
}
}
}
UI์์ ์ํ ๋ ๋๋ง
- UI ์์ ViewModel์ด ๊ด๋ฆฌํ๋ UIState๋ฅผ ๊ตฌ๋ ํ์ฌ ์ํ์ ๋ฐ๋ผ UI๋ฅผ ์ ๋ฐ์ดํธํ ์ ์์ต๋๋ค.
@Composable
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.observeAsState()
when (uiState) {
is UIState.Loading -> {
// ๋ก๋ฉ ํ๋ฉด ํ์
CircularProgressIndicator()
}
is UIState.Success -> {
// ๋ฐ์ดํฐ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ๋ก๋๋์์ ๋
val data = (uiState as UIState.Success).data
Text("Data: $data")
}
is UIState.Empty -> {
// ๋ฐ์ดํฐ๊ฐ ์์ ๋
Text("No data available.")
}
is UIState.Error -> {
// ์๋ฌ ๋ฐ์ ์
val error = (uiState as UIState.Error).exception
Text("Error: ${error.message}")
}
}
}
- ๋ก๋ฉ ์ฑ๊ณต, ์คํจ, ์๋ฃ ๋ฑ ๋ค์ํ ์ํ์ ๋ฐ๋ผ UI๊ฐ ๋ ๋๋ง ๋ฉ๋๋ค.
์ํ ๊ด๋ฆฌ์ ์ค์์ฑ
- UIState๋ ์ฑ์์ ๋ค์ํ ์ํ๋ฅผ ๊ด๋ฆฌํ๊ณ , ๊ทธ ์ํ์ ๋ฐ๋ผ UI๋ฅผ ๋์ ์ผ๋ก ๋ณํ์ํฌ ์ ์๋๋ก ๋์์ฃผ๋ ์ค์ํ ํจํด์ ๋๋ค.
- Android์์๋ ViewModel๊ณผ LiveData, StateFlow๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฌํ ์ํ๋ฅผ ๊ด๋ฆฌํ๊ณ UI๊ฐ ์ด๋ฅผ ๊ตฌ๋ ํ๋๋ก ํ์ฌ ์ ์ ํ ํ๋ฉด์ ๋ ๋๋งํ ์ ์์ต๋๋ค.
์ผ๊ด์ฑ ์๋ UI ์ ๊ณต
- ์ฑ์ ์ํ ๋ณํ์ ๋ฐ๋ผ ์ผ๊ด๋๊ฒ UI๋ฅผ ๊ฐฑ์ ํ ์ ์์ต๋๋ค.
์ ์ง๋ณด์์ฑ
- ๊ฐ ์ํ์ ๋ํ ์ฒ๋ฆฌ๊ฐ ๋ถ๋ฆฌ๋์ด ์์ผ๋ฏ๋ก ์ฝ๋๊ฐ ๋ ์ฝ๊ธฐ ์ฝ๊ณ ์ ์ง๋ณด์ํ๊ธฐ ์ฌ์์ง๋๋ค.
๋ฒ๊ทธ ๋ฐฉ์ง
- ์ํ์ ๋ฐ๋ผ UI๋ฅผ ๋ช ์์ ์ผ๋ก ์ฒ๋ฆฌํจ์ผ๋ก์จ ์ค๋ฅ, ๋น List ๋ฑ์ ์์ธ์ ์ธ ๊ฒฝ์ฐ๋ฅผ ์ฝ๊ฒ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.