{"id":46377,"date":"2023-01-01T12:14:20","date_gmt":"2023-01-01T03:14:20","guid":{"rendered":"https:\/\/www.charlezz.com\/?p=46377"},"modified":"2023-01-01T23:04:32","modified_gmt":"2023-01-01T14:04:32","slug":"android-mvi-%eb%9d%bc%ec%9d%b4%eb%b8%8c%eb%9f%ac%eb%a6%ac-orbit-mvi","status":"publish","type":"post","link":"https:\/\/charlezz.com\/?p=46377","title":{"rendered":"Android MVI \ub77c\uc774\ube0c\ub7ec\ub9ac Orbit-MVI"},"content":{"rendered":"\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" loading=\"lazy\" width=\"1024\" height=\"307\" src=\"https:\/\/www.charlezz.com\/wordpress\/wp-content\/uploads\/2023\/01\/www.charlezz.com-logo-1-1024x307.png\" alt=\"\" class=\"wp-image-46383\" srcset=\"https:\/\/charlezz.com\/wordpress\/wp-content\/uploads\/2023\/01\/www.charlezz.com-logo-1-1024x307.png 1024w, https:\/\/charlezz.com\/wordpress\/wp-content\/uploads\/2023\/01\/www.charlezz.com-logo-1-300x90.png 300w, https:\/\/charlezz.com\/wordpress\/wp-content\/uploads\/2023\/01\/www.charlezz.com-logo-1-768x230.png 768w, https:\/\/charlezz.com\/wordpress\/wp-content\/uploads\/2023\/01\/www.charlezz.com-logo-1-1536x461.png 1536w, https:\/\/charlezz.com\/wordpress\/wp-content\/uploads\/2023\/01\/www.charlezz.com-logo-1.png 2000w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>\uc774\uc804 \ud3ec\uc2a4\ud305 <a href=\"https:\/\/www.charlezz.com\/?p=46365\">Android \ud504\ub85c\uc81d\ud2b8\uc5d0 MVI \ub3c4\uc785\ud558\uae30<\/a> \ub97c \uba3c\uc800 \uc77d\ub294 \uac83\uc744 \uad8c\uc7a5\ud569\ub2c8\ub2e4.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Orbit \uac1c\uc694<\/h2>\n\n\n\n<p>Orbit\uc740 \uc548\ub4dc\ub85c\uc774\ub4dc \ubfd0\ub9cc \uba40\ud2f0\ud50c\ub7ab\ud3fc\uc744 \uc9c0\uc6d0\ud558\ub294 Redux\/MVI \uac19\uc740 \ub77c\uc774\ube0c\ub7ec\ub9ac \uc774\uba70, \uc27d\uace0 \uac00\ubcbc\uc6b4 \uac83\uc774 \ud2b9\uc9d5\uc774\ub2e4. \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 \uc544\ub798\uc758 \ub9c1\ud06c\uc5d0\uc11c \ucc38\uc870\ud558\uc790<\/p>\n\n\n\n<ul><li><a href=\"https:\/\/github.com\/orbit-mvi\/orbit-mvi\">orbit-mvi github \uc800\uc7a5\uc18c<\/a><\/li><li><a href=\"https:\/\/orbit-mvi.org\/\">\uacf5\uc2dd \ud398\uc774\uc9c0<\/a><\/li><\/ul>\n\n\n\n<p>orbit\uc744 \ud504\ub85c\uc81d\ud2b8\uc5d0 \ucd94\uac00\ud558\uae30 \uc704\ud574 build.gradle\uc5d0 \ub2e4\uc74c \uc758\uc874\uc131\uc744 \ucd94\uac00 \ud560 \uc218 \uc788\ub2e4.(<a href=\"https:\/\/search.maven.org\/artifact\/org.orbit-mvi\/orbit-viewmodel\">\ucd5c\uc2e0\ubc84\uc804 \ud655\uc778<\/a>)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>implementation(\"org.orbit-mvi:orbit-core:&lt;latest-version&gt;\")\n\/\/ or, if on Android:\nimplementation(\"org.orbit-mvi:orbit-viewmodel:&lt;latest-version&gt;\")\n\/\/ If using Jetpack Compose include\nimplementation(\"org.orbit-mvi:orbit-compose:&lt;latest-version&gt;\")\n\n\/\/ Tests\ntestImplementation(\"org.orbit-mvi:orbit-test:&lt;latest-version&gt;\")<\/code><\/pre>\n\n\n\n<p>Orbit\uc740 \ub2e4\uc74c\uacfc \uac19\uc740 \ud2b9\uc9d5\uc744 \uac00\uc9c4\ub2e4.<\/p>\n\n\n\n<ul><li>\uc27d\uace0, \uc548\uc804\ud55c \ud0c0\uc785, \ucf54\ub8e8\ud2f4 \uc2a4\ud0c0\uc77c \ubc0f \ud655\uc7a5 API \uc9c0\uc6d0<\/li><li>iOS \ubc0f \uc548\ub4dc\ub85c\uc774\ub4dc\ub97c \ud0c0\uac8c\ud305\ud55c \uba40\ud2f0\ud50c\ub7ab\ud3fc \uc9c0\uc6d0<\/li><li>\ucf54\ud2c0\ub9b0 \ucf54\ub8e8\ud2f4 \uc644\ubcbd \uc9c0\uc6d0<\/li><li>\uc0dd\uba85\uc8fc\uae30\uc5d0 \uc548\uc804\ud55c flow \uc218\uc9d1<\/li><li>SavedState\ub97c \ud3ec\ud568\ud55c ViewModel \uc9c0\uc6d0<\/li><li>\uac04\ub2e8\ud55c \ub2e8\uc704 \ud14c\uc2a4\ud2b8 \uc9c0\uc6d0<\/li><li>\ub0b4\uc7a5\ub41c Espresso \uc720\ud734 \uc790\uc6d0 \uc9c0\uc6d0<\/li><li>RxJava \ubc0f LiveData\uc640 \ud638\ud658 \uc9c0\uc6d0<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Orbit \uc744 \uc368\uc57c \ud558\ub294 \uc774\uc720<\/h2>\n\n\n\n<p>MVI \ud328\ud134\uc744 \uad6c\ud604\ud558\uae30 \uc704\ud574\uc11c \ubcc4\ub3c4\uc758 \ub77c\uc774\ube0c\ub7ec\ub9ac \ub610\ub294 \ud504\ub808\uc784\uc6cc\ud06c\uac00 \ud544\uc218\ub294 \uc544\ub2c8\uc9c0\ub9cc, \ub0b4\uac00 Orbit\uc744 \uc120\ud0dd\ud55c \uc774\uc720\ub294 \ub2e4\uc74c\uacfc \uac19\ub2e4.<\/p>\n\n\n\n<ul><li>MVI\uc758 \uac1c\ub150\uc744 \uadf8\ub300\ub85c \ub530\ub984<\/li><li>\ud0c0 \ub77c\uc774\ube0c\ub7ec\ub9ac\uc5d0 \ube44\ud574 \ubc30\uc6b0\uae30 \uc26c\uc6c0<\/li><li>\ubcf4\uc77c\ub7ec\ud50c\ub808\uc774\ud2b8 \uc81c\uac70<\/li><li>\uc0ac\uc6a9\ud558\uae30 \uc26c\uc6b4 \ucf54\ub8e8\ud2f4<\/li><\/ul>\n\n\n\n<p>\uadf8 \uc678\uc5d0\ub3c4 Matthew Dolan\uc758 <a href=\"https:\/\/appmattus.medium.com\/top-android-mvi-libraries-in-2021-de1afe890f27\">\uc544\ud2f0\ud074<\/a> \uc744 \uc77d\uc5b4\ubcf4\uba74 \ub2e4\ub978 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub4e4\uacfc \uc885\ud569\uc801\uc73c\ub85c \ube44\uad50\ud55c \ub0b4\uc6a9\uc774 \uc788\ub294\ub370, \uc0c1\ub2f9\ubd80\ubd84 \uacf5\uac10\ud558\uae30 \ub54c\ubb38\uc5d0 orbit\uc744 \uc120\ud0dd\ud588\ub2e4.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1400\" height=\"905\" src=\"https:\/\/www.charlezz.com\/wordpress\/wp-content\/uploads\/2023\/01\/www.charlezz.com-android-mvi-orbit-mvi-1-2-a83hdqw0egi9kdhujuzq.webp\" alt=\"\" class=\"wp-image-46385\"\/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">\uae30\ubcf8\uc801\uc778 \uc0ac\uc6a9\ubc95<\/h2>\n\n\n\n<p>Orbit\uc740 \uc0c1\ud0dc(State)\uc640 \ubd80\uc218\ud6a8\uacfc(Side Effects)\ub97c \uad00\ub9ac\ud558\ub294 Container\ub77c\ub294 \uac1c\ub150\uc744 \uc815\uc758\ud558\uace0 \uc0ac\uc6a9\ud558\uace0 \uc788\ub2e4. \uc77c\ubc18\uc801\uc73c\ub85c ViewModel\uc774 Container Host\uac00 \ub418\uc5b4 Container\ub97c \uad00\ub9ac\ud558\uac8c \ub418\ub294\ub370, \uc774\ub85c \uc778\ud574 \uc0c1\ud0dc \ubc0f \ubd80\uc218\ud6a8\uacfc\ub97c \ub2e4\ub8e8\uae30\uac00 \uc26c\uc6cc\uc9c0\uace0, \ubcf4\uc77c\ub7ec\ud50c\ub808\uc774\ud2b8\ub97c \uac10\uc18c \uc2dc\ud0ac \uc218 \uc788\ub2e4.<\/p>\n\n\n\n<p>\uc6b0\uc120 \uacf5\uc2dd\ubb38\uc11c\uc5d0 \ub098\uc628 \uc608\uc81c\ub97c \uae30\uc900\uc73c\ub85c \uc124\uba85\ud558\uc790\uba74 \uc0c1\ud0dc\uc640 \ubd80\uc218\ud6a8\uacfc\ub97c \uba3c\uc800 \ub2e4\uc74c\uacfc \uac19\uc774 \uc815\uc758\ud55c\ub2e4<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>data class CalculatorState(\n    val total: Int = 0\n)\n\nsealed class CalculatorSideEffect {\n    data class Toast(val text: String) : CalculatorSideEffect()\n}<\/code><\/pre>\n\n\n\n<p>ViewModel\uc5d0\uc11c \ucee8\ud14c\uc774\ub108 \ud65c\uc6a9\uc744 \uc704\ud574 \ub2e4\uc74c\uacfc \uac19\uc774 \ucf54\ub4dc\ub97c \uc791\uc131\ud55c\ub2e4.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class CalculatorViewModel: ContainerHost&lt;CalculatorState, CalculatorSideEffect&gt;, ViewModel() {\n\n    \/\/ Include `orbit-viewmodel` for the factory function\n    override val container = container&lt;CalculatorState, CalculatorSideEffect&gt;(CalculatorState())\n\n    fun add(number: Int) = intent {\n        postSideEffect(CalculatorSideEffect.Toast(\"Adding $number to ${state.total}!\"))\n\n        reduce {\n            state.copy(total = state.total + number)\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p>\uc704 \ucf54\ub4dc\uc758 \ub0b4\uc6a9\uc740 \ub2e4\uc74c\uacfc \uac19\ub2e4.<\/p>\n\n\n\n<ul><li>\uc77c\ubc18\uc801\uc73c\ub85c \uc548\ub4dc\ub85c\uc774\ub4dc\uc5d0\uc11c\ub294 ViewModel\uc744 ContainerHost(\uc778\ud130\ud398\uc774\uc2a4)\ub85c \uad6c\ud604\ud55c\ub2e4.<\/li><li>\uc704 \uc778\ud130\ud398\uc774\uc2a4\ub97c \uad6c\ud604\ud558\uac8c \ub418\uba74 container\ub97c \uc0dd\uc131\ud574\uc57c\ud558\ub294\ub370, container&lt;State, SideEffect&gt;(&#8230;) \ud329\ud1a0\ub9ac \ud568\uc218\ub97c \ud65c\uc6a9\ud560 \uc218 \uc788\ub2e4.<\/li><li>intent, reduce, postSideEffect \uc640 \uac19\uc740 dsl\uc744 \ud65c\uc6a9\ud558\uc5ec \uc0c1\ud0dc \ubc0f \ubd80\uc218\ud6a8\uacfc\ub97c \ubcc0\uacbd\ud55c\ub2e4.<\/li><\/ul>\n\n\n\n<p>orbit\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 DSL\uc758 \uc758\ubbf8\ub294 \ub2e4\uc74c\uacfc \uac19\ub2e4<\/p>\n\n\n\n<ul><li>intent : \ucee8\ud14c\uc774\ub108 \ub0b4\uc5d0 \uc788\ub294 \uc0c1\ud0dc \ubc0f \ubd80\uc218\ud6a8\uacfc\ub97c \ubcc0\uacbd\ud558\uae30 \uc704\ud55c \ube4c\ub4dc \ud568\uc218<\/li><li>reduce : \ud604\uc7ac \uc0c1\ud0dc\uc640 \ub4e4\uc5b4\uc628 \uc774\ubca4\ud2b8\ub97c \ud1a0\ub300\ub85c \uc0c8\ub85c\uc6b4 \uc0c1\ud0dc\ub97c \ub9cc\ub4e4\uc5b4 \ub0b8\ub2e4.<\/li><li>postSideEffect : \uc0c1\ud0dc \ubcc0\uacbd\uacfc \uad00\ub828 \uc5c6\ub294 \uc774\ubca4\ud2b8\ub4e4\uc744 \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \ubd80\uc218\ud6a8\uacfc\ub97c \ubc1c\uc0dd \uc2dc\ud0a8\ub2e4.<\/li><\/ul>\n\n\n\n<p>\uc774\uc81c ViewModel(ContainerHost)\uc744 Activity \ub610\ub294 Fragment \uc5d0 \uc5f0\uacb0\ud574\ubcf4\uc790<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>class CalculatorActivity: AppCompatActivity() {\n\n    private val viewModel by viewModel&lt;CalculatorViewModel&gt;()\n\n    override fun onCreate(savedState: Bundle?) {\n        ...\n        addButton.setOnClickListener { viewModel.add(1234) }\n\n        \/\/ orbit-viewmodel \ubaa8\ub4c8\uc744 \uc0ac\uc6a9\ud558\uc5ec Lifecycle.State.STARTED\uc77c \ub54c,\n        \/\/ \uc0c1\ud0dc\uc640 \ubd80\uc218\ud6a8\uacfc\ub97c observe \ud55c\ub2e4.\n        viewModel.observe(state = ::render, sideEffect = ::handleSideEffect)\n\n        \/\/ \ub610\ub294 \uc2a4\ud2b8\ub9bc\uc744 \uc9c1\uc811\uc801\uc73c\ub85c observe \ud560 \uc218\ub3c4 \uc788\ub2e4.\n        lifecycleScope.launch {\n            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {\n                launch {\n                    viewModel.container.stateFlow.collect { render(it) }\n                }\n                launch {\n                    viewModel.container.sideEffectFlow.collect { handleSideEffect(it) }\n                }\n            }\n        }\n    }\n\n    private fun render(state: CalculatorState) {\n        ...\n    }\n\n    private fun handleSideEffect(sideEffect: CalculatorSideEffect) {\n        when (sideEffect) {\n            is CalculatorSideEffect.Toast -&gt; toast(sideEffect.text)\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p>viewModel.container \ub97c \ud1b5\ud574 stateFlow(\uc0c1\ud0dc)\uc640 sideEffectFlow(\ubd80\uc218\ud6a8\uacfc)\uc5d0 \uc811\uadfc \ud560 \uc218 \uc788\uc73c\uba70, \uc774\ub97c \uc9c1\uc811 \uc218\uc9d1\ud558\uac70\ub098 orbit-viewmodel \ubaa8\ub4c8 \uc758\uc874\uc131\ucd94\uac00\ub97c \ud1b5\ud574 \uc190\uc27d\uac8c viewModel.observe(&#8230;)\ub97c \ud638\ucd9c \ud560 \uc218\ub3c4 \uc788\ub2e4.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">\uc608\uc81c\ucf54\ub4dc orbit\uc73c\ub85c \ub9c8\uc774\uadf8\ub808\uc774\uc158<\/h2>\n\n\n\n<p><meta charset=\"utf-8\"><a href=\"https:\/\/www.charlezz.com\/?p=46365\">Android \ud504\ub85c\uc81d\ud2b8\uc5d0 MVI \ub3c4\uc785\ud558\uae30<\/a> \uc5d0\uc11c \uc124\uba85\ud55c \uc5d0\uc81c \ucf54\ub4dc\uc5d0\uc11c \uae30\uc874 ViewModel \ucf54\ub4dc\ub97c orbit \uc2a4\ud0c0\uc77c\ub85c \ub9c8\uc774\uadf8\ub808\uc774\uc158 \ud574\ubcf4\uc790.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ AS-IS : orbit \uc0ac\uc6a9 \uc804\n@HiltViewModel\nclass MainViewModel @Inject constructor(\n    private val repository: MainRepository\n) : ViewModel() {\n\n    private val events = Channel&lt;MainEvent&gt;()\n\n    val state: StateFlow&lt;MainState&gt; = events.receiveAsFlow()\n        .runningFold(MainState(), ::reduceState)\n        .stateIn(viewModelScope, SharingStarted.Eagerly, MainState())\n\n    private val _sideEffects = Channel&lt;String&gt;()\n\n    val sideEffects = _sideEffects.receiveAsFlow()\n\n    private fun reduceState(current: MainState,event:MainEvent):MainState{\n        return when(event){\n            MainEvent.Loading -&gt; {\n                current.copy(loading = true)\n            }\n            is MainEvent.Loaded -&gt; {\n                current.copy(loading = false, users = event.users)\n            }\n        }\n    }\n\n    fun fetchUser() {\n        viewModelScope.launch {\n            events.send(MainEvent.Loading)\n            val users = repository.getUsers()\n            events.send(MainEvent.Loaded(users = users))\n            _sideEffects.send(\"${users.size} user(s) loaded\")\n        }\n    }\n\n}\n\n\n\/\/ TO-BE : orbit \uc0ac\uc6a9 \ud6c4\n@HiltViewModel\nclass MainViewModel @Inject constructor(\n    private val repository: MainRepository\n) : ViewModel(), ContainerHost&lt;MainState, String&gt; {\n\n    override val container: Container&lt;MainState, String&gt; = container(MainState())\n\n    fun fetchUser() = intent{\n        viewModelScope.launch {\n            reduce { state.copy(loading = true) }\n            val users = repository.getUsers()\n            reduce { state.copy(users = users, loading = false) }\n            postSideEffect(\"${users.size} user(s) loaded\")\n        }\n    }\n\n}<\/code><\/pre>\n\n\n\n<p>\ub3d9\uc77c\ud55c \uae30\ub2a5\uc744 \ud558\ub294 \ucf54\ub4dc \uc778\ub370 \ucf54\ub4dc \ub77c\uc778 \uc218\uac00 \uc0c1\ub2f9 \uc218 \uc904\uc5b4\ub4e0 \uac83\uc744 \ud655\uc778\ud560 \uc218 \uc788\ub2e4.<\/p>\n\n\n\n<p>(<a href=\"https:\/\/github.com\/Charlezz\/MVIExample\/blob\/orbit\/app\/src\/main\/java\/com\/charlezz\/mviexample\/ui\/MainViewModel.kt\">\uc608\uc81c\ucf54\ub4dc<\/a>)<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\uc774\uc804 \ud3ec\uc2a4\ud305 Android \ud504\ub85c\uc81d\ud2b8\uc5d0 MVI \ub3c4\uc785\ud558\uae30 \ub97c \uba3c\uc800 \uc77d\ub294 \uac83\uc744 \uad8c\uc7a5\ud569\ub2c8\ub2e4. Orbit \uac1c\uc694 Orbit\uc740 \uc548\ub4dc\ub85c\uc774\ub4dc \ubfd0\ub9cc \uba40\ud2f0\ud50c\ub7ab\ud3fc\uc744 \uc9c0\uc6d0\ud558\ub294 Redux\/MVI \uac19\uc740 \ub77c\uc774\ube0c\ub7ec\ub9ac \uc774\uba70, \uc27d\uace0 \uac00\ubcbc\uc6b4 \uac83\uc774 \ud2b9\uc9d5\uc774\ub2e4. \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 \uc544\ub798\uc758 \ub9c1\ud06c\uc5d0\uc11c \ucc38\uc870\ud558\uc790 orbit-mvi github \uc800\uc7a5\uc18c \uacf5\uc2dd \ud398\uc774\uc9c0 orbit\uc744 \ud504\ub85c\uc81d\ud2b8\uc5d0 \ucd94\uac00\ud558\uae30 \uc704\ud574 build.gradle\uc5d0 \ub2e4\uc74c \uc758\uc874\uc131\uc744 \ucd94\uac00 \ud560 \uc218 \uc788\ub2e4.(\ucd5c\uc2e0\ubc84\uc804 \ud655\uc778) Orbit\uc740 \ub2e4\uc74c\uacfc [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"om_disable_all_campaigns":false,"inline_featured_image":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0},"categories":[38,3,5],"tags":[],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/charlezz.com\/index.php?rest_route=\/wp\/v2\/posts\/46377"}],"collection":[{"href":"https:\/\/charlezz.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/charlezz.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/charlezz.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/charlezz.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=46377"}],"version-history":[{"count":7,"href":"https:\/\/charlezz.com\/index.php?rest_route=\/wp\/v2\/posts\/46377\/revisions"}],"predecessor-version":[{"id":46397,"href":"https:\/\/charlezz.com\/index.php?rest_route=\/wp\/v2\/posts\/46377\/revisions\/46397"}],"wp:attachment":[{"href":"https:\/\/charlezz.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=46377"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/charlezz.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=46377"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/charlezz.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=46377"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}