마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

MVVM 패턴 적용중 알맞게 헀는지 체크좀 부탁드립니다.

0 추천

https://ibb.co/gZGL9nc

https://ibb.co/QfgQZFF

 

링크의 사진 형태인데. 단순 탭 레이아웃에 각 탭마다 서로 다른 리스트 데이터를 보여줍니다.

MVVM 패턴을 적용중인데 아직 낫설어 제대로 했는지 검사좀 해주시면 감사하겠습니다.

Fragment.kt

class WorkoutListTabPageFragment : Fragment() {
    private var _binding : FragmentWorkoutListTabPageBinding? = null
    private val binding get() = _binding!!
    private lateinit var adapter: WorkoutListAdapter
    private lateinit var part: String

    private val viewModel: WorkoutListViewModel by viewModels { WorkoutListViewModelFactory(resources) }

    companion object {
        @JvmStatic
        fun newInstance(param: String) =
            WorkoutListTabPageFragment().apply {
                arguments = Bundle().apply {
                    putString("part", param)
                }
            }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let { bundle ->
            part = bundle.getString("part") ?: "null"
        }
    }

    override fun onCreateView(inflater: LayoutInflater,
                              container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        _binding = FragmentWorkoutListTabPageBinding.inflate(inflater, container, false)
        binding.apply {
            adapter = WorkoutListAdapter()
            rv.adapter = adapter
        }

        viewModel.setList(part)
        viewModel.part.observe(viewLifecycleOwner) { _ ->
            adapter.addItems(viewModel.getList())
            adapter.notifyDataSetChanged()
        }

        return binding.root
    }
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

 

ViewModel.kt

class WorkoutListViewModel(private val resources: Resources) : ViewModel(){
    private var _part :MutableLiveData<String> = MutableLiveData()
    private var _list : MutableLiveData<List<String>> = MutableLiveData(arrayListOf())
    private val workoutListSource : WorkoutListSource by lazy { WorkoutListLocalSource(resources) }

    val list = _list
    val part = _part

    fun setList(part : String) {
        _part.value = part
        when(_part.value) {
            "가슴" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.CHEST)
            "등" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.BACK)
            "하체" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.LEG)
            "어깨" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.SHOULDER)
            "이두" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.BICEPS)
            "삼두" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.TRICEPS)
            "복근" -> _list.value = workoutListSource.getWorkoutListByPart(BodyType.ABS)
        }
    }

    fun getList() : List<String> = list.value!!
}

 

이정도인데 아직 데이터베이스를 사용하고 있지 않아서 다른 샘플코드를 보면

viewmodel에서 repository 클래스를 만들고 데이터베이스를 이용하여 데이터를 가져오는 방식을 사용하던데.

 

저는 아직 데이터베이스를 사용하지않아서 repository 클래스는 구현하지 않았습니다.. 

물론 레포지토리 클래스만 만들어서 데이터베이스를 사용하지 않고도 데이터를 가져올순 있겠지만

아직은 그렇게 하지 않았어요.

 

질문이 있는데,

프래그먼트에서 viewmodel의 part를 observe해서 part가 바뀌면 즉 탭이 변경된다는 뜻이겠죠?

해당 탭에 해당하는 다른 리스트를 보여줘야하는데, 여기서 getList를 이용해서

뷰모델에 있는 list를 가져오는데 이게 즉 데이터를 가져온다는 의미인데. 이렇게 해도 되나요?

그러니까 list를 직접 관찰하는게 아닌 part만 관찰해서 part마다 list를 가져오는 방식이에요.

 

또.. viewmodel을 사용하다보니 제가 무의식적으료 viewmodel 클래스에서 list도 MutableLiveData를 

사용하였는데 위에서 언급한것처럼 part를 관찰하지도 않으므로 굳이 MutableLiveData의 사용은

무의미할까요? 그냥 ArrayList를 사용해도 무방하겠죠?

 

codeslave (3,940 포인트) 님이 2021년 8월 6일 질문
Observe가 필요없는 데이터라면 굳이 LiveData를 사용하실 필요는 없습니다. 그냥 List를 사용하시고 private으로 만드세요.

1개의 답변

0 추천

데이터베이스를 지금 사용하지 않더라도 data 패키지에 인터페이스를 만드시고 Fake DB를 구현하셔서 작업하는 걸 권장드려요. 일반적으로 데이터베이스에 필요한 데이터 구조와 View에 필요한 데이터구조가 달라지기 때문에, 이걸 나중에 하려고 미루면 코드의 수정이 아주 많아 질 수도 있습니다. Fake DB에 어떤 데이터베이스에서 사용할 데이터 타입을 두고 Map 같은 걸 사용해서 데이터를 리턴해주는 식으로 간단하게 구현하면 됩니다.

interface Entity<ID> {
    val id: String
}

data class BodyPartEntity(
      override val Id: String,
      val name: String,
): Entity<String>

interface Dao<ID, T> {
    fun getList(): T
    fun getOne(id: ID): T?
    fun save(id:ID, data: T)
    fun delete(id: ID)
}

class BodyPartDao: Dao<String, BodyPartEntity> {
    private val storage = hashMapOf<String, BodyPartEntity>)()

    override fun getList(): T = storage.values().toList()
  
    override fun getOne(id: String): T? = storage.getOrNull(id)

    fun save(id:ID, data: T){
       storage[id] = data
   }

   fun delete(id: ID) {
     stroage.delete[id]
   }
}

object DatabaseFactory {
  val bodyPartDao: Dao<String, BodyPartEntity> by lazy {
         BodyPartDao()
  }
}  

 

이런 식으로 만드셔서 ViewModel 에서는 interface를 가져다 사용하세요.

val bodyPartDao: Dao<String, BodyPartEntity>  get() = DaoFactory.bodyPartDao

추후에 데이터베이스를 추가힐 때는 인터페이스는 그대로 두시고  실제 Database 를 이용한 구현 클래스를 하나 만드시면 되겠죠.
이래야 수정사항이ㅣ 적어집니다. 단위테스트도 가능해 지구요.

그리고 아래코드는 약간의 수정이 필요합니다.

class WorkoutListViewModel(private val resources: Resources)

ViewModel에는 라이프사이클 옵저버같은 라이프사이클 관련 코드외에는 안드로이드 플랫폼을 참조하면 안됩니다. ViewModel 은 단위테스트가 가능하도록 설계가 되었기때문에 안드로이드 플랫폼에 대한 참조를 가능한 가지지 말아야 합니다.

class WorkoutListViewModel(private val workoutListSource: WorkoutListSource) : ViewModel(){

}

위처럼, WorkoutListSource 를 바로 Inject 하세요.

 

그리고 LiveData를 observe할 때는

viewModel.part.observe(viewLifecycleOwner) { _ ->
       adapter.addItems(viewModel.getList())
        ....
}

 

ViewModel 에서 postValue할 때 View에 필요한 데이터도 같이 전달하세요.

sealed class WorkoutListViewState {
   data class BodyPart(val selectedPart: String): WorkoutListViewState()
   data class BodyParts(val parts: List<String>): WorkoutListViewState()
}

class WorkoutListViewModel(private val resources: Resources) : ViewModel(){
    private var _part :MutableLiveData<BodyPart> = MutableLiveData()
    private var _list : MutableLiveData<BodyParts> = MutableLiveData(arrayListOf())
}


viewModel.part.observe(view.lifecycleOwner, ::updateUi)

fun updateUi(viewState: WorkoutListViewState) {
     when (viewState) {
        is WorkoutListViewState.BodyPart -> updateBodyPart(viewState)
        is WorkoutListViewState.BodyParts -> updateBodyParts(viewState)
     }
} 

fun updateBodyPart(WorkoutListViewState.BodyPart) {

}

fun updateBodyParts(WorkoutListViewState.BodyParts {

}

 

 뷰에서는 가능하면 ViewModel이 주는 데이터를 가져다 그대로 사용할 수 있는 Passive View로 만들어 주시는 게 좋습니다.

 

spark (224,800 포인트) 님이 2021년 8월 6일 답변
감사합니다..코드를 잘 이해는 못했지만 ㅠㅠ.데이터베이스 얘기가 나온김에 질문좀 드리겠습니다. 앱개발할때 데이터베이스의 사용이 거의 필수 불가결한데,
찾아보니 데이터베이스를 사용할때 흔히 두가지 경우가 있는 것같습니다.

1.단말기 내부의 데이터베이스
2.서버쪽 데이터베이스

이 두가지 모두 데이터베이스인데, 어떤 상황에 무엇을 써야하나요?
하나의 앱에 로컬과 서버 데이터베이스 둘다 사용해도 되나요?

본문의 링크사진에 해당하는 경우에는, 로컬 DB같은것을 사용해도될까요..
그건 전적으로 님의 상황에 따른 거라 제가 어떤 것을 사용하라고 말씀을 드릴 수가 없어요. 말씀드릴 수 있는 것은 user experience가 좋은 앱을 만들려면 서버의 API를 호출하여 사용하더라도, 로컬  cache 용도로 데이터베이스를 많이 사용한다는 점입니다. 그리고 서버를 구축할 수 있는 여력이 안된다면 일단은 로컬데이터베이스나 Firebase같은 걸 사용하셔야 겠죠.
선생님 단말기의 로컬 DB를 사용할 경우 앱을 삭제하면 데이터베이스의 내용이 삭제 된다고 들었습니다. 서버Db의 경우는 유지되구요. 그렇다면 만약에 본문 내용처럼
운동 목록들을 DB에 저장한다고하고 하면, 앱삭제시에는 이 데이터도 매번 날아가며,
앱 재설치시 새로 데이터를 넣거나 그래야하는건가요?
개발자 문서에 가면 어떤 것이 백업이 되고 안되고가 자세하게 나와 있습니다.
https://developer.android.com/guide/topics/data/autobackup
...