- MVVM + UDA
- ์ํคํ
์ณ, ๋ฐ์ธ๋ฉ ๋ฑ์ ๋ํ ๊ตฌ์กฐ์ ์ค๋ช
(MVVM, Model ์์๋ฐ์์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์, JSON๊ตฌ์กฐ, DataBindHelper ๊ตฌํ)
- ์ํคํ ์ณ - MV(VC)VM(Action in Model)
- ์๋ฒ์์ Response๋ฐ๋ JSON ๊ตฌ์กฐ - ex) ์ ์ ๋ฆฌ์คํธ Response
- ๋ฐ์ดํฐ ํด๋์ค - Model
- Data๋ฅผ ์์๋ฐ์ ์์ ํด๋์ค - ex) User.kt
- BaseViewModel
- BaseStateViewModel
- ๋ทฐ ๋ชจ๋ธ - ex) LikeTabViewModel
- VM์์ ๊ตฌ๋ ํ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ๋ทฐ์ ๋ฟ๋ ค์ฃผ๋ VC - ex) LikeTabFragment
- Data์ ์ก์ ํธ๋ค๋ฌ์ ๊ธฐ๋ฅ์ ์ ์ํด์ฃผ๋ ํฌํผ - DataBindHelper
- DataListAdapter๋ฅผ ์ด์ฉํ type๋ณ ์ ๋ถ๋ฅ
- ๊ฐ ํ์ ์ ๋ง๋ ์ธ์คํด์ค๋ฅผ ๋งคํ - DataMapper
- ๊ฐ ๋ ์ด์์์ ๋ง๋ ๋ทฐ ํ๋ ๋งคํ - DataViewHolderMapper
- ๋ทฐ ํ๋ ๊ตฌํ - ex) UserViewHolder.kt
- ๋ฐ์ดํฐ์ ๋ํ ๋์ ์ธ ๊ฐฑ์
์ํคํ ์ณ, ๋ฐ์ธ๋ฉ ๋ฑ์ ๋ํ ๊ตฌ์กฐ์ ์ค๋ช (MVVM, Model ์์๋ฐ์์ ์ฒ๋ฆฌํ๋ ๋ฐฉ์, JSON๊ตฌ์กฐ, DataBindHelper ๊ตฌํ)
Jetpack์์ ์ ๊ณตํ๋ AAC-ViewModel, ์ฝ๋ฃจํด์ ์ด์ฉํ์ฌ MVVM + UDA ์ํคํ ์ฒ๋ฅผ ๊ตฌํํ์์ต๋๋ค. ๊ทธ๋์ DataBinding ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ง ์์ผ๋ฉฐ, DataBindHelper๋ฅผ ์ด์ฉํ์ฌ ๋ฆฌ์คํธ์ ์๋ ๋ชจ๋ธ์ ์ก์ ์ ์ ์ํ์ฌ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉํ๋ ๊ตฌ์กฐ๋ก ์ด๋ฃจ์ด์ ธ ์์ต๋๋ค. ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ ์ด์ฉํ์ง ์๋ ์ด์ ๋ ์ถํ ๋ก์ง์ ๋ํ ๊ณ ๋ํ๊ฐ ์ด๋ฃจ์ด ์ก์ ๋ iOS์ ์๋๋ก์ด๋ ์ํคํ ์ฒ๋ฅผ ๋์ผํ๊ฒ ๊ฐ์ ธ๊ฐ์ผ๋ก์จ ์ ์ง๋ณด์์ฑ์ ๋์ด๊ณ ์ด๋ค ๊ฐ๋ฐ์๊ฐ ์๋ ๊ฐ๋ฐํ๋๋ฐ ์์ด ์ํํ๊ฒ ํ๊ธฐ ์ํจ์ ๋๋ค.(๋ฌผ๋ก , ์ํคํ ์ณ๋ฅผ ๋ฐ๋์ ๋์ผํ๊ฒ ๊ฐ์ ธ๊ฐ ํ์๋ ์์ต๋๋ค. ์ด๊ฒ์ ์ฒํด์ง ์ํฉ์ ๋ฐ๋ผ ๋ค๋ฅธ ๊ฒ์ด๋ผ ์๊ฐํฉ๋๋ค.)
{
data : [
{
"type": "user.cell",
"id": 100,
"scheme": "dncapp://users/100",
...
},
],
"meta": {
"pagenation": {...},
...
}
}
- ์ํคํ ์ณ ๋ฐ ๋ฐ์ดํฐ ๊ตฌ์กฐ์ ์๊ฐ์ ์ค ๋ฌธ์ - https://jsonapi.org
์ ์์๋ ์๋ฒ์์ ๋ฐ์ Json Element ์ค type๋ฅผ ํตํด ๋ฆฌ์คํธ Cell์ ๊ตฌ์กฐํ ํ์ฌ ๋ฐ๊ณ ์์ต๋๋ค.๊ฐ ๋ชจ๋ธ ํ์ ์ ๋ค์ cell์ด ์ค๋์ง, detail ๋ฑ ์ด๋ค ํ์ ์ด ์ค๋์ง์ ๋ฐ๋ผ ๋ฆฌ์คํธํ๋ฉด์ธ์ง, ์์ธํ๋ฉด์ธ์ง ๋ณด์ฌ์ง๋ ๋ทฐ์ ๋งคํํ๋ ์์ ์ ๊ฑฐ์น๊ฒ ๋ฉ๋๋ค.
@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 class User(
val login: String,
val number: Int,
val avatarUrl: String
) : Data() {
...
var favoriteHandler: DataHandler = { }
fun runFavorite() = favoriteHandler.invoke(this)
}
์ ์์์ ๊ฒฝ์ฐ ์ ์ ๋ฆฌ์คํธ ํ์์ ํ์ํ ๋ฐ์ดํฐ ํด๋์ค User์ ๋๋ค. Data๋ฅผ ์์๋ฐ๊ณ ํ์ํ ํ๋กํผํฐ ๋ฐ ๋์์ ๋ํด ์ ์ํ ์ ์์ผ๋ฉฐ, ๋์ ์ธ ๋ฐ์ดํฐ ๊ฐฑ์ ์ ์ํด Handler๋ผ๋ ํ๋๋ฅผ ๋์ด ์ฒ๋ฆฌํฉ๋๋ค.
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๊ฐ ์์ต๋๋ค. ์์ธํ ๊ฒ์ ์ฝ๋๋ฅผ ์ฐธ๊ณ ๋ถํ๋๋ฆฝ๋๋ค.
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() }
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๋ฅผ ๊ฐ๊ณตํด ํตํด ๋ฐ์ดํฐ ๋ฆฌ์คํธ๋ฅผ ๋ฐ์ ํ ์ํ๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ๊ฐฑ์ ํด ์ค๋๋ค.
@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)
}
...
}
@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
}
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์ ํธ๋ค๋ฌ๋ฅผ ํธ๋ฆฌ๊ฑฐํฉ๋๋ค.
- ์๋ฒ์์ ๋ด๋ ค์ฃผ๋ ์๋ต์ 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 ์ธ์คํด์ค๋ก ๊ฐ์ฒดํ ํ์ฌ ๋ฐํ ๋๋ ๋ฐฉ์์ ๋๋ค.
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๋ฅผ ๋ฐํํ๊ฒ ๋ฉ๋๋ค.
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}์ด ์ญ์ ๋์ต๋๋ค."
)
)
}
}
...
}