일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- Android Compose
- power menu
- AAC ViewModel
- Hilt 에러
- Sticky Header RecyclerView
- android orbit
- hilt
- RecyclerView Sticky Header
- 코루틴
- 백준 2615
- runCatching
- 안드로이드 무한 스크롤
- 백준
- 코루틴 공식 문서
- 코틀린 에러 핸들링
- 코루틴 공식문서
- 스레드 vs 코루틴
- Android Custom View
- Unsupported metadata version. Check that your Kotlin version is >= 1.0: java.lang.IllegalStateException
- viewmodel
- 힐트
- Thread vs Coroutine
- Kotlin Serialization
- Coroutine
- 안드로이드 커스텀 뷰
- BOJ
- android compose orbit
- 안드로이드 컴포즈
- Android Compose Navigation
- power menu 라이브러리
- Today
- Total
Beeeam
Orbit 본문
Orbit 라이브러리를 사용하면 MVI 패턴을 더 쉽게 적용할 수 있다. 물론 다른 라이브러리들을 사용해도 MVI 패턴을 적용하는데 도움을 받을 수 있다.

위의 이미지는 다른 라이브러리들과 orbit을 비교한 표이다. 위의 표를 통해서 orbit 라이브러리가 다양한 면에서 장점을 가지고 있음을 알 수 있다.
밑에서 더 자세히 볼 수 있음
https://appmattus.medium.com/top-android-mvi-libraries-in-2021-de1afe890f27
Top Android MVI libraries in 2021
Comparing redux and MVVM+ style MVI libraries
appmattus.medium.com
Use Orbit
Orbit 라이브러리는 상태(state)와 부수효과(side effect)를 담는 Container 라는 개념을 사용한다. 그리고 ViewModel을 ContainerHost로 사용하여 ViewModel 에서 상태와 부수효과를 관리하는데 이 때문에 상태관리가 편해진다.
상태와 부수효과는 DSL을 사용하여 변경하는데 의미는 다음과 같다.
- intent: Container 내의 상태 및 부수효과를 변경하기 위한 빌드 함수
- reduce: 현재 상태와 들어온 이벤트를 기반으로 상태를 생성
- postSideEffect: 상태와 관련 없는 부수효과를 발생
다음은 Orbit Github에 있는 예제 코드이다.
Define contract
data class CalculatorState(
val total: Int = 0
)
sealed class CalculatorSideEffect {
data class Toast(val text: String) : CalculatorSideEffect()
}
상태와 부수효과를 위와 같이 정의한다.
ViewModel
class CalculatorViewModel: ContainerHost<CalculatorState, CalculatorSideEffect>, ViewModel() {
// Include `orbit-viewmodel` for the factory function
override val container = container<CalculatorState, CalculatorSideEffect>(CalculatorState())
fun add(number: Int) = intent {
postSideEffect(CalculatorSideEffect.Toast("Adding $number to ${state.total}!"))
reduce {
state.copy(total = state.total + number)
}
}
}
ViewModel을 ContainerHost로 구현한다.
상태나 부수효과는 intent, reduce, postSideEffect를 사용하여 변경한다.
Connect ViewModel to Activity
@Composable
fun CalculatorScreen(viewModel: CalculatorViewModel) {
val state = viewModel.collectAsState().value
viewModel.collectSideEffect { handleSideEffect(it) }
// render UI using data from 'state'
...
}
private fun handleSideEffect(sideEffect: CalculatorSideEffect) {
when (sideEffect) {
is CalculatorSideEffect.Toast -> toast(sideEffect.text)
}
}
Orbit Example
다음 코드는 본인이 Orbit 라이브러리를 사용하여 구현한 간단한 카운트 앱이다.
Contract
data class MainState(
val isLoading: Boolean = true,
val count: Int = 0,
)
sealed interface MainSideEffect {
data class ToastMsg(val msg: String) : MainSideEffect
}
위에서 살펴본 예제의 Contract와 큰 차이는 없다.
ViewModel
class MainViewModel @Inject constructor() : ContainerHost<MainState, MainSideEffect>, ViewModel() {
override val container: Container<MainState, MainSideEffect> = container(MainState())
fun loading() = intent {
showLoadingScreen()
delay(2000L)
hideLoadingScreen()
}
fun updateCount(count: Int) = intent {
if (state.count > count) {
toastMsg("Count Minus, Count: $count")
} else {
toastMsg("Count Add, Count: $count")
}
reduce { state.copy(count = count) }
}
private fun toastMsg(msg: String) = intent {
postSideEffect(MainSideEffect.ToastMsg(msg))
}
private fun showLoadingScreen() = intent { reduce { state.copy(isLoading = true) } }
private fun hideLoadingScreen() = intent { reduce { state.copy(isLoading = false) } }
}
위에서 설명했던 것처럼 ViewModel에서 intent, reduce, postSideEffect를 사용하여 상태 및 부수효과를 변경하면 된다.
Activity
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MVIPracticeTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
) {
MainRoute()
}
}
}
}
}
@Composable
fun MainRoute(
viewModel: MainViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val uiState = viewModel.collectAsState().value
viewModel.collectSideEffect { sideEffect ->
when (sideEffect) {
is MainSideEffect.ToastMsg -> Toast.makeText(context, sideEffect.msg, Toast.LENGTH_SHORT).show()
}
}
LaunchedEffect(key1 = Unit) {
viewModel.loading()
}
MainScreen(
uiState = uiState,
updateCount = viewModel::updateCount,
)
}
@Composable
fun MainScreen(
uiState: MainState = MainState(),
updateCount: (Int) -> Unit = {},
) {
Box(
modifier = Modifier.fillMaxSize(),
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = uiState.count.toString(),
fontSize = 30.sp,
)
Row(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 40.dp),
horizontalArrangement = Arrangement.spacedBy(40.dp),
) {
UpdateButton(
title = "Add",
onClick = { updateCount(uiState.count + 1) },
)
UpdateButton(
title = "Minus",
onClick = { updateCount(uiState.count - 1) },
)
}
if (uiState.isLoading) {
LoadingScreen()
}
}
}
@Composable
fun LoadingScreen() {
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
modifier = Modifier.size(48.dp),
strokeWidth = 6.dp,
color = Color.Black,
)
}
}
@Composable
fun UpdateButton(
modifier: Modifier = Modifier,
title: String,
onClick: () -> Unit,
) {
Box(
modifier = modifier
.clip(RoundedCornerShape(10.dp))
.background(Color.Black)
.clickable(onClick = onClick),
) {
Text(
modifier = Modifier
.align(Alignment.Center)
.padding(20.dp),
text = title,
fontSize = 20.sp,
fontWeight = FontWeight.ExtraBold,
color = Color.White,
)
}
}
Activity는 예제 코드와 차이가 있다.
제일 큰 차이점은 Route의 존재이다. Screen 컴포저블만 사용해도 충분히 구현 가능하지만 Screen 컴포저블은 이름처럼 화면 즉 UI의 역할만을 하도록 하기 위해서 분리하였다. 그래서 Route에서는 상태 및 부수효과에 대한 처리를 하고 Screen에서는 뷰를 정의하도록 했다.
viewModel.collectAsState() 함수를 사용하여 ViewModel로 부터 상태 값들을 수집하고
viewModel.collectSideEffect 함수를 사용하여 부수효과들을 수집하고 이에 대한 처리를 진행한다.
위의 프로젝트는 밑에서 확인할 수 있다.
https://github.com/BEEEAM-J/MVI_Practice
GitHub - BEEEAM-J/MVI_Practice
Contribute to BEEEAM-J/MVI_Practice development by creating an account on GitHub.
github.com
'Kotlin' 카테고리의 다른 글
직렬화, 역직렬화, Kotlin Serialization (0) | 2024.01.22 |
---|---|
[Kotlin In Action] 6장. 코틀린 타입 시스템 (0) | 2023.08.18 |
[Kotlin In Action] 5장. 람다로 프로그래밍 (0) | 2023.08.04 |
[Kotlin In Action] 4장. 클래스, 객체, 인터페이스 (0) | 2023.07.26 |
[Kotlin In Action] 3장. 함수 정의와 호출 (0) | 2023.07.14 |