Skip to content

YAPP-Github/21st-Android-Team-2-Android

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Find Things Architecture

๋ชฉ์ฐจ

MVVM + UDA

App Architecture


์•„ํ‚คํ…์ณ, ๋ฐ”์ธ๋”ฉ ๋“ฑ์— ๋Œ€ํ•œ ๊ตฌ์กฐ์  ์„ค๋ช…(MVVM, Model ์ƒ์†๋ฐ›์•„์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹, JSON๊ตฌ์กฐ, DataBindHelper ๊ตฌํ˜„)

์•„ํ‚คํ…์ณ - MV(VC)VM(Action in Model)

Jetpack์—์„œ ์ œ๊ณตํ•˜๋Š” AAC-ViewModel, ์ฝ”๋ฃจํ‹ด์„ ์ด์šฉํ•˜์—ฌ MVVM + UDA ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ DataBinding ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉฐ, DataBindHelper๋ฅผ ์ด์šฉํ•˜์—ฌ ๋ฆฌ์ŠคํŠธ์˜ ์žˆ๋Š” ๋ชจ๋ธ์— ์•ก์…˜์„ ์ •์˜ํ•˜์—ฌ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉํ•˜๋Š” ๊ตฌ์กฐ๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์ด์šฉํ•˜์ง€ ์•Š๋Š” ์ด์œ ๋Š” ์ถ”ํ›„ ๋กœ์ง์— ๋Œ€ํ•œ ๊ณ ๋„ํ™”๊ฐ€ ์ด๋ฃจ์–ด ์กŒ์„ ๋•Œ iOS์™€ ์•ˆ๋“œ๋กœ์ด๋“œ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋™์ผํ•˜๊ฒŒ ๊ฐ€์ ธ๊ฐ์œผ๋กœ์จ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๋†’์ด๊ณ  ์–ด๋–ค ๊ฐœ๋ฐœ์ž๊ฐ€ ์™€๋„ ๊ฐœ๋ฐœํ•˜๋Š”๋ฐ ์žˆ์–ด ์›ํ™œํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค.(๋ฌผ๋ก , ์•„ํ‚คํ…์ณ๋ฅผ ๋ฐ˜๋“œ์‹œ ๋™์ผํ•˜๊ฒŒ ๊ฐ€์ ธ๊ฐˆ ํ•„์š”๋Š” ์—†์Šต๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ์ฒ˜ํ•ด์ง„ ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.)

์„œ๋ฒ„์—์„œ Response๋ฐ›๋Š” JSON ๊ตฌ์กฐ - ex) ์œ ์ € ๋ฆฌ์ŠคํŠธ Response

{
  data : [
    {
      "type": "user.cell",
      "id": 100,
      "scheme": "dncapp://users/100",
      ...
    },
  ],
  "meta": {
      "pagenation": {...},
      ...
  }
}
  • ์•„ํ‚คํ…์ณ ๋ฐ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์— ์˜๊ฐ์„ ์ค€ ๋ฌธ์„œ - https://jsonapi.org

์œ„ ์˜ˆ์‹œ๋Š” ์„œ๋ฒ„์—์„œ ๋ฐ›์€ Json Element ์ค‘ type๋ฅผ ํ†ตํ•ด ๋ฆฌ์ŠคํŠธ Cell์„ ๊ตฌ์กฐํ™” ํ•˜์—ฌ ๋ฐ›๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.๊ฐ ๋ชจ๋ธ ํƒ€์ž…์˜ ๋’ค์— cell์ด ์˜ค๋Š”์ง€, detail ๋“ฑ ์–ด๋–ค ํƒ€์ž…์ด ์˜ค๋Š”์ง€์— ๋”ฐ๋ผ ๋ฆฌ์ŠคํŠธํ™”๋ฉด์ธ์ง€, ์ƒ์„ธํ™”๋ฉด์ธ์ง€ ๋ณด์—ฌ์ง€๋Š” ๋ทฐ์™€ ๋งคํ•‘ํ•˜๋Š” ์ž‘์—…์„ ๊ฑฐ์น˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ ํด๋ž˜์Šค - Model

@Parcelize
open class Data(
    open val id: Long = 0,
    open val type: String = CellType.EMPTY_CELL.type,
    open val scheme: String? = null
) : Parcelable

๋ชจ๋“  ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์˜ ๊ธฐ๋ณธ์ด ๋˜๋Š” Data ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ ์„œ๋ฒ„์—์„œ ๋‚ด๋ ค์ฃผ๋Š” ํ”„๋กœํผํ‹ฐ๋กœ id, type, scheme์ด ์žˆ์œผ๋ฉฐ, DataBindHelper๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ํ•ธ๋“ค๋Ÿฌ ๊ตฌํ˜„์„ ํ•ด์ฃผ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

var handler: DataHandler = { }
var detailHandler: DataHandler = { }
var deleteHandler: DataHandlera = { }

ํ•ด๋‹น ๋ชจ๋ธ์˜ ์•ก์…˜์— ๋Œ€ํ•œ ๋™์ž‘์€ run...() ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๋™์ž‘ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

Data๋ฅผ ์ƒ์†๋ฐ›์€ ์ž์‹ ํด๋ž˜์Šค - ex) User.kt

data class User(
    val login: String,
    val number: Int,
    val avatarUrl: String
) : Data() {
  ...
  var favoriteHandler: DataHandler = { }

  fun runFavorite() = favoriteHandler.invoke(this)
}

์œ„ ์˜ˆ์‹œ์˜ ๊ฒฝ์šฐ ์œ ์ € ๋ฆฌ์ŠคํŠธ ํ‘œ์‹œ์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค User์ž…๋‹ˆ๋‹ค. Data๋ฅผ ์ƒ์†๋ฐ›๊ณ  ํ•„์š”ํ•œ ํ”„๋กœํผํ‹ฐ ๋ฐ ๋™์ž‘์— ๋Œ€ํ•ด ์ •์˜ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋™์ ์ธ ๋ฐ์ดํ„ฐ ๊ฐฑ์‹ ์„ ์œ„ํ•ด Handler๋ผ๋Š” ํ•„๋“œ๋ฅผ ๋‘์–ด ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

BaseViewModel

abstract class BaseViewModel: ViewModel() {

    protected val jobs = mutableListOf<Job>()

    open fun fetchData(): Job = viewModelScope.launch {  }

    override fun onCleared() {
        jobs.forEach {
            if (it.isCancelled.not())
                it.cancel()
        }
        super.onCleared()
    }

}

๊ธฐ๋ณธ์ ์œผ๋กœ ๋ชจ๋“  ํ”„๋กœ์ ํŠธ์— ๊ตฌํ˜„๋˜๋Š” ViewModel์€ ๋ชจ๋‘ BaseViewModel์„ ์ƒ์† ๋ฐ›์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ํ™”๋ฉด์€ ๊ธฐ๋ณธ์ ์œผ๋กœ data๋ฅผ fetchํ•œ๋‹ค๋Š” ํŒ๋‹จํ•˜์— open function์„ ๊ตฌํ˜„ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ๊ตฌ๋…ํ•˜๋Š” ํ™”๋ฉด์€ BaseFragment, BaseActivity๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž์„ธํ•œ ๊ฒƒ์€ ์ฝ”๋“œ๋ฅผ ์ฐธ๊ณ  ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

BaseStateViewModel

abstract class BaseStateViewModel<S : State, SE : SideEffect> : BaseViewModel() {

    protected abstract val _stateFlow: MutableStateFlow<S>
    val stateFlow: StateFlow<S>
        get() = _stateFlow

    protected abstract val _sideEffectFlow: MutableSharedFlow<SE>
    val sideEffectFlow: SharedFlow<SE>
        get() = _sideEffectFlow

    protected inline fun <reified S : State> withState(accessState: (S) -> Unit): Boolean {
        if (stateFlow.value is S) {
            accessState(stateFlow.value as S)
            return true
        }
        return false
    }

    protected fun setState(state: S) {
        _stateFlow.value = state
    }

    protected fun postSideEffect(sideEffect: SE) = viewModelScope.launch {
        _sideEffectFlow.emit(sideEffect)
    }

}

BaseStateViewModel์€ State์™€ SideEffect๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์ƒ์†๋ฐ›๋Š” ํด๋ž˜์Šค์—์„œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๋„ˆ๋ฆญ์œผ๋กœ State์™€ SideEffect๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ตฌํ˜„์ฒด ํด๋ž˜์Šค์˜ ํƒ€์ž…์„ ๋ช…์‹œํ•˜์—ฌ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ, Activity ๋ฐ Fragment์—์„œ flow ์ŠคํŠธ๋ฆผ์— ๋Œ€ํ•œ ๊ตฌ๋…์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ, stateFlow ๋ณ€์ˆ˜ ๋ฐ sideEffectFlow์— ์ ‘๊ทผํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

  • fun withState((S) -> Unit): Boolean : StateFlow์˜ ํ˜„์žฌ State ํƒ€์ž… ๋ฐ ํ•ด๋‹นํ•˜๋Š” ์ƒํƒœ์ธ ๊ฒฝ์šฐ ๊ฐ’์— accessํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ๋žŒ๋‹ค ์ธ์ž๋Š” immutableํ•˜๋ฉฐ, ์™ธ๋ถ€ ๋žŒ๋‹ค ๋ธ”๋ก์—์„œ ์ ‘๊ทผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

  • fun setState(state: S) : stateFlow์˜ ํ˜„์žฌ ๊ฐ’์„ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค. StateFlow์—๋Š” Screen์˜ State๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํƒœ๋“ค์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

  • ex) LikeTabState.kt

    sealed class LikeTabState: State {
    
        object Uninitialized : LikeTabState()
    
        object Loading : LikeTabState()
    
        data class Success(
            val dataList: List<Data>
        ) : LikeTabState()
    
        data class Error(
            val e: Throwable
        ) : LikeTabState()
    }
  • fun postSideEffect(sideEffect: SE): Job : sideEffect์˜ ๊ฐ’์„ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋•Œ, SideEffect๋Š” Screen์— ํฌํ•จ๋˜๋Š” ์ƒํƒœ๊ฐ€ ์•„๋‹Œ Fire & Forget๋˜๋Š” One-Shot Event๊ฐ€ ํ•ด๋‹น๋ฉ๋‹ˆ๋‹ค.

    • ShowToast, ShowDialog, StartActivity ๋“ฑโ€ฆ
  • ex) LikeTabSideEffect.kt

    sealed class LikeTabSideEffect: SideEffect {
    
        data class ShowToast(val message: String): LikeTabSideEffect()
    
    }

๋ทฐ ๋ชจ๋ธ - ex) LikeTabViewModel

class LikeTabViewModel : BaseStateViewModel<LikeTabState, LikeTabSideEffect>() {

    override val _stateFlow: MutableStateFlow<LikeTabState>
        get() = MutableStateFlow(LikeTabState.Uninitialized)
    override val _sideEffectFlow: MutableSharedFlow<LikeTabSideEffect>
        get() = MutableSharedFlow()

    override fun fetchData(): Job = viewModelScope.launch {
        setState(LikeTabState.Loading)
        setState(
            LikeTabState.Success(
                (0..5).map {
                    LikeItem(
                        id = it.toLong(),
                        name = "์ฆ๊ฒจ์ฐพ๊ธฐ ์•„์ดํ…œ ${it}๋ฒˆ"
                    )
                }
            )
        )
    ...
    }

    ...

}

์˜ˆ์‹œ๋กœ ๋“  LikeTabViewModel์€ BaseStateViewModel์„ ์ƒ์†๋ฐ›์•„ ์‚ฌ์šฉํ•˜๋ฉฐ, ๋ชจ๋“  ๋ชจ๋ธ์„ ๋ฆฌ์ŠคํŠธ์— ๋‹ด์•„ ๊ฐ€๊ณต ๋ฐ ๋ฐฉ์ถœํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

์ด ๋•Œ, ์ƒํƒœ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด LikeTabState, LikeTabSideEffect์„ ์ œ๋„ˆ๋ฆญ์— ๋ช…์‹œํ•˜๋ฉฐ, ์ด์— ๋”ฐ๋ผ ๊ฐ ์ „์—ญ ๋ณ€์ˆ˜๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉ ํ•ด์ค๋‹ˆ๋‹ค.

์ด๋ฅผ ์ƒ์†๋ฐ›๋Š” ๋ฆฌ์ŠคํŠธ ํ™”๋ฉด์„ ์œ„ํ•œ ๋ทฐ ๋ชจ๋ธ์—์„œ fetchData(): Job ํ•จ์ˆ˜๋กœ ๋„คํŠธ์›Œํฌ์—์„œ ๋ฐ›์•„์˜จ respnse body๋ฅผ ๊ฐ€๊ณตํ•ด ํ†ตํ•ด ๋ฐ์ดํ„ฐ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ฐ›์€ ํ›„ ์ƒํƒœ๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฐฑ์‹  ํ•ด ์ค๋‹ˆ๋‹ค.

VM์—์„œ ๊ตฌ๋…ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„ ๋ทฐ์— ๋ฟŒ๋ ค์ฃผ๋Š” VC - ex) LikeTabFragment

@AndroidEntryPoint
class LikeTabFragment : BaseStateFragment<LikeTabViewModel, FragmentLikeTabBinding>() {
  ...
  override fun observeData(): Job {
        val job = viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                launch {
                    vm.stateFlow.collect { state ->
                        when (state) {
                            is LikeTabState.Uninitialized -> Unit
                            is LikeTabState.Loading -> handleLoading(state)
                            is LikeTabState.Success -> handleSuccess(state)
                            is LikeTabState.Error -> handleError(state)
                        }
                    }
                }
                launch {
                    vm.sideEffectFlow.collect { sideEffect ->
                        when (sideEffect) {
                            is LikeTabSideEffect.ShowToast -> {
                                Toast.makeText(requireContext(), sideEffect.message, Toast.LENGTH_SHORT).show()
                            }
                        }
                    }
                }
            }
        }
        return job
    }
}

์ด๋ฅผ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋Š” LikeTabFragment์—์„œ ๊ตฌ๋…์ค‘์ธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›๊ฒŒ๋˜๋ฉด, ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„ DataBindHelper์— ๋ชจ๋“  ๋ชจ๋ธ ์ธ์Šคํ„ด์Šค์— ํ•„์š”ํ•œ ํ•ธ๋“ค๋Ÿฌ์— ๋Œ€ํ•œ ์ •์˜๋ฅผ ํ•ฉ๋‹ˆ๋‹ค.๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์•ก์…˜ ๋ฐ˜์˜์ด ๋๋‚˜๋ฉด, ์–ด๋Œ‘ํ„ฐ์— ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค.

@AndroidEntryPoint
class LikeTabFragment : BaseStateFragment<LikeTabViewModel, FragmentLikeTabBinding>() {

    ...
    @Inject
    lateinit var dataBindHelper: DataBindHelper

    ...

    private fun handleSuccess(likeTabState: LikeTabState.Success) {
	dataBindHelper.bindList(likeTabState.dataList, vm)
	dataListAdapter?.submitList(likeTabState.dataList)
    }

    ...

}

Data์˜ ์•ก์…˜ํ•ธ๋“ค๋Ÿฌ์˜ ๊ธฐ๋Šฅ์„ ์ •์˜ํ•ด์ฃผ๋Š” ํ—ฌํผ - DataBindHelper

@Singleton
class DataBindHelper @Inject constructor(
    @HomeLikeItemQualifier //๊ฐ ๋ชจ๋“ˆ์— ๋™์ผ Data์— ๋Œ€ํ•œ ํ•ธ๋“ค๋Ÿฌ ๋Œ€์‘์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ถ”๊ฐ€
    private val homeLikeItemBinder: LikeItemBinder,
    @CategoryLikeItemQualifier //๊ฐ ๋ชจ๋“ˆ์— ๋™์ผ Data์— ๋Œ€ํ•œ ํ•ธ๋“ค๋Ÿฌ ๋Œ€์‘์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ถ”๊ฐ€
    private val categoryLikeItemBinder: LikeItemBinder,
) {

    @SuppressLint("CheckResult")
    fun bindList(dataList: List<Data>, viewModel: BaseViewModel) {
        dataList.forEach { data ->
            bindData(data, viewModel)
        }
    }

    private fun bindData(data: Data, viewModel: BaseViewModel) {
        when(data.type) {
            CellType.LIKE_CELL -> {
                homeLikeItemBinder.bindData(data as LikeItem, viewModel)
								//categoryLikeItemBinder.bindData(data as LikeItem, viewModel)
            }
            else -> { }
        }
    }

}

์ด๋ฅผ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋Š” ํ™”๋ฉด์—์„œ์—์„œ DataBindHelper์— ๋ชจ๋“  ๋ชจ๋ธ ์ธ์Šคํ„ด์Šค์— ํ•„์š”ํ•œ ํ•ธ๋“ค๋Ÿฌ์— ๋Œ€ํ•œ ์ •์˜๋ฅผ ํ•ฉ๋‹ˆ๋‹ค.

ํ•ธ๋“ค๋Ÿฌ ์ฒ˜๋ฆฌ๋Š” ํƒ€์ž… ๋ฐ ViewModel์„ ์–ด๋–ค ๊ฒƒ์„ ๋“ค๊ณ  ์žˆ๋ƒ์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ๋ฅผ ๋‹ค๋ฅด๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐ ๋ชจ๋“ˆ์— ๋™์ผ Data์— ๋Œ€ํ•œ ํ•ธ๋“ค๋Ÿฌ ๋Œ€์‘์ด ํ•„์š”ํ•˜๋‹ค๋ฉด :feature:common ๋ชจ๋“ˆ์—์„œ๋Š” interface๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ •์˜ํ•˜๊ณ , ๊ตฌํ˜„์ฒด๋Š” ๊ฐ :feature:{feature} ๋ชจ๋“ˆ์— ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

@Singleton
class HomeLikeItemBinder @Inject constructor(): LikeItemBinder {

    override fun bindData(data: LikeItem, viewModel: BaseViewModel) {
        when (viewModel) {
            is LikeTabViewModel -> setLikeTabViewModelHandler(data, viewModel)
            is HomeTabViewModel -> setHomeTabViewModelHandler(data, viewModel)
        }
    }

    private fun setLikeTabViewModelHandler(item: LikeItem, viewModel: LikeTabViewModel) {
        item.deleteHandler = { data ->
            viewModel.deleteItem(data as LikeItem)
        }
        item.updateHandler = { data ->
            viewModel.updateCount(data as LikeItem)
        }
    }

    ...
}

์ด์— ๋Œ€ํ•œ interface๋ฅผ ์—…์บ์ŠคํŒ… ํ•˜๋Š” DI๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด {feature_name}DataBinderModule ๋กœ ์ด๋ฆ„์„ ์ •์˜ํ•˜์—ฌ ํŒจํ‚ค์ง€์— ๊ตฌํ–”ํ•ฉ๋‹ˆ๋‹ค.

package com.yapp.itemfinder.home.binder.di

...

@Module
@InstallIn(SingletonComponent::class)
abstract class HomeDataBinderModule {

    @Binds
    @Singleton
    @HomeLikeItemQualifier
    abstract fun bindLikeItemBinder(
        homeLikeItemBinder: HomeLikeItemBinder
    ): LikeItemBinder

}

DataListAdapter๋ฅผ ์ด์šฉํ•œ type๋ณ„ ์…€ ๋ถ„๋ฅ˜

class DataListAdapter<D : Data> : ListAdapter<D, DataViewHolder<D>>(
    object : DiffUtil.ItemCallback<D>() {
        override fun areItemsTheSame(oldItem: D, newItem: D): Boolean =
            oldItem.id == newItem.id && oldItem.type == newItem.type

        @SuppressLint("DiffUtilEquals")
        override fun areContentsTheSame(oldItem: D, newItem: D): Boolean =
            oldItem === newItem
    }
) {

    override fun getItemViewType(position: Int) = getItem(position).type.ordinal

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataViewHolder<D> {
        return DataViewHolderMapper.map(parent, CellType.values()[viewType])
    }

    override fun onBindViewHolder(holder: DataViewHolder<D>, position: Int) {
        val safePosition = holder.adapterPosition
        if (safePosition != RecyclerView.NO_POSITION) {
            @Suppress("UNCHECKED_CAST")
            val model = getItem(position) as D
            with(holder) {
                bindData(model)
                bindViews(model)
            }
        }
    }
}
  • DiffUtil์„ ํ†ตํ•ด Data ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์€ UI Model์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.
dataListAdapter?.submitList(homeTabState.dataList)
  • Adapter๋ฅผ ์ƒ์„ฑ ํ›„, ํ™”๋ฉด์—์„œ adapter์˜ submitList(dataList: List)๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.
override fun getItemViewType(position: Int)DataLayoutMapper
  • ๋งคํผ๋ฅผ ํ†ตํ•ด ๊ฐ ์ธ์Šคํ„ด์Šค์˜ type๊ฐ’๊ณผ ๋น„๊ตํ•˜์—ฌ ๋งž๋Š” layoutId๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataViewHolder<T>
  • viewType Id๋ฅผ ๋ฐ›์•„๋‚ด์–ด ํ•ด๋‹น layoutId์— ๋งž๋Š” ViewHolder ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
override fun onBindViewHolder(holder: DataViewHolder<T>, position: Int)
  • DataViewHolder ๋Š” ์ถ”์ƒํด๋ž˜์Šค์ด๋ฉฐ, ํ•˜์œ„ ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ–๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
abstract fun reset() // VH์—์„œ ๋“ค๊ณ  ์žˆ๋Š” ๋ทฐ๋ฅผ ์ดˆ๊ธฐํ™” ํ•ฉ๋‹ˆ๋‹ค.

open fun bindData(data: D) {
    reset() // ๋ฐ์ดํ„ฐ๋ฅผ ๋ทฐ์— ๋ฐ”์ธ๋”ฉํ•ฉ๋‹ˆ๋‹ค.
} 

abstract fun observeData(data: T) // ๋ฐ˜์˜์ด ๋  ์ˆ˜ ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ Data๋‚ด Vaiable์„ ํ†ตํ•ด ๊ตฌ๋…ํ•ด ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค.

abstract fun bindViews(data: T) // ๋ทฐ์— ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋‹ฌ๊ณ , ์•ก์…˜์ด ์ผ์–ด๋‚˜๋Š” ๊ฒฝ์šฐ Data์˜ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ํŠธ๋ฆฌ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

๊ฐ ํƒ€์ž…์— ๋งž๋Š” ์ธ์Šคํ„ด์Šค๋ฅผ ๋งคํ•‘ - DataMapper

  • ์„œ๋ฒ„์—์„œ ๋‚ด๋ ค์ฃผ๋Š” ์‘๋‹ต์— type์ด ๋“ค์–ด์˜ค๋ฉด, ์ด๋ฅผ ์ ํ•ฉํ•œ ๋ฐ์ดํ„ฐ ํƒ€์ž…์œผ๋กœ ์ปจ๋ฒ„ํŠธ ํ•ด์ค๋‹ˆ๋‹ค.
@Singleton
class DataMapper @Inject constructor(
    @ApiGsonQualifier
    private val apiGson: Gson,
) {

    fun map(json: JsonObject): Data? =
        when (json.get("type").asString) {
            CellType.EMPTY_CELL.name -> convertJsonType(json, Data::class)
            CellType.CATEGORY_CELL.name -> convertJsonType(json, Category::class)
            CellType.LIKE_CELL.name -> convertJsonType(json, LikeItem::class)
            else -> null
        }

    private fun convertJsonType(json: JsonObject, clazz: KClass<out Data>): Data {
        return apiGson.fromJson(json.toString(), clazz.java)
    }
}

๋ณด์—ฌ์ง€๋Š” ์˜ˆ๋กœ LIKE_CELL์„ ๋ฐ›๊ฒŒ๋˜๋ฉด, ๋ฆฌ์ŠคํŠธ ์…€์˜ ๋ชจ๋ธ์„ ์ง€์นญํ•˜๋ฉฐ, Gson์„ ํ†ตํ•ด ํŒŒ์‹ฑ๋˜์–ด Data ์ธ์Šคํ„ด์Šค๋กœ ๊ฐ์ฒดํ™” ํ•˜์—ฌ ๋ฐ˜ํ™˜ ๋˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

๊ฐ ๋ ˆ์ด์•„์›ƒ์— ๋งž๋Š” ๋ทฐ ํ™€๋” ๋งคํ•‘ - DataViewHolderMapper

object DataViewHolderMapper {
		
    @Suppress("UNCHECKED_CAST")
    fun <D: Data> map(
        parent: ViewGroup,
        type: CellType,
    ): DataViewHolder<D> {
        val inflater = LayoutInflater.from(parent.context)
        val viewHolder = when (type) {
            CellType.EMPTY_CELL -> null
            CellType.CATEGORY_CELL -> CategoryViewHolder(ViewholderStorageBinding.inflate(inflater,parent,false))
            CellType.LIKE_CELL -> LikeViewHolder(LikeItemBinding.inflate(inflater, parent, false))
        }

        return viewHolder as DataViewHolder<D>
    }

}

DataListAdapter์—์„œ DataViewHolder.mapํ•จ์ˆ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ onCreateViewHolder(ViewGroup, int) ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด layoutId๋ฅผ ๋ฐ›์•„ ๊ฐ ๋ ˆ์ด์•„์›ƒ์— ์ผ์น˜ํ•˜๋Š” ViewHodler๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋ทฐ ํ™€๋” ๊ตฌํ˜„ - ex) UserViewHolder.kt

class LikeViewHolder(
    val binding: LikeItemBinding
) : DataViewHolder<LikeItem>(binding) {

    override fun reset() {
        // TODO
    }

    override fun bindData(data: LikeItem) {
        super.bindData(data)
        binding.likeItemTv.text = data.name
    }

    override fun bindViews(data: LikeItem) {
        binding.likeItemTv.setOnClickListener { data.goLikeDetailPage() }
        binding.deleteBtn.setOnClickListener {
            data.deleteLikeItem()
        }
        binding.likeItemTv.setOnClickListener {
            data.updateLikeItem()
        }
    }

}

DataViewHolder๋Š” ๊ณตํ†ต์ ์œผ๋กœ Data์„ ์ƒ์†๋ฐ›์€ ๋ชจ๋“  ํด๋ž˜์Šค์— ๋Œ€ํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”์ธ๋”ฉํ•˜๋Š” bindData(T : Data) , bindViews(T : Data) ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ์‹œ์™€ ๊ฐ™์ด LikeItem์„ ์‚ฌ์šฉํ•˜๋Š” ๋ทฐํ™€๋”๋Š” DataViewHolder๋ฅผ ์ƒ์†๋ฐ›์•„ ์‚ฌ์šฉํ•˜๋ฉฐ, DataBindHelper์—์„œ ์ •์˜ํ•œ ๋ชจ๋ธ์— ๋Œ€ํ•œ ์•ก์…˜์„ LikeItem.deleteLikeItem(), LikeItem.updateLikeItem() ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ํ˜ธ์ถœํ•˜์—ฌ ๋™์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ๋™์ ์ธ ๊ฐฑ์‹ 

๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ตฌ์กฐ์ƒ ๋‹ค๋ฅธ ํ™”๋ฉด์˜ ๋ฐ์ดํ„ฐ ๋ฆฌ์ŠคํŠธ๋„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฐฑ์‹ ๋˜์–ด์•ผ ํ•  ํ•„์š”๊ฐ€ ์žˆ๊ธฐ๋•Œ๋ฌธ์— ๋ชจ๋ธ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ–๊ณ  ์žˆ๋Š” ๋ทฐ ๋ชจ๋ธ์—์„œ๋Š” ๋ฐ์ดํ„ฐ ๊ฐฑ์‹  ๋กœ์ง์ด ์š”๊ตฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์ƒํƒœ๋ฅผ ๊ณตํ†ต์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” BaseStateViewModel์—์„œ State์˜ ์ƒํƒœ์— ์ ‘๊ทผํ•˜์—ฌ ๊ฐ’์„ ๊ฐฑ์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

class LikeTabViewModel : BaseStateViewModel<LikeTabState, LikeTabSideEffect>() {

    ...
	
    fun deleteItem(item: LikeItem): Job = viewModelScope.launch {
        val withState = withState<LikeTabState.Success> { state ->
            setState(
                state.copy(
                    dataList = state.dataList.toMutableList().apply {
                        remove(item)
                    }
                )
            )
            postSideEffect(
                LikeTabSideEffect.ShowToast(
                    "${item}์ด ์‚ญ์ œ๋์Šต๋‹ˆ๋‹ค."
                )
            )
        }
    }

    ...

}

About

21th-Android-Team-2-Android

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published