Android Architecture - Picplz/picplz-aos GitHub Wiki

์•ˆ๋“œ๋กœ์ด๋“œ ์•„ํ‚คํ…์ฒ˜ ๋ฐ ํŒจํ„ด

๊ธฐ๋ณธ ์›์น™

  • ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ๊ถŒ์žฅํ•˜๋Š” 3๊ฐ€์ง€ ๊ณ„์ธต์— ๋”ฐ๋ผ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • MVIํŒจํ„ด์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ํ๋ฆ„์ด ๋‹จ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋ฃจ์–ด์ง€๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.




ํด๋” ๊ตฌ์กฐ

โ”œโ”€โ”€ Application.kt
โ”œโ”€โ”€ MainActivity.kt
โ”œโ”€โ”€ data
โ”‚   โ”œโ”€โ”€ model
โ”‚   โ”œโ”€โ”€ repository
โ”‚   โ”œโ”€โ”€ service
โ”‚   โ””โ”€โ”€ source
โ”œโ”€โ”€ di
โ”œโ”€โ”€ domain
โ”‚   โ”œโ”€โ”€ model
โ”‚   โ”œโ”€โ”€ repository
โ”‚   โ””โ”€โ”€ usecase
โ”œโ”€โ”€ navigation
โ”‚   โ””โ”€โ”€ NavHost.kt
โ”œโ”€โ”€ ui
โ”‚   โ”œโ”€โ”€ component
โ”‚   โ”‚   โ””โ”€โ”€ login
โ”‚   โ”‚       โ””โ”€โ”€ LoginItem.kt
โ”‚   โ”œโ”€โ”€ screen
โ”‚   โ”‚   โ”œโ”€โ”€ common
โ”‚   โ”‚   โ””โ”€โ”€ login
โ”‚   โ”‚       โ”œโ”€โ”€ LoginIntent.kt
โ”‚   โ”‚       โ”œโ”€โ”€ LoginScreen.kt
โ”‚   โ”‚       โ”œโ”€โ”€ LoginSideEffect.kt
โ”‚   โ”‚       โ””โ”€โ”€ LoginState.kt
โ”‚   โ””โ”€โ”€ theme
โ”‚       โ”œโ”€โ”€ Color.kt
โ”‚       โ”œโ”€โ”€ Font.kt
โ”‚       โ”œโ”€โ”€ Theme.kt
โ”‚       โ””โ”€โ”€ Type.kt
โ””โ”€โ”€ viewmodel




MVI ํŒจํ„ด

image

MVI (Model-View-Intent) ํŒจํ„ด์—์„œ๋Š” ๋ฐ์ดํ„ฐ ํ๋ฆ„์ด ๋‹จ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋ฃจ์–ด์ง„๋‹ค.

์ƒํƒœ ๋ณ€๊ฒฝ์€ Intent๋กœ ๋ช…ํ™•ํžˆ ์ •์˜๋œ๋‹ค. View์—์„œ ๋ฐœ์ƒํ•œ Intent๋ฅผ ํ†ตํ•ด Model์ด ๋ณ€๊ฒฝ๋˜๊ณ , Model์ด ๋ณ€๊ฒฝ๋˜๋ฉด ๋ฐ˜์‘ํ•˜์—ฌ View๊ฐ€ ๋ณ€๊ฒฝ๋œ๋‹ค.



Group 1597880837



์žฅ์ 

Intent์—์„œ ์ƒํƒœ ๋ณ€๊ฒฝ์ด๋ผ๋Š” ์˜๋„(Intent)๊ฐ€ ์ •์˜๋œ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ๊ทธ ์˜๋„๊ฐ€ ๋ช…ํ™•ํ•ด์ง„๋‹ค. ๋ฐ์ดํ„ฐ ํ๋ฆ„์ด ๋‹จ๋ฐฉํ–ฅ์ด๋ฉฐ ์ถ”์ ํ•˜๊ธฐ๊ฐ€ ์‰ฝ๋‹ค.

UI ์ด๋ฒคํŠธ๊ฐ€ ๋ฐ”๋กœ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์œผ๋กœ ๋ฐ˜์˜๋˜๋Š” ๊ฒƒ(์˜ˆ: ๋ฒ„ํŠผ ํด๋ฆญ -> count++)๋ณด๋‹ค๋Š”, ์ด๋Ÿฌํ•œ ์˜๋„(Intent)๊ฐ€ ์ •์˜๋œ ๊ฐ์ฒด๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ์ด๋ฅผ ํ†ตํ•ด ๋™์ž‘์ด ๋ฐœ์ƒํ•˜๋„๋ก ํ•˜๋Š” ๊ฒƒ(๋ฒ„ํŠผ ํด๋ฆญ -> ๊ฐฏ์ˆ˜ ์ฆ๊ฐ€ -> count ++)์ด ๋ช…์‹œ์„ฑ์„ ๋†’์ž…๋‹ˆ๋‹ค. ๊ธฐ์กด ์ฝ”๋“œ ์ž‘์„ฑ์‹œ์—๋„ ui event ์Šค์ฝ”ํ”„์—์„œ ์ง์ ‘ ๋™์ž‘์ด ์ด๋ฃจ์–ด์ง€๊ธฐ๋ณด๋‹ค๋Š”, ๋ช…ํ™•ํ•˜๊ฒŒ ์ •์˜๋œ ์ค‘๊ฐ„ ๋กœ์ง์„ ๊ฑฐ์ณ์„œ ๋™์ž‘์„ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ๊ฒƒ์„ ์„ ํ˜ธํ–ˆ์Šต๋‹ˆ๋‹ค. mvi ํŒจํ„ด์€ ์ด๋ฅผ ์ฒด๊ณ„ํ™”ํ•˜๊ณ  intent๋“ค์„ ๋ถ„๋ฆฌํ•ด์„œ ๋ชจ์•„์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด ๋ฐ์ดํ„ฐ ํ๋ฆ„๊ณผ ๊ทธ ํ๋ฆ„์˜ ์˜๋„๋ฅผ ๋ณด๋‹ค ์‰ฝ๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๊ณ , ๋””๋ฒ„๊น…ํ•˜๊ธฐ์—๋„ ํŽธํ•ฉ๋‹ˆ๋‹ค.

์ƒํƒœ๋Š” ๋ถˆ๋ณ€(Immutable)ํ•˜๊ฒŒ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์„ ์›์น™์œผ๋กœ ํ•œ๋‹ค. ๋‹จ์ผ ์ƒํƒœ ๊ด€๋ฆฌ ๋ฐฉ์‹์€ ์•ˆ๋“œ๋กœ์ด๋“œ์˜ Recomposition ๊ฐœ๋…๊ณผ ์ž˜ ๋งž์•„๋–จ์–ด์ง„๋‹ค. ์†์„ฑ์— ์ง์ ‘ ์ ‘๊ทผํ•˜์—ฌ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๋Œ€์‹ , ๋ถˆ๋ณ€์˜ ์ƒํƒœ ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ๋ณ€๊ฒฝํ•  ๋•Œ๋Š” ๊ธฐ์กด ๊ฐ์ฒด๋ฅผ ๋ณต์ œํ•œ ์ƒˆ๋กœ์šด ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ด์„œ ์ „์ฒด State๋ฅผ ๋ณ€๊ฒฝํ•œ๋‹ค. ์ด๋กœ์จ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ , ์Šค๋ ˆ๋“œ ์•ˆ์ „์„ฑ์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ๋‹ค.




์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ

์•ฑ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ™”์‹œํ‚ค๊ฑฐ๋‚˜, ์™ธ๋ถ€๋กœ๋ถ€ํ„ฐ ๋‚ด๋ถ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ™”์‹œํ‚ค๋Š” ํ–‰์œ„

  • mvi ํŒจํ„ด์—์„œ state๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์ด intent์ด๋ผ ์ •์˜ํ–ˆ์ง€๋งŒ, state๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ ์™ธ์—๋„ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋“ค์ด ์กด์žฌ, ์ด๋ฅผ ๋Œ€์‘ํ•˜๋Š” ๊ฒƒ์ด side-effect
  • Intent์— ํ•ด๋‹นํ•˜์ง€ ์•Š์ง€๋งŒ View์—์„œ ๋™์ž‘ํ•œ๋‹ค

์˜ˆ์‹œ) ํ† ์ŠคํŠธ ์ถœ๋ ฅ, ๋„ค๋น„๊ฒŒ์ดํŠธ๋ฅผ ํ†ตํ•ด ๋‹ค๋ฅธ ์Šคํฌ๋ฆฐ์œผ๋กœ ์ด๋™, ํŒŒ์ผ ํ”ผ์ปค ์ถœ๋ ฅ

์ด ํ”„๋กœ์ ํŠธ์—์„œ๋Š” intent์— ํ•จ๊ป˜ ์ •์˜ํ•ด์„œ viewModel์—์„œ ๋™์ž‘์‹œํ‚ค์ง€๋งŒ intent์—์„œ ํ•œ๋ฒˆ๋” sideEffect์—์„œ ์ •์˜ํ•œ ํด๋ž˜์Šค๋ฅผ ๋™์ž‘์‹œํ‚ค๋Š” ๋กœ์ง์„ ์ ์šฉ




์ ์šฉ ์˜ˆ์‹œ

Model ์ •์˜

// ui/viewmodel/SignUpClientState.kt
package com.hm.picplz.ui.screen.sign_up

import ...

data class SignUpClientState(
    val currentStep: Int? = 0,
    val isLoading: Boolean = false,
    val error: Throwable? = null,
    val userInfo: User = emptyUserData,
    val nickname: String = "",
    val profileImageUri: Uri? = null
){
    companion object {
        fun idle(): SignUpClientState {
            return SignUpClientState(
                currentStep = 0,
                isLoading = false,
                error = null,
                userInfo = emptyUserData,
                nickname = "",
                profileImageUri = null
            )
        }
    }
}

data class๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ์˜์—ญ์—์„œ ์‚ฌ์šฉํ•  state๋“ค์„ ์ •์˜ํ•ด ์ค€๋‹ค.


data class

์ฝ”ํ‹€๋ฆฐ์—์„œ ๊ฐ์ฒด๋ฅผ ์ •์˜ํ•ด์ฃผ๋Š” ๋ฐฉ์‹

์ผ๋ฐ˜ class์™€ ๋‹ค๋ฅธ ์ 

  • ์ปดํŒŒ์ผ ์‹œ ์ž๋™์œผ๋กœ equals(), hashCode(), toString(), copy(), componentN()๊ณผ ๊ฐ™์€ ๋ฉ”์†Œ๋“œ๋“ค์„ ์ƒ์„ฑํ•ด์ค€๋‹ค.
  • ์ƒ์„ฑ์ž์— ํ”„๋กœํผํ‹ฐ๋ฅผ ์ •์˜ํ•˜๊ณ  ํด๋ž˜์Šค์˜ ํ•„๋“œ๋กœ๋„ ๋‹ค์‹œ ์ƒ์„ฑํ•ด์•ผ ํ•˜๋Š” class์™€ ๋‹ฌ๋ฆฌ ์ƒ์„ฑ์ž์— ํ”„๋กœํผํ‹ฐ๋งŒ ์ž…๋ ฅํ•˜๋ฉด ํ•„๋“œ๋„ ์ƒ์„ฑํ•ด์ค€๋‹ค. mviํŒจํ„ด์—์„œ state๋Š” ๋ถˆ๋ณ€ํ•˜๊ฒŒ ์œ ์ง€ํ•  ๊ฒƒ์ด๊ณ , ์ƒˆ๋กœ์šด state๋ฅผ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด .copy()๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— data class๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

companion object

ํด๋ž˜์Šค ๋‚ด์—์„œ ์„ ์–ธ๋œ ๊ฐ์ฒด. ๋‚ด๋ถ€์—์„œ๋ฟ ์•„๋‹ˆ๋ผ ์™ธ๋ถ€์—์„œ๋„ ์„ ์–ธํ•˜์—ฌ ์™ธ๋ถ€์—์„œ๋„ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด ์„ ์–ธ

idle(): state์˜ ์ดˆ๊ธฐ ์ƒํƒœ๋ฅผ ์„ ์–ธํ•˜๋Š” ๋ฉ”์„œ๋“œ

companion object๋กœ ์„ ์–ธํ–ˆ๊ธฐ์— ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ ๋„ idle()์„ ์„ ์–ธํ•  ์ˆ˜ ์žˆ๋‹ค.



viewModel์—์„œ ํ˜ธ์ถœ

// ui/sign_up/sign_up_client/SignUpClientViewModel
package com.hm.picplz.viewmodel

import ...

class SignUpClientViewModel : ViewModel() {
    private val _state = MutableStateFlow<SignUpClientState>(SignUpClientState.idle())
    val state: StateFlow<SignUpClientState> get() = _state
    ...
}

viewModel์€ State๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , intent๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด State๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ์ž‘์—…์ด ์ด๋ฃจ์–ด์ง€๋Š” ์˜์—ญ

_state

  • ํƒ€์ž…: MutableStateFlow<SignUpClientState> - ๋ณ€๊ฒฝ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ ํ๋ฆ„ ํƒ€์ž…
  • ์ดˆ๊ธฐ๊ฐ’: SignUpClientState.idle()
  • ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋Š” ๋ณ€์ˆ˜, ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— private์œผ๋กœ ์„ ์–ธ๋˜์–ด ViewModel ๋‚ด๋ถ€์—์„œ๋งŒ ์ˆ˜์ • ๊ฐ€๋Šฅ

state

  • ํƒ€์ž…: StateFlow<SignUpClientState> - ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒํƒœ ํ๋ฆ„ ํƒ€์ž…
  • get() = ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜
  • view์—์„œ ์ด state๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ ์‚ฌ์šฉ
    • view์—์„œ๋Š” state๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ์ œ๊ณต



Intent ์ •์˜

// ui/sign_up/sign_up_client/SignUpClientIntent.kt
package com.hm.picplz.ui.screen.sign_up.sign_up_client

import ...

sealed class SignUpClientIntent {
    data class SetUserInfo(val userInfo: User) : SignUpClientIntent()
    data class SetNickname(val newNickname: String) : SignUpClientIntent()
    data class SetProfileImageUri(val newProfileImageUri: Uri?) : SignUpClientIntent()
    data object NavigateToPrev : SignUpClientIntent()
    data class ChangeStep(val stepNum: Int) : SignUpClientIntent()
    data object ClickSubmitButton : SignUpClientIntent()
}

Intent๋Š” ์ƒํƒœ๋ฅผ ์กฐ์ž‘ํ•˜๋Š” ์ด๋ฒคํŠธ

์ด ํŒŒ์ผ ๋‚ด์—์„œ ์ด ์ด๋ฒคํŠธ๋“ค์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์ •์˜


sealed class

์ƒ์†ํ•  ์ˆ˜ ์žˆ๋Š” ํ•˜์œ„ ํด๋ž˜์Šค์˜ ์ •์˜๋ฅผ ์ œํ•œํ•˜๋Š” ํด๋ž˜์Šค

์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค

  • ๊ฐ™์€ ํŒŒ์ผ ๋‚ด์—์„œ๋งŒ ํ•˜์œ„ ํด๋ž˜์Šค๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด ํŒŒ์ผ ๋‚ด์— ๋ช…ํ™•ํ•˜๊ฒŒ ํ•˜๋‚˜์˜ ๊ณ„์ธต์œผ๋กœ ํ•˜์œ„ ํด๋ž˜์Šค๋“ค์„ ์ •์˜ ๊ฐ€๋Šฅ
  • when๋ฌธ์„ ์‚ฌ์šฉํ•  ๋•Œ ๋ชจ๋“  ํ•˜์œ„ ํด๋ž˜์Šค๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋„๋ก ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ํ™•์ธํ•ด์ฃผ์–ด ์—„๊ฒฉํ•œ ์ œ์–ด๊ฐ€ ๊ฐ€๋Šฅ

ํด๋ž˜์Šค ์ƒ์†

data class SetUserInfo(val userInfo: User) : SignUpClientIntent()์—์„œ : SignUpClientIntent() ๋Š” SetUserInfo๋ผ๋Š” ํ•˜์œ„ ํด๋ž˜์Šค๊ฐ€ SignUpClientIntent๋ผ๋Š” ํด๋ž˜์Šค๋ฅผ ์ƒ์†ํ–ˆ๋‹ค๋Š” ์˜๋ฏธ

-> SignUpClientIntent ํƒ€์ž…์˜ ๋ณ€์ˆ˜์—์„œ SetUserInfo๋ผ๋Š” ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ณ  viewModel์—์„œ ์ด๋ฅผ ์ด์šฉํ•ด์„œ handleIntentํ•จ์ˆ˜์˜ ๋ณ€์ˆ˜๋กœ SignUpClientIntent๋ฅผ ๋ฐ›๊ณ  ๊ทธ ํ•˜์œ„ ํด๋ž˜์Šค๋กœ ๊ฐ๊ฐ์˜ ์ •์˜๋œ ๋™์ž‘๋“ค์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋‹ค.

  • when๋ฌธ์„ ์ด์šฉํ•ด ํ•˜์œ„ ํด๋ž˜์Šค์˜ ๊ฒฝ์šฐ๋ฅผ ๋‚˜๋ˆ„๊ณ  ๋ชจ๋“  ํ•˜์œ„ ํด๋ž˜์Šค๊ฐ€ ์กฐ๊ฑด์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š”์ง€ ๊ฒ€์ฆ ๊ฐ€๋Šฅ



data object์™€ data class

๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ - data class

  • ์—ฌ๋Ÿฌ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค
  • ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด๊ณ  ์ „๋‹ฌํ•˜๋Š” ์šฉ๋„๋กœ ์‚ฌ์šฉ, ์ƒํƒœ ๊ด€๋ฆฌ์— ์“ฐ์ธ๋‹ค

๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์ง€ ์•Š๋Š” ๊ฒฝ์šฐ - data object

  • ์‹ฑ๊ธ€ํ†ค : ๋‹จ ํ•˜๋‚˜์˜ ์ธ์Šคํ„ด์Šค๋งŒ ์กด์žฌ
  • ๋‹จ์ผ ์ƒํƒœ ๊ด€๋ฆฌ์— ์“ฐ์ž„

data {}

data๋ฅผ ์•ž์— ๋ถ™์ด๋Š” ๊ฒƒ์€ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ์— ์‚ฌ์šฉ๋œ๋‹ค๋Š” ๋œป์œผ๋กœ sealed class(ํ•˜์œ„ ํด๋ž˜์Šค์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ํด๋ž˜์Šค)์˜ ํ•˜์œ„ ํด๋ž˜์Šค์— ์ ํ•ฉ

  • ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ์— ํ•„์š”ํ•œ ๋ฉ”์†Œ๋“œ(equals(), hashCode(), toString())๋“ค์„ ์ œ๊ณต

image


์ข…ํ•ฉ

ํ•˜์œ„ ํด๋ž˜์Šค๋“ค์˜ ์ƒํƒœ ๊ด€๋ฆฌ์— ์“ฐ์ด๋Š” sealed class ์ƒ์œ„ํด๋ž˜์Šค๋กœ Intent ์ •์˜, ๊ทธ ์ƒ์œ„ ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์€ ํ•˜์œ„ ํด๋ž˜์Šค๋“ค์— ๊ฐ ๋™์ž‘๋“ค์„ ์ •์˜ํ•˜๋Š”๋ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ data class, ์—†๋Š” ๊ฒฝ์šฐ data object๋กœ ์ •์˜



viewmodel์—์„œ intent ์ œ์–ด

class SignUpClientViewModel : ViewModel() {
    private val _state = MutableStateFlow<SignUpClientState>(SignUpClientState.idle())
    val state: StateFlow<SignUpClientState> get() = _state

    ...

    fun handleIntent(intent: SignUpClientIntent) {
        when (intent) {
            is SetUserInfo -> {}
            is SetNickname -> {
                val newNicknameState = _state.value.copy(nickname = intent.newNickname)
                _state.value = newNicknameState
            }
            is SetProfileImageUri -> {
                val newProfileImageUrlState = _state.value.copy(profileImageUri = intent.newProfileImageUri)
                _state.value = newProfileImageUrlState
            }
            is NavigateToPrev -> {
                ...
            }
            is ChangeStep -> {
                val newStepState = _state.value.copy(currentStep = intent.stepNum)
                _state.value = newStepState
            }
            is ClickSubmitButton -> {
                ...
            }
        }
    }
}

handleIntent

handleIntent๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์„ ์–ธํ•˜๊ณ  intent๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •, intent์˜ ํƒ€์ž…์€ intentํŒŒ์ผ์—์„œ ์•ž์„œ ์ •์˜ํ•œ SignUpClientIntent ํด๋ž˜์Šค๋‹ค.

view์—์„œ viewModel.handleIntent({์ƒ์œ„ ํด๋ž˜์Šค}.{์ƒ์†๋ฐ›์€ ํ•˜์œ„ ํด๋ž˜์Šค})๋กœ ํ˜ธ์ถœํ•œ๋‹ค.

// ui/sign_up/sign_up_client/SignUpClientScreeen
        ...
        onClickBackIcon = { viewModel.handleIntent(SignUpClientIntent.NavigateToPrev) },
        ...

when์—์„œ ๊ฐ ๋™์ž‘๋ณ„๋กœ ๊ฒฝ์šฐ๋ฅผ ๋‚˜๋ˆ ์„œ ๋™์ž‘์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค

  • sealed class๋กœ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ชจ๋“  ํ•˜์œ„ ํด๋ž˜์Šค๋“ค์„ ์กฐ๊ฑด์— ๊ฑธ์–ด์คฌ๋Š”์ง€ ๊ฒ€์‚ฌํ•ด์ค€๋‹ค

state ๋ณ€๊ฒฝ

mvi ํŒจํ„ด์—์„œ state ๊ฐ์ฒด๋Š” ๋ถˆ๋ณ€ํ•จ์„ ์œ ์ง€ํ•ด์•ผํ•œ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์—

// don`t
is SetNickname -> {
    _state.value.nickname = intent.newNickname
}

์ฒ˜๋Ÿผ ์†์„ฑ์— ์ ‘๊ทผํ•ด์„œ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ

// do
is SetNickname -> {
    _state.update{ it.copy(nichname = intent.newNickname) }
}

์ด๋ ‡๊ฒŒ ์ˆ˜์ •ํ•˜๊ณ ์ž ํ•˜๋Š” ์†์„ฑ์„ ๋ณ€๊ฒฝํ•œ ๋ณต์ œ๋œ ์ „์ฒด state ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ ํ›„ ์ „์ฒด state๋ฅผ ๋ฐ”๊พธ์–ด์ค€๋‹ค.

  • state๋ฅผ data class๋กœ ์„ ์–ธํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— .copy()์™€ ๊ฐ™์€ ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.



์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ ์ •์˜

package com.hm.picplz.ui.screen.sign_up.sign_up_client

import android.net.Uri

sealed class SignUpClientSideEffect {
    data class SubmitProfileInfo(
        val nickname: String,
        val profileImageUrl: Uri?,
    ) : SignUpClientSideEffect()
    data object ShowFileUploadDialog : SignUpClientSideEffect()
    data object NavigateToPrev : SignUpClientSideEffect()
}

Intent ์ •์˜ํ•  ๋•Œ์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์ „์ฒด seal class๋กœ ์ „์ฒด ํด๋ž˜์Šค๋ฅผ ์ •์˜ํ•˜๊ณ  ๋ฐ์ดํ„ฐ ๋ณด์œ  ์—ฌ๋ถ€์— ๋”ฐ๋ผ data class์™€ data object๋กœ ๊ฐ ๋™์ž‘์— ํ•ด๋‹นํ•˜๋Š” ํ•˜์œ„ ํด๋ž˜์Šค๋“ค ์ •์˜



viewModel์—์„œ Intent๋ฅผ ํ†ตํ•ด ์‚ฌ์ดํŠธ ์ดํŽ™ํŠธ emit

package com.hm.picplz.viewmodel

import ...
class SignUpClientViewModel : ViewModel() {
    ...

    private val _sideEffect = MutableSharedFlow<SignUpClientSideEffect>()
    val sideEffect: SharedFlow<SignUpClientSideEffect> get() = _sideEffect

    fun handleIntent(intent: SignUpClientIntent) {
        when (intent) {
            is SetUserInfo -> {}
            is SetNickname -> {
                ...
            }
            is SetProfileImageUri -> {
                ...
            }
            is NavigateToPrev -> {
                viewModelScope.launch {
                    _sideEffect.emit(SignUpClientSideEffect.NavigateToPrev)
                }
            }
            is ChangeStep -> {
                ...
            }
            is ClickSubmitButton -> {
                viewModelScope.launch {
                    _sideEffect.emit(
                        SignUpClientSideEffect.SubmitProfileInfo(
                            nickname = _state.value.nickname,
                            profileImageUrl = _state.value.profileImageUri,
                        )
                    )
                }
            }
        }
    }
}

_sideEffect

  • ํƒ€์ž… : MutableSharedFlow
  • ์ด๋ฒคํŠธ๋ฅผ emit์‹œํ‚ค๊ณ  collector๋“ค์—๊ฒŒ ์ „๋‹ฌํ•˜๋Š” ์—ญํ• 
  • ๋น„๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰
MutableSharedFlow vs MutableStateFlow
  • MutableSharedFlow

    • ์ด๋ฒคํŠธ ๊ด€๋ฆฌํ•  ๋•Œ ์‚ฌ์šฉ flow
    • ์—ฌ๋Ÿฌ event๋ฅผ ์—ฐ์†์ ์œผ๋กœ emitํ•  ์ˆ˜ ์žˆ๋‹ค
    • ๋‹จ๋ฐœ์ ์œผ๋กœ ๋ฐœ์ƒํ•˜๊ณ  collector๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ emitํ•œ ์ด๋ฒคํŠธ๋Š” ์‚ฌ๋ผ์ง„๋‹ค
    • ์—ฌ๋Ÿฌ collector๋ฅผ ๋‘๊ณ  event๋ฅผ ๋ฐ›๊ฒŒ๋” ํ•  ์ˆ˜ ์žˆ์Œ
    • replay์™€ buffer ์„ค์ •์„ ํ†ตํ•ด ์ด๋ฒคํŠธ๋ฅผ ์„ค์ •๋œ ๋ฒ”์œ„ ๋‚ด์—์„œ ๋‹ค์‹œ ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก ๊ด€๋ฆฌ
  • MutableStateFlow(state์— ์‚ฌ์šฉ๋œ flow)

    • ์ƒํƒœ ๊ด€๋ฆฌ์— ์ฃผ๋กœ ์‚ฌ์šฉํ•˜๋Š” flow
    • ์ตœ์‹ ์˜ state๋ฅผ ์œ ์ง€(๋ฐ”๋€Œ๋ฉด ์ด์ „์˜ ๊ฐ’์€ ์œ ์ง€๋˜์ง€ ์•Š์Œ)
    • ์ดˆ๊ธฐ๊ฐ’์ด ํ•„์š”ํ•˜๊ณ , collector๊ฐ€ ์—†์–ด๋„ ์ƒํƒœ ์กด์žฌ
    • ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ, collector๋“ค์—๊ฒŒ ์ตœ์‹ ์˜ state ์ „๋‹ฌ



view์—์„œ ์ด๋ฒคํŠธ collectํ•ด์„œ ์‹คํ–‰

package com.hm.picplz.ui.screen.sign_up.sign_up_client

import ...

@Composable
fun SignUpClientScreen(
    modifier: Modifier = Modifier,
    navController: NavController,
    userInfo: User = emptyUserData,
    viewModel: SignUpClientViewModel = viewModel(),
) {
    val currentState = viewModel.state.collectAsState().value

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        containerColor = MainThemeColor.White
    ) { innerPadding ->
        when (currentState.currentStep) {
            0 -> SignUpClientNicknameView(
                modifier = modifier,
                currentState = currentState,
                viewModel = viewModel,
                innerPadding = innerPadding
            )
            1 -> SignUpClientProfileImageView(
                modifier = modifier,
                currentState = currentState,
                viewModel = viewModel,
                innerPadding = innerPadding
            )
            else -> {}
        }
    }
    val context = LocalContext.current

    LaunchedEffect(Unit) {
        viewModel.sideEffect.collectLatest { sideEffect ->
            when (sideEffect) {
                is SignUpClientSideEffect.SubmitProfileInfo -> {
                    Toast.makeText(context, "๊ฐ€์ž…", Toast.LENGTH_SHORT).show()
                }
                is SignUpClientSideEffect.ShowFileUploadDialog -> {}
                is SignUpClientSideEffect.NavigateToPrev -> {
                    navController.popBackStack()
                }
            }
        }
    }
}

side effect๋Š” context, navController๋“ฑ view์—์„œ ์กฐ์ž‘ํ•ด์•ผ ํ•˜๋Š” ์š”์†Œ๋“ค์„ ์ฃผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ui ์š”์†Œ์ด๊ธฐ ๋•Œ๋ฌธ์— view์—์„œ ๋™์ž‘์„ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค viewModel์—์„œ sideEffect๊ฐ€ emitํ•œ ์ด๋ฒคํŠธ๋ฅผ collectํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ทธ ๊ฐ ์ด๋ฒคํŠธ์— ๋”ฐ๋ผ ์‹คํ–‰๋˜๋Š” ๋™์ž‘ ์ฝ”๋“œ๊ฐ€ ์ž‘์„ฑ๋œ๋‹ค.



LaunchedEffect(key๊ฐ’) {}

์•ˆ๋“œ๋กœ์ด๋“œ ์ œํŠธํŒฉ compose์˜ composable์ด ์ƒ์„ฑ๋  ๋•Œ ์‹คํ–‰๋˜๋Š” ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜ key๊ฐ’์ด ๋ณ€ํ• ๋•Œ๋งˆ๋‹ค ์‹คํ–‰

LaunchedEffect(Unit) {
    // ์‹คํ–‰
}
  • key๊ฐ’์— Unit(์•„๋ฌด๊ฒƒ๋„ ์—†๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•˜๋Š” ๊ฐ์ฒด)์„ ๋„ฃ์„ ๊ฒฝ์šฐ Composable์ด ์ฒ˜์Œ ๋ Œ๋”๋ง ๋  ๋•Œ ํ•œ๋ฒˆ๋งŒ ์‹คํ–‰๋œ๋‹ค



collectLatest

Flow๋ฅผ collectํ•˜๋Š” ์—ฐ์‚ฐ์ž ๊ฐ€์žฅ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ collect, ๊ธฐ์กด ์ž‘์—…์ค‘์ผ ๋•Œ์—๋„ ์ƒˆ๋กœ์šด ์ตœ์‹  ๋ฐ์ดํ„ฐ๊ฐ€ collect๋˜๋ฉด ์ตœ์‹ ๋ฐ์ดํ„ฐ์— ๋ฐ˜์‘ ๋งŒ์•ฝ ํŠน์ • side-effect๊ฐ€ ์‹คํ–‰์ค‘์ธ๋ฐ ๋‹ค๋ฅธ side-effect๋ฅผ ์‹คํ–‰์‹œํ‚ฌ(collectํ• )๊ฒฝ์šฐ ๊ธฐ์กด ์ž‘์—…์„ ์ทจ์†Œํ•˜๊ณ  ์ƒˆ๋กœ์šด side-effect ์ทจ์†Œํ•˜์ง€ ์•Š๊ณ  ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰ํ•˜๊ณ ์ž ํ•  ๋•Œ๋Š” collect ์‚ฌ์šฉ





์ฐธ๊ณ  ๋งํฌ

https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-api-guidelines.md

https://developer.android.com/develop/ui/compose/documentation?hl=ko

โš ๏ธ **GitHub.com Fallback** โš ๏ธ